From e6aa89460d21f39df24e793b6eeee4be1e2fe5f8 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Fri, 10 Feb 2017 16:56:04 +1300 Subject: [PATCH] Update to use new config backend --- src/Core/Config/Config.php | 1076 ++++++++++-------------------- src/Core/Config/ConfigLoader.php | 68 ++ src/Core/Core.php | 37 +- 3 files changed, 441 insertions(+), 740 deletions(-) create mode 100644 src/Core/Config/ConfigLoader.php diff --git a/src/Core/Config/Config.php b/src/Core/Config/Config.php index 9cc3d5b3f96..c08899292aa 100644 --- a/src/Core/Config/Config.php +++ b/src/Core/Config/Config.php @@ -5,760 +5,372 @@ 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; -/** - * The configuration system works like this: - * - * Each class has a set of named properties - * - * Each named property can contain either - * - * - An array - * - A non-array value - * - * If the value is an array, each value in the array may also be one of those - * three types. - * - * A property can have a value specified in multiple locations, each of which - * have a hard coded or explicit priority. We combine all these values together - * into a "composite" value using rules that depend on the priority order of - * the locations to give the final value, using these rules: - * - * - If the value is an array, each array is added to the _beginning_ of the - * composite array in ascending priority order. If a higher priority item has - * a non-integer key which is the same as a lower priority item, the value of - * those items is merged using these same rules, and the result of the merge - * is located in the same location the higher priority item would be if there - * was no key clash. Other than in this key-clash situation, within the - * particular array, order is preserved. - * - * - If the value is not an array, the highest priority value is used without - * any attempt to merge. - * - * It is an error to have mixed types of the same named property in different - * locations (but an error will not necessarily be raised due to optimizations - * in the lookup code). - * - * The exception to this is "false-ish" values - empty arrays, empty strings, - * etc. When merging a non-false-ish value with a false-ish value, the result - * will be the non-false-ish value regardless of priority. When merging two - * false-ish values the result will be the higher priority false-ish value. - * - * The locations that configuration values are taken from in highest -> lowest - * priority order. - * - * - Any values set via a call to Config#update. - * - * - The configuration values taken from the YAML files in _config directories - * (internally sorted in before / after order, where the item that is latest - * is highest priority). - * - * - Any static set on an "additional static source" class (such as an - * extension) named the same as the name of the property. - * - * - Any static set on the class named the same as the name of the property. - * - * - The composite configuration value of the parent class of this class. - * - * At some of these levels you can also set masks. These remove values from the - * composite value at their priority point rather than add. They are much - * simpler. They consist of a list of key / value pairs. When applied against - * the current composite value: - * - * - If the composite value is a sequential array, any member of that array - * that matches any value in the mask is removed. - * - * - If the composite value is an associative array, any member of that array - * that matches both the key and value of any pair in the mask is removed. - * - * - If the composite value is not an array, if that value matches any value - * in the mask it is removed. - */ -class Config -{ - - /** - * A marker instance for the "anything" singleton value. Don't access - * directly, even in-class, always use self::anything() - * - * @var Object - */ - private static $_anything = null; - - /** - * @var bool - */ - protected $collectConfigPHPSettings; - - /** - * @var bool - */ - protected $configPHPIsSafe; - - /** - * Get a marker class instance that is used to do a "remove anything with - * this key" by adding $key => Config::anything() to the suppress array - * - * @return Object - */ - public static function anything() - { - if (self::$_anything === null) { - self::$_anything = new stdClass(); - } - - return self::$_anything; - } - - // -- Source options bitmask -- - - /** - * source options bitmask value - merge all parent configuration in as - * lowest priority. - * - * @const - */ - const INHERITED = 0; - - /** - * source options bitmask value - only get configuration set for this - * specific class, not any of it's parents. - * - * @const - */ - const UNINHERITED = 1; - - /** - * 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; - - /** - * @const source options bitmask value - do not use additional statics - * sources (such as extension) - */ - const EXCLUDE_EXTRA_SOURCES = 4; - - // -- get_value_type response enum -- - - /** - * Return flag for get_value_type indicating value is a scalar (or really - * just not-an-array, at least ATM) - * - * @const - */ - const ISNT_ARRAY = 1; - - /** - * Return flag for get_value_type indicating value is an array. - * @const - */ - const IS_ARRAY = 2; - - /** - * Get whether the value is an array or not. Used to be more complicated, - * but still nice sugar to have an enum to compare and not just a true / - * false value. - * - * @param mixed $val The value - * - * @return int One of ISNT_ARRAY or IS_ARRAY - */ - protected static function get_value_type($val) - { - if (is_array($val)) { - return self::IS_ARRAY; - } - - return self::ISNT_ARRAY; - } - - /** - * What to do if there's a type mismatch. - * - * @throws UnexpectedValueException - */ - protected static function type_mismatch() - { - throw new UnexpectedValueException('Type mismatch in configuration. All values for a particular property must' - . ' contain the same type (or no value at all).'); - } - - /** - * @todo If we can, replace next static & static methods with DI once that's in - */ - protected static $instance; - - /** - * Get the current active Config instance. - * - * Configs should not normally be manually created. - * - * In general use you will use this method to obtain the current Config - * instance. - * - * @return Config - */ - public static function inst() - { - if (!self::$instance) { - self::$instance = new Config(); - } - - return self::$instance; - } - - /** - * 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; - } - - /** - * 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 Config Reference to new active Config instance - */ - public static function nest() - { - $current = self::$instance; - - $new = clone $current; - $new->nestedFrom = $current; - return self::set_instance($new); - } - - /** - * Change the active Config back to the Config instance the current active - * Config object was copied from. - * - * @return Config Reference to new active Config instance - */ - public static function unnest() - { - if (self::inst()->nestedFrom) { - self::set_instance(self::inst()->nestedFrom); - } 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(); - } - - /** - * @var array - */ - protected $cache; - - /** - * Each copy of the Config object need's it's own cache, so changes don't - * leak through to other instances. - */ - public function __construct() - { - $this->cache = new Config_MemCache(); - } - - public function __clone() - { - $this->cache = clone $this->cache; - } - - /** - * @var Config - The config instance this one was copied from when - * Config::nest() was called. - */ - protected $nestedFrom = null; - - /** - * @var array - Array of arrays. Each member is an nested array keyed as - * $class => $name => $value, where value is a config value to treat as - * the highest priority item. - */ - protected $overrides = array(); - - /** - * @var array $suppresses Array of arrays. Each member is an nested array - * keyed as $class => $name => $value, where value is a config value suppress - * from any lower priority item. - */ - protected $suppresses = array(); - - /** - * @var array - */ - protected $staticManifests = array(); - - /** - * @param ConfigStaticManifest $manifest - */ - public function pushConfigStaticManifest(ConfigStaticManifest $manifest) +class Config { + + /** + * source options bitmask value - merge all parent configuration in as + * lowest priority. + * + * @const + */ + const INHERITED = 0; + + /** + * source options bitmask value - only get configuration set for this + * specific class, not any of it's parents. + * + * @const + */ + const UNINHERITED = 1; + + /** + * 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; + + /** + * @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 array + */ + protected $cache; + + /** + * @var CacheItemPoolInterface; + */ + protected $persistentCache; + + /** + * In memory cache is used per-request to prevent unnecessary calls to cache + * which can have latency. + * + * @var array + */ + protected $memoryCache = []; + + /** + * @var Config - The config instance this one was copied from when + * Config::nest() was called. + */ + protected $nestedFrom = null; + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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() { - array_unshift($this->staticManifests, $manifest); - - $this->cache->clean(); - } - - /** @var [array] - The list of settings pulled from config files to search through */ - protected $manifests = array(); - - /** - * Add another manifest to the list of config manifests to search through. - * - * WARNING: Config manifests to not merge entries, and do not solve before/after rules inter-manifest - - * instead, the last manifest to be added always wins - * @param ConfigManifest $manifest - */ - public function pushConfigYamlManifest(ConfigManifest $manifest) + $manifest = ConfigLoader::instance()->getManifest()->getNest(); + ConfigLoader::instance()->pushManifest($manifest); + return static::inst(); + } + + /** + * Change the active Config back to the Config instance the current active + * Config object was copied from. + * + * @return self + */ + public static function unnest() { - array_unshift($this->manifests, $manifest); - - // Now that we've got another yaml config manifest we need to clean the cache - $this->cache->clean(); - // We also need to clean the cache if the manifest's calculated config values change - $manifest->registerChangeCallback(array($this->cache, 'clean')); - - // @todo: Do anything with these. They're for caching after config.php has executed - $this->collectConfigPHPSettings = true; - $this->configPHPIsSafe = false; - - $manifest->activateConfig(); - - $this->collectConfigPHPSettings = false; - } - - /** @var [Config_ForClass] - The list of Config_ForClass instances, keyed off class */ - static protected $for_class_instances = array(); + 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); + } + + public function update($class, $name, $val) { + Deprecation::notice('5.0', 'Use Config::inst()->merge() to merge config'); + return $this->merge($class, $name, $val); + } /** - * Get an accessor that returns results by class by default. + * Remove config key * - * 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 + * @param string $class + * @param string $name + * @return $this */ - public function forClass($class) - { - if (isset(self::$for_class_instances[$class])) { - return self::$for_class_instances[$class]; - } else { - return self::$for_class_instances[$class] = new Config_ForClass($class); + 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; + } /** - * Merge a lower priority associative array into an existing higher priority associative array, as per the class - * docblock rules + * Replace a config value * - * It is assumed you've already checked that you've got two associative arrays, not scalars or sequential arrays - * - * @param $dest array - The existing high priority associative array - * @param $src array - The low priority associative array to merge in + * @param string $class + * @param string $name + * @param mixed $val + * @return $this */ - public static function merge_array_low_into_high(&$dest, $src) + public function set($class, $name, $val) { - foreach ($src as $k => $v) { - if (!$v) { - continue; - } elseif (is_int($k)) { - $dest[] = $v; - } elseif (isset($dest[$k])) { - $newType = self::get_value_type($v); - $currentType = self::get_value_type($dest[$k]); - - // Throw error if types don't match - if ($currentType !== $newType) { - self::type_mismatch(); - } - - if ($currentType == self::IS_ARRAY) { - self::merge_array_low_into_high($dest[$k], $v); - } else { - continue; - } - } else { - $dest[$k] = $v; - } + $manifest = ConfigLoader::instance()->getManifest(); + $updated = []; + if ($manifest->exists($class)) { + $updated = $manifest->get($class); } + $updated[$name] = $val; + $manifest->set($class, $updated); + return $this; } /** - * Merge a higher priority assocative array into an existing lower priority associative array, as per the class - * docblock rules. - * - * Much more expensive that the other way around, as there's no way to insert an associative k/v pair into an - * array at the top of the array + * Merges the existing config with a new value. + * If one or both values are not array, the result will be a replacement() * - * @static - * @param $dest array - The existing low priority associative array - * @param $src array - The high priority array to merge in + * @param string $class + * @param string $name + * @param mixed $value + * @return $this */ - public static function merge_array_high_into_low(&$dest, $src) + public function merge($class, $name, $value) { - $res = $src; - self::merge_array_low_into_high($res, $dest); - $dest = $res; + $manifest = ConfigLoader::instance()->getManifest(); + $strategy = new Priority; + $merge = [ + $class => ['value' => [$name => $value]] + ]; + $strategy->merge($merge, $manifest); + return $this; } - public static function merge_high_into_low(&$result, $value) - { - $newType = self::get_value_type($value); - - if (!$result) { - $result = $value; - } else { - $currentType = self::get_value_type($result); - if ($currentType !== $newType) { - self::type_mismatch(); - } - - if ($currentType == self::ISNT_ARRAY) { - $result = $value; - } else { - self::merge_array_high_into_low($result, $value); - } - } - } - - public static function merge_low_into_high(&$result, $value, $suppress) - { - $newType = self::get_value_type($value); - - if ($suppress) { - if ($newType == self::IS_ARRAY) { - $value = self::filter_array_by_suppress_array($value, $suppress); - if (!$value) { - return; - } - } else { - if (self::check_value_contained_in_suppress_array($value, $suppress)) { - return; - } - } - } - - if (!$result) { - $result = $value; - } else { - $currentType = self::get_value_type($result); - if ($currentType !== $newType) { - self::type_mismatch(); - } - - if ($currentType == self::ISNT_ARRAY) { - return; // PASS - } else { - self::merge_array_low_into_high($result, $value); - } - } - } - - public static function check_value_contained_in_suppress_array($v, $suppresses) - { - foreach ($suppresses as $suppress) { - list($sk, $sv) = $suppress; - if ($sv === self::anything() || $v == $sv) { - return true; - } - } - return false; - } - - protected static function check_key_or_value_contained_in_suppress_array($k, $v, $suppresses) - { - foreach ($suppresses as $suppress) { - list($sk, $sv) = $suppress; - if (($sk === self::anything() || $k == $sk) && ($sv === self::anything() || $v == $sv)) { - return true; - } - } - return false; - } - - protected static function filter_array_by_suppress_array($array, $suppress) - { - $res = array(); - - foreach ($array as $k => $v) { - $suppressed = self::check_key_or_value_contained_in_suppress_array($k, $v, $suppress); - - if (!$suppressed) { - if (is_numeric($k)) { - $res[] = $v; - } else { - $res[$k] = $v; - } - } - } - - return $res; - } - - protected $extraConfigSources = array(); - - public function extraConfigSourcesChanged($class) - { - unset($this->extraConfigSources[$class]); - $this->cache->clean("__{$class}"); - } - - protected function getUncached($class, $name, $sourceOptions, &$result, $suppress, &$tags) - { - $tags[] = "__{$class}"; - $tags[] = "__{$class}__{$name}"; - - // If result is already not something to merge into, just return it - if ($result !== null && !is_array($result)) { - return $result; - } - - // First, look through the override values - foreach ($this->overrides as $k => $overrides) { - if (isset($overrides[$class][$name])) { - $value = $overrides[$class][$name]; - - self::merge_low_into_high($result, $value, $suppress); - if ($result !== null && !is_array($result)) { - return $result; - } - } - - if (isset($this->suppresses[$k][$class][$name])) { - $suppress = $suppress - ? array_merge($suppress, $this->suppresses[$k][$class][$name]) - : $this->suppresses[$k][$class][$name]; - } - } - - $nothing = null; - - // Then the manifest values - foreach ($this->manifests as $manifest) { - $value = $manifest->get($class, $name, $nothing); - if ($value !== $nothing) { - self::merge_low_into_high($result, $value, $suppress); - if ($result !== null && !is_array($result)) { - return $result; - } - } - } - - $sources = array($class); - - // Include extensions only if not flagged not to, and some have been set - if (($sourceOptions & self::EXCLUDE_EXTRA_SOURCES) != self::EXCLUDE_EXTRA_SOURCES) { - // If we don't have a fresh list of extra sources, get it from the class itself - if (!array_key_exists($class, $this->extraConfigSources)) { - $this->extraConfigSources[$class] = Object::get_extra_config_sources($class); - } - - // Update $sources with any extra sources - $extraSources = $this->extraConfigSources[$class]; - if ($extraSources) { - $sources = array_merge($sources, $extraSources); - } - } - - $value = $nothing = null; - - foreach ($sources as $staticSource) { - if (is_array($staticSource)) { - $value = isset($staticSource[$name]) ? $staticSource[$name] : $nothing; - } else { - foreach ($this->staticManifests as $i => $statics) { - $value = $statics->get($staticSource, $name, $nothing); - if ($value !== $nothing) { - break; - } - } - } - - if ($value !== $nothing) { - self::merge_low_into_high($result, $value, $suppress); - if ($result !== null && !is_array($result)) { - return $result; - } - } - } - - // Finally, merge in the values from the parent class - if (($sourceOptions & self::UNINHERITED) != self::UNINHERITED && - (($sourceOptions & self::FIRST_SET) != self::FIRST_SET || $result === null) - ) { - $parent = get_parent_class($class); - if ($parent) { - $this->getUncached($parent, $name, $sourceOptions, $result, $suppress, $tags); - } - } - - return $result; - } - - /** - * Get the config value associated for a given class and property - * - * This merges all current sources and overrides together to give final value - * todo: Currently this is done every time. This function is an inner loop function, so we really need to be - * caching heavily here. - * - * @param $class string - The name of the class to get the value for - * @param $name string - The property to get the value for - * @param int $sourceOptions Bitmask which can be set to some combintain of Config::UNINHERITED, - * Config::FIRST_SET, and Config::EXCLUDE_EXTENSIONS. - * - * Config::UNINHERITED does not include parent classes when merging configuration fragments - * Config::FIRST_SET stops inheriting once the first class that sets a value (even an empty value) is encoutered - * Config::EXCLUDE_EXTRA_SOURCES does not include any additional static sources (such as extensions) - * - * Config::INHERITED is a utility constant that can be used to mean "none of the above", equvilient to 0 - * Setting both Config::UNINHERITED and Config::FIRST_SET behaves the same as just Config::UNINHERITED - * - * should the parent classes value be merged in as the lowest priority source? - * @param $result mixed Reference to a variable to put the result in. Also returned, so this can be left - * as null safely. If you do pass a value, it will be treated as the highest priority - * value in the result chain - * @param $suppress array Internal use when called by child classes. Array of mask pairs to filter value by - * @return mixed The value of the config item, or null if no value set. Could be an associative array, - * sequential array or scalar depending on value (see class docblock) - */ - public function get($class, $name, $sourceOptions = 0, &$result = null, $suppress = null) - { - // Have we got a cached value? Use it if so - $key = $class.$name.$sourceOptions; - - list($cacheHit, $result) = $this->cache->checkAndGet($key); - if (!$cacheHit) { - $tags = array(); - $result = null; - $this->getUncached($class, $name, $sourceOptions, $result, $suppress, $tags); - $this->cache->set($key, $result, $tags); - } - - return $result; - } - - /** - * Update a configuration value - * - * Configuration is modify only. The value passed is merged into the existing configuration. If you want to - * replace the current array value, you'll need to call remove first. - * - * @param string $class The class to update a configuration value for - * @param string $name The configuration property name to update - * @param mixed $val The value to update with - * - * Arrays are recursively merged into current configuration as "latest" - for associative arrays the passed value - * replaces any item with the same key, for sequential arrays the items are placed at the end of the array, for - * non-array values, this value replaces any existing value - * - * You will get an error if you try and override array values with non-array values or vice-versa - */ - public function update($class, $name, $val) - { - if (is_null($val)) { - $this->remove($class, $name); - } else { - if (!isset($this->overrides[0][$class])) { - $this->overrides[0][$class] = array(); - } - - if (!array_key_exists($name, $this->overrides[0][$class])) { - $this->overrides[0][$class][$name] = $val; - } else { - self::merge_high_into_low($this->overrides[0][$class][$name], $val); - } - } - - $this->cache->clean("__{$class}__{$name}"); - } - - /** - * Remove a configuration value - * - * You can specify a key, a key and a value, or neither. Either argument can be Config::anything(), which is - * what is defaulted to if you don't specify something - * - * This removes any current configuration value that matches the key and/or value specified - * - * Works like this: - * - Check the current override array, and remove any values that match the arguments provided - * - Keeps track of the arguments passed to this method, and in get filters everything _except_ the current - * override array to exclude any match - * - * This way we can re-set anything removed by a call to this function by calling set. Because the current override - * array is only filtered immediately on calling this remove method, that value will then be exposed. However, - * every other source is filtered on request, so no amount of changes to parent's configuration etc can override a - * remove call. - * - * @param string $class The class to remove a configuration value from - * @param string $name The configuration name - * @param mixed $key An optional key to filter against. - * If referenced config value is an array, only members of that array that match this key will be removed - * Must also match value if provided to be removed - * @param mixed $value And optional value to filter against. - * If referenced config value is an array, only members of that array that match this value will be removed - * If referenced config value is not an array, value will be removed only if it matches this argument - * Must also match key if provided and referenced config value is an array to be removed - * - * Matching is always by "==", not by "===" - */ - public function remove($class, $name, $key = null, $value = null) - { - if (func_num_args() < 3) { - $key = self::anything(); - } - if (func_num_args() < 4) { - $value = self::anything(); - } - - $suppress = array($key, $value); - - if (isset($this->overrides[0][$class][$name])) { - $value = $this->overrides[0][$class][$name]; - - if (is_array($value)) { - $this->overrides[0][$class][$name] = self::filter_array_by_suppress_array($value, array($suppress)); - } else { - if (self::check_value_contained_in_suppress_array($value, array($suppress))) { - unset($this->overrides[0][$class][$name]); - } - } - } - - if (!isset($this->suppresses[0][$class])) { - $this->suppresses[0][$class] = array(); - } - if (!isset($this->suppresses[0][$class][$name])) { - $this->suppresses[0][$class][$name] = array(); - } - - $this->suppresses[0][$class][$name][] = $suppress; - - $this->cache->clean("__{$class}__{$name}"); - } } diff --git a/src/Core/Config/ConfigLoader.php b/src/Core/Config/ConfigLoader.php new file mode 100644 index 00000000000..e94a846b493 --- /dev/null +++ b/src/Core/Config/ConfigLoader.php @@ -0,0 +1,68 @@ +manifests[count($this->manifests) - 1]; + } + + /** + * Returns true if this class loader has a manifest. + * + * @return bool + */ + public function hasManifest() + { + return (bool)$this->manifests; + } + + /** + * Pushes a class manifest instance onto the top of the stack. + * + * @param ConfigCollectionInterface $manifest + */ + public function pushManifest(ConfigCollectionInterface $manifest) + { + $this->manifests[] = $manifest; + } + + /** + * @return ConfigCollectionInterface + */ + public function popManifest() + { + return array_pop($this->manifests); + } +} diff --git a/src/Core/Core.php b/src/Core/Core.php index efd99c10f6f..6ce90fb13b5 100644 --- a/src/Core/Core.php +++ b/src/Core/Core.php @@ -1,7 +1,15 @@ 'SilverStripe\\Core\\Injector\\SilverStripeServiceConfigurationLocator')); +$injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class)); Injector::set_inst($injector); /////////////////////////////////////////////////////////////////////////////// @@ -84,13 +94,24 @@ $loader->registerAutoloader(); $loader->pushManifest($manifest); -// Now that the class manifest is up, load the static configuration -$configManifest = new ConfigStaticManifest(); -Config::inst()->pushConfigStaticManifest($configManifest); - -// And then the yaml configuration -$configManifest = new ConfigManifest(BASE_PATH, false, $flush); -Config::inst()->pushConfigYamlManifest($configManifest); +// Build config manifest +$configManifest = new CachedConfigCollection( + new FilesystemAdapter('configcache', 0, getTempFolder()), + function () { + return new ConfigCollection([ + // Load private statics first + new PrivateStaticTransformer(function () { + return ClassLoader::instance()->getManifest()->getClasses(); + }), + // Then load yml config + new YamlTransformer(BASE_PATH, Finder::create() + ->in(BASE_PATH . '/*/*/_config') + ->files() + ->name('/\.(yml|yaml)$/')), + ]); + } +); +ConfigLoader::instance()->pushManifest($configManifest); // Load template manifest SilverStripe\View\ThemeResourceLoader::instance()->addSet('$default', new SilverStripe\View\ThemeManifest(