API Substitute core config system with new silverstripe/config module

This commit is contained in:
Damian Mooyman 2017-02-22 16:12:46 +13:00
parent 72ddac2a56
commit a6e9a7111b
20 changed files with 598 additions and 1065 deletions

View File

@ -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",

View File

@ -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')`
#### <a name="overview-general-removed"></a>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`
#### <a name="overview-general-deprecated"></a>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.
### <a name="overview-orm"></a>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`
### <a name="overview-i18n"></a>i18n API

View File

@ -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

View File

@ -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);
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace SilverStripe\Core\Config;
use SilverStripe\Config\Collections\ConfigCollectionInterface;
/**
* Registers config sources via ConfigCollectionInterface
*/
class ConfigLoader
{
/**
* @var self
*/
private static $instance;
/**
* @var ConfigCollectionInterface[] map of config collections
*/
protected $manifests = array();
/**
* @return self
*/
public static function instance()
{
return self::$instance ? self::$instance : self::$instance = new self();
}
/**
* Returns the currently active class manifest instance that is used for
* loading classes.
*
* @return ConfigCollectionInterface
*/
public function getManifest()
{
return $this->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;
}
}

View File

@ -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);
}
}

View File

@ -1,80 +0,0 @@
<?php
namespace SilverStripe\Core\Config;
class Config_MemCache
{
protected $cache;
protected $i = 0;
protected $c = 0;
protected $tags = array();
public function __construct()
{
$this->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();
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,171 @@
<?php
namespace SilverStripe\Core\Config;
use SilverStripe\Config\Collections\CachedConfigCollection;
use SilverStripe\Config\Collections\MemoryConfigCollection;
use SilverStripe\Config\Transformer\PrivateStaticTransformer;
use SilverStripe\Config\Transformer\YamlTransformer;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Middleware\ExtensionMiddleware;
use SilverStripe\Core\Config\Middleware\InheritanceMiddleware;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ModuleLoader;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Finder\Finder;
/**
* Factory for silverstripe configs
*/
class CoreConfigCreator
{
/**
* @var static
*/
protected static $inst = null;
/**
* @return static
*/
public static function inst()
{
if (!self::$inst) {
self::$inst = new static();
}
return self::$inst;
}
/**
* Create root application config.
* This will be an immutable cached config,
* which conditionally generates a nested "core" config.
*
* @param bool $flush
* @return CachedConfigCollection
*/
public function createRoot($flush)
{
$instance = new CachedConfigCollection();
// Set root cache
$instance->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);
});
}
}

View File

@ -1,115 +0,0 @@
<?php
namespace SilverStripe\Core\Config;
use Exception;
use IteratorAggregate;
/**
* A Directed Acyclic Graph - used for doing topological sorts on dependencies, such as the before/after conditions
* in config yaml fragments
*/
class DAG implements IteratorAggregate
{
/**
* The nodes/vertices in the graph. Should be a numeric sequence of items (no string keys, no gaps).
* @var array|null
*/
protected $data;
/**
* The edges in the graph, in $to_idx => [$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);
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace SilverStripe\Core\Config;
use Exception;
/**
* Exception thrown when the {@link SilverStripe\Core\Config\DAG} class is unable to resolve sorting the DAG due
* to cyclic dependencies.
*/
class DAG_CyclicException extends Exception
{
public $dag;
/**
* @param string $message The Exception message
* @param DAG $dag The remainder of the Directed Acyclic Graph (DAG) after the last successful sort
*/
public function __construct($message, $dag)
{
$this->dag = $dag;
parent::__construct($message);
}
}

View File

@ -1,57 +0,0 @@
<?php
namespace SilverStripe\Core\Config;
use Iterator;
class DAG_Iterator implements Iterator
{
protected $data;
protected $dag;
protected $dagkeys;
protected $i;
public function __construct($data, $dag)
{
$this->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;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace SilverStripe\Core\Config\Middleware;
use Generator;
use InvalidArgumentException;
use SilverStripe\Config\MergeStrategy\Priority;
use SilverStripe\Config\Middleware\Middleware;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Object;
class ExtensionMiddleware implements \SilverStripe\Config\Middleware\Middleware
{
use MiddlewareCommon;
/**
* Get config for a class
*
* @param string $class
* @param mixed $options
* @param callable $next
* @return string
*/
public function getClassConfig($class, $options, $next)
{
// Get base config
$config = $next($class, $options);
if (!$this->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;
}
}
}
}
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace SilverStripe\Core\Config\Middleware;
use SilverStripe\Config\MergeStrategy\Priority;
use SilverStripe\Config\Middleware\Middleware;
use SilverStripe\Core\ClassInfo;
class InheritanceMiddleware implements Middleware
{
use MiddlewareCommon;
/**
* Get config for a class
*
* @param string $class
* @param mixed $options
* @param callable $next
* @return string
*/
public function getClassConfig($class, $options, $next)
{
// Check if enabled
if (!$this->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;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace SilverStripe\Core\Config\Middleware;
/**
* Abstract flag-aware middleware
*/
trait MiddlewareCommon
{
/**
* Disable flag
*
* @var int
*/
protected $disableFlag = 0;
public function __construct($disableFlag = 0)
{
$this->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);
}
}

View File

@ -1,13 +1,15 @@
<?php
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\CoreConfigCreator;
use SilverStripe\Core\Config\ConfigLoader;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator;
use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ConfigStaticManifest;
use SilverStripe\Core\Manifest\ConfigManifest;
use SilverStripe\Control\Director;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\i18n\i18n;
/**
@ -56,7 +58,7 @@ gc_enable();
// Initialise the dependency injector as soon as possible, as it is
// subsequently used by some of the following code
$injector = new Injector(array('locator' => '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();

View File

@ -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;

View File

@ -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.

View File

@ -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';
}
/**

View File

@ -1,35 +0,0 @@
<?php
namespace SilverStripe\Core\Manifest;
use ReflectionClass;
/**
* Allows access to config values set on classes using private statics.
*/
class ConfigStaticManifest
{
/**
* @param string $class
* @param string $name
* @return mixed
*/
public function get($class, $name)
{
if (class_exists($class)) {
// The config system is case-sensitive so we need to check the exact value
$reflection = new ReflectionClass($class);
if (strcmp($reflection->name, $class) === 0) {
if ($reflection->hasProperty($name)) {
$property = $reflection->getProperty($name);
if ($property->isStatic() && $property->isPrivate()) {
$property->setAccessible(true);
return $property->getValue();
}
}
}
}
return null;
}
}