Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/config #1244

Merged
merged 10 commits into from Mar 12, 2013
245 changes: 203 additions & 42 deletions core/Config.php
Expand Up @@ -164,12 +164,43 @@ static public function set_instance($instance) {
}

/**
* Empty construction, otherwise calling singleton('Config') (not the right way to get the current active config
* instance, but people might) gives an error
* Make the newly active Config be a copy of the current active Config instance.
*
* You can then make changes to the configuration by calling update and remove on the new
* value returned by Config::inst(), and then discard those changes later by calling unnest
*/
static public function nest() {
$current = self::$instance;

$new = clone $current;
$new->nestedFrom = $current;
self::set_instance($new);
}

/**
* Change the active Config back to the Config instance the current active Config object
* was copied from
*/
static public function unnest() {
self::set_instance(self::$instance->nestedFrom);
}

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_LRU();
}

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();
Expand All @@ -178,6 +209,13 @@ public function __construct() {
* where value is a config value suppress from any lower priority item */
protected $suppresses = array();

protected $staticManifests = array();

public function pushConfigStaticManifest(SS_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();

Expand All @@ -187,8 +225,9 @@ public function __construct() {
* 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) {
public function pushConfigYamlManifest(SS_ConfigManifest $manifest) {
array_unshift($this->manifests, $manifest->yamlConfig);
$this->cache->clean();

// @todo: Do anything with these. They're for caching after config.php has executed
$this->collectConfigPHPSettings = true;
Expand Down Expand Up @@ -342,34 +381,17 @@ static protected function filter_array_by_suppress_array($array, $suppress) {
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 $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 $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)
*/
public function get($class, $name, $sourceOptions = 0, &$result = null, $suppress = null) {
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;

Expand Down Expand Up @@ -397,19 +419,32 @@ public function get($class, $name, $sourceOptions = 0, &$result = null, $suppres
}
}

// Then look at the static variables
$nothing = new stdClass();

$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) {
$extraSources = Object::get_extra_config_sources($class);
// 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 $value = Object::static_lookup($staticSource, $name, $nothing);
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);
Expand All @@ -418,14 +453,53 @@ public function get($class, $name, $sourceOptions = 0, &$result = null, $suppres
}

// Finally, merge in the values from the parent class
if (($sourceOptions & self::UNINHERITED) != self::UNINHERITED
&& (($sourceOptions & self::FIRST_SET) != self::FIRST_SET || $result === null)) {
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 ($parent) $this->getUncached($parent, $name, $sourceOptions, $result, $suppress, $tags);
}

if ($name == 'routes') {
print_r($result); die;
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 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 $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)
*/
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;

if (($result = $this->cache->get($key)) === false) {
$tags = array();
$result = null;
$this->getUncached($class, $name, $sourceOptions, $result, $suppress, $tags);
$this->cache->set($key, $result, $tags);
}

return $result;
Expand All @@ -452,6 +526,8 @@ public function update($class, $name, $val) {

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

$this->cache->clean("__{$class}__{$name}");
}

/**
Expand Down Expand Up @@ -512,6 +588,91 @@ public function remove($class, $name) {

}

class Config_LRU {
const SIZE = 1000;

protected $cache;
protected $indexing;

protected $i = 0;
protected $c = 0;

public function __construct() {
$this->cache = new SplFixedArray(self::SIZE);

// Pre-fill with stdClass instances. By reusing we avoid object-thrashing
for ($i = 0; $i < self::SIZE; $i++) {
$this->cache[$i] = new stdClass();
$this->cache[$i]->key = null;
}

$this->indexing = array();
}

public function set($key, $val, $tags = array()) {
// Find an index to set at
$replacing = null;

// Target count - not always the lowest, but guaranteed to exist (or hit an empty item)
$target = $this->c - self::SIZE + 1;
$i = $stop = $this->i;

do {
if (!($i--)) $i = self::SIZE-1;
$item = $this->cache[$i];

if ($item->key === null) { $replacing = null; break; }
else if ($item->c <= $target) { $replacing = $item; break; }
}
while ($i != $stop);

if ($replacing) unset($this->indexing[$replacing->key]);

$this->indexing[$key] = $this->i = $i;

$obj = $this->cache[$i];
$obj->key = $key;
$obj->value = $val;
$obj->tags = $tags;
$obj->c = ++$this->c;
}

private $hit = 0;
private $miss = 0;

public function stats() {
return $this->miss ? ($this->hit / $this->miss) : 0;
}

public function get($key) {
if (isset($this->indexing[$key])) {
$this->hit++;

$res = $this->cache[$this->indexing[$key]];
$res->c = ++$this->c;
return $res->value;
}

$this->miss++;
return false;
}

public function clean($tag = null) {
if ($tag) {
foreach ($this->cache as $i => $v) {
if ($v->key !== null && in_array($tag, $v->tags)) {
unset($this->indexing[$v->key]);
$this->cache[$i]->key = null;
}
}
}
else {
for ($i = 0; $i < self::SIZE; $i++) $this->cache[$i]->key = null;
$this->indexing = array();
}
}
}

class Config_ForClass {
protected $class;

Expand Down
6 changes: 5 additions & 1 deletion core/Core.php
Expand Up @@ -288,9 +288,13 @@
require_once BASE_PATH . '/vendor/autoload.php';
}

// Now that the class manifest is up, load the configuration
$configManifest = new SS_ConfigStaticManifest(BASE_PATH, false, $flush);
Config::inst()->pushConfigStaticManifest($configManifest);

// Now that the class manifest is up, load the configuration
$configManifest = new SS_ConfigManifest(BASE_PATH, false, $flush);
Config::inst()->pushConfigManifest($configManifest);
Config::inst()->pushConfigYamlManifest($configManifest);

SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest(
BASE_PATH, project(), false, isset($_GET['flush'])
Expand Down