mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
ENHANCEMENT: Add config layer - the Config access class itself, and the ConfigManfiest builder which parses in the yaml config files
This commit is contained in:
parent
7125cd4f27
commit
d355cd5baf
502
core/Config.php
Normal file
502
core/Config.php
Normal file
@ -0,0 +1,502 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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 hardcoded 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 optimisations 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-sh 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 {
|
||||
|
||||
/** @var Object - A marker instance for the "anything" singleton value. Don't access directly, even in-class, always use self::anything() */
|
||||
static private $_anything = null;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* todo: Does this follow the SS coding conventions? Config::get_anything_marker_instance() is a lot less elegant.
|
||||
* @return Object
|
||||
*/
|
||||
static public function anything() {
|
||||
if (self::$_anything === null) self::$_anything = new stdClass();
|
||||
return self::$_anything;
|
||||
}
|
||||
|
||||
// -- Source options bitmask --
|
||||
|
||||
/** @const source options bitmask value - merge all parent configuration in as lowest priority */
|
||||
const INHERITED = 0;
|
||||
/** @const source options bitmask value - only get configuration set for this specific class, not any of it's parents */
|
||||
const UNINHERITED = 1;
|
||||
/** @const source options bitmask value - inherit, but stop on the first class that actually provides a value (event an empty value) */
|
||||
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 --
|
||||
|
||||
/** @const Return flag for get_value_type indicating value is a scalar (or really just not-an-array, at least ATM)*/
|
||||
const ISNT_ARRAY = 1;
|
||||
/** @const Return flag for get_value_type indicating value is an array */
|
||||
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 $val any - The value
|
||||
* @return int - One of ISNT_ARRAY or IS_ARRAY
|
||||
*/
|
||||
static protected 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
|
||||
*/
|
||||
static protected 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 */
|
||||
static protected $instance;
|
||||
|
||||
/**
|
||||
* Get the current active Config instance.
|
||||
*
|
||||
* Configs should not normally be manually created.
|
||||
* In general use you will use this method to obtain the current Config instance.
|
||||
*
|
||||
* @return Config
|
||||
*/
|
||||
static public function inst() {
|
||||
if (!self::$instance) self::$instance = singleton('Config');
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current active Config instance.
|
||||
*
|
||||
* Configs 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
|
||||
*
|
||||
* @return Config
|
||||
*/
|
||||
static public function set_instance($instance) {
|
||||
self::$instance = $instance;
|
||||
global $_SINGLETONS;
|
||||
$_SINGLETONS['Config'] = $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty construction, otherwise calling singleton('Config') (not the right way to get the current active config
|
||||
* instance, but people might) gives an error
|
||||
*/
|
||||
function __construct() {
|
||||
}
|
||||
|
||||
/** @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] - 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] - 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
|
||||
*/
|
||||
public function pushConfigManifest(SS_ConfigManifest $manifest) {
|
||||
array_unshift($this->manifests, $manifest->yamlConfig);
|
||||
|
||||
// @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;
|
||||
}
|
||||
|
||||
static $extra_static_sources = array();
|
||||
|
||||
static function add_static_source($forclass, $donorclass) {
|
||||
self::$extra_static_sources[$forclass][] = $donorclass;
|
||||
}
|
||||
|
||||
/** @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
|
||||
* @return Config_ForClass
|
||||
*/
|
||||
public function forClass($class) {
|
||||
if (isset(self::$for_class_instances[$class])) {
|
||||
return self::$for_class_instances[$class];
|
||||
}
|
||||
else {
|
||||
return self::$for_class_instances[$class] = new Config_ForClass($class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
static function merge_array_low_into_high(&$dest, $src) {
|
||||
foreach ($src as $k => $v) {
|
||||
if (!$v) {
|
||||
continue;
|
||||
}
|
||||
else if (is_int($k)) {
|
||||
$dest[] = $v;
|
||||
}
|
||||
else if (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
|
||||
*/
|
||||
static function merge_array_high_into_low(&$dest, $src) {
|
||||
$res = $src;
|
||||
self::merge_array_low_into_high($res, $dest);
|
||||
$dest = $res;
|
||||
}
|
||||
|
||||
static function merge_high_into_low(&$result, $value) {
|
||||
if (!$value) return;
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static protected 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;
|
||||
}
|
||||
|
||||
static protected 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 null $result array|scalar - 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 null $suppress array - Internal use when called by child classes. Array of mask pairs to filter value by
|
||||
* @return array|scalar - 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)
|
||||
*/
|
||||
function get($class, $name, $sourceOptions = 0, &$result = null, $suppress = null) {
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
|
||||
// Then the manifest values
|
||||
foreach($this->manifests as $manifest) {
|
||||
if (isset($manifest[$class][$name])) {
|
||||
self::merge_low_into_high($result, $manifest[$class][$name], $suppress);
|
||||
if ($result !== null && !is_array($result)) return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Then look at the static variables
|
||||
$nothing = new stdClass();
|
||||
$classes = 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) && isset(self::$extra_static_sources[$class])) {
|
||||
$classes = array_merge($classes, self::$extra_static_sources[$class]);
|
||||
}
|
||||
|
||||
foreach ($classes as $staticSource) {
|
||||
$value = Object::static_lookup($staticSource, $name, $nothing);
|
||||
|
||||
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->get($parent, $name, $sourceOptions, $result, $suppress);
|
||||
}
|
||||
|
||||
if ($name == 'routes') {
|
||||
print_r($result); die;
|
||||
}
|
||||
|
||||
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 value, you'll need to call remove first.
|
||||
*
|
||||
* @param $class string - The class to update a configuration value for
|
||||
* @param $name string - The configuration property name to update
|
||||
* @param $value any - 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
|
||||
*/
|
||||
function update($class, $name, $val) {
|
||||
if (!isset($this->overrides[0][$class])) $this->overrides[0][$class] = array();
|
||||
|
||||
if (!isset($this->overrides[0][$class][$name])) $this->overrides[0][$class][$name] = $val;
|
||||
else self::merge_high_into_low($this->overrides[0][$class][$name], $val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $class string - The class to remove a configuration value from
|
||||
* @param $name string - The configuration name
|
||||
* @param $key any - 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 $value any - 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 "==="
|
||||
*/
|
||||
function remove($class, $name) {
|
||||
$argc = func_num_args();
|
||||
$key = $argc > 2 ? func_get_arg(2) : self::anything();
|
||||
$value = $argc > 3 ? func_get_arg(3) : 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Config_ForClass {
|
||||
protected $class;
|
||||
|
||||
function __construct($class) {
|
||||
$this->class = $class;
|
||||
}
|
||||
|
||||
function __get($name) {
|
||||
return Config::inst()->get($this->class, $name);
|
||||
}
|
||||
|
||||
function __set($name, $val) {
|
||||
return Config::inst()->update($this->class, $name, $val);
|
||||
}
|
||||
|
||||
function get($name, $sourceOptions = 0) {
|
||||
return Config::inst()->get($this->class, $name, $sourceOptions);
|
||||
}
|
||||
|
||||
function forClass($class) {
|
||||
return Config::inst()->forClass($class);
|
||||
}
|
||||
}
|
74
core/DAG.php
Normal file
74
core/DAG.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A Directed Acyclic Graph - used for doing topological sorts on dependencies, such as the before/after conditions
|
||||
* in config yaml fragments
|
||||
*/
|
||||
class SS_DAG {
|
||||
/** @var array|null - The nodes/vertices in the graph. Should be a numeric sequence of items (no string keys, no gaps). */
|
||||
protected $data;
|
||||
|
||||
/** @var array - The edges in the graph, in $to_idx => [$from_idx1, $from_idx2, ...] format */
|
||||
protected $dag;
|
||||
|
||||
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 $item anything - The item to add to the graph
|
||||
*/
|
||||
function additem($item) {
|
||||
$this->data[] = $item;
|
||||
$this->dag[] = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an edge from one vertex to another
|
||||
* @param $from integer|any - The index in $data of the node/vertex, or the node/vertex itself, that the edge goes from
|
||||
* @param $to integer|any - The index in $data of the node/vertex, or the node/vertex itself, that the edge goes to
|
||||
*
|
||||
* When passing actual nodes (as opposed to indexes), uses array_search with strict = true to find
|
||||
*/
|
||||
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)
|
||||
*/
|
||||
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) throw new Exception("DAG has cyclic requirements");
|
||||
return $sorted;
|
||||
}
|
||||
}
|
@ -255,7 +255,55 @@ abstract class Object {
|
||||
|
||||
return $class;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the value of a static property of a class, even in that property is declared protected (but not private), without any inheritance,
|
||||
* merging or parent lookup if it doesn't exist on the given class
|
||||
*
|
||||
* In 5.3, we can do this fast using $foo::$bar syntax, but this method then needs to be in a base class of $class
|
||||
* to bust the protected def
|
||||
*
|
||||
* @static
|
||||
* @param $class - The class to get the static from
|
||||
* @param $name - The property to get from the class
|
||||
* @param null $default - The value to return if property doesn't exist on class
|
||||
* @return any - The value of the static property $name on class $class, or $default if that property is not defined
|
||||
*/
|
||||
public static function static_lookup($class, $name, $default = null) {
|
||||
if (version_compare(PHP_VERSION, '5.4', '>=') && is_subclass_of($class, 'Object')) {
|
||||
if (isset($class::$$name)) {
|
||||
$parent = get_parent_class($class);
|
||||
if (!$parent || !isset($parent::$$name) || $parent::$$name !== $class::$$name) return $class::$$name;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
else {
|
||||
// TODO: This gets set once, then not updated, so any changes to statics after this is called the first time for any class won't be exposed
|
||||
static $static_properties = array();
|
||||
|
||||
if (!isset($static_properties[$class])) {
|
||||
$reflection = new ReflectionClass($class);
|
||||
$static_properties[$class] = $reflection->getStaticProperties();
|
||||
}
|
||||
|
||||
if (isset($static_properties[$class][$name])) {
|
||||
$value = $static_properties[$class][$name];
|
||||
|
||||
$parent = get_parent_class($class);
|
||||
if (!$parent) return $value;
|
||||
|
||||
if (!isset($static_properties[$parent])) {
|
||||
$reflection = new ReflectionClass($parent);
|
||||
$static_properties[$parent] = $reflection->getStaticProperties();
|
||||
}
|
||||
|
||||
if (!isset($static_properties[$parent][$name]) || $static_properties[$parent][$name] !== $value) return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a static variable, taking into account SS's inbuild static caches and pseudo-statics
|
||||
*
|
||||
|
493
core/manifest/ConfigManifest.php
Normal file
493
core/manifest/ConfigManifest.php
Normal file
@ -0,0 +1,493 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* A utility class which builds a manifest of configuration items
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage manifest
|
||||
*/
|
||||
class SS_ConfigManifest {
|
||||
|
||||
/** @var array All the values needed to be collected to determine the correct combination of fragements for the current environment. */
|
||||
protected $variantKeySpec = array();
|
||||
|
||||
/** @var array All the _config.php files. Need to be included every request & can't be cached. Not variant specific. */
|
||||
protected $phpConfigSources = array();
|
||||
|
||||
/** @var array All the _config/*.yml fragments pre-parsed and sorted in ascending include order. Not variant specific. */
|
||||
protected $yamlConfigFragments = array();
|
||||
|
||||
/** @var array The calculated config from _config/*.yml, sorted, filtered and merged. Variant specific. */
|
||||
public $yamlConfig = array();
|
||||
|
||||
/** @var array A side-effect of collecting the _config fragments is the calculation of all module directories, since the definition
|
||||
* of a module is "a directory that contains either a _config.php file or a _config directory */
|
||||
public $modules = array();
|
||||
|
||||
/** Adds a path as a module */
|
||||
function addModule($path) {
|
||||
$module = basename($path);
|
||||
if (isset($this->modules[$module]) && $this->modules[$module] != $path) {
|
||||
user_error("Module ".$module." in two places - ".$path." and ".$this->modules[$module]);
|
||||
}
|
||||
$this->modules[$module] = $path;
|
||||
}
|
||||
|
||||
/** Returns true if the passed module exists */
|
||||
function moduleExists($module) {
|
||||
return array_key_exists($module, $this->modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs and initialises a new configuration object, either loading
|
||||
* from the cache or re-scanning for classes.
|
||||
*
|
||||
* @param string $base The project base path.
|
||||
* @param bool $forceRegen Force the manifest to be regenerated.
|
||||
*/
|
||||
public function __construct($base, $includeTests = false, $forceRegen = false ) {
|
||||
$this->base = $base;
|
||||
|
||||
// Get the Zend Cache to load/store cache into
|
||||
$this->cache = SS_Cache::factory('SS_Configuration', 'Core', array(
|
||||
'automatic_serialization' => true,
|
||||
'lifetime' => null
|
||||
));
|
||||
|
||||
// Unless we're forcing regen, try loading from cache
|
||||
if (!$forceRegen) {
|
||||
// The PHP config sources are always needed
|
||||
$this->phpConfigSources = $this->cache->load('php_config_sources');
|
||||
|
||||
// Get the variant key spec - if this isn't present, we can't figure out what variant we're in so it's full regen time
|
||||
if ($this->variantKeySpec = $this->cache->load('variant_key_spec')) {
|
||||
// Try getting the pre-filtered & merged config for this variant
|
||||
if (!($this->yamlConfig = $this->cache->load('yaml_config_'.$this->variantKey()))) {
|
||||
// Otherwise, if we do have the yaml config fragments (and we should since we have a variant key spec) work out the config for this variant
|
||||
if ($this->yamlConfigFragments = $this->cache->load('yaml_config_fragments')) {
|
||||
$this->buildYamlConfigVariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have a config yet, we need to do a full regen to get it
|
||||
if (!$this->yamlConfig) {
|
||||
$this->regenerate($includeTests);
|
||||
$this->buildYamlConfigVariant();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Includes all of the php _config.php files found by this manifest. Called by SS_Config when adding this manifest
|
||||
* @return void
|
||||
*/
|
||||
public function activateConfig() {
|
||||
foreach ($this->phpConfigSources as $config) {
|
||||
require_once $config;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string that uniquely identifies this variant. The variant is the combination of classes, modules, environment,
|
||||
* environment variables and constants that selects which yaml fragments actually make it into the configuration because of "only"
|
||||
* and "except" rules.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function variantKey() {
|
||||
$key = $this->variantKeySpec; // Copy to fill in actual values
|
||||
|
||||
if (isset($key['environment'])) $key['environment'] = Director::isDev() ? 'dev' : (Director::isTest() ? 'test' : 'live');
|
||||
|
||||
if (isset($key['envvars'])) foreach ($key['envvars'] as $variable => $foo) {
|
||||
$key['envvars'][$variable] = isset($_ENV[$variable]) ? $_ENV[$variable] : null;
|
||||
}
|
||||
|
||||
if (isset($key['constants'])) foreach ($key['constants'] as $variable => $foo) {
|
||||
$key['constants'][$variable] = defined($variable) ? constant($variable) : null;
|
||||
}
|
||||
|
||||
return sha1(serialize($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely regenerates the manifest file. Scans through finding all php _config.php and yaml _config/*.ya?ml files,
|
||||
* parses the yaml files into fragments, sorts them and figures out what values need to be checked to pick the
|
||||
* correct variant.
|
||||
*
|
||||
* Does _not_ build the actual variant
|
||||
*
|
||||
* @param bool $cache Cache the result.
|
||||
*/
|
||||
public function regenerate($includeTests = false, $cache = true) {
|
||||
$finder = new ManifestFileFinder();
|
||||
$finder->setOptions(array(
|
||||
'name_regex' => '/_config.php$/',
|
||||
'ignore_tests' => !$includeTests,
|
||||
'file_callback' => array($this, 'addSourceConfigFile')
|
||||
));
|
||||
$finder->find($this->base);
|
||||
|
||||
$finder = new ManifestFileFinder();
|
||||
$finder->setOptions(array(
|
||||
'name_regex' => '/\.ya?ml$/',
|
||||
'ignore_tests' => !$includeTests,
|
||||
'file_callback' => array($this, 'addYAMLConfigFile')
|
||||
));
|
||||
$finder->find($this->base);
|
||||
|
||||
$this->prefilterYamlFragments();
|
||||
$this->sortYamlFragments();
|
||||
$this->buildVariantKeySpec();
|
||||
|
||||
if ($cache) {
|
||||
$this->cache->save($this->phpConfigSources, 'php_config_sources');
|
||||
$this->cache->save($this->yamlConfigFragments, 'yaml_config_fragments');
|
||||
$this->cache->save($this->variantKeySpec, 'variant_key_spec');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle finding a php file. We just keep a record of all php files found, we don't include them
|
||||
* at this stage
|
||||
*
|
||||
* Public so that ManifestFileFinder can call it. Not for general use.
|
||||
*/
|
||||
public function addSourceConfigFile($basename, $pathname, $depth) {
|
||||
$this->phpConfigSources[] = $pathname;
|
||||
// Add this module too
|
||||
$this->addModule(dirname($pathname));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle finding a yml file. Parse the file by spliting it into header/fragment pairs,
|
||||
* and normalising some of the header values (especially: give anonymous name if none assigned,
|
||||
* splt/complete before and after matchers)
|
||||
*
|
||||
* Public so that ManifestFileFinder can call it. Not for general use.
|
||||
*/
|
||||
public function addYAMLConfigFile($basename, $pathname, $depth) {
|
||||
if (!preg_match('{/([^/]+)/_config/}', $pathname, $match)) return;
|
||||
|
||||
// Keep track of all the modules we've seen
|
||||
$this->addModule(dirname(dirname($pathname)));
|
||||
|
||||
// We use Symfony Yaml since it's the most complete. It still doesn't handle all of YAML, but it's better than
|
||||
// nothing.
|
||||
require_once 'thirdparty/symfony-yaml/lib/sfYamlParser.php';
|
||||
$parser = new sfYamlParser();
|
||||
|
||||
// The base header
|
||||
$base = array(
|
||||
'module' => $match[1],
|
||||
'file' => basename(basename($basename, '.yml'), '.yaml')
|
||||
);
|
||||
|
||||
// YAML parsers really should handle this properly themselves, but neither spyc nor symfony-yaml do. So we
|
||||
// follow in their vein and just do what we need, not what the spec says
|
||||
$parts = preg_split('/^---$/m', file_get_contents($pathname), -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
// If only one document, it's a headerless fragment. So just add it with an anonymous name
|
||||
if (count($parts) == 1) {
|
||||
$this->yamlConfigFragments[] = $base + array(
|
||||
'name' => 'anonymous-1',
|
||||
'fragment' => $parser->parse($parts[0])
|
||||
);
|
||||
}
|
||||
// Otherwise it's a set of header/document pairs
|
||||
else {
|
||||
// If we got an odd number of parts the config file doesn't have a header for every document
|
||||
if (count($parts) % 2 != 0) user_error("Configuration file $basename does not have an equal number of headers and config blocks");
|
||||
|
||||
// Step through each pair
|
||||
for ($i = 0; $i < count($parts); $i+=2) {
|
||||
// Make all the first-level keys of the header lower case
|
||||
$header = array_change_key_case($parser->parse($parts[$i]), CASE_LOWER);
|
||||
|
||||
// Assign a name if non assigned already
|
||||
if (!isset($header['name'])) $header['name'] = 'anonymous-'.(1+$i/2);
|
||||
|
||||
// Parse & normalise the before and after if present
|
||||
foreach (array('before', 'after') as $order) {
|
||||
if (isset($header[$order])) {
|
||||
// First, splice into parts (multiple before or after parts are allowed, comma separated)
|
||||
$orderparts = preg_split('/\s+,\s+/', $header[$order], PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
// For each, parse out into module/file#name, and set any missing to "*"
|
||||
$header[$order] = array();
|
||||
foreach($orderparts as $part) {
|
||||
preg_match('! (\*|\w+) (?:\/(\*|\w+) (?:\*|\#(\w+))? )? !x', $part, $match);
|
||||
$header[$order][] = array(
|
||||
'module' => $match[1],
|
||||
'file' => isset($match[2]) ? $match[2] : '*',
|
||||
'name' => isset($match[3]) ? $match[3] : '*'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And add to the fragments list
|
||||
$this->yamlConfigFragments[] = $base + $header + array(
|
||||
'fragment' => $parser->parse($parts[$i+1])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the YAML fragments so that the "before" and "after" rules are met.
|
||||
* Throws an error if there's a loop
|
||||
*
|
||||
* We can't use regular sorts here - we need a topological sort. Easiest
|
||||
* way is with a DAG, so build up a DAG based on the before/after rules, then
|
||||
* sort that.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function sortYamlFragments() {
|
||||
$frags = $this->yamlConfigFragments;
|
||||
|
||||
// Build a directed graph
|
||||
$dag = new SS_DAG($frags);
|
||||
|
||||
foreach ($frags as $i => $frag) {
|
||||
foreach ($frags as $j => $otherfrag) {
|
||||
if ($i == $j) continue;
|
||||
|
||||
$order = $this->relativeOrder($frag, $otherfrag);
|
||||
|
||||
if ($order == 'before') $dag->addedge($i, $j);
|
||||
elseif ($order == 'after') $dag->addedge($j, $i);
|
||||
}
|
||||
}
|
||||
|
||||
$this->yamlConfigFragments = $dag->sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string "after", "before" or "undefined" depending on whether the YAML fragment array element passed as $a should
|
||||
* be positioned after, before, or either compared to the YAML fragment array element passed as $b
|
||||
*
|
||||
* @param $a Array - a YAML config fragment as loaded by addYAMLConfigFile
|
||||
* @param $b Array - a YAML config fragment as loaded by addYAMLConfigFile
|
||||
* @return string "after", "before" or "undefined"
|
||||
*/
|
||||
protected function relativeOrder($a, $b) {
|
||||
$matchesSomeRule = array();
|
||||
|
||||
// Do the same thing for after and before
|
||||
foreach (array('after'=>'before', 'before'=>'after') as $rulename => $opposite) {
|
||||
$matchesSomeRule[$rulename] = false;
|
||||
|
||||
// If no rule specified, we don't match it
|
||||
if (isset($a[$rulename])) {
|
||||
|
||||
foreach ($a[$rulename] as $rule) {
|
||||
$matchesRule = true;
|
||||
|
||||
foreach(array('module', 'file', 'name') as $part) {
|
||||
$partMatches = true;
|
||||
|
||||
// If part is *, we match _unless_ the opposite rule has a non-* matcher than also matches $b
|
||||
if ($rule[$part] == '*') {
|
||||
if (isset($a[$opposite])) foreach($a[$opposite] as $oppositeRule) {
|
||||
if ($oppositeRule[$part] == $b[$part]) { $partMatches = false; break; }
|
||||
}
|
||||
}
|
||||
else {
|
||||
$partMatches = ($rule[$part] == $b[$part]);
|
||||
}
|
||||
|
||||
$matchesRule = $matchesRule && $partMatches;
|
||||
if (!$matchesRule) break;
|
||||
}
|
||||
|
||||
$matchesSomeRule[$rulename] = $matchesSomeRule[$rulename] || $matchesRule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it matches both rules - problem if so
|
||||
if ($matchesSomeRule['before'] && $matchesSomeRule['after']) {
|
||||
user_error('Config fragment requires itself to be both before _and_ after another fragment', E_USER_ERROR);
|
||||
}
|
||||
|
||||
return $matchesSomeRule['before'] ? 'before' : ($matchesSomeRule['after'] ? 'after' : 'undefined');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function filters the loaded yaml fragments, removing any that can't ever have their "only" and "except" rules
|
||||
* match
|
||||
*
|
||||
* Some tests in "only" and "except" rules need to be checked per request, but some are manifest based -
|
||||
* these are invariant over requests and only need checking on manifest rebuild. So we can prefilter these before
|
||||
* saving yamlConfigFragments to speed up the process of checking the per-request variant/
|
||||
*/
|
||||
function prefilterYamlFragments() {
|
||||
$matchingFragments = array();
|
||||
|
||||
foreach ($this->yamlConfigFragments as $i => $fragment) {
|
||||
$failsonly = isset($fragment['only']) && !$this->matchesPrefilterVariantRules($fragment['only']);
|
||||
$matchesexcept = isset($fragment['except']) && $this->matchesPrefilterVariantRules($fragment['except']);
|
||||
|
||||
if (!$failsonly && !$matchesexcept) $matchingFragments[] = $fragment;
|
||||
}
|
||||
|
||||
$this->yamlConfigFragments = $matchingFragments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the prefilterable parts of the rule aren't met, and true if they are
|
||||
*
|
||||
* @param $rules array - A hash of rules as allowed in the only or except portion of a config fragment header
|
||||
* @return bool - True if the rules are met, false if not. (Note that depending on whether we were passed an only or an except rule,
|
||||
* which values means accept or reject a fragment
|
||||
*/
|
||||
function matchesPrefilterVariantRules($rules) {
|
||||
foreach ($rules as $k => $v) {
|
||||
switch (strtolower($k)) {
|
||||
case 'classexists':
|
||||
if (!ClassInfo::exists($v)) return false;
|
||||
break;
|
||||
|
||||
case 'moduleexists':
|
||||
if (!$this->moduleExists($v)) return false;
|
||||
break;
|
||||
|
||||
default:
|
||||
// NOP
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the variant key spec - the list of values that need to be build to give a key that uniquely identifies this variant.
|
||||
*/
|
||||
function buildVariantKeySpec() {
|
||||
$this->variantKeySpec = array();
|
||||
|
||||
foreach ($this->yamlConfigFragments as $fragment) {
|
||||
if (isset($fragment['only'])) $this->addVariantKeySpecRules($fragment['only']);
|
||||
if (isset($fragment['except'])) $this->addVariantKeySpecRules($fragment['except']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds any variables referenced in the passed rules to the $this->variantKeySpec array
|
||||
*/
|
||||
function addVariantKeySpecRules($rules) {
|
||||
foreach ($rules as $k => $v) {
|
||||
switch (strtolower($k)) {
|
||||
case 'classexists':
|
||||
case 'moduleexists':
|
||||
// Classes and modules are a special case - we can pre-filter on config regenerate because we already know
|
||||
// if the class or module exists
|
||||
break;
|
||||
|
||||
case 'environment':
|
||||
$this->variantKeySpec['environment'] = true;
|
||||
break;
|
||||
|
||||
case 'envvarset':
|
||||
if (!isset($this->variantKeySpec['envvars'])) $this->variantKeySpec['envvars'] = array();
|
||||
$this->variantKeySpec['envvars'][$k] = $k;
|
||||
break;
|
||||
|
||||
case 'constantdefined':
|
||||
if (!isset($this->variantKeySpec['constants'])) $this->variantKeySpec['constants'] = array();
|
||||
$this->variantKeySpec['constants'][$k] = $k;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (!isset($this->variantKeySpec['envvars'])) $this->variantKeySpec['envvars'] = array();
|
||||
if (!isset($this->variantKeySpec['constants'])) $this->variantKeySpec['constants'] = array();
|
||||
$this->variantKeySpec['envvars'][$k] = $this->variantKeySpec['constants'][$k] = $k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates which yaml config fragments are applicable in this variant, and merge those all together into
|
||||
* the $this->yamlConfig propperty
|
||||
*/
|
||||
function buildYamlConfigVariant($cache = true) {
|
||||
$this->yamlConfig = array();
|
||||
|
||||
foreach ($this->yamlConfigFragments as $i => $fragment) {
|
||||
$failsonly = isset($fragment['only']) && !$this->matchesVariantRules($fragment['only']);
|
||||
$matchesexcept = isset($fragment['except']) && $this->matchesVariantRules($fragment['except']);
|
||||
|
||||
if (!$failsonly && !$matchesexcept) $this->mergeInYamlFragment($this->yamlConfig, $fragment['fragment']);
|
||||
}
|
||||
|
||||
if ($cache) {
|
||||
$this->cache->save($this->yamlConfig, 'yaml_config_'.$this->variantKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the non-prefilterable parts of the rule aren't met, and true if they are
|
||||
*/
|
||||
function matchesVariantRules($rules) {
|
||||
foreach ($rules as $k => $v) {
|
||||
switch (strtolower($k)) {
|
||||
case 'classexists':
|
||||
case 'moduleexists':
|
||||
break;
|
||||
|
||||
case 'environment':
|
||||
switch (strtolower($v)) {
|
||||
case 'live':
|
||||
if (!Director::isLive()) return false;
|
||||
break;
|
||||
case 'test':
|
||||
if (!Director::isTest()) return false;
|
||||
break;
|
||||
case 'dev':
|
||||
if (!Director::isDev()) return false;
|
||||
break;
|
||||
default:
|
||||
user_error('Unknown environment '.$v.' in config fragment', E_USER_ERROR);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'envvarset':
|
||||
if (isset($_ENV[$k])) break;
|
||||
return false;
|
||||
|
||||
case 'constantdefined':
|
||||
if (defined($k)) break;
|
||||
return false;
|
||||
|
||||
default:
|
||||
if (isset($_ENV[$k]) && $_ENV[$k] == $v) break;
|
||||
if (defined($k) && constant($k) == $v) break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively merge a yaml fragment's configuration array into the primary merged configuration array.
|
||||
* @param $into
|
||||
* @param $fragment
|
||||
* @return void
|
||||
*/
|
||||
function mergeInYamlFragment(&$into, $fragment) {
|
||||
foreach ($fragment as $k => $v) {
|
||||
if (is_array($v) || is_object($v)) {
|
||||
if (isset($into[$k])) { $sub = $into[$k]; $this->mergeInYamlFragment($sub, $v); $into[$k] = $sub; }
|
||||
else $into[$k] = $v;
|
||||
}
|
||||
else if (is_numeric($k)) $into[] = $v;
|
||||
else $into[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
/**
|
||||
* An extension to the default file finder with some extra filters to faciliate
|
||||
* autoload and template manifest generation:
|
||||
* - Only modules with _config.php files arescanned.
|
||||
* - Only modules with _config.php files are scanned.
|
||||
* - If a _manifest_exclude file is present inside a directory it is ignored.
|
||||
* - Assets and module language directories are ignored.
|
||||
* - Module tests directories are skipped if the ignore_tests option is not
|
||||
@ -14,6 +14,7 @@
|
||||
class ManifestFileFinder extends SS_FileFinder {
|
||||
|
||||
const CONFIG_FILE = '_config.php';
|
||||
const CONFIG_DIR = '_config';
|
||||
const EXCLUDE_FILE = '_manifest_exclude';
|
||||
const LANG_DIR = 'lang';
|
||||
const TESTS_DIR = 'tests';
|
||||
@ -53,6 +54,7 @@ class ManifestFileFinder extends SS_FileFinder {
|
||||
$depth == 1
|
||||
&& !($this->getOption('include_themes') && $basename == THEMES_DIR)
|
||||
&& !file_exists($pathname . '/' . self::CONFIG_FILE)
|
||||
&& !file_exists($pathname . '/' . self::CONFIG_DIR)
|
||||
);
|
||||
|
||||
if ($lackingConfig) {
|
||||
|
66
tests/core/ConfigTest.php
Normal file
66
tests/core/ConfigTest.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
class ConfigTest_DefinesFoo extends Object {
|
||||
protected static $foo = 1;
|
||||
}
|
||||
|
||||
class ConfigTest_DefinesBar extends ConfigTest_DefinesFoo {
|
||||
public static $bar = 2;
|
||||
}
|
||||
|
||||
class ConfigTest_DefinesFooAndBar extends ConfigTest_DefinesFoo {
|
||||
protected static $foo = 3;
|
||||
public static $bar = 3;
|
||||
}
|
||||
|
||||
class ConfigTest_DefinesFooDoesntExtendObject {
|
||||
protected static $foo = 4;
|
||||
}
|
||||
|
||||
class ConfigTest extends SapphireTest {
|
||||
|
||||
function testMerges() {
|
||||
$result = array('A' => 1, 'B' => 2, 'C' => 3);
|
||||
Config::merge_array_low_into_high($result, array('C' => 4, 'D' => 5));
|
||||
$this->assertEquals($result, array('A' => 1, 'B' => 2, 'C' => 3, 'D' => 5));
|
||||
|
||||
$result = array('A' => 1, 'B' => 2, 'C' => 3);
|
||||
Config::merge_array_high_into_low($result, array('C' => 4, 'D' => 5));
|
||||
$this->assertEquals($result, array('A' => 1, 'B' => 2, 'C' => 4, 'D' => 5));
|
||||
|
||||
$result = array('A' => 1, 'B' => 2, 'C' => array(1, 2, 3));
|
||||
Config::merge_array_low_into_high($result, array('C' => array(4, 5, 6), 'D' => 5));
|
||||
$this->assertEquals($result, array('A' => 1, 'B' => 2, 'C' => array(1, 2, 3, 4, 5, 6), 'D' => 5));
|
||||
|
||||
$result = array('A' => 1, 'B' => 2, 'C' => array(1, 2, 3));
|
||||
Config::merge_array_high_into_low($result, array('C' => array(4, 5, 6), 'D' => 5));
|
||||
$this->assertEquals($result, array('A' => 1, 'B' => 2, 'C' => array(4, 5, 6, 1, 2, 3), 'D' => 5));
|
||||
|
||||
$result = array('A' => 1, 'B' => 2, 'C' => array('Foo' => 1, 'Bar' => 2), 'D' => 3);
|
||||
Config::merge_array_low_into_high($result, array('C' => array('Bar' => 3, 'Baz' => 4)));
|
||||
$this->assertEquals($result, array('A' => 1, 'B' => 2, 'C' => array('Foo' => 1, 'Bar' => 2, 'Baz' => 4), 'D' => 3));
|
||||
|
||||
$result = array('A' => 1, 'B' => 2, 'C' => array('Foo' => 1, 'Bar' => 2), 'D' => 3);
|
||||
Config::merge_array_high_into_low($result, array('C' => array('Bar' => 3, 'Baz' => 4)));
|
||||
$this->assertEquals($result, array('A' => 1, 'B' => 2, 'C' => array('Foo' => 1, 'Bar' => 3, 'Baz' => 4), 'D' => 3));
|
||||
}
|
||||
|
||||
function testStaticLookup() {
|
||||
$this->assertEquals(Object::static_lookup('ConfigTest_DefinesFoo', 'foo'), 1);
|
||||
$this->assertEquals(Object::static_lookup('ConfigTest_DefinesFoo', 'bar'), null);
|
||||
|
||||
$this->assertEquals(Object::static_lookup('ConfigTest_DefinesBar', 'foo'), null);
|
||||
$this->assertEquals(Object::static_lookup('ConfigTest_DefinesBar', 'bar'), 2);
|
||||
|
||||
$this->assertEquals(Object::static_lookup('ConfigTest_DefinesFooAndBar', 'foo'), 3);
|
||||
$this->assertEquals(Object::static_lookup('ConfigTest_DefinesFooAndBar', 'bar'), 3);
|
||||
|
||||
$this->assertEquals(Object::static_lookup('ConfigTest_DefinesFooDoesntExtendObject', 'foo'), 4);
|
||||
$this->assertEquals(Object::static_lookup('ConfigTest_DefinesFooDoesntExtendObject', 'bar'), null);
|
||||
}
|
||||
|
||||
function testFragmentOrder() {
|
||||
// $manifest = new SS_ConfigManifest(BASE_PATH, false, true);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user