2011-12-22 03:17:41 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A utility class which builds a manifest of configuration items
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2012-04-12 08:02:46 +02:00
|
|
|
* @package framework
|
2011-12-22 03:17:41 +01:00
|
|
|
* @subpackage manifest
|
|
|
|
*/
|
|
|
|
class SS_ConfigManifest {
|
|
|
|
|
2013-07-01 05:25:21 +02:00
|
|
|
/** @var string - The base path used when building the manifest */
|
|
|
|
protected $base;
|
|
|
|
|
|
|
|
/** @var string - A string to prepend to all cache keys to ensure all keys are unique to just this $base */
|
|
|
|
protected $key;
|
|
|
|
|
|
|
|
/** @var bool - Whether `test` directories should be searched when searching for configuration */
|
|
|
|
protected $includeTests;
|
|
|
|
|
|
|
|
/**
|
2012-09-26 23:34:00 +02:00
|
|
|
* All the values needed to be collected to determine the correct combination of fragements for
|
|
|
|
* the current environment.
|
|
|
|
* @var array
|
|
|
|
*/
|
2013-07-09 13:01:46 +02:00
|
|
|
protected $variantKeySpec = false;
|
2011-12-22 03:17:41 +01:00
|
|
|
|
2012-09-26 23:34:00 +02:00
|
|
|
/**
|
|
|
|
* All the _config.php files. Need to be included every request & can't be cached. Not variant specific.
|
|
|
|
* @var array
|
|
|
|
*/
|
2011-12-22 03:17:41 +01:00
|
|
|
protected $phpConfigSources = array();
|
|
|
|
|
2012-09-26 23:34:00 +02:00
|
|
|
/**
|
|
|
|
* All the _config/*.yml fragments pre-parsed and sorted in ascending include order. Not variant specific.
|
|
|
|
* @var array
|
|
|
|
*/
|
2011-12-22 03:17:41 +01:00
|
|
|
protected $yamlConfigFragments = array();
|
|
|
|
|
2012-09-26 23:34:00 +02:00
|
|
|
/**
|
|
|
|
* The calculated config from _config/*.yml, sorted, filtered and merged. Variant specific.
|
|
|
|
* @var array
|
|
|
|
*/
|
2011-12-22 03:17:41 +01:00
|
|
|
public $yamlConfig = array();
|
|
|
|
|
2013-07-01 05:25:21 +02:00
|
|
|
/**
|
|
|
|
* The variant key state as when yamlConfig was loaded
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $yamlConfigVariantKey = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var [callback] A list of callbacks to be called whenever the content of yamlConfig changes
|
|
|
|
*/
|
|
|
|
protected $configChangeCallbacks = array();
|
|
|
|
|
2012-09-26 23:34:00 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
* @var array
|
|
|
|
*/
|
2011-12-22 03:17:41 +01:00
|
|
|
public $modules = array();
|
|
|
|
|
|
|
|
/** Adds a path as a module */
|
2012-09-19 12:07:39 +02:00
|
|
|
public function addModule($path) {
|
2011-12-22 03:17:41 +01:00
|
|
|
$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 */
|
2012-09-19 12:07:39 +02:00
|
|
|
public function moduleExists($module) {
|
2011-12-22 03:17:41 +01:00
|
|
|
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;
|
2013-06-28 01:25:14 +02:00
|
|
|
$this->key = sha1($base).'_';
|
2013-07-01 05:25:21 +02:00
|
|
|
$this->includeTests = $includeTests;
|
2011-12-22 03:17:41 +01:00
|
|
|
|
|
|
|
// Get the Zend Cache to load/store cache into
|
2013-07-09 13:01:46 +02:00
|
|
|
$this->cache = $this->getCache();
|
2011-12-22 03:17:41 +01:00
|
|
|
|
|
|
|
// Unless we're forcing regen, try loading from cache
|
|
|
|
if (!$forceRegen) {
|
|
|
|
// The PHP config sources are always needed
|
2013-06-28 01:25:14 +02:00
|
|
|
$this->phpConfigSources = $this->cache->load($this->key.'php_config_sources');
|
2012-04-07 03:09:26 +02:00
|
|
|
// Get the variant key spec
|
2013-06-28 01:25:14 +02:00
|
|
|
$this->variantKeySpec = $this->cache->load($this->key.'variant_key_spec');
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
2013-07-01 05:25:21 +02:00
|
|
|
// If we don't have a variantKeySpec (because we're forcing regen, or it just wasn't in the cache), generate it
|
2017-01-14 23:13:47 +01:00
|
|
|
if (false === $this->phpConfigSources || false === $this->variantKeySpec) {
|
2011-12-22 03:17:41 +01:00
|
|
|
$this->regenerate($includeTests);
|
|
|
|
}
|
2013-07-01 05:25:21 +02:00
|
|
|
|
|
|
|
// At this point $this->variantKeySpec will always contain something valid, so we can build the variant
|
|
|
|
$this->buildYamlConfigVariant();
|
|
|
|
}
|
|
|
|
|
2013-07-09 13:01:46 +02:00
|
|
|
/**
|
|
|
|
* Provides a hook for mock unit tests despite no DI
|
|
|
|
* @return Zend_Cache_Frontend
|
|
|
|
*/
|
|
|
|
protected function getCache()
|
|
|
|
{
|
|
|
|
return SS_Cache::factory('SS_Configuration', 'Core', array(
|
|
|
|
'automatic_serialization' => true,
|
2017-07-18 17:51:03 +02:00
|
|
|
'lifetime' => null,
|
|
|
|
'cache_id_prefix' => 'SS_Configuration_',
|
2013-07-09 13:01:46 +02:00
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2013-07-01 05:25:21 +02:00
|
|
|
/**
|
|
|
|
* Register a callback to be called whenever the calculated merged config changes
|
|
|
|
*
|
|
|
|
* In some situations the merged config can change - for instance, code in _config.php can cause which Only
|
|
|
|
* and Except fragments match. Registering a callback with this function allows code to be called when
|
|
|
|
* this happens.
|
|
|
|
*
|
|
|
|
* @param callback $callback
|
|
|
|
*/
|
|
|
|
public function registerChangeCallback($callback) {
|
|
|
|
$this->configChangeCallbacks[] = $callback;
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
2013-07-01 05:25:21 +02:00
|
|
|
|
|
|
|
if ($this->variantKey() != $this->yamlConfigVariantKey) $this->buildYamlConfigVariant();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the (merged) config value for the given class and config property name
|
|
|
|
*
|
|
|
|
* @param string $class - The class to get the config property value for
|
|
|
|
* @param string $name - The config property to get the value for
|
|
|
|
* @param any $default - What to return if no value was contained in any YAML file for the passed $class and $name
|
|
|
|
* @return any - The merged set of all values contained in all the YAML configuration files for the passed
|
|
|
|
* $class and $name, or $default if there are none
|
|
|
|
*/
|
|
|
|
public function get($class, $name, $default=null) {
|
|
|
|
if (isset($this->yamlConfig[$class][$name])) return $this->yamlConfig[$class][$name];
|
|
|
|
else return $default;
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2012-09-26 23:34:00 +02:00
|
|
|
* 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"
|
2011-12-22 03:17:41 +01:00
|
|
|
* and "except" rules.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2011-12-22 03:17:41 +01:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function variantKey() {
|
|
|
|
$key = $this->variantKeySpec; // Copy to fill in actual values
|
|
|
|
|
2012-09-26 23:34:00 +02:00
|
|
|
if (isset($key['environment'])) {
|
|
|
|
$key['environment'] = Director::isDev() ? 'dev' : (Director::isTest() ? 'test' : 'live');
|
|
|
|
}
|
2011-12-22 03:17:41 +01:00
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2012-09-26 23:34:00 +02:00
|
|
|
* 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.
|
2011-12-22 03:17:41 +01:00
|
|
|
*
|
|
|
|
* Does _not_ build the actual variant
|
|
|
|
*
|
|
|
|
* @param bool $cache Cache the result.
|
|
|
|
*/
|
|
|
|
public function regenerate($includeTests = false, $cache = true) {
|
2012-04-07 03:09:26 +02:00
|
|
|
$this->phpConfigSources = array();
|
|
|
|
$this->yamlConfigFragments = array();
|
|
|
|
$this->variantKeySpec = array();
|
|
|
|
|
2011-12-22 03:17:41 +01:00
|
|
|
$finder = new ManifestFileFinder();
|
|
|
|
$finder->setOptions(array(
|
2012-05-18 06:15:02 +02:00
|
|
|
'name_regex' => '/(^|[\/\\\\])_config.php$/',
|
2011-12-22 03:17:41 +01:00
|
|
|
'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) {
|
2013-06-28 01:25:14 +02:00
|
|
|
$this->cache->save($this->phpConfigSources, $this->key.'php_config_sources');
|
|
|
|
$this->cache->save($this->yamlConfigFragments, $this->key.'yaml_config_fragments');
|
|
|
|
$this->cache->save($this->variantKeySpec, $this->key.'variant_key_spec');
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)));
|
|
|
|
|
2012-04-17 23:34:57 +02:00
|
|
|
// Use the Zend copy of this script to prevent class conflicts when RailsYaml is included
|
2012-09-26 23:34:00 +02:00
|
|
|
require_once 'thirdparty/zend_translate_railsyaml/library/Translate/Adapter/thirdparty/sfYaml/lib/'
|
|
|
|
. 'sfYamlParser.php';
|
2011-12-22 03:17:41 +01:00
|
|
|
$parser = new sfYamlParser();
|
|
|
|
|
|
|
|
// The base header
|
|
|
|
$base = array(
|
|
|
|
'module' => $match[1],
|
|
|
|
'file' => basename(basename($basename, '.yml'), '.yaml')
|
|
|
|
);
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2012-04-16 03:58:23 +02:00
|
|
|
// Make sure the linefeeds are all converted to \n, PCRE '$' will not match anything else.
|
|
|
|
$fileContents = str_replace(array("\r\n", "\r"), "\n", file_get_contents($pathname));
|
|
|
|
|
2011-12-22 03:17:41 +01:00
|
|
|
// 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
|
2012-04-16 03:58:23 +02:00
|
|
|
$parts = preg_split('/^---$/m', $fileContents, -1, PREG_SPLIT_NO_EMPTY);
|
2011-12-22 03:17:41 +01:00
|
|
|
|
|
|
|
// 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
|
2012-09-26 23:34:00 +02:00
|
|
|
if (count($parts) % 2 != 0) {
|
2013-03-21 19:48:54 +01:00
|
|
|
user_error("Configuration file '$pathname' does not have an equal number of headers and config blocks");
|
2012-09-26 23:34:00 +02:00
|
|
|
}
|
2011-12-22 03:17:41 +01:00
|
|
|
|
|
|
|
// 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)
|
2012-08-27 06:00:54 +02:00
|
|
|
if (is_array($header[$order])) $orderparts = $header[$order];
|
|
|
|
else $orderparts = preg_split('/\s*,\s*/', $header[$order], -1, PREG_SPLIT_NO_EMPTY);
|
2011-12-22 03:17:41 +01:00
|
|
|
|
|
|
|
// For each, parse out into module/file#name, and set any missing to "*"
|
|
|
|
$header[$order] = array();
|
|
|
|
foreach($orderparts as $part) {
|
2013-07-01 04:51:32 +02:00
|
|
|
preg_match('! (?P<module>\*|[^\/#]+)? (\/ (?P<file>\*|\w+))? (\# (?P<fragment>\*|\w+))? !x',
|
2012-09-26 23:34:00 +02:00
|
|
|
$part, $match);
|
2012-08-28 05:44:40 +02:00
|
|
|
|
2011-12-22 03:17:41 +01:00
|
|
|
$header[$order][] = array(
|
2012-08-28 05:44:40 +02:00
|
|
|
'module' => isset($match['module']) && $match['module'] ? $match['module'] : '*',
|
|
|
|
'file' => isset($match['file']) && $match['file'] ? $match['file'] : '*',
|
|
|
|
'name' => isset($match['fragment']) && $match['fragment'] ? $match['fragment'] : '*'
|
2011-12-22 03:17:41 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// And add to the fragments list
|
|
|
|
$this->yamlConfigFragments[] = $base + $header + array(
|
|
|
|
'fragment' => $parser->parse($parts[$i+1])
|
|
|
|
);
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
}
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sorts the YAML fragments so that the "before" and "after" rules are met.
|
|
|
|
* Throws an error if there's a loop
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2011-12-22 03:17:41 +01:00
|
|
|
* 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.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2011-12-22 03:17:41 +01:00
|
|
|
* @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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-08-27 06:08:10 +02:00
|
|
|
try {
|
|
|
|
$this->yamlConfigFragments = $dag->sort();
|
|
|
|
}
|
|
|
|
catch (SS_DAG_CyclicException $e) {
|
|
|
|
|
|
|
|
if (!Director::isLive() && isset($_REQUEST['debug'])) {
|
|
|
|
$res = '<h1>Remaining config fragment graph</h1>';
|
|
|
|
$res .= '<dl>';
|
|
|
|
|
|
|
|
foreach ($e->dag as $node) {
|
2012-09-26 23:34:00 +02:00
|
|
|
$res .= "<dt>{$node['from']['module']}/{$node['from']['file']}#{$node['from']['name']}"
|
|
|
|
. " marked to come after</dt><dd><ul>";
|
2012-08-27 06:08:10 +02:00
|
|
|
foreach ($node['to'] as $to) {
|
|
|
|
$res .= "<li>{$to['module']}/{$to['file']}#{$to['name']}</li>";
|
|
|
|
}
|
|
|
|
$res .= "</ul></dd>";
|
|
|
|
}
|
|
|
|
|
|
|
|
$res .= '</dl>';
|
|
|
|
echo $res;
|
|
|
|
}
|
|
|
|
|
2012-09-26 23:34:00 +02:00
|
|
|
user_error('Based on their before & after rules two fragments both need to be before/after each other',
|
|
|
|
E_USER_ERROR);
|
2012-08-27 06:08:10 +02:00
|
|
|
}
|
|
|
|
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2011-12-22 03:17:41 +01:00
|
|
|
/**
|
2012-09-26 23:34:00 +02:00
|
|
|
* 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
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2011-12-22 03:17:41 +01:00
|
|
|
* @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) {
|
2012-08-27 06:03:03 +02:00
|
|
|
$matches = array();
|
|
|
|
|
2011-12-22 03:17:41 +01:00
|
|
|
// Do the same thing for after and before
|
2012-08-27 06:03:03 +02:00
|
|
|
foreach (array('before', 'after') as $rulename) {
|
|
|
|
$matches[$rulename] = array();
|
|
|
|
|
|
|
|
// Figure out for each rule, which part matches
|
|
|
|
if (isset($a[$rulename])) foreach ($a[$rulename] as $rule) {
|
|
|
|
$match = array();
|
|
|
|
|
|
|
|
foreach(array('module', 'file', 'name') as $part) {
|
|
|
|
// If part is *, we match _unless_ the opposite rule has a non-* matcher than also matches $b
|
|
|
|
if ($rule[$part] == '*') {
|
|
|
|
$match[$part] = 'wild';
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$match[$part] = ($rule[$part] == $b[$part]);
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
}
|
2012-08-27 06:03:03 +02:00
|
|
|
|
|
|
|
$matches[$rulename][] = $match;
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
}
|
2012-08-27 06:03:03 +02:00
|
|
|
|
|
|
|
// Figure out the specificness of each match. 1 an actual match, 0 for a wildcard match, remove if no match
|
|
|
|
$matchlevel = array('before' => -1, 'after' => -1);
|
|
|
|
|
|
|
|
foreach (array('before', 'after') as $rulename) {
|
|
|
|
foreach ($matches[$rulename] as $i => $rule) {
|
|
|
|
$level = 0;
|
|
|
|
|
|
|
|
foreach ($rule as $part => $partmatches) {
|
|
|
|
if ($partmatches === false) continue 2;
|
|
|
|
if ($partmatches === true) $level += 1;
|
|
|
|
}
|
|
|
|
|
2012-09-26 23:34:00 +02:00
|
|
|
if ($matchlevel[$rulename] === false || $level > $matchlevel[$rulename]) {
|
|
|
|
$matchlevel[$rulename] = $level;
|
|
|
|
}
|
2012-08-27 06:03:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($matchlevel['before'] === -1 && $matchlevel['after'] === -1) {
|
|
|
|
return 'undefined';
|
|
|
|
}
|
|
|
|
else if ($matchlevel['before'] === $matchlevel['after']) {
|
2011-12-22 03:17:41 +01:00
|
|
|
user_error('Config fragment requires itself to be both before _and_ after another fragment', E_USER_ERROR);
|
|
|
|
}
|
2012-08-27 06:03:03 +02:00
|
|
|
else {
|
|
|
|
return ($matchlevel['before'] > $matchlevel['after']) ? 'before' : 'after';
|
|
|
|
}
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2012-09-26 23:34:00 +02:00
|
|
|
* This function filters the loaded yaml fragments, removing any that can't ever have their "only" and "except"
|
|
|
|
* rules match.
|
2011-12-22 03:17:41 +01:00
|
|
|
*
|
|
|
|
* 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/
|
|
|
|
*/
|
2012-09-19 12:07:39 +02:00
|
|
|
public function prefilterYamlFragments() {
|
2011-12-22 03:17:41 +01:00
|
|
|
$matchingFragments = array();
|
|
|
|
|
|
|
|
foreach ($this->yamlConfigFragments as $i => $fragment) {
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = true;
|
2011-12-22 03:17:41 +01:00
|
|
|
|
2013-06-28 03:46:32 +02:00
|
|
|
if (isset($fragment['only'])) {
|
|
|
|
$matches = $matches && ($this->matchesPrefilterVariantRules($fragment['only']) !== false);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($fragment['except'])) {
|
|
|
|
$matches = $matches && ($this->matchesPrefilterVariantRules($fragment['except']) !== true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($matches) $matchingFragments[] = $fragment;
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$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
|
2012-09-26 23:34:00 +02:00
|
|
|
* @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,
|
2014-08-15 08:53:05 +02:00
|
|
|
* which values means accept or reject a fragment
|
2011-12-22 03:17:41 +01:00
|
|
|
*/
|
2012-09-19 12:07:39 +02:00
|
|
|
public function matchesPrefilterVariantRules($rules) {
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = "undefined"; // Needs to be truthy, but not true
|
|
|
|
|
2011-12-22 03:17:41 +01:00
|
|
|
foreach ($rules as $k => $v) {
|
|
|
|
switch (strtolower($k)) {
|
|
|
|
case 'classexists':
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = $matches && ClassInfo::exists($v);
|
2011-12-22 03:17:41 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'moduleexists':
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = $matches && $this->moduleExists($v);
|
2011-12-22 03:17:41 +01:00
|
|
|
break;
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2011-12-22 03:17:41 +01:00
|
|
|
default:
|
|
|
|
// NOP
|
|
|
|
}
|
2013-06-28 03:46:32 +02:00
|
|
|
|
|
|
|
if ($matches === false) return $matches;
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
2013-06-28 03:46:32 +02:00
|
|
|
return $matches;
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2012-09-26 23:34:00 +02:00
|
|
|
* Builds the variant key spec - the list of values that need to be build to give a key that uniquely identifies
|
|
|
|
* this variant.
|
2011-12-22 03:17:41 +01:00
|
|
|
*/
|
2012-09-19 12:07:39 +02:00
|
|
|
public function buildVariantKeySpec() {
|
2011-12-22 03:17:41 +01:00
|
|
|
$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
|
|
|
|
*/
|
2012-09-19 12:07:39 +02:00
|
|
|
public function addVariantKeySpecRules($rules) {
|
2011-12-22 03:17:41 +01:00
|
|
|
foreach ($rules as $k => $v) {
|
|
|
|
switch (strtolower($k)) {
|
|
|
|
case 'classexists':
|
|
|
|
case 'moduleexists':
|
2012-09-26 23:34:00 +02:00
|
|
|
// Classes and modules are a special case - we can pre-filter on config regenerate because we
|
|
|
|
// already know if the class or module exists
|
2011-12-22 03:17:41 +01:00
|
|
|
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
|
2013-07-01 05:25:21 +02:00
|
|
|
*
|
|
|
|
* Checks cache and takes care of loading yamlConfigFragments if they aren't already present, but expects
|
|
|
|
* $variantKeySpec to already be set
|
2011-12-22 03:17:41 +01:00
|
|
|
*/
|
2012-09-19 12:07:39 +02:00
|
|
|
public function buildYamlConfigVariant($cache = true) {
|
2013-07-01 05:25:21 +02:00
|
|
|
// Only try loading from cache if we don't have the fragments already loaded, as there's no way to know if a
|
|
|
|
// given variant is stale compared to the complete set of fragments
|
|
|
|
if (!$this->yamlConfigFragments) {
|
|
|
|
// First try and just load the exact variant
|
|
|
|
if ($this->yamlConfig = $this->cache->load($this->key.'yaml_config_'.$this->variantKey())) {
|
|
|
|
$this->yamlConfigVariantKey = $this->variantKey();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Otherwise try and load the fragments so we can build the variant
|
|
|
|
else {
|
|
|
|
$this->yamlConfigFragments = $this->cache->load($this->key.'yaml_config_fragments');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we still don't have any fragments we have to build them
|
|
|
|
if (!$this->yamlConfigFragments) {
|
|
|
|
$this->regenerate($this->includeTests, $cache);
|
|
|
|
}
|
|
|
|
|
2011-12-22 03:17:41 +01:00
|
|
|
$this->yamlConfig = array();
|
2013-07-01 05:25:21 +02:00
|
|
|
$this->yamlConfigVariantKey = $this->variantKey();
|
2011-12-22 03:17:41 +01:00
|
|
|
|
|
|
|
foreach ($this->yamlConfigFragments as $i => $fragment) {
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = true;
|
|
|
|
|
|
|
|
if (isset($fragment['only'])) {
|
|
|
|
$matches = $matches && ($this->matchesVariantRules($fragment['only']) !== false);
|
|
|
|
}
|
2011-12-22 03:17:41 +01:00
|
|
|
|
2013-06-28 03:46:32 +02:00
|
|
|
if (isset($fragment['except'])) {
|
|
|
|
$matches = $matches && ($this->matchesVariantRules($fragment['except']) !== true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($matches) $this->mergeInYamlFragment($this->yamlConfig, $fragment['fragment']);
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($cache) {
|
2013-06-28 01:25:14 +02:00
|
|
|
$this->cache->save($this->yamlConfig, $this->key.'yaml_config_'.$this->variantKey());
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
2013-07-01 05:25:21 +02:00
|
|
|
|
|
|
|
// Since yamlConfig has changed, call any callbacks that are interested
|
|
|
|
foreach ($this->configChangeCallbacks as $callback) call_user_func($callback);
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns false if the non-prefilterable parts of the rule aren't met, and true if they are
|
|
|
|
*/
|
2012-09-19 12:07:39 +02:00
|
|
|
public function matchesVariantRules($rules) {
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = "undefined"; // Needs to be truthy, but not true
|
|
|
|
|
2011-12-22 03:17:41 +01:00
|
|
|
foreach ($rules as $k => $v) {
|
|
|
|
switch (strtolower($k)) {
|
|
|
|
case 'classexists':
|
|
|
|
case 'moduleexists':
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'environment':
|
|
|
|
switch (strtolower($v)) {
|
|
|
|
case 'live':
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = $matches && Director::isLive();
|
2011-12-22 03:17:41 +01:00
|
|
|
break;
|
|
|
|
case 'test':
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = $matches && Director::isTest();
|
2011-12-22 03:17:41 +01:00
|
|
|
break;
|
|
|
|
case 'dev':
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = $matches && Director::isDev();
|
2011-12-22 03:17:41 +01:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
user_error('Unknown environment '.$v.' in config fragment', E_USER_ERROR);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'envvarset':
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = $matches && isset($_ENV[$v]);
|
|
|
|
break;
|
2011-12-22 03:17:41 +01:00
|
|
|
|
|
|
|
case 'constantdefined':
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = $matches && defined($v);
|
|
|
|
break;
|
2011-12-22 03:17:41 +01:00
|
|
|
|
|
|
|
default:
|
2013-06-28 03:46:32 +02:00
|
|
|
$matches = $matches && (
|
|
|
|
(isset($_ENV[$k]) && $_ENV[$k] == $v) ||
|
|
|
|
(defined($k) && constant($k) == $v)
|
|
|
|
);
|
|
|
|
break;
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
2013-06-28 03:46:32 +02:00
|
|
|
|
|
|
|
if ($matches === false) return $matches;
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
2013-06-28 03:46:32 +02:00
|
|
|
return $matches;
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Recursively merge a yaml fragment's configuration array into the primary merged configuration array.
|
|
|
|
* @param $into
|
|
|
|
* @param $fragment
|
|
|
|
* @return void
|
|
|
|
*/
|
2012-09-19 12:07:39 +02:00
|
|
|
public function mergeInYamlFragment(&$into, $fragment) {
|
2016-02-12 06:25:01 +01:00
|
|
|
if (is_array($fragment) || ($fragment instanceof Traversable)) {
|
|
|
|
foreach ($fragment as $k => $v) {
|
|
|
|
Config::merge_high_into_low($into[$k], $v);
|
|
|
|
}
|
2011-12-22 03:17:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-03-24 04:04:52 +01:00
|
|
|
}
|