diff --git a/src/Core/Config/Config.php b/src/Core/Config/Config.php index c08899292aa..0be26029ddd 100644 --- a/src/Core/Config/Config.php +++ b/src/Core/Config/Config.php @@ -2,375 +2,197 @@ namespace SilverStripe\Core\Config; -use SilverStripe\Core\Object; -use SilverStripe\Core\Manifest\ConfigStaticManifest; -use SilverStripe\Core\Manifest\ConfigManifest; -use micmania1\config\ConfigCollectionInterface; -use micmania1\config\MergeStrategy\Priority; -use Psr\Cache\CacheItemPoolInterface; -use SilverStripe\Dev\Deprecation; -use UnexpectedValueException; -use stdClass; +use BadMethodCallException; +use InvalidArgumentException; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; -class Config { +abstract class Config +{ + /** + * Config is returned uninherited + * + * @const + */ + const UNINHERITED = 0; - /** - * source options bitmask value - merge all parent configuration in as - * lowest priority. - * - * @const - */ - const INHERITED = 0; + /** + * @deprecated + * @const + */ + const INHERITED = 1; - /** - * source options bitmask value - only get configuration set for this - * specific class, not any of it's parents. - * - * @const - */ - const UNINHERITED = 1; + /** + * @deprecated + * @const + */ + const FIRST_SET = 2; - /** - * source options bitmask value - inherit, but stop on the first class - * that actually provides a value (event an empty value). - * - * @const - */ - const FIRST_SET = 2; + /** + * Source options bitmask value - do not use additional statics + * sources (such as extension) + * + * @const + */ + const EXCLUDE_EXTRA_SOURCES = 4; - /** - * @const source options bitmask value - do not use additional statics - * sources (such as extension) - */ - const EXCLUDE_EXTRA_SOURCES = 4; + /** + * @var Config + */ + protected static $instance; - /** - * @var Config - */ - protected static $instance; + /** + * Get the current active Config instance. + * + * In general use you will use this method to obtain the current Config + * instance. It assumes the config instance has already been set. + * + * @return Config + */ + public static function inst() + { + return self::$instance; + } - /** - * @var array - */ - protected $cache; + /** + * Return instance to modifiable config, if available + * + * @return MutableConfig + */ + public static function modify() + { + $instance = static::inst(); + if ($instance instanceof MutableConfig) { + return $instance; + } + throw new BadMethodCallException("Config must be nested before modification"); + } - /** - * @var CacheItemPoolInterface; - */ - protected $persistentCache; + /** + * Set the current active {@link Config} instance. + * + * {@link Config} objects should not normally be manually created. + * + * A use case for replacing the active configuration set would be for + * creating an isolated environment for unit tests. + * + * @param Config $instance New instance of Config to assign + * @return Config Reference to new active Config instance + */ + public static function set_instance($instance) + { + self::$instance = $instance; + return $instance; + } - /** - * In memory cache is used per-request to prevent unnecessary calls to cache - * which can have latency. - * - * @var array - */ - protected $memoryCache = []; + /** + * Initialise root config instance with default manifest + * + * @return Config + */ + public static function init() + { + if (static::inst()) { + throw new InvalidArgumentException("Cannot invoke init on config twice"); + } - /** - * @var Config - The config instance this one was copied from when - * Config::nest() was called. - */ - protected $nestedFrom = null; + // Create api-level cache + $cache = new FilesystemAdapter('configapicache'); + $inst = new ImmutableConfig($cache); + static::set_instance($inst); + return $inst; + } - /** - * Get the current active Config instance. - * - * In general use you will use this method to obtain the current Config - * instance. It assumes the config instance has already been set. - * - * @return Config - */ - public static function inst() { - return self::$instance; - } + /** + * Build nested config. + * All nested configs are mutable. + * + * @return MutableConfig + */ + abstract public function cloneNest(); - /** - * Set the current active {@link Config} instance. - * - * {@link Config} objects should not normally be manually created. - * - * A use case for replacing the active configuration set would be for - * creating an isolated environment for unit tests. - * - * @param Config $instance New instance of Config to assign - * @return Config Reference to new active Config instance - */ - public static function set_instance($instance) { - self::$instance = $instance; - return $instance; - } + /** + * Get parent config + * + * @return Config + */ + abstract public function getNestedFrom(); - /** - * Make the newly active {@link Config} be a copy of the current active - * {@link Config} instance. - * - * You can then make changes to the configuration by calling update and - * remove on the new value returned by {@link Config::inst()}, and then discard - * those changes later by calling unnest. - * - * @return self Reference to new active Config instance - */ - public static function nest() + /** + * Make the newly active {@link Config} be a copy of the current active + * {@link Config} instance. + * + * You can then make changes to the configuration by calling update and + * remove on the new value returned by {@link Config::inst()}, and then discard + * those changes later by calling unnest. + * + * @return self Reference to new active Config instance + */ + public static function nest() { - $manifest = ConfigLoader::instance()->getManifest()->getNest(); - ConfigLoader::instance()->pushManifest($manifest); - return static::inst(); - } + // Clone current config and nest + $new = self::inst()->cloneNest(); + return self::set_instance($new); + } - /** - * Change the active Config back to the Config instance the current active - * Config object was copied from. + /** + * Change the active Config back to the Config instance the current active + * Config object was copied from. * * @return self - */ - public static function unnest() + */ + public static function unnest() { - ConfigLoader::instance()->popManifest(); - if (!ConfigLoader::instance()->hasManifest()) { - user_error( - "Unable to unnest root Config, please make sure you don't have mis-matched nest/unnest", - E_USER_WARNING - ); - } - return self::inst(); - } - - /** - * Get an accessor that returns results by class by default. - * - * Shouldn't be overridden, since there might be many Config_ForClass instances already held in the wild. Each - * Config_ForClass instance asks the current_instance of Config for the actual result, so override that instead - * - * @param $class - * @return Config_ForClass - */ - public function forClass($class) { - return new Config_ForClass($class); - } - - /** - * Get the value of a config property class.name - * - * @var string $class - * @var string $name - * @var int $sourceOptions - * - * @return mixed - */ - public function get($class, $name = null, $sourceOptions = 0) { - if(($sourceOptions & self::FIRST_SET) == self::FIRST_SET) { - throw new \Exception(sprintf('Using FIRST_SET on %s.%s', $class, $name)); - } - - // Have we got a cached value? Use it if so - $key = md5(strtolower($class.'-'.$sourceOptions)); - - if(isset($this->memoryCache[$key])) { - $classConfig = $this->memoryCache[$key]; - - // If no name is passed, return all config - if(is_null($name)) { - return $this->memoryCache[$key]; - } - - return isset($classConfig[$name]) ? $classConfig[$name] : null; - } - - $item = $this->persistentCache->getItem($key); - if(!$item->isHit()) { - // Go and get entire class config (uncached) - $classConfig = $this->getClassConfig($class, $sourceOptions); - - $item->set($classConfig); - $this->persistentCache->saveDeferred($item); - } - - $this->memoryCache[$key] = $item->get(); - - // If no name is passed, we return all config - if(is_null($name)) { - return $this->memoryCache[$key]; - } - - // Return only the config for the given name - return isset($this->memoryCache[$key][$name]) ? - $this->memoryCache[$key][$name] - : null; - } - - /** - * Get the class config for the given class with the given source options - * - * @param string $class - * @param int $sourceOtions - * - * @return array|null - */ - public function getClassConfig($class, $sourceOptions = 0) { - $classConfig = $this->collection->get($class); - - if($this->shouldApplyExtraConfig($class, $sourceOptions)) { - $this->applyExtraConfig($class, $sourceOptions, $classConfig); - } - - if($this->shouldInheritConfig($class, $sourceOptions, $classConfig)) { - $this->applyInheritedConfig($class, $sourceOptions, $classConfig); - } - - return $classConfig; - } - - /** - * Applied config to a class from its extensions - * - * @param string $class - * @param int $sourceOptions - * @param mixed $classConfig - */ - protected function applyExtraConfig($class, $sourceOptions, &$classConfig) { - $extraSources = Object::get_extra_config_sources($class); - if(empty($extraSources)) { - return; - } - - $priority = new Priority; - foreach($extraSources as $source) { - if(is_string($source)) { - $source = $this->getClassConfig( - $source, - self::UNINHERITED | self::EXCLUDE_EXTRA_SOURCES - ); - } - - if(is_array($source)) { - if(is_null($classConfig) || !is_array($classConfig)) { - $classConfig = $source; - continue; - } - - $classConfig = $priority->mergeArray($classConfig, $source); - } else if (!is_null($source)) { - $classConfig = $source; - } - } - } - - /** - * Adds the inherited config to a class config - * - * @param string $class - * @param int $sourceOptions - * @param mixed $classConfig - */ - protected function applyInheritedConfig($class, $sourceOptions, &$classConfig) { - $parent = get_parent_class($class); - if ($parent) { - $parentConfig = $this->getClassConfig($parent, $sourceOptions); - - if(is_array($classConfig) && is_array($parentConfig)) { - $strategy = new Priority; - $classConfig = $strategy->mergeArray($classConfig, $parentConfig); - } else if(is_null($classConfig) && !is_null($parentConfig)) { - $classConfig = $parentConfig; - } - } - } - - /** - * A check to test if we should include extra config (data extensions) - * - * @param string $class - * @param int $sourceOptions - * @param mixed $result - * - * @return boolean - */ - protected function shouldApplyExtraConfig($class, $sourceOptions) { - if($class instanceof Extension) { - return false; - } - - return ($sourceOptions & self::EXCLUDE_EXTRA_SOURCES) != self::EXCLUDE_EXTRA_SOURCES; - } - - /** - * A check to test if we should inherit config from parent classes for the given - * source option. - * - * @param string $class - * @param int $sourceOptions - * @param mixed $classConfig - * - * @return boolean - */ - protected function shouldInheritConfig($class, $sourceOptions, &$classConfig) { - return class_exists($class) - && ($sourceOptions & self::UNINHERITED) != self::UNINHERITED - && (($sourceOptions & self::FIRST_SET) != self::FIRST_SET || $classConfig === null); - } + $parent = static::inst()->getNestedFrom(); + if ($parent) { + static::set_instance($parent); + } else { + user_error( + "Unable to unnest root Config, please make sure you don't have mis-matched nest/unnest", + E_USER_WARNING + ); + } + return self::inst(); + } - public function update($class, $name, $val) { - Deprecation::notice('5.0', 'Use Config::inst()->merge() to merge config'); - return $this->merge($class, $name, $val); - } /** - * Remove config key + * Get an accessor that returns results by class by default. + * + * Shouldn't be overridden, since there might be many Config_ForClass instances already held in the wild. Each + * Config_ForClass instance asks the current_instance of Config for the actual result, so override that instead * * @param string $class - * @param string $name - * @return $this + * @return Config_ForClass */ - public function remove($class, $name) { - $manifest = ConfigLoader::instance()->getManifest(); - $updated = []; - if ($manifest->exists($class)) { - $updated = $manifest->get($class); - } - unset($updated[$name]); - $manifest->set($class, $updated); - return $this; - } + public function forClass($class) + { + return new Config_ForClass($class); + } + /** - * Replace a config value + * Validate any deprecated options * - * @param string $class - * @param string $name - * @param mixed $val - * @return $this + * @param int $sourceOptions + * @throws InvalidArgumentException */ - public function set($class, $name, $val) + protected function validateSourceOption($sourceOptions) { - $manifest = ConfigLoader::instance()->getManifest(); - $updated = []; - if ($manifest->exists($class)) { - $updated = $manifest->get($class); + if ($sourceOptions & self::FIRST_SET) { + throw new InvalidArgumentException('FIRST_SET is no longer supported'); + } + if ($sourceOptions & self::INHERITED) { + throw new InvalidArgumentException('INHERITED is no longer supported'); } - $updated[$name] = $val; - $manifest->set($class, $updated); - return $this; } /** - * Merges the existing config with a new value. - * If one or both values are not array, the result will be a replacement() + * Get the value of a config property class.name * * @param string $class * @param string $name - * @param mixed $value - * @return $this + * @var int $sourceOptions Either UNINHERTED or EXCLUDE_EXTRA_SOURCES constants + * @return mixed */ - public function merge($class, $name, $value) - { - $manifest = ConfigLoader::instance()->getManifest(); - $strategy = new Priority; - $merge = [ - $class => ['value' => [$name => $value]] - ]; - $strategy->merge($merge, $manifest); - return $this; - } - + abstract public function get($class, $name = null, $sourceOptions = 0); } diff --git a/src/Core/Config/ConfigLoader.php b/src/Core/Config/ConfigLoader.php index e94a846b493..cb25eaf38ad 100644 --- a/src/Core/Config/ConfigLoader.php +++ b/src/Core/Config/ConfigLoader.php @@ -65,4 +65,16 @@ public function popManifest() { return array_pop($this->manifests); } + + /** + * Nest the current manifest + * + * @return ConfigCollectionInterface + */ + public function nest() + { + $manifest = $this->getManifest()->nest(); + $this->pushManifest($manifest); + return $manifest; + } } diff --git a/src/Core/Config/Config_ForClass.php b/src/Core/Config/Config_ForClass.php index 58cbf652085..431b604d6a7 100644 --- a/src/Core/Config/Config_ForClass.php +++ b/src/Core/Config/Config_ForClass.php @@ -2,9 +2,10 @@ namespace SilverStripe\Core\Config; +use SilverStripe\Dev\Deprecation; + class Config_ForClass { - /** * @var string $class */ @@ -33,19 +34,45 @@ public function __get($name) */ public function __set($name, $val) { - $this->update($name, $val); + $this->set($name, $val); } /** * Explicit pass-through to Config::update() * * @param string $name - * @param mixed $val + * @param mixed $value + * @return $this + */ + public function update($name, $value) + { + Deprecation::notice('5.0', 'Use merge() instead'); + return $this->merge($name, $value); + } + + /** + * Merge a given config + * + * @param string $name + * @param mixed $value + * @return $this + */ + public function merge($name, $value) + { + Config::modify()->merge($this->class, $name, $value); + return $this; + } + + /** + * Replace config value + * + * @param string $name + * @param mixed $value * @return $this */ - public function update($name, $val) + public function set($name, $value) { - Config::inst()->update($this->class, $name, $val); + Config::modify()->set($this->class, $name, $value); return $this; } @@ -77,7 +104,7 @@ public function get($name, $sourceOptions = 0) */ public function remove($name) { - Config::inst()->remove($this->class, $name); + Config::modify()->remove($this->class, $name); return $this; } diff --git a/src/Core/Config/Configurable.php b/src/Core/Config/Configurable.php index 8e1ad857430..52a05883bd9 100644 --- a/src/Core/Config/Configurable.php +++ b/src/Core/Config/Configurable.php @@ -39,7 +39,7 @@ public function stat($name) */ public function set_stat($name, $value) { - Config::inst()->update(get_class($this), $name, $value); + Config::modify()->merge(get_class($this), $name, $value); } /** diff --git a/src/Core/Config/ImmutableConfig.php b/src/Core/Config/ImmutableConfig.php new file mode 100644 index 00000000000..a0f605da5b3 --- /dev/null +++ b/src/Core/Config/ImmutableConfig.php @@ -0,0 +1,238 @@ +persistentCache = $persistentCache; + } + + /** + * Get the value of a config property class.name + * + * @param string $class + * @param string $name + * @var int $sourceOptions + * @return mixed + */ + public function get($class, $name = null, $sourceOptions = 0) + { + $this->validateSourceOption($sourceOptions); + + // Load config from cache + $classConfig = $this->getCachedClassConfig($class, $sourceOptions, $cacheItem); + if (!isset($classConfig)) { + $classConfig = $this->loadClassConfig($class, $sourceOptions, $cacheItem); + if (!isset($classConfig)) { + return null; + } + } + + // Return either name, or whole-class config + if ($name) { + return isset($classConfig[$name]) ? $classConfig[$name] : null; + } + return $classConfig; + } + + /** + * Load class config from manifest and apply local modifications. + * Applies these modifications to the cache as well as necessary. + * + * @param string $class + * @param int $sourceOptions + * @param CacheItemInterface $cacheItem + * @return array + */ + protected function loadClassConfig($class, $sourceOptions, $cacheItem) + { + // Load raw cache + $classConfig = $this->getClassConfig($class, $sourceOptions); + + // Save raw config to persistant cache + $cacheItem->set($classConfig); + $this->persistentCache->saveDeferred($cacheItem); + + // Save in local cache + if (!isset($this->memoryCache[$class])) { + $this->memoryCache[$class] = []; + } + $this->memoryCache[$class][$sourceOptions] = $classConfig; + return $classConfig; + } + + /** + * Load config for a given class from cache. + * This config result will include local modifications(). + * Additionally this method will hydrate local cache from persistent + * cache as-needed. + * + * @param string $class + * @param int $sourceOptions + * @param CacheItemInterface $cacheItem Cache item which should be used + * to write back to persistent store + * @return array|null Cached config (with local modifications) or null if not cached + */ + protected function getCachedClassConfig($class, $sourceOptions, CacheItemInterface &$cacheItem) + { + $cacheItem = null; + + // First hit from in-memory cache + if (isset($this->memoryCache[$class][$sourceOptions])) { + return $this->memoryCache[$class][$sourceOptions]; + } + + // Get from persistent cache + $key = $this->getClassConfigCacheKey($class, $sourceOptions); + $cacheItem = $this->persistentCache->getItem($key); + if (!$cacheItem->isHit()) { + return null; + } + + // Load from persistent -> local cache + $classConfig = $cacheItem->get(); + if (!isset($this->memoryCache[$class])) { + $this->memoryCache[$class] = []; + } + $this->memoryCache[$class][$sourceOptions] = $classConfig; + return $classConfig; + } + + /** + * Get the class config for the given class with the given source options + * + * @param string $class + * @param int $sourceOptions + * @return array + */ + protected function getClassConfig($class, $sourceOptions = 0) + { + $classConfig = ConfigLoader::instance()->getManifest()->get($class); + + if ($this->shouldApplyExtraConfig($class, $sourceOptions)) { + $this->applyExtraConfig($class, $sourceOptions, $classConfig); + } + + return $classConfig; + } + + /** + * Applied config to a class from its extensions + * + * @param string $class + * @param int $sourceOptions + * @param mixed $classConfig + */ + protected function applyExtraConfig($class, $sourceOptions, &$classConfig) + { + $extraSources = Object::get_extra_config_sources($class); + if (empty($extraSources)) { + return; + } + + $priority = new Priority; + foreach ($extraSources as $source) { + if (is_string($source)) { + $source = $this->getClassConfig( + $source, + self::UNINHERITED | self::EXCLUDE_EXTRA_SOURCES + ); + } + + if (is_array($source)) { + if (is_null($classConfig) || !is_array($classConfig)) { + $classConfig = $source; + continue; + } + + $classConfig = $priority->mergeArray($classConfig, $source); + } elseif (!is_null($source)) { + $classConfig = $source; + } + } + } + + /** + * A check to test if we should include extra config (data extensions) + * + * @param string $class + * @param int $sourceOptions + * @return bool + */ + protected function shouldApplyExtraConfig($class, $sourceOptions) + { + if (is_a($class, Extension::class, true)) { + return false; + } + + return ($sourceOptions & self::EXCLUDE_EXTRA_SOURCES) != self::EXCLUDE_EXTRA_SOURCES; + } + + /** + * Get cache key + * + * @param string $class + * @param int $sourceOptions + * @return string + */ + protected function getClassConfigCacheKey($class, $sourceOptions) + { + $parts = [strtolower($class), $sourceOptions]; + return md5(implode('-', $parts)); + } + + /** + * Build nested config + * + * @return MutableConfig + */ + public function cloneNest() + { + // Nested child of immutable config is mutable config + return new MutableConfig($this); + } + + /** + * Get parent config + * + * @return Config + */ + public function getNestedFrom() + { + // Immutable config has no parent + return null; + } +} diff --git a/src/Core/Config/MutableConfig.php b/src/Core/Config/MutableConfig.php new file mode 100644 index 00000000000..de7945b33cc --- /dev/null +++ b/src/Core/Config/MutableConfig.php @@ -0,0 +1,361 @@ + [ + * $name => [ + * 'type' => MERGE | SET | REMOVE + * 'value' => $value // ignored for REMOVE + * ] + * ] + * ] + * + * @var array + */ + protected $modifications = array(); + + public function __construct(ImmutableConfig $parent) + { + $this->rootConfig = $parent; + $this->nestedFrom = $parent; + } + + /** + * @return Config + */ + public function getNestedFrom() + { + return $this->nestedFrom; + } + + /** + * Set nested from + * + * @param Config $config + * @return $this + */ + protected function setNestedFrom(Config $config) + { + $this->nestedFrom = $config; + return $this; + } + + public function update($class, $name, $val) + { + Deprecation::notice( + '5.0', + 'Use Config::modify()->merge() to merge config, or Config::modify()->set() to replace' + ); + return $this->merge($class, $name, $val); + } + + /** + * Remove config key. + * Note: This will NOT affect the config of subclasses of $class + * + * @param string $class + * @param string $name + * @return $this + */ + public function remove($class, $name) + { + // Apply remove override + if (!isset($this->modifications[$class])) { + $this->modifications[$class] = []; + } + $this->modifications[$class][$name] = [ + 'type' => self::REMOVE, + ]; + + // Remove $name from all cached classes + if (isset($this->memoryCache[$class])) { + foreach ($this->memoryCache[$class] as $option => $data) { + unset($this->memoryCache[$class][$option][$name]); + } + } + return $this; + } + + /** + * Replace a config value. + * + * Note: This will NOT affect the config of subclasses of $class + * + * @param string $class + * @param string $name + * @param mixed $value + * @return $this + */ + public function set($class, $name, $value) + { + // Apply remove override + if (!isset($this->modifications[$class])) { + $this->modifications[$class] = []; + } + $this->modifications[$class][$name] = [ + 'type' => self::SET, + 'value' => $value, + ]; + + // Overwrite this value for any cached configs + if (isset($this->memoryCache[$class])) { + foreach ($this->memoryCache[$class] as $option => $data) { + $this->memoryCache[$class][$option][$name] = $value; + } + } + return $this; + } + + /** + * Merges the existing config with a new value. + * If one or both values are not array, the result will be a replacement() + * Note: This will NOT affect the config of subclasses of $class + * + * @param string $class + * @param string $name + * @param mixed $value + * @return $this + */ + public function merge($class, $name, $value) + { + // Apply remove override + if (!isset($this->modifications[$class])) { + $this->modifications[$class] = []; + } + $override = [ + 'type' => self::MERGE, + 'value' => $value, + ]; + + // Merge override with prior value + if (isset($this->modifications[$class][$name])) { + switch ($this->modifications[$class][$name]['type']) { + case self::REMOVE: + // remove + merge = replace + $override['type'] = self::SET; + break; + case self::SET: + // replace + merge = replace + $priority = new Priority(); + $override['type'] = self::SET; + $override['value'] = $priority->mergeArray( + $value, + $this->modifications[$class][$name]['value'] + ); + break; + case self::MERGE: + // merge + merge = merge + $priority = new Priority(); + $override['value'] = $priority->mergeArray( + $value, + $this->modifications[$class][$name]['value'] + ); + break; + default: + throw new LogicException("Invalid override : " . $this->modifications[$class][$name]['type']); + } + } + + // Save override + $this->modifications[$class][$name] = $override; + + // Overwrite this value for any cached configs + if (isset($this->memoryCache[$class])) { + foreach ($this->memoryCache[$class] as $option => $data) { + $this->memoryCache[$class][$option] = $this->applyModification($name, $override, $data); + } + } + return $this; + } + + /** + * Build nested config + * + * @return MutableConfig + */ + public function cloneNest() + { + $new = clone $this; + $new->setNestedFrom($this); + return $new; + } + + /** + * Get the value of a config property class.name + * + * @param string $class + * @param string $name + * @var int $sourceOptions Either UNINHERTED or EXCLUDE_EXTRA_SOURCES constants + * @return mixed + */ + public function get($class, $name = null, $sourceOptions = 0) + { + $this->validateSourceOption($sourceOptions); + + // Load config from cache + $classConfig = $this->getCachedClassConfig($class, $sourceOptions); + if (!isset($classConfig)) { + $classConfig = $this->loadClassConfig($class, $sourceOptions); + if (!isset($classConfig)) { + return null; + } + } + + // Return either name, or whole-class config + if ($name) { + return isset($classConfig[$name]) ? $classConfig[$name] : null; + } + return $classConfig; + } + + /** + * Get cached version of this config + * + * @param string $class + * @param int $sourceOptions + * @return array|null + */ + protected function getCachedClassConfig($class, $sourceOptions) + { + // Mutable config changes are recorded only in-memory + if (isset($this->memoryCache[$class][$sourceOptions])) { + return $this->memoryCache[$class][$sourceOptions]; + } + return null; + } + + /** + * @param string $class + * @param int $sourceOptions + * @return array|null + */ + protected function loadClassConfig($class, $sourceOptions) + { + // Fail over to loading from back-end + $classConfig = $this->rootConfig->get($class, null, $sourceOptions); + if (!isset($classConfig)) { + return null; // Null means no class, and cannot be modified + } + + // Modify config + $modifiedConfig = $this->applyModifications($class, $classConfig); + + // Load from persistent -> local cache + if (!isset($this->memoryCache[$class])) { + $this->memoryCache[$class] = []; + } + $this->memoryCache[$class][$sourceOptions] = $modifiedConfig; + return $modifiedConfig; + } + + /** + * Apply all mutations to the given config for this class + * + * @param string $class + * @param array $config Un-mutated config + * @return array Modified config + */ + protected function applyModifications($class, $config) + { + // Check for overrides + if (empty($this->modifications[$class])) { + return $config; + } + + foreach ($this->modifications[$class] as $name => $modification) { + $config = $this->applyModification($name, $modification, $config); + } + return $config; + } + + /** + * Apply a single modification + * + * @param string $name Property name being modified + * @param array $modification + * @param array $config + * @return array + */ + protected function applyModification($name, $modification, $config) + { + switch ($modification['type']) { + case self::REMOVE: + unset($config[$name]); + return $config; + case self::SET: + $config[$name] = $modification['value']; + return $config; + case self::MERGE: + // Merge without source is same as replace + if (!array_key_exists($name, $config)) { + $config[$name] = $modification['value']; + return $config; + } + + // Merge using priority + $priority = new Priority(); + $config[$name] = $priority->mergeArray( + $modification['value'], + $config[$name] + ); + return $config; + default: + throw new BadMethodCallException("Invalid override : " . $modification['type']); + } + } +} diff --git a/src/Core/Core.php b/src/Core/Core.php index 6ce90fb13b5..7fa20ccb028 100644 --- a/src/Core/Core.php +++ b/src/Core/Core.php @@ -112,6 +112,7 @@ function () { } ); ConfigLoader::instance()->pushManifest($configManifest); +Config::init(); // Load template manifest SilverStripe\View\ThemeResourceLoader::instance()->addSet('$default', new SilverStripe\View\ThemeManifest( diff --git a/src/Core/Manifest/ClassManifest.php b/src/Core/Manifest/ClassManifest.php index 6a6acd1fe0a..b2192ef9359 100644 --- a/src/Core/Manifest/ClassManifest.php +++ b/src/Core/Manifest/ClassManifest.php @@ -386,8 +386,8 @@ public function getOwnerModule($class) */ protected function setDefaults() { - $this->classes['sstemplateparser'] = FRAMEWORK_PATH.'/View/SSTemplateParser.php'; - $this->classes['sstemplateparseexception'] = FRAMEWORK_PATH.'/View/SSTemplateParseException.php'; + $this->classes['sstemplateparser'] = FRAMEWORK_PATH.'/src/View/SSTemplateParser.php'; + $this->classes['sstemplateparseexception'] = FRAMEWORK_PATH.'/src/View/SSTemplateParseException.php'; } /**