From 3a84c0ab9022feeed11adf8f9023fd5f2d4459ba Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 19 Aug 2015 06:27:43 -0700 Subject: [PATCH] Civi::settings() - Add short hand for accessing domain settings --- CRM/Core/BAO/Setting.php | 99 ++----- Civi.php | 11 + Civi/Core/Container.php | 12 + Civi/Core/SettingsBag.php | 270 ++++++++++++++++++ Civi/Core/SettingsManager.php | 168 +++++++++++ .../phpunit/Civi/Core/SettingsManagerTest.php | 131 +++++++++ tests/phpunit/api/v3/SettingTest.php | 11 +- 7 files changed, 626 insertions(+), 76 deletions(-) create mode 100644 Civi/Core/SettingsBag.php create mode 100644 Civi/Core/SettingsManager.php create mode 100644 tests/phpunit/Civi/Core/SettingsManagerTest.php diff --git a/CRM/Core/BAO/Setting.php b/CRM/Core/BAO/Setting.php index 4ef45dd27b..cf2d834efb 100644 --- a/CRM/Core/BAO/Setting.php +++ b/CRM/Core/BAO/Setting.php @@ -224,61 +224,21 @@ class CRM_Core_BAO_Setting extends CRM_Core_DAO_Setting { $contactID = NULL, $domainID = NULL ) { - - $overrideGroup = array(); - if (NULL !== ($override = self::getOverride($group, $name, NULL))) { - if (isset($name)) { - return $override; - } - else { - $overrideGroup = $override; - } - } - - if (empty($domainID)) { - $domainID = CRM_Core_Config::domainID(); - } - $cacheKey = self::inCache($group, $name, $componentID, $contactID, TRUE, $domainID); - - if ($group && !isset($name) && $cacheKey) { - // check value against the cache, and unset key if values are different - $valueDifference = CRM_Utils_Array::multiArrayDiff($overrideGroup, self::$_cache[$cacheKey]); - if (!empty($valueDifference)) { - $cacheKey = ''; + /** @var \Civi\Core\SettingsManager $manager */ + $manager = \Civi::service('settings_manager'); + $settings = ($contactID === NULL) ? $manager->getBagByDomain($domainID) : $manager->getBagByContact($domainID, $contactID); + if (TRUE) { + if ($name === NULL) { + CRM_Core_Error::debug_log_message("Deprecated: Group='$group'. Name should be provided.\n"); } - } - - if (!$cacheKey) { - $dao = self::dao($group, NULL, $componentID, $contactID, $domainID); - $dao->find(); - - $values = array(); - while ($dao->fetch()) { - if (NULL !== ($override = self::getOverride($group, $dao->name, NULL))) { - $values[$dao->name] = $override; - } - elseif ($dao->value) { - $values[$dao->name] = unserialize($dao->value); - } - else { - $values[$dao->name] = NULL; - } + if ($componentID !== NULL) { + CRM_Core_Error::debug_log_message("Deprecated: Group='$group'. Name='$name'. Component should be omitted\n"); } - $dao->free(); - - if (!isset($name)) { - // merge db and override group values - // When no $name is present, the getItem() function should return an array - // consisting of the sum of all override settings + all settings present in - // the database for the given $group (with the overrides taking precedence, - // and applying even if the setting is not defined in the database). - // - $values = array_merge($values, $overrideGroup); + if ($defaultValue !== NULL) { + CRM_Core_Error::debug_log_message("Deprecated: Group='$group'. Name='$name'. Defaults should come from metadata\n"); } - - $cacheKey = self::setCache($values, $group, $componentID, $contactID, $domainID); } - return $name ? CRM_Utils_Array::value($name, self::$_cache[$cacheKey], $defaultValue) : self::$_cache[$cacheKey]; + return $name ? $settings->get($name) : $settings->all(); } /** @@ -373,14 +333,10 @@ class CRM_Core_BAO_Setting extends CRM_Core_DAO_Setting { $createdID = NULL, $domainID = NULL ) { - $fields = array(); - $fieldsToSet = self::validateSettingsInput(array($name => $value), $fields); - //We haven't traditionally validated inputs to setItem, so this breaks things. - //foreach ($fieldsToSet as $settingField => &$settingValue) { - // self::validateSetting($settingValue, $fields['values'][$settingField]); - //} - - return self::_setItem($fields['values'][$name], $value, $group, $name, $componentID, $contactID, $createdID, $domainID); + /** @var \Civi\Core\SettingsManager $manager */ + $manager = \Civi::service('settings_manager'); + $settings = ($contactID === NULL) ? $manager->getBagByDomain($domainID) : $manager->getBagByContact($domainID, $contactID); + $settings->set($name, $value); } /** @@ -485,10 +441,13 @@ class CRM_Core_BAO_Setting extends CRM_Core_DAO_Setting { * @return array */ public static function setItems(&$params, $domains = NULL) { - $originalDomain = CRM_Core_Config::domainID(); - if (empty($domains)) { - $domains[] = $originalDomain; - } + /** @var \Civi\Core\SettingsManager $manager */ + $manager = \Civi::service('settings_manager'); + $domains = empty($domains) ? array(CRM_Core_Config::domainID()) : $domains; + + // FIXME: redundant validation + // FIXME: this whole thing should just be a loop to call $settings->add() on each domain. + $reloadConfig = FALSE; $fields = $config_keys = array(); $fieldsToSet = self::validateSettingsInput($params, $fields); @@ -498,23 +457,16 @@ class CRM_Core_BAO_Setting extends CRM_Core_DAO_Setting { } foreach ($domains as $domainID) { + if ($domainID != CRM_Core_Config::domainID()) { $reloadConfig = TRUE; CRM_Core_BAO_Domain::setDomain($domainID); } $result[$domainID] = array(); + $realSettingsToSet = array(); // need to separate config_backend stuff foreach ($fieldsToSet as $name => $value) { if (empty($fields['values'][$name]['config_only'])) { - CRM_Core_BAO_Setting::_setItem( - $fields['values'][$name], - $value, - $fields['values'][$name]['group_name'], - $name, - CRM_Utils_Array::value('component_id', $params), - CRM_Utils_Array::value('contact_id', $params), - CRM_Utils_Array::value('created_id', $params), - $domainID - ); + $realSettingsToSet[$name] = $value; } if (!empty($fields['values'][$name]['prefetch'])) { if (!empty($fields['values'][$name]['config_key'])) { @@ -524,6 +476,7 @@ class CRM_Core_BAO_Setting extends CRM_Core_DAO_Setting { } $result[$domainID][$name] = $value; } + $manager->getBagByDomain($domainID)->add($realSettingsToSet); if ($reloadConfig) { CRM_Core_Config::singleton($reloadConfig, $reloadConfig); } diff --git a/Civi.php b/Civi.php index d4fe840ce6..20d78b670a 100644 --- a/Civi.php +++ b/Civi.php @@ -74,4 +74,15 @@ class Civi { self::$statics = array(); } + /** + * Obtain the domain settings. + * + * @param int|null $domainID + * For the default domain, leave $domainID as NULL. + * @return \Civi\Core\SettingsBag + */ + public static function settings($domainID = NULL) { + return Civi\Core\Container::singleton()->get('settings_manager')->getBagByDomain($domainID); + } + } diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index 7b4b611468..2be91acca1 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -103,6 +103,18 @@ class Container { )) ->setFactoryClass('CRM_Cxn_BAO_Cxn')->setFactoryMethod('createRegistrationClient'); + $container->setDefinition('cache.settings', new Definition( + 'CRM_Utils_Cache_SqlGroup', + array( + array('group' => 'Settings', 'prefetch' => 0), + ) + )); + + $container->setDefinition('settings_manager', new Definition( + 'Civi\Core\SettingsManager', + array(new Reference('cache.settings')) + )); + $container->setDefinition('pear_mail', new Definition('Mail')) ->setFactoryClass('CRM_Utils_Mail')->setFactoryMethod('createMailer'); diff --git a/Civi/Core/SettingsBag.php b/Civi/Core/SettingsBag.php new file mode 100644 index 0000000000..55a696ce44 --- /dev/null +++ b/Civi/Core/SettingsBag.php @@ -0,0 +1,270 @@ + mixed $value). + */ + protected $defaults; + + /** + * @var array + * Array(string $settingName => mixed $value). + */ + protected $mandatory; + + /** + * The result of combining default values, mandatory + * values, and user values. + * + * @var array|NULL + * Array(string $settingName => mixed $value). + */ + protected $combined; + + /** + * @var array + */ + protected $values; + + /** + * @param int $domainId + * The domain for which we want settings. + * @param int|NULL $contactId + * The contact for which we want settings. Use NULL for domain settings. + * @param array $defaults + * Array(string $settingName => mixed $value). + * @param array $mandatory + * Array(string $settingName => mixed $value). + */ + public function __construct($domainId, $contactId, $defaults, $mandatory) { + $this->domainId = $domainId; + $this->contactId = $contactId; + $this->defaults = $defaults; + $this->mandatory = $mandatory; + $this->combined = NULL; + } + + /** + * Load all settings that apply to this domain or contact. + * + * @return $this + */ + public function load() { + $this->values = array(); + $dao = $this->createDao(); + $dao->find(); + while ($dao->fetch()) { + $this->values[$dao->name] = is_string($dao->value) ? unserialize($dao->value) : NULL; + } + $this->combined = NULL; + return $this; + } + + /** + * Add a batch of settings. Save them. + * + * @param array $settings + * Array(string $settingName => mixed $settingValue). + * @return $this + */ + public function add(array $settings) { + foreach ($settings as $key => $value) { + $this->set($key, $value); + } + return $this; + } + + /** + * Get a list of all effective settings. + * + * @return array + * Array(string $settingName => mixed $settingValue). + */ + public function all() { + if ($this->combined === NULL) { + $this->combined = $this->combine( + array($this->defaults, $this->values, $this->mandatory) + ); + } + return $this->combined; + } + + /** + * Determine the effective value. + * + * @param string $key + * @return mixed + */ + public function get($key) { + $all = $this->all(); + return isset($all[$key]) ? $all[$key] : NULL; + } + + /** + * Determine the explicitly designated value, regardless of + * any default or mandatory values. + * + * @param string $key + * @return null + */ + public function getExplicit($key) { + return (isset($this->values[$key]) ? $this->values[$key] : NULL); + } + + /** + * Determine if the entity has explicitly designated a value. + * + * Note that get() may still return other values based on + * mandatory values or defaults. + * + * @param string $key + * @return bool + */ + public function hasExplict($key) { + // NULL means no designated value. + return isset($this->values[$key]); + } + + /** + * Removes any explicit settings. This restores the default. + * + * @param string $key + * @return $this + */ + public function revert($key) { + // It might be better to DELETE (to avoid long-term leaks), + // but setting NULL is simpler for now. + return $this->set($key, NULL); + } + + /** + * Add a single setting. Save it. + * + * @param string $key + * @param mixed $value + * @return $this + */ + public function set($key, $value) { + $this->setDb($key, $value); + $this->values[$key] = $value; + $this->combined = NULL; + return $this; + } + + /** + * @return \CRM_Core_DAO_Setting + */ + protected function createDao() { + $dao = new \CRM_Core_DAO_Setting(); + $dao->domain_id = $this->domainId; + if ($this->contactId === NULL) { + $dao->is_domain = 1; + } + else { + $dao->contact_id = $this->contactId; + $dao->is_domain = 0; + } + return $dao; + } + + /** + * Combine a series of arrays, excluding any + * null values. Later values override earlier + * values. + * + * @param $arrays + * @return array + */ + protected function combine($arrays) { + $combined = array(); + foreach ($arrays as $array) { + foreach ($array as $k => $v) { + if ($v !== NULL) { + $combined[$k] = $v; + } + } + } + return $combined; + } + + /** + * @param $key + * @param $value + */ + protected function setDb($name, $value) { + $fields = array(); + $fieldsToSet = \CRM_Core_BAO_Setting::validateSettingsInput(array($name => $value), $fields); + //We haven't traditionally validated inputs to setItem, so this breaks things. + //foreach ($fieldsToSet as $settingField => &$settingValue) { + // self::validateSetting($settingValue, $fields['values'][$settingField]); + //} + // NOTE: We don't have any notion of createdID + \CRM_Core_BAO_Setting::_setItem($fields['values'][$name], $value, '', $name, NULL, $this->contactId, NULL, $this->domainId); + + //$dao = $this->createDao(); + //$dao->name = $key; + //$dao->group_name = ''; + //$dao->find(); + //$serializedValue = ($value === NULL ? 'null' : serialize($value)); + //if ($dao->value !== $serializedValue) { + // $dao->created_date = \CRM_Utils_Time::getTime('Ymdhis'); + // $dao->value = $serializedValue; + // $dao->save(); + //} + } + +} diff --git a/Civi/Core/SettingsManager.php b/Civi/Core/SettingsManager.php new file mode 100644 index 0000000000..b6c403e8a4 --- /dev/null +++ b/Civi/Core/SettingsManager.php @@ -0,0 +1,168 @@ + SettingsBag $bag). + */ + protected $bagsByDomain = array(), $bagsByContact = array(); + + /** + * @var array + */ + protected $mandatory = NULL; + + /** + * @param \CRM_Utils_Cache_Interface $cache + * @param NULL|array $mandatory + */ + public function __construct($cache, $mandatory = NULL) { + $this->cache = $cache; + $this->mandatory = $mandatory; + } + + /** + * @param int $domainId + * @return SettingsBag + */ + public function getBagByDomain($domainId) { + if ($domainId === NULL) { + $domainId = \CRM_Core_Config::domainID(); + } + + if (!isset($this->bagsByDomain[$domainId])) { + $defaults = $this->getDefaults('domain'); + // Filter $mandatory to only include domain-settings. + $mandatory = \CRM_Utils_Array::subset($this->getMandatory(), array_keys($defaults)); + $this->bagsByDomain[$domainId] = new SettingsBag($domainId, NULL, $defaults, $mandatory); + $this->bagsByDomain[$domainId]->load(); + } + return $this->bagsByDomain[$domainId]; + } + + /** + * @param int $domainId + * @param int $contactId + * @return SettingsBag + */ + public function getBagByContact($domainId, $contactId) { + if ($domainId === NULL) { + $domainId = \CRM_Core_Config::domainID(); + } + + $key = "$domainId:$contactId"; + if (!isset($this->bagsByContact[$key])) { + $defaults = $this->getDefaults('contact'); + // Filter $mandatory to only include domain-settings. + $mandatory = \CRM_Utils_Array::subset($this->getMandatory(), array_keys($defaults)); + $this->bagsByContact[$key] = new SettingsBag($domainId, $contactId, $defaults, $mandatory); + $this->bagsByContact[$key]->load(); + } + return $this->bagsByContact[$key]; + } + + /** + * Determine the default settings. + * + * @param string $entity + * Ex: 'domain' or 'contact'. + * @return array + * Array(string $settingName => mixed $value). + */ + public function getDefaults($entity) { + $cacheKey = 'defaults:' . $entity; + $defaults = $this->cache->get($cacheKey); + if (!is_array($defaults)) { + $specs = \CRM_Core_BAO_Setting::getSettingSpecification(NULL, array( + 'is_contact' => ($entity === 'contact' ? 1 : 0), + )); + $defaults = array(); + foreach ($specs as $key => $spec) { + $defaults[$key] = \CRM_Utils_Array::value('default', $spec); + } + $this->cache->set($cacheKey, $defaults); + } + return $defaults; + } + + /** + * Get a list of mandatory/overriden settings. + * + * @return array + * Array(string $settingName => mixed $value). + */ + public function getMandatory() { + if ($this->mandatory === NULL) { + if (isset($GLOBALS['civicrm_setting'])) { + $this->mandatory = self::parseMandatorySettings($GLOBALS['civicrm_setting']); + } + else { + $this->mandatory = array(); + } + } + return $this->mandatory; + } + + /** + * Parse + * + * @param array $civicrm_setting + * Ex: $civicrm_setting['Group Name']['field'] = 'value'. + * Group names are an historical quirk; ignore them. + * @return array + */ + public static function parseMandatorySettings($civicrm_setting) { + $tmp = array(); + if (is_array($civicrm_setting)) { + foreach ($civicrm_setting as $group => $settings) { + foreach ($settings as $k => $v) { + if ($v !== NULL) { + $tmp[$k] = $v; + } + } + } + return $tmp; + } + return $tmp; + } + +} diff --git a/tests/phpunit/Civi/Core/SettingsManagerTest.php b/tests/phpunit/Civi/Core/SettingsManagerTest.php new file mode 100644 index 0000000000..60263f3fd3 --- /dev/null +++ b/tests/phpunit/Civi/Core/SettingsManagerTest.php @@ -0,0 +1,131 @@ +useTransaction(TRUE); + + $this->domainDefaults = array( + 'd1' => 'alpha', + 'd2' => 'beta', + 'd3' => 'gamma', + ); + $this->contactDefaults = array( + 'c1' => 'alpha', + 'c2' => 'beta', + 'c3' => 'gamma', + ); + $this->mandates = array( + 'foo' => array( + 'd3' => 'GAMMA!', + ), + 'bar' => array( + 'c3' => 'GAMMA MAN!', + ), + ); + } + + /** + * Test mingled reads/writes of settings for two different domains. + */ + public function testTwoDomains() { + $da = \CRM_Core_DAO::createTestObject('CRM_Core_DAO_Domain'); + $db = \CRM_Core_DAO::createTestObject('CRM_Core_DAO_Domain'); + + $manager = $this->createManager(); + + $daSettings = $manager->getBagByDomain($da->id); + $daSettings->set('d1', 'un'); + $this->assertEquals('un', $daSettings->get('d1')); + $this->assertEquals('beta', $daSettings->get('d2')); + $this->assertEquals('GAMMA!', $daSettings->get('d3')); + + $dbSettings = $manager->getBagByDomain($db->id); + $this->assertEquals('alpha', $dbSettings->get('d1')); + $this->assertEquals('beta', $dbSettings->get('d2')); + $this->assertEquals('GAMMA!', $dbSettings->get('d3')); + + $managerRedux = $this->createManager(); + + $daSettingsRedux = $managerRedux->getBagByDomain($da->id); + $this->assertEquals('un', $daSettingsRedux->get('d1')); + $this->assertEquals('beta', $daSettingsRedux->get('d2')); + $this->assertEquals('GAMMA!', $daSettingsRedux->get('d3')); + } + + /** + * Test mingled reads/writes of settings for two different contacts. + */ + public function testTwoContacts() { + $domain = \CRM_Core_DAO::createTestObject('CRM_Core_DAO_Domain'); + $ca = \CRM_Core_DAO::createTestObject('CRM_Contact_DAO_Contact'); + $cb = \CRM_Core_DAO::createTestObject('CRM_Contact_DAO_Contact'); + + $manager = $this->createManager(); + + $caSettings = $manager->getBagByContact($domain->id, $ca->id); + $caSettings->set('c1', 'un'); + $this->assertEquals('un', $caSettings->get('c1')); + $this->assertEquals('beta', $caSettings->get('c2')); + $this->assertEquals('GAMMA MAN!', $caSettings->get('c3')); + + $cbSettings = $manager->getBagByContact($domain->id, $cb->id); + $this->assertEquals('alpha', $cbSettings->get('c1')); + $this->assertEquals('beta', $cbSettings->get('c2')); + $this->assertEquals('GAMMA MAN!', $cbSettings->get('c3')); + + // Read settings from freshly initialized objects. + $manager = $this->createManager(); + + $caSettingsRedux = $manager->getBagByContact($domain->id, $ca->id); + $this->assertEquals('un', $caSettingsRedux->get('c1')); + $this->assertEquals('beta', $caSettingsRedux->get('c2')); + $this->assertEquals('GAMMA MAN!', $caSettingsRedux->get('c3')); + } + + public function testCrossOver() { + $domain = \CRM_Core_DAO::createTestObject('CRM_Core_DAO_Domain'); + $contact = \CRM_Core_DAO::createTestObject('CRM_Contact_DAO_Contact'); + + $manager = $this->createManager(); + + // Store different values for the 'monkeywrench' setting on domain and contact + + $domainSettings = $manager->getBagByDomain($domain->id); + $domainSettings->set('monkeywrench', 'from domain'); + $this->assertEquals('from domain', $domainSettings->get('monkeywrench')); + + $contactSettings = $manager->getBagByContact($domain->id, $contact->id); + $contactSettings->set('monkeywrench', 'from contact'); + $this->assertEquals('from contact', $contactSettings->get('monkeywrench')); + + // Read settings from freshly initialized objects. + $manager = $this->createManager(); + + $domainSettings = $manager->getBagByDomain($domain->id); + $this->assertEquals('from domain', $domainSettings->get('monkeywrench')); + + $contactSettings = $manager->getBagByContact($domain->id, $contact->id); + $this->assertEquals('from contact', $contactSettings->get('monkeywrench')); + } + + /** + * @return SettingsManager + */ + protected function createManager() { + $cache = new \CRM_Utils_Cache_Arraycache(array()); + $cache->set('defaults:domain', $this->domainDefaults); + $cache->set('defaults:contact', $this->contactDefaults); + $manager = new SettingsManager($cache, SettingsManager::parseMandatorySettings($this->mandates)); + return $manager; + } + +} diff --git a/tests/phpunit/api/v3/SettingTest.php b/tests/phpunit/api/v3/SettingTest.php index c935547cdd..27bc07a814 100644 --- a/tests/phpunit/api/v3/SettingTest.php +++ b/tests/phpunit/api/v3/SettingTest.php @@ -364,9 +364,11 @@ class api_v3_SettingTest extends CiviUnitTestCase { // the caching of data to all duplicates the caching of data to the empty string CRM_Core_BAO_Cache::setItem($data, 'CiviCRM setting Spec', 'All'); CRM_Core_BAO_Cache::setItem($data, 'CiviCRM setting Specs', 'settingsMetadata__'); + Civi::cache('settings')->flush(); $fields = $this->callAPISuccess('setting', 'getfields', array('filters' => array('group_name' => 'Test Settings'))); $this->assertArrayHasKey('test_key', $fields['values']); $this->callAPISuccess('setting', 'create', array('test_key' => 'keyset')); + $this->assertEquals('keyset', Civi::settings()->get('test_key')); $result = $this->callAPISuccess('setting', 'getvalue', array('name' => 'test_key', 'group' => 'Test Settings')); $this->assertEquals('keyset', $result); } @@ -518,9 +520,9 @@ class api_v3_SettingTest extends CiviUnitTestCase { } /** - * Tests filling missing params. + * Settings should respect their defaults */ - public function testFill() { + public function testDefaults() { $domparams = array( 'name' => 'B Team Domain', ); @@ -540,7 +542,10 @@ class api_v3_SettingTest extends CiviUnitTestCase { ); $result = $this->callAPISuccess('setting', 'get', $params); $this->assertAPISuccess($result, "in line " . __LINE__); - $this->assertArrayNotHasKey('tag_unconfirmed', $result['values'][$dom['id']], 'setting for domain 3 should not be set. Debug this IF domain test is passing'); + $this->assertEquals('Unconfirmed', $result['values'][$dom['id']]['tag_unconfirmed']); + + // The 'fill' operation is no longer necessary, but third parties might still use it, so let's + // make sure it doesn't do anything weird (crashing or breaking values). $result = $this->callAPISuccess('setting', 'fill', $params); $this->assertAPISuccess($result, "in line " . __LINE__); $result = $this->callAPISuccess('setting', 'get', $params); -- 2.25.1