silverstripe-framework/core/manifest/ConfigStaticManifest.php

480 lines
13 KiB
PHP

<?php
/**
* Allows access to config values set on classes using private statics.
*
* @package framework
* @subpackage manifest
*/
class SS_ConfigStaticManifest_Reflection extends SS_ConfigStaticManifest {
/**
* Constructs and initialises a new config static manifest, either loading the data
* from the cache or re-scanning for classes.
*
* @param string $base The manifest base path.
* @param bool $includeTests Include the contents of "tests" directories.
* @param bool $forceRegen Force the manifest to be regenerated.
* @param bool $cache If the manifest is regenerated, cache it.
*/
public function __construct($base, $includeTests = false, $forceRegen = false, $cache = true) {
// Stubbed as these parameters are not needed for the newer SS_ConficStaticManifest version.
}
/**
* Completely regenerates the manifest file.
*/
public function regenerate($cache = true) {
Deprecation::notice('4.0', 'This method is no longer available as ' . __CLASS__ . ' uses Reflection.');
}
/**
* @param string $class
* @param string $name
* @param null $default
*
* @return mixed|null
*/
public function get($class, $name, $default = null) {
if(class_exists($class)) {
// The config system is case-sensitive so we need to check the exact value
$reflection = new ReflectionClass($class);
if(strcmp($reflection->name, $class) === 0) {
if($reflection->hasProperty($name)) {
$property = $reflection->getProperty($name);
if($property->isStatic()) {
if(!$property->isPrivate()) {
Deprecation::notice('4.0', "Config static $class::\$$name must be marked as private",
Deprecation::SCOPE_GLOBAL);
return null;
}
$property->setAccessible(true);
return $property->getValue();
}
}
}
}
return null;
}
public function getStatics() {
Deprecation::notice('4.0', 'This method is no longer available as ' . __CLASS__ . ' uses Reflection.');
return array();
}
}
/**
* A utility class which builds a manifest of the statics defined in all classes, along with their
* access levels and values
*
* We use this to make the statics that the Config system uses as default values be truely immutable.
*
* It has the side effect of allowing Config to avoid private-level access restrictions, so we can
* optionally catch attempts to modify the config statics (otherwise the modification will appear
* to work, but won't actually have any effect - the equvilent of failing silently)
*
* @package framework
* @subpackage manifest
*/
class SS_ConfigStaticManifest {
protected $base;
protected $tests;
protected $cache;
protected $key;
protected $index;
protected $statics;
static protected $initial_classes = array(
'Object', 'ViewableData', 'Injector', 'Director'
);
/**
* Constructs and initialises a new config static manifest, either loading the data
* from the cache or re-scanning for classes.
*
* @param string $base The manifest base path.
* @param bool $includeTests Include the contents of "tests" directories.
* @param bool $forceRegen Force the manifest to be regenerated.
* @param bool $cache If the manifest is regenerated, cache it.
*/
public function __construct($base, $includeTests = false, $forceRegen = false, $cache = true) {
$this->base = $base;
$this->tests = $includeTests;
$cacheClass = defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE : 'ManifestCache_File';
$this->cache = new $cacheClass('staticmanifest'.($includeTests ? '_tests' : ''));
$this->key = sha1($base);
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];
if (isset($info['key']) && $details = $this->cache->load($this->key.'_'.$info['key'])) {
$this->statics += $details;
}
if (!isset($this->statics[$class])) {
$this->handleFile(null, $info['path'], null);
}
}
else {
$this->statics[$class] = false;
}
}
if (isset($this->statics[$class][$name])) {
$static = $this->statics[$class][$name];
if ($static['access'] != T_PRIVATE) {
Deprecation::notice('4.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 $static['value'];
}
return $default;
}
/**
* Completely regenerates the manifest file.
*/
public function regenerate($cache = true) {
$this->index = array('$statics' => array());
$this->statics = array();
$finder = new ManifestFileFinder();
$finder->setOptions(array(
'name_regex' => '/^([^_].*\.php)$/',
'ignore_files' => array('index.php', 'main.php', 'cli-script.php', 'SSTemplateParser.php'),
'ignore_tests' => !$this->tests,
'file_callback' => array($this, 'handleFile')
));
$finder->find($this->base);
if($cache) {
$keysets = array();
foreach ($this->statics as $class => $details) {
if (in_array($class, self::$initial_classes)) {
$this->index['$statics'][$class] = $details;
}
else {
$key = sha1($class);
$this->index[$class]['key'] = $key;
$keysets[$key][$class] = $details;
}
}
foreach ($keysets as $key => $details) {
$this->cache->save($details, $this->key.'_'.$key);
}
$this->cache->save($this->index, $this->key);
}
}
public function handleFile($basename, $pathname, $depth) {
$parser = new SS_ConfigStaticManifest_Parser($pathname);
$parser->parse();
$this->index = array_merge($this->index, $parser->getInfo());
$this->statics = array_merge($this->statics, $parser->getStatics());
}
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)
*
* @package framework
* @subpackage manifest
*/
class SS_ConfigStaticManifest_Parser {
protected $info = array();
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;
}
function getInfo() {
return $this->info;
}
function getStatics() {
return $this->statics;
}
/**
* 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 | mixed - 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;
}
/**
* Get the previous token processed. Does *not* decrement the pointer
*
* @param bool $ignoreWhitespace - if true will skip any whitespace tokens & only return non-whitespace ones
* @return null | mixed - Either the previous token or null if there isn't one
*/
protected function lastToken($ignoreWhitespace = true) {
// Subtract 1 as the pointer is always 1 place ahead of the current token
$pos = $this->pos - 1;
do {
if($pos <= 0) return null;
$pos--;
$prev = $this->tokens[$pos];
}
while($ignoreWhitespace && is_array($prev) && $prev[0] == T_WHITESPACE);
return $prev;
}
/**
* Get the next set of tokens that form a string to process,
* incrementing the pointer
*
* @param bool $ignoreWhitespace - if true will skip any whitespace tokens
* & only return non-whitespace ones
* @return null|string - Either the next string or null if there isn't one
*/
protected function nextString($ignoreWhitespace = true) {
static $stop = array('{', '}', '(', ')', '[', ']');
$string = '';
while ($this->pos < $this->length) {
$next = $this->tokens[$this->pos];
if (is_string($next)) {
if (!in_array($next, $stop)) {
$string .= $next;
} else {
break;
}
} else if ($next[0] == T_STRING) {
$string .= $next[1];
} else if ($next[0] != T_WHITESPACE || !$ignoreWhitespace) {
break;
}
$this->pos++;
}
if ($string === '') {
return null;
} else {
return $string;
}
}
/**
* 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 = ($token === (array)$token) ? $token[0] : $token;
if($type == T_CLASS) {
$lastToken = $this->lastToken();
$lastType = ($lastToken === (array)$lastToken) ? $lastToken[0] : $lastToken;
// Ignore class keyword if it's being used for class name resolution: ClassName::class
if ($lastType === T_PAAMAYIM_NEKUDOTAYIM) {
continue;
}
$next = $this->nextString();
if($next === null) {
user_error("Couldn\'t parse {$this->path} when building config static manifest", E_USER_ERROR);
}
$class = $next;
}
else if($type == T_NAMESPACE) {
$namespace = '';
while(true) {
$next = $this->next();
if($next == ';') {
break;
} elseif($next[0] == T_NS_SEPARATOR) {
$namespace .= $next[1];
$next = $this->next();
}
if(!is_string($next) && $next[0] != T_STRING) {
user_error("Couldn\'t parse {$this->path} when building config static manifest", E_USER_ERROR);
}
$namespace .= is_string($next) ? $next : $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, see: ".$this->path, E_USER_ERROR);
}
else if($type == T_PUBLIC || $type == T_PRIVATE || $type == T_PROTECTED) {
$access = $type;
}
else if($type == T_STATIC && $class && $depth == $clsdepth) {
$this->parseStatic($access, $namespace ? $namespace.'\\'.$class : $class);
$access = 0;
}
else {
$access = 0;
}
}
}
/**
* 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 = ($token === (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 if($type == T_COMMENT || $type == T_DOC_COMMENT) {
// NOP
}
else {
user_error('Unexpected token ("' . token_name($type) . '") when building static manifest in class "'
. $class . '": '.print_r($token, true), E_USER_ERROR);
}
}
if($token == '=') {
$depth = 0;
while($token = ($this->pos >= $this->length) ? null : $this->tokens[$this->pos++]) {
$type = ($token === (array)$token) ? $token[0] : $token;
// Track array nesting depth
if($type == T_ARRAY || $type == '[') {
$depth += 1;
} elseif($type == ')' || $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 .= ($token === (array)$token) ? $token[1] : $token;
}
}
}
if (!isset($this->info[$class])) {
$this->info[$class] = array(
'path' => $this->path,
'mtime' => filemtime($this->path),
);
}
if(!isset($this->statics[$class])) {
$this->statics[$class] = array();
}
$value = trim($value);
if ($value) {
$value = eval('static $temp = '.$value.";\n".'return $temp'.";\n");
}
else {
$value = null;
}
$this->statics[$class][$variable] = array(
'access' => $access,
'value' => $value
);
if($token == ',') $this->parseStatic($access, $class);
}
}