From a6e9a7111bf7b6d5d79e90611d6443cbce2204f2 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Wed, 22 Feb 2017 16:12:46 +1300 Subject: [PATCH] API Substitute core config system with new silverstripe/config module --- composer.json | 4 +- docs/en/04_Changelogs/4.0.0.md | 13 + main.php | 3 +- src/Core/Config/Config.php | 724 +----------------- src/Core/Config/ConfigLoader.php | 90 +++ src/Core/Config/Config_ForClass.php | 62 +- src/Core/Config/Config_MemCache.php | 80 -- src/Core/Config/Configurable.php | 36 +- src/Core/Config/CoreConfigCreator.php | 171 +++++ src/Core/Config/DAG.php | 115 --- src/Core/Config/DAG_CyclicException.php | 25 - src/Core/Config/DAG_Iterator.php | 57 -- .../Config/Middleware/ExtensionMiddleware.php | 84 ++ .../Middleware/InheritanceMiddleware.php | 36 + .../Config/Middleware/MiddlewareCommon.php | 49 ++ src/Core/Core.php | 26 +- src/Core/Extensible.php | 47 +- src/Core/Extension.php | 2 +- src/Core/Manifest/ClassManifest.php | 4 +- src/Core/Manifest/ConfigStaticManifest.php | 35 - 20 files changed, 598 insertions(+), 1065 deletions(-) create mode 100644 src/Core/Config/ConfigLoader.php delete mode 100644 src/Core/Config/Config_MemCache.php create mode 100644 src/Core/Config/CoreConfigCreator.php delete mode 100644 src/Core/Config/DAG.php delete mode 100644 src/Core/Config/DAG_CyclicException.php delete mode 100644 src/Core/Config/DAG_Iterator.php create mode 100644 src/Core/Config/Middleware/ExtensionMiddleware.php create mode 100644 src/Core/Config/Middleware/InheritanceMiddleware.php create mode 100644 src/Core/Config/Middleware/MiddlewareCommon.php delete mode 100644 src/Core/Manifest/ConfigStaticManifest.php diff --git a/composer.json b/composer.json index ab7990283..e554b661b 100644 --- a/composer.json +++ b/composer.json @@ -25,8 +25,10 @@ "swiftmailer/swiftmailer": "~5.4", "symfony/cache": "^3.3@dev", "symfony/config": "^2.8", + "symfony/cache": "^3.1", "symfony/translation": "^2.8", - "vlucas/phpdotenv": "^2.4" + "vlucas/phpdotenv": "^2.4", + "silverstripe/config": "^1@dev" }, "require-dev": { "phpunit/PHPUnit": "~4.8", diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index be56bca02..7986a07c6 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -997,6 +997,11 @@ specific functions. * Added a server requirement for the php-intl extension (shipped by default with most PHP distributions) * Replaced Zend_Date and Zend_Locale with the php-intl extension. * Consistently use CLDR date formats (rather than a mix of CLDR and date() formats) +* Moved config into a new module: [silverstripe/config](https://github.com/silverstripe/silverstripe-config/). + See upgrading notes below. +* Falsey config values (null, 0, false, etc) can now replace non-falsey values. +* Introduced new ModuleLoader manifest, which allows modules to be found via composer name. + E.g. `$cms = ModuleLoader::instance()->getManifest()->getModule('silverstripe/cms')` #### General and Core Removed API @@ -1033,6 +1038,7 @@ specific functions. * Removed TextParser and BBCodeParser. These are available in an archived module, [silverstripe-archive/bbcodeparser](https://github.com/silverstripe-archive/silverstripe-bbcodeparser) * Removed `ViewableData::ThemeDir`. Use `ThemeResourceLoader::findThemedResource` in conjunction with `SSViewer::get_themes` instead. +* Removed `Config::FIRST_SET` and `Config::INHERITED` #### General and Core Deprecated API @@ -1040,6 +1046,8 @@ A very small number of methods were chosen for deprecation, and will be removed * `ClassInfo::baseDataClass` - Use `DataObject::getSchema()->baseDataClass()` instead. * `ClassInfo::table_for_object_field` - Use `DataObject::getSchema()->tableForField()` instead +* `Config::inst()->update()` is deprecated. Use `Config::modify()->set()` or `Config::modify()->merge()` + instead. ### ORM API @@ -1437,6 +1445,11 @@ New `TimeField` methods replace `getConfig()` / `setConfig()` * Removed `DatetimeField`, `DateField` and `TimeField` methods `getConfig` and `setConfig`. Individual getters and setters for individual options are provided instead. See above for list of new methods. * Removed `NumericField_Readonly`. Use `setReadonly(true)` instead. +* `SSViewer` deprecated methods removed: + * `set_source_file_comments()` + * `get_source_file_comments()` + * `getOption` + * `setOption` ### i18n API diff --git a/main.php b/main.php index 1f04991fe..8bcd53be6 100644 --- a/main.php +++ b/main.php @@ -163,7 +163,8 @@ $chain // Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin if (Director::isDev() || !Security::database_is_ready() || Permission::check('ADMIN')) { - return $reloadToken->reloadWithToken(); + $reloadToken->reloadWithToken(); + return; } // Fail and redirect the user to the login page diff --git a/src/Core/Config/Config.php b/src/Core/Config/Config.php index 9cc3d5b3f..4dcc7fa3c 100644 --- a/src/Core/Config/Config.php +++ b/src/Core/Config/Config.php @@ -2,126 +2,15 @@ namespace SilverStripe\Core\Config; -use SilverStripe\Core\Object; -use SilverStripe\Core\Manifest\ConfigStaticManifest; -use SilverStripe\Core\Manifest\ConfigManifest; -use UnexpectedValueException; -use stdClass; +use InvalidArgumentException; +use SilverStripe\Config\Collections\ConfigCollectionInterface; +use SilverStripe\Config\Collections\MutableConfigCollectionInterface; -/** - * 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 +abstract 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. @@ -130,104 +19,44 @@ class Config */ 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. + * instance. It assumes the config instance has already been set. * - * @return Config + * @return ConfigCollectionInterface */ public static function inst() { - if (!self::$instance) { - self::$instance = new Config(); - } - - return self::$instance; + return ConfigLoader::instance()->getManifest(); } /** - * Set the current active {@link Config} instance. + * Make this config available to be modified * - * {@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 + * @return MutableConfigCollectionInterface */ - public static function set_instance($instance) + public static function modify() { - self::$instance = $instance; - return $instance; + $instance = static::inst(); + if ($instance instanceof MutableConfigCollectionInterface) { + return $instance; + } + + // By default nested configs should become mutable + $instance = static::nest(); + if ($instance instanceof MutableConfigCollectionInterface) { + return $instance; + } + + throw new InvalidArgumentException("Nested config could not be made mutable"); } /** @@ -238,527 +67,48 @@ class Config * 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 + * @return ConfigCollectionInterface Active config */ public static function nest() { - $current = self::$instance; - - $new = clone $current; - $new->nestedFrom = $current; - return self::set_instance($new); + // Clone current config and nest + $new = self::inst()->nest(); + ConfigLoader::instance()->pushManifest($new); + return $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 + * @return ConfigCollectionInterface */ public static function unnest() { - if (self::inst()->nestedFrom) { - self::set_instance(self::inst()->nestedFrom); - } else { + // Unnest unless we would be left at 0 manifests + $loader = ConfigLoader::instance(); + if ($loader->countManifests() < 2) { user_error( "Unable to unnest root Config, please make sure you don't have mis-matched nest/unnest", E_USER_WARNING ); + } else { + $loader->popManifest(); } - return self::inst(); + return static::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) - { - 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) - { - 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(); - /** * 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 + * @param string $class * @return Config_ForClass */ - public function forClass($class) + public static 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); - } - } - - /** - * Merge a lower priority associative array into an existing higher priority associative array, as per the class - * docblock rules - * - * 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 - */ - public static function merge_array_low_into_high(&$dest, $src) - { - 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; - } - } - } - - /** - * 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 - * - * @static - * @param $dest array - The existing low priority associative array - * @param $src array - The high priority array to merge in - */ - public static function merge_array_high_into_low(&$dest, $src) - { - $res = $src; - self::merge_array_low_into_high($res, $dest); - $dest = $res; - } - - 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}"); + return new Config_ForClass($class); } } diff --git a/src/Core/Config/ConfigLoader.php b/src/Core/Config/ConfigLoader.php new file mode 100644 index 000000000..caa50201a --- /dev/null +++ b/src/Core/Config/ConfigLoader.php @@ -0,0 +1,90 @@ +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); + } + + /** + * Check number of manifests + * + * @return int + */ + public function countManifests() + { + return count($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 58cbf6520..b89e5007b 100644 --- a/src/Core/Config/Config_ForClass.php +++ b/src/Core/Config/Config_ForClass.php @@ -2,20 +2,21 @@ namespace SilverStripe\Core\Config; +use SilverStripe\Dev\Deprecation; + class Config_ForClass { - /** * @var string $class */ protected $class; /** - * @param string $class + * @param string|object $class */ public function __construct($class) { - $this->class = $class; + $this->class = is_object($class) ? get_class($class) : $class; } /** @@ -33,19 +34,45 @@ class Config_ForClass */ 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, $val) + public function update($name, $value) { - Config::inst()->update($this->class, $name, $val); + 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 set($name, $value) + { + Config::modify()->set($this->class, $name, $value); return $this; } @@ -61,12 +88,12 @@ class Config_ForClass /** * @param string $name - * @param int $sourceOptions + * @param mixed $options * @return mixed */ - public function get($name, $sourceOptions = 0) + public function get($name, $options = 0) { - return Config::inst()->get($this->class, $name, $sourceOptions); + return Config::inst()->get($this->class, $name, $options); } /** @@ -77,7 +104,7 @@ class Config_ForClass */ public function remove($name) { - Config::inst()->remove($this->class, $name); + Config::modify()->remove($this->class, $name); return $this; } @@ -88,6 +115,17 @@ class Config_ForClass */ public function forClass($class) { - return Config::inst()->forClass($class); + return Config::forClass($class); + } + + /** + * Get uninherited config + * + * @param string $name Name of config + * @return mixed + */ + public function uninherited($name) + { + return $this->get($name, Config::UNINHERITED); } } diff --git a/src/Core/Config/Config_MemCache.php b/src/Core/Config/Config_MemCache.php deleted file mode 100644 index 8159f29e0..000000000 --- a/src/Core/Config/Config_MemCache.php +++ /dev/null @@ -1,80 +0,0 @@ -cache = array(); - } - - public function set($key, $val, $tags = array()) - { - foreach ($tags as $t) { - if (!isset($this->tags[$t])) { - $this->tags[$t] = array(); - } - $this->tags[$t][$key] = true; - } - - $this->cache[$key] = array($val, $tags); - } - - private $hit = 0; - private $miss = 0; - - public function stats() - { - return $this->miss ? ($this->hit / $this->miss) : 0; - } - - public function get($key) - { - list($hit, $result) = $this->checkAndGet($key); - return $hit ? $result : false; - } - - /** - * Checks for a cache hit and returns the value as a multi-value return - * - * @param string $key - * @return array First element boolean, isHit. Second element the actual result. - */ - public function checkAndGet($key) - { - if (array_key_exists($key, $this->cache)) { - ++$this->hit; - return array(true, $this->cache[$key][0]); - } else { - ++$this->miss; - return array(false, null); - } - } - - public function clean($tag = null) - { - if ($tag) { - if (isset($this->tags[$tag])) { - foreach ($this->tags[$tag] as $k => $dud) { - // Remove the key from everywhere else it is tagged - $ts = $this->cache[$k][1]; - foreach ($ts as $t) { - unset($this->tags[$t][$k]); - } - unset($this->cache[$k]); - } - unset($this->tags[$tag]); - } - } else { - $this->cache = array(); - $this->tags = array(); - } - } -} diff --git a/src/Core/Config/Configurable.php b/src/Core/Config/Configurable.php index 8e1ad8574..57f3173a3 100644 --- a/src/Core/Config/Configurable.php +++ b/src/Core/Config/Configurable.php @@ -2,6 +2,8 @@ namespace SilverStripe\Core\Config; +use SilverStripe\Dev\Deprecation; + /** * Provides extensions to this object to integrate it with standard config API methods. * @@ -17,29 +19,19 @@ trait Configurable */ public static function config() { - return Config::inst()->forClass(get_called_class()); + return Config::forClass(get_called_class()); } /** - * Gets the first set value for the given config option + * Get inherited config value * * @param string $name * @return mixed */ public function stat($name) { - return Config::inst()->get(get_class($this), $name, Config::FIRST_SET); - } - - /** - * Update the config value for a given property - * - * @param string $name - * @param mixed $value - */ - public function set_stat($name, $value) - { - Config::inst()->update(get_class($this), $name, $value); + Deprecation::notice('5.0', 'Use ->get'); + return $this->config()->get($name); } /** @@ -50,6 +42,20 @@ trait Configurable */ public function uninherited($name) { - return Config::inst()->get(get_class($this), $name, Config::UNINHERITED); + return $this->config()->uninherited($name); + } + + /** + * Update the config value for a given property + * + * @param string $name + * @param mixed $value + * @return $this + */ + public function set_stat($name, $value) + { + Deprecation::notice('5.0', 'Use ->config()->set()'); + $this->config()->set($name, $value); + return $this; } } diff --git a/src/Core/Config/CoreConfigCreator.php b/src/Core/Config/CoreConfigCreator.php new file mode 100644 index 000000000..57bd284f5 --- /dev/null +++ b/src/Core/Config/CoreConfigCreator.php @@ -0,0 +1,171 @@ +setPool(new FilesystemAdapter('configcache', 0, getTempFolder())); + $instance->setFlush($flush); + + // Set collection creator + $instance->setCollectionCreator(function () { + return $this->createCore(); + }); + + return $instance; + } + + /** + * Rebuild new uncached config, which is mutable + * + * @return MemoryConfigCollection + */ + public function createCore() + { + $config = new MemoryConfigCollection(); + + // Set default middleware + $config->setMiddlewares([ + new InheritanceMiddleware(Config::UNINHERITED), + new ExtensionMiddleware(Config::EXCLUDE_EXTRA_SOURCES), + ]); + + // Transform + $config->transform([ + $this->buildStaticTransformer(), + $this->buildYamlTransformer() + ]); + + return $config; + } + + /** + * @return YamlTransformer + */ + protected function buildYamlTransformer() + { + // Get all module dirs + $modules = ModuleLoader::instance()->getManifest()->getModules(); + $dirs = []; + foreach ($modules as $module) { + // Load from _config dirs + $path = $module->getPath() . '/_config'; + if (is_dir($path)) { + $dirs[] = $path; + } + } + + return $this->buildYamlTransformerForPath($dirs); + } + + /** + * @return PrivateStaticTransformer + */ + public function buildStaticTransformer() + { + return new PrivateStaticTransformer(function () { + $classes = ClassLoader::instance()->getManifest()->getClasses(); + return array_keys($classes); + }); + } + + /** + * @param array|string $dirs Base dir to load from + * @return YamlTransformer + */ + public function buildYamlTransformerForPath($dirs) + { + // Construct + $transformer = YamlTransformer::create( + BASE_PATH, + Finder::create() + ->in($dirs) + ->files() + ->name('/\.(yml|yaml)$/') + ); + + // Add default rules + $envvarset = function ($var, $value = null) { + if (getenv($var) === false) { + return false; + } + if ($value) { + return getenv($var) === $value; + } + return true; + }; + $constantdefined = function ($const, $value = null) { + if (!defined($const)) { + return false; + } + if ($value) { + return constant($const) === $value; + } + return true; + }; + return $transformer + ->addRule('classexists', function ($class) { + return class_exists($class); + }) + ->addRule('envvarset', $envvarset) + ->addRule('constantdefined', $constantdefined) + ->addRule( + 'envorconstant', + // Composite rule + function ($name, $value = null) use ($envvarset, $constantdefined) { + return $envvarset($name, $value) || $constantdefined($name, $value); + } + ) + ->addRule('environment', function ($env) { + $current = Director::get_environment_type(); + return strtolower($current) === strtolower($env); + }) + ->addRule('moduleexists', function ($module) { + return ModuleLoader::instance()->getManifest()->moduleExists($module); + }); + } +} diff --git a/src/Core/Config/DAG.php b/src/Core/Config/DAG.php deleted file mode 100644 index a70d4a977..000000000 --- a/src/Core/Config/DAG.php +++ /dev/null @@ -1,115 +0,0 @@ - [$from_idx1, $from_idx2, ...] format - * @var array - */ - protected $dag; - - public function __construct($data = null) - { - $data = $data ? array_values($data) : array(); - - $this->data = $data; - $this->dag = array_fill_keys(array_keys($data), array()); - } - - /** - * Add another node/vertex - * @param mixed $item The item to add to the graph - */ - public function additem($item) - { - $this->data[] = $item; - $this->dag[] = array(); - } - - /** - * Add an edge from one vertex to another. - * - * When passing actual nodes (as opposed to indexes), uses array_search with strict = true to find - * - * @param int $from The index in $data of the node/vertex, or the node/vertex - * itself, that the edge goes from - * @param int $to The index in $data of the node/vertex, or the node/vertex - * itself, that the edge goes to - * @throws Exception - */ - public function addedge($from, $to) - { - $i = is_numeric($from) ? $from : array_search($from, $this->data, true); - $j = is_numeric($to) ? $to : array_search($to, $this->data, true); - - if ($i === false) { - throw new Exception("Couldnt find 'from' item in data when adding edge to DAG"); - } - if ($j === false) { - throw new Exception("Couldnt find 'to' item in data when adding edge to DAG"); - } - - if (!isset($this->dag[$j])) { - $this->dag[$j] = array(); - } - $this->dag[$j][] = $i; - } - - /** - * Sort graph so that each node (a) comes before any nodes (b) where an edge exists from a to b - * @return array - The nodes - * @throws Exception - If the graph is cyclic (and so can't be sorted) - */ - public function sort() - { - $data = $this->data; - $dag = $this->dag; - $sorted = array(); - - while (true) { - $withedges = array_filter($dag, 'count'); - $starts = array_diff_key($dag, $withedges); - - if (!count($starts)) { - break; - } - - foreach ($starts as $i => $foo) { - $sorted[] = $data[$i]; - } - - foreach ($withedges as $j => $deps) { - $withedges[$j] = array_diff($withedges[$j], array_keys($starts)); - } - - $dag = $withedges; - } - - if ($dag) { - $remainder = new DAG($data); - $remainder->dag = $dag; - throw new DAG_CyclicException("DAG has cyclic requirements", $remainder); - } - return $sorted; - } - - public function getIterator() - { - return new DAG_Iterator($this->data, $this->dag); - } -} diff --git a/src/Core/Config/DAG_CyclicException.php b/src/Core/Config/DAG_CyclicException.php deleted file mode 100644 index 32f190a69..000000000 --- a/src/Core/Config/DAG_CyclicException.php +++ /dev/null @@ -1,25 +0,0 @@ -dag = $dag; - parent::__construct($message); - } -} diff --git a/src/Core/Config/DAG_Iterator.php b/src/Core/Config/DAG_Iterator.php deleted file mode 100644 index 1a4c39870..000000000 --- a/src/Core/Config/DAG_Iterator.php +++ /dev/null @@ -1,57 +0,0 @@ -data = $data; - $this->dag = $dag; - $this->rewind(); - } - - public function key() - { - return $this->i; - } - - public function current() - { - $res = array(); - - $res['from'] = $this->data[$this->i]; - - $res['to'] = array(); - foreach ($this->dag[$this->i] as $to) { - $res['to'][] = $this->data[$to]; - } - - return $res; - } - - public function next() - { - $this->i = array_shift($this->dagkeys); - } - - public function rewind() - { - $this->dagkeys = array_keys($this->dag); - $this->next(); - } - - public function valid() - { - return $this->i !== null; - } -} diff --git a/src/Core/Config/Middleware/ExtensionMiddleware.php b/src/Core/Config/Middleware/ExtensionMiddleware.php new file mode 100644 index 000000000..e78b6c1ed --- /dev/null +++ b/src/Core/Config/Middleware/ExtensionMiddleware.php @@ -0,0 +1,84 @@ +enabled($options)) { + return $config; + } + + foreach ($this->getExtraConfig($class, $config) as $extra) { + $config = Priority::mergeArray($extra, $config); + } + return $config; + } + + /** + * Applied config to a class from its extensions + * + * @param string $class + * @param array $classConfig + * @return Generator + */ + protected function getExtraConfig($class, $classConfig) + { + if (empty($classConfig['extensions'])) { + return; + } + + $extensions = $classConfig['extensions']; + foreach ($extensions as $extension) { + list($extensionClass, $extensionArgs) = Object::parse_class_spec($extension); + if (!class_exists($extensionClass)) { + throw new InvalidArgumentException("$class references nonexistent $extensionClass in 'extensions'"); + } + + // Init extension + call_user_func(array($extensionClass, 'add_to_class'), $class, $extensionClass, $extensionArgs); + + // Check class hierarchy from root up + foreach (ClassInfo::ancestry($extensionClass) as $extensionClassParent) { + // Merge config from extension + $extensionConfig = Config::inst()->get($extensionClassParent, null, true); + if ($extensionConfig) { + yield $extensionConfig; + } + if (ClassInfo::has_method_from($extensionClassParent, 'get_extra_config', $extensionClassParent)) { + $extensionConfig = call_user_func( + [ $extensionClassParent, 'get_extra_config' ], + $class, + $extensionClass, + $extensionArgs + ); + if ($extensionConfig) { + yield $extensionConfig; + } + } + } + } + } +} diff --git a/src/Core/Config/Middleware/InheritanceMiddleware.php b/src/Core/Config/Middleware/InheritanceMiddleware.php new file mode 100644 index 000000000..647954e10 --- /dev/null +++ b/src/Core/Config/Middleware/InheritanceMiddleware.php @@ -0,0 +1,36 @@ +enabled($options)) { + return $next($class, $options); + } + + // Merge hierarchy + $config = []; + foreach (ClassInfo::ancestry($class) as $nextClass) { + $nextConfig = $next($nextClass, $options); + $config = Priority::mergeArray($nextConfig, $config); + } + return $config; + } +} diff --git a/src/Core/Config/Middleware/MiddlewareCommon.php b/src/Core/Config/Middleware/MiddlewareCommon.php new file mode 100644 index 000000000..5ff36ffe1 --- /dev/null +++ b/src/Core/Config/Middleware/MiddlewareCommon.php @@ -0,0 +1,49 @@ +disableFlag = $disableFlag; + } + + protected function enabled($options) + { + if ($options === true) { + return false; + } + if (!$this->disableFlag) { + return true; + } + if (is_array($options)) { + if (!isset($options['disableFlag'])) { + return true; + } + $options = $options['disableFlag']; + } + + return ($options & $this->disableFlag) !== $this->disableFlag; + } + + public function serialize() + { + return json_encode([$this->disableFlag]); + } + + public function unserialize($serialized) + { + list($this->disableFlag) = json_decode($serialized, true); + } +} diff --git a/src/Core/Core.php b/src/Core/Core.php index 93105fc8e..abc1731af 100644 --- a/src/Core/Core.php +++ b/src/Core/Core.php @@ -1,13 +1,15 @@ 'SilverStripe\\Core\\Injector\\SilverStripeServiceConfigurationLocator')); +$injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class)); Injector::set_inst($injector); /////////////////////////////////////////////////////////////////////////////// @@ -76,13 +78,16 @@ $loader = ClassLoader::instance(); $loader->registerAutoloader(); $loader->pushManifest($manifest); -// Now that the class manifest is up, load the static configuration -$configManifest = new ConfigStaticManifest(); -Config::inst()->pushConfigStaticManifest($configManifest); +// Init module manifest +$moduleManifest = new ModuleManifest(BASE_PATH, false, $flush); +ModuleLoader::instance()->pushManifest($moduleManifest); -// And then the yaml configuration -$configManifest = new ConfigManifest(BASE_PATH, false, $flush); -Config::inst()->pushConfigYamlManifest($configManifest); +// Build config manifest +$configManifest = CoreConfigCreator::inst()->createRoot($flush); +ConfigLoader::instance()->pushManifest($configManifest); + +// After loading config, boot _config.php files +ModuleLoader::instance()->getManifest()->activateConfig(); // Load template manifest SilverStripe\View\ThemeResourceLoader::instance()->addSet('$default', new SilverStripe\View\ThemeManifest( @@ -103,7 +108,6 @@ if (Director::isLive()) { /** * Load error handlers */ - $errorHandler = Injector::inst()->get('ErrorHandler'); $errorHandler->start(); diff --git a/src/Core/Extensible.php b/src/Core/Extensible.php index 7e92c0c56..6b4953d90 100644 --- a/src/Core/Extensible.php +++ b/src/Core/Extensible.php @@ -122,11 +122,7 @@ trait Extensible if (in_array($class, self::$unextendable_classes)) { continue; } - $extensions = Config::inst()->get( - $class, - 'extensions', - Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES - ); + $extensions = Config::inst()->get($class, 'extensions', true); if ($extensions) { foreach ($extensions as $extension) { @@ -217,8 +213,10 @@ trait Extensible } } - Config::inst()->update($class, 'extensions', array($extension)); - Config::inst()->extraConfigSourcesChanged($class); + Config::modify() + ->merge($class, 'extensions', array( + $extension + )); Injector::inst()->unregisterNamedObject($class); @@ -234,6 +232,8 @@ trait Extensible /** * Remove an extension from a class. + * Note: This will not remove extensions from parent classes, and must be called + * directly on the class assigned the extension. * * Keep in mind that this won't revert any datamodel additions * of the extension at runtime, unless its used before the @@ -252,22 +252,23 @@ trait Extensible { $class = get_called_class(); - Config::inst()->remove($class, 'extensions', Config::anything(), $extension); - - // remove any instances of the extension with parameters - $config = Config::inst()->get($class, 'extensions'); - - if ($config) { - foreach ($config as $k => $v) { - // extensions with parameters will be stored in config as - // ExtensionName("Param"). - if (preg_match(sprintf("/^(%s)\(/", preg_quote($extension, '/')), $v)) { - Config::inst()->remove($class, 'extensions', Config::anything(), $v); - } + // Build filtered extension list + $found = false; + $config = Config::inst()->get($class, 'extensions', true) ?: []; + foreach ($config as $key => $candidate) { + // extensions with parameters will be stored in config as ExtensionName("Param"). + if (strcasecmp($candidate, $extension) === 0 || + stripos($candidate, $extension.'(') === 0 + ) { + $found = true; + unset($config[$key]); } } - - Config::inst()->extraConfigSourcesChanged($class); + // Don't dirty cache if no changes + if (!$found) { + return; + } + Config::modify()->set($class, 'extensions', $config); // unset singletons to avoid side-effects Injector::inst()->unregisterAllObjects(); @@ -292,7 +293,7 @@ trait Extensible */ public static function get_extensions($class, $includeArgumentString = false) { - $extensions = Config::inst()->get($class, 'extensions'); + $extensions = Config::forClass($class)->get('extensions', Config::EXCLUDE_EXTRA_SOURCES); if (empty($extensions)) { return array(); } @@ -329,7 +330,7 @@ trait Extensible $sources = null; // Get a list of extensions - $extensions = Config::inst()->get($class, 'extensions', Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES); + $extensions = Config::inst()->get($class, 'extensions', true); if (!$extensions) { return null; diff --git a/src/Core/Extension.php b/src/Core/Extension.php index 85221d3d8..0914ab635 100644 --- a/src/Core/Extension.php +++ b/src/Core/Extension.php @@ -21,7 +21,7 @@ abstract class Extension * This is used by extensions designed to be applied to controllers. * It works the same way as {@link Controller::$allowed_actions}. */ - private static $allowed_actions = null; + private static $allowed_actions = []; /** * The object this extension is applied to. diff --git a/src/Core/Manifest/ClassManifest.php b/src/Core/Manifest/ClassManifest.php index 27c0e4bd4..65bd67a30 100644 --- a/src/Core/Manifest/ClassManifest.php +++ b/src/Core/Manifest/ClassManifest.php @@ -386,8 +386,8 @@ class ClassManifest */ 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'; } /** diff --git a/src/Core/Manifest/ConfigStaticManifest.php b/src/Core/Manifest/ConfigStaticManifest.php deleted file mode 100644 index f7304ae0c..000000000 --- a/src/Core/Manifest/ConfigStaticManifest.php +++ /dev/null @@ -1,35 +0,0 @@ -name, $class) === 0) { - if ($reflection->hasProperty($name)) { - $property = $reflection->getProperty($name); - if ($property->isStatic() && $property->isPrivate()) { - $property->setAccessible(true); - return $property->getValue(); - } - } - } - } - return null; - } -}