From 5dbaf8dec1a9d1305ff0cd2e519e3182f63da605 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 10 Sep 2015 16:36:28 -0700 Subject: [PATCH] CRM-16373 - Civi\Core\SettingsManager - Load defaults *after* core bootstrap. --- CRM/Core/Config.php | 1 + Civi/Core/SettingsBag.php | 61 ++++++-- Civi/Core/SettingsManager.php | 141 ++++++++++++++---- .../phpunit/Civi/Core/SettingsManagerTest.php | 20 +-- 4 files changed, 172 insertions(+), 51 deletions(-) diff --git a/CRM/Core/Config.php b/CRM/Core/Config.php index ec10378a29..664bf345ee 100644 --- a/CRM/Core/Config.php +++ b/CRM/Core/Config.php @@ -101,6 +101,7 @@ class CRM_Core_Config extends CRM_Core_Config_Variables { CRM_Utils_Hook::config(self::$_singleton); self::$_singleton->authenticate(); + Civi::service('settings_manager')->useDefaults(); } return self::$_singleton; } diff --git a/Civi/Core/SettingsBag.php b/Civi/Core/SettingsBag.php index 0496cfcf3f..6d8810bbf6 100644 --- a/Civi/Core/SettingsBag.php +++ b/Civi/Core/SettingsBag.php @@ -87,28 +87,52 @@ class SettingsBag { * The domain for which we want settings. * @param int|NULL $contactId * The contact for which we want settings. Use NULL for domain settings. + */ + public function __construct($domainId, $contactId) { + $this->domainId = $domainId; + $this->contactId = $contactId; + $this->filteredValues = array(); + $this->combined = NULL; + } + + /** + * Set/replace the default values. + * * @param array $defaults * Array(string $settingName => mixed $value). + * @return $this + */ + public function loadDefaults($defaults) { + $this->defaults = $defaults; + $this->filteredValues = array(); + $this->combined = NULL; + return $this; + } + + /** + * Set/replace the mandatory values. + * * @param array $mandatory * Array(string $settingName => mixed $value). + * @return $this */ - public function __construct($domainId, $contactId, $defaults, $mandatory) { - $this->domainId = $domainId; - $this->contactId = $contactId; - $this->defaults = $defaults; + public function loadMandatory($mandatory) { $this->mandatory = $mandatory; + $this->filteredValues = array(); $this->combined = NULL; + return $this; } /** - * Load all settings that apply to this domain or contact. + * Load all explicit settings that apply to this domain or contact. * * @return $this */ - public function load() { + public function loadValues() { $this->values = array(); - $dao = $this->createDao(); - $dao->find(); + // Note: Don't use Setting DAO. It requires fields() which requires + // translations -- which are keyed off settings! + $dao = \CRM_Core_DAO::executeQuery($this->createQuery()->toSQL()); while ($dao->fetch()) { $this->values[$dao->name] = ($dao->value !== NULL) ? unserialize($dao->value) : NULL; } @@ -267,19 +291,24 @@ class SettingsBag { } /** - * @return \CRM_Core_DAO_Setting + * @return \CRM_Utils_SQL_Select */ - protected function createDao() { - $dao = new \CRM_Core_DAO_Setting(); - $dao->domain_id = $this->domainId; + protected function createQuery() { + $select = \CRM_Utils_SQL_Select::from('civicrm_setting') + ->select('id, group_name, name, value, domain_id, contact_id, is_domain, component_id, created_date, created_id') + ->where('domain_id = #id', array( + 'id' => $this->domainId, + )); if ($this->contactId === NULL) { - $dao->is_domain = 1; + $select->where('is_domain = 1'); } else { - $dao->contact_id = $this->contactId; - $dao->is_domain = 0; + $select->where('contact_id = #id', array( + 'id' => $this->contactId, + )); + $select->where('is_domain = 0'); } - return $dao; + return $select; } /** diff --git a/Civi/Core/SettingsManager.php b/Civi/Core/SettingsManager.php index b6c403e8a4..6f4930dd34 100644 --- a/Civi/Core/SettingsManager.php +++ b/Civi/Core/SettingsManager.php @@ -31,6 +31,26 @@ namespace Civi\Core; * Class SettingsManager * @package Civi\Core * + * The SettingsManager is responsible for tracking settings across various + * domains and users. + * + * Generally, for any given setting, there are three levels where values + * can be declared: + * + * - Mandatory values (which come from a global $civicrm_setting). + * - Explicit values (which are chosen by the user and stored in the DB). + * - Default values (which come from the settings metadata). + * + * Note: During the early stages of bootstrap, default values are not be available. + * Loading the defaults requires loading metadata from various sources. However, + * near the end of bootstrap, one calls SettingsManager::useDefaults() to fetch + * and merge the defaults. + * + * Note: In a typical usage, there will only be one active domain and one + * active contact (each having its own bag) within a given request. However, + * in some edge-cases, you may need to work with multiple domains/contacts + * at the same time. + * * @see SettingsManagerTest */ class SettingsManager { @@ -48,12 +68,23 @@ class SettingsManager { /** * @var array + * Array(string $entity => array(string $settingName => mixed $value)). + * Ex: $mandatory['domain']['uploadDir']. */ protected $mandatory = NULL; + /** + * Whether to use defaults. + * + * @var bool + */ + protected $useDefaults = FALSE; + /** * @param \CRM_Utils_Cache_Interface $cache + * A semi-durable location to store metadata. * @param NULL|array $mandatory + * Ex: $mandatory['domain']['uploadDir']. */ public function __construct($cache, $mandatory = NULL) { $this->cache = $cache; @@ -61,7 +92,33 @@ class SettingsManager { } /** - * @param int $domainId + * Ensure that all defaults and mandatory values are included with + * all current and future bags. + * + * @return $this + */ + public function useDefaults() { + if (!$this->useDefaults) { + $this->useDefaults = TRUE; + + if (!empty($this->bagsByDomain)) { + foreach ($this->bagsByDomain as $bag) { + $bag->loadDefaults($this->getDefaults('domain')); + } + } + + if (!empty($this->bagsByContact)) { + foreach ($this->bagsByContact as $bag) { + $bag->loadDefaults($this->getDefaults('contact')); + } + } + } + + return $this; + } + + /** + * @param int|NULL $domainId * @return SettingsBag */ public function getBagByDomain($domainId) { @@ -70,18 +127,17 @@ class SettingsManager { } 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(); + $this->bagsByDomain[$domainId] = new SettingsBag($domainId, NULL); + $this->bagsByDomain[$domainId]->loadValues() + ->loadMandatory($this->getMandatory('domain')) + ->loadDefaults($this->getDefaults('domain')); } return $this->bagsByDomain[$domainId]; } /** - * @param int $domainId - * @param int $contactId + * @param int|NULL $domainId + * @param int|NULL $contactId * @return SettingsBag */ public function getBagByContact($domainId, $contactId) { @@ -91,11 +147,10 @@ class SettingsManager { $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(); + $this->bagsByContact[$key] = new SettingsBag($domainId, $contactId); + $this->bagsByContact[$key]->loadValues() + ->loadDefaults($this->getDefaults('contact')) + ->loadMandatory($this->getMandatory('contact')); } return $this->bagsByContact[$key]; } @@ -108,7 +163,11 @@ class SettingsManager { * @return array * Array(string $settingName => mixed $value). */ - public function getDefaults($entity) { + protected function getDefaults($entity) { + if (!$this->useDefaults) { + return array(); + } + $cacheKey = 'defaults:' . $entity; $defaults = $this->cache->get($cacheKey); if (!is_array($defaults)) { @@ -127,10 +186,13 @@ class SettingsManager { /** * Get a list of mandatory/overriden settings. * + * @param string $entity + * Ex: 'domain' or 'contact'. * @return array * Array(string $settingName => mixed $value). */ - public function getMandatory() { + protected function getMandatory($entity) { + // Prepare and cache list of all mandatory settings. if ($this->mandatory === NULL) { if (isset($GLOBALS['civicrm_setting'])) { $this->mandatory = self::parseMandatorySettings($GLOBALS['civicrm_setting']); @@ -139,11 +201,22 @@ class SettingsManager { $this->mandatory = array(); } } - return $this->mandatory; + + return \CRM_Utils_Array::value($entity, $this->mandatory, array()); } /** - * Parse + * Parse mandatory settings. + * + * In previous versions, settings were broken down into verbose+dynamic group names, e.g. + * + * $civicrm_settings['Foo Bar Preferences']['foo'] = 'bar'; + * + * We now simplify to two simple groups, 'domain' and 'contact'. + * + * $civicrm_settings['domain']['foo'] = 'bar'; + * + * However, the old groups are grand-fathered in as aliases. * * @param array $civicrm_setting * Ex: $civicrm_setting['Group Name']['field'] = 'value'. @@ -151,18 +224,36 @@ class SettingsManager { * @return array */ public static function parseMandatorySettings($civicrm_setting) { - $tmp = array(); + $rewriteGroups = array( + \CRM_Core_BAO_Setting::ADDRESS_STANDARDIZATION_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::CAMPAIGN_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::CONTRIBUTE_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::DEVELOPER_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::DIRECTORY_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::EVENT_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::LOCALIZATION_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::MAP_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::MEMBER_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::MULTISITE_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::PERSONAL_PREFERENCES_NAME => 'contact', + \CRM_Core_BAO_Setting::SEARCH_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME => 'domain', + \CRM_Core_BAO_Setting::URL_PREFERENCES_NAME => 'domain', + ); + if (is_array($civicrm_setting)) { - foreach ($civicrm_setting as $group => $settings) { - foreach ($settings as $k => $v) { - if ($v !== NULL) { - $tmp[$k] = $v; - } + foreach ($rewriteGroups as $oldGroup => $newGroup) { + if (!isset($civicrm_setting[$newGroup])) { + $civicrm_setting[$newGroup] = array(); + } + if (isset($civicrm_setting[$oldGroup])) { + $civicrm_setting[$newGroup] = array_merge($civicrm_setting[$oldGroup], $civicrm_setting[$newGroup]); + unset($civicrm_setting[$oldGroup]); } } - return $tmp; } - return $tmp; + return $civicrm_setting; } } diff --git a/tests/phpunit/Civi/Core/SettingsManagerTest.php b/tests/phpunit/Civi/Core/SettingsManagerTest.php index a3b09f27ab..dfeec81b09 100644 --- a/tests/phpunit/Civi/Core/SettingsManagerTest.php +++ b/tests/phpunit/Civi/Core/SettingsManagerTest.php @@ -28,10 +28,10 @@ class SettingsManagerTest extends \CiviUnitTestCase { 'c3' => 'gamma', ); $this->mandates = array( - 'foo' => array( + 'Mailing Preferences' => array( 'd3' => 'GAMMA!', ), - 'bar' => array( + 'contact' => array( 'c3' => 'GAMMA MAN!', ), ); @@ -44,7 +44,7 @@ class SettingsManagerTest extends \CiviUnitTestCase { $da = \CRM_Core_DAO::createTestObject('CRM_Core_DAO_Domain'); $db = \CRM_Core_DAO::createTestObject('CRM_Core_DAO_Domain'); - $manager = $this->createManager(); + $manager = $this->createManager()->useDefaults(); $daSettings = $manager->getBagByDomain($da->id); $daSettings->set('d1', 'un'); @@ -57,7 +57,7 @@ class SettingsManagerTest extends \CiviUnitTestCase { $this->assertEquals('beta', $dbSettings->get('d2')); $this->assertEquals('GAMMA!', $dbSettings->get('d3')); - $managerRedux = $this->createManager(); + $managerRedux = $this->createManager()->useDefaults(); $daSettingsRedux = $managerRedux->getBagByDomain($da->id); $this->assertEquals('un', $daSettingsRedux->get('d1')); @@ -73,7 +73,7 @@ class SettingsManagerTest extends \CiviUnitTestCase { $ca = \CRM_Core_DAO::createTestObject('CRM_Contact_DAO_Contact'); $cb = \CRM_Core_DAO::createTestObject('CRM_Contact_DAO_Contact'); - $manager = $this->createManager(); + $manager = $this->createManager()->useDefaults(); $caSettings = $manager->getBagByContact($domain->id, $ca->id); $caSettings->set('c1', 'un'); @@ -87,7 +87,7 @@ class SettingsManagerTest extends \CiviUnitTestCase { $this->assertEquals('GAMMA MAN!', $cbSettings->get('c3')); // Read settings from freshly initialized objects. - $manager = $this->createManager(); + $manager = $this->createManager()->useDefaults(); $caSettingsRedux = $manager->getBagByContact($domain->id, $ca->id); $this->assertEquals('un', $caSettingsRedux->get('c1')); @@ -99,7 +99,7 @@ class SettingsManagerTest extends \CiviUnitTestCase { $domain = \CRM_Core_DAO::createTestObject('CRM_Core_DAO_Domain'); $contact = \CRM_Core_DAO::createTestObject('CRM_Contact_DAO_Contact'); - $manager = $this->createManager(); + $manager = $this->createManager()->useDefaults(); // Store different values for the 'monkeywrench' setting on domain and contact @@ -112,7 +112,7 @@ class SettingsManagerTest extends \CiviUnitTestCase { $this->assertEquals('from contact', $contactSettings->get('monkeywrench')); // Read settings from freshly initialized objects. - $manager = $this->createManager(); + $manager = $this->createManager()->useDefaults(); $domainSettings = $manager->getBagByDomain($domain->id); $this->assertEquals('from domain', $domainSettings->get('monkeywrench')); @@ -123,7 +123,7 @@ class SettingsManagerTest extends \CiviUnitTestCase { public function testPaths() { $domain = \CRM_Core_DAO::createTestObject('CRM_Core_DAO_Domain'); - $manager = $this->createManager(); + $manager = $this->createManager()->useDefaults(); $settings = $manager->getBagByDomain($domain->id); $this->assertEquals('foo', $settings->get('myrelpath')); @@ -140,7 +140,7 @@ class SettingsManagerTest extends \CiviUnitTestCase { public function testUrl() { $domain = \CRM_Core_DAO::createTestObject('CRM_Core_DAO_Domain'); - $manager = $this->createManager(); + $manager = $this->createManager()->useDefaults(); $settings = $manager->getBagByDomain($domain->id); $this->assertEquals('sites/foo', $settings->get('myrelurl')); -- 2.25.1