From 6b986cb17d101eecec6e79ba10d2b27389d8b857 Mon Sep 17 00:00:00 2001 From: Hamish Friedlander Date: Tue, 26 Feb 2013 16:44:48 +1300 Subject: [PATCH] Extract statics via code analysis rather than introspection --- core/Config.php | 31 ++- core/Core.php | 6 +- core/manifest/ConfigStaticManifest.php | 309 +++++++++++++++++++++++++ dev/TestRunner.php | 4 + 4 files changed, 341 insertions(+), 9 deletions(-) create mode 100644 core/manifest/ConfigStaticManifest.php diff --git a/core/Config.php b/core/Config.php index 5a945fb5a..95a771550 100644 --- a/core/Config.php +++ b/core/Config.php @@ -181,6 +181,13 @@ class Config { * 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(); @@ -190,7 +197,7 @@ class Config { * 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(); @@ -384,9 +391,6 @@ class Config { } } - // 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 @@ -401,9 +405,18 @@ class Config { 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); @@ -412,8 +425,10 @@ class Config { } // 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->getUncached($parent, $name, $sourceOptions, $result, $suppress, $tags); } diff --git a/core/Core.php b/core/Core.php index 0ff5d2425..541f56122 100644 --- a/core/Core.php +++ b/core/Core.php @@ -288,9 +288,13 @@ if(file_exists(BASE_PATH . '/vendor/autoload.php')) { 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']) diff --git a/core/manifest/ConfigStaticManifest.php b/core/manifest/ConfigStaticManifest.php new file mode 100644 index 000000000..0e42ef8a0 --- /dev/null +++ b/core/manifest/ConfigStaticManifest.php @@ -0,0 +1,309 @@ +base = $base; + $this->tests = $includeTests; + + $this->cache = SS_Cache::factory('SS_ConfigStatics', 'Core', array( + 'automatic_serialization' => true, + 'lifetime' => null + )); + + $this->key = 'sc_'.sha1($base . ($includeTests ? '!tests' : '')); + + if(!$forceRegen) { + $this->index = $this->cache->load($this->key); + } + + if($this->index) { + $this->statics = $this->index['$statics']; + } + else { + $this->regenerate($cache); + } + } + + public function get($class, $name, $default) { + if (!isset($this->statics[$class])) { + if (isset($this->index[$class])) { + $info = $this->index[$class]; + $this->statics[$class] = $this->cache->load($this->key.'_'.sha1($class)); + + if (!isset($this->statics[$class])) { + $this->handleFile(null, $info['path'], null); + } + } + else { + $this->statics[$class] = false; + } + } + + $statics = $this->statics; + + if (isset($statics[$class]) && $statics[$class] && array_key_exists($name, $statics[$class])) { + if ($statics[$class][$name]['access'] != T_PRIVATE) { + Deprecation::notice('3.1.0', "Config static $class::\$$name must be marked as private", Deprecation::SCOPE_GLOBAL); + // Don't warn more than once per static + $this->statics[$class][$name]['access'] = T_PRIVATE; + } + + return $statics[$class][$name]['value']; + } + + return $default; + } + + /** + * Completely regenerates the manifest file. + */ + public function regenerate($cache = true) { + $this->statics = array(); + + $finder = new ManifestFileFinder(); + $finder->setOptions(array( + 'name_regex' => '/^([^_].*\.php)$/', + 'ignore_files' => array('index.php', 'main.php', 'cli-script.php'), + 'ignore_tests' => !$this->tests, + 'file_callback' => array($this, 'handleFile') + )); + + $finder->find($this->base); + + $index = array('$statics' => array()); + + foreach ($this->statics as $class => $details) { + $this->cache->save($details, $this->key.'_'.sha1($class)); + + $index[$class] = array( + 'path' => $details['path'], + 'mtime' => filemtime($details['path']) + ); + + if (in_array($class, self::$initial_classes)) { + $index['$statics'][$class] = $details; + } + } + + if($cache) { + $this->cache->save($index, $this->key); + } + } + + public function handleFile($basename, $pathname, $depth) { + $parser = new SS_ConfigStaticManifest_Parser($pathname); + $this->statics = array_merge($this->statics, $parser->parse()); + } + + public function getStatics() { + return $this->statics; + } +} + +/** + * A parser that processes a PHP file, using PHP's built in parser to get a string of tokens, + * then processing them to find the static class variables, their access levels & values + * + * We can't do this using TokenisedRegularExpression because we need to keep track of state + * as we process the token list (when we enter and leave a namespace or class, when we see + * an access level keyword, etc) + */ +class SS_ConfigStaticManifest_Parser { + + protected $statics = array(); + + protected $path; + protected $tokens; + protected $length; + protected $pos; + + function __construct($path) { + $this->path = $path; + $file = file_get_contents($path); + + $this->tokens = token_get_all($file); + $this->length = count($this->tokens); + $this->pos = 0; + } + + /** + * Get the next token to process, incrementing the pointer + * + * @param bool $ignoreWhitespace - if true will skip any whitespace tokens & only return non-whitespace ones + * @return null | int - Either the next token or null if there isn't one + */ + protected function next($ignoreWhitespace = true) { + do { + if($this->pos >= $this->length) return null; + $next = $this->tokens[$this->pos++]; + } + while($ignoreWhitespace && is_array($next) && $next[0] == T_WHITESPACE); + + return $next; + } + + /** + * Parse the given file to find the static variables declared in it, along with their access & values + */ + function parse() { + $depth = 0; $namespace = null; $class = null; $clsdepth = null; $access = 0; + + while($token = $this->next()) { + $type = is_array($token) ? $token[0] : $token; + + if($type == T_CLASS) { + $next = $this->next(); + + if($next[0] != T_STRING) { + user_error("Couldn\'t parse {$this->path} when building config static manifest", E_USER_ERROR); + } + + $class = $next[1]; + } + else if($type == T_NAMESPACE) { + $next = $this->next(); + + if($next[0] != T_STRING) { + user_error("Couldn\'t parse {$this->path} when building config static manifest", E_USER_ERROR); + } + + $namespace = $next[1]; + } + else if($type == '{' || $type == T_CURLY_OPEN || $type == T_DOLLAR_OPEN_CURLY_BRACES){ + $depth += 1; + if($class && !$clsdepth) $clsdepth = $depth; + } + else if($type == '}') { + $depth -= 1; + if($depth < $clsdepth) $class = $clsdepth = null; + if($depth < 0) user_error("Hmm - depth calc wrong, hit negatives", E_USER_ERROR); + } + else if($type == T_PUBLIC || $type == T_PRIVATE || $type == T_PROTECTED) { + $access = $type; + } + else if($type == T_STATIC) { + if($class && $depth == $clsdepth) $this->parseStatic($access, $namespace ? $namespace.'\\'.$class : $class); + } + else { + $access = ''; + } + } + + return $this->statics; + } + + /** + * During parsing we've found a "static" keyword. Parse out the variable names and value + * assignments that follow. + * + * Seperated out from parse partially so that we can recurse if there are multiple statics + * being declared in a comma seperated list + */ + function parseStatic($access, $class) { + $variable = null; + $value = ''; + + while($token = $this->next()) { + $type = is_array($token) ? $token[0] : $token; + + if($type == T_PUBLIC || $type == T_PRIVATE || $type == T_PROTECTED) { + $access = $type; + } + else if($type == T_FUNCTION) { + return; + } + else if($type == T_VARIABLE) { + $variable = substr($token[1], 1); // Cut off initial "$" + } + else if($type == ';' || $type == ',' || $type == '=') { + break; + } + else { + echo "What's this?\n"; + print_r($token); + return; + } + } + + if($token == '=') { + $depth = 0; + + while($token = $this->next(false)){ + $type = is_array($token) ? $token[0] : $token; + + // Track array nesting depth + if($type == T_ARRAY) { + $depth += 1; + } + else if($type == ')') { + $depth -= 1; + } + + // Parse out the assignment side of a static declaration, ending on either a ';' or a ',' outside an array + if($type == T_WHITESPACE) { + $value .= ' '; + } + else if($type == ';' || ($type == ',' && !$depth)) { + break; + } + // Statics can reference class constants with self:: (and that won't work in eval) + else if($type == T_STRING && $token[1] == 'self') { + $value .= $class; + } + else { + $value .= is_array($token) ? $token[1] : $token; + } + } + } + + if(!isset($this->statics[$class])) { + $this->statics[$class] = array( + 'path' => $this->path, + 'statics' => array() + ); + } + + $this->statics[$class][$variable] = array( + 'access' => $access, + 'value' => eval('return '.$value.';') + ); + + if($token == ',') $this->parseStatic($access, $class); + } +} \ No newline at end of file diff --git a/dev/TestRunner.php b/dev/TestRunner.php index cf9395d5e..eed1fc347 100644 --- a/dev/TestRunner.php +++ b/dev/TestRunner.php @@ -88,6 +88,10 @@ class TestRunner extends Controller { SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest( BASE_PATH, project(), true, isset($_GET['flush']) )); + + Config::inst()->pushConfigStaticManifest(new SS_ConfigStaticManifest( + BASE_PATH, true, isset($_GET['flush']) + )); } public function init() {