Completed
Push — new-committers ( 29cb6f...bcba16 )
by Sam
12:18 queued 33s
created

SS_ConfigStaticManifest_Parser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 8
rs 9.4285
cc 1
eloc 6
nc 1
nop 1
1
<?php
2
/**
3
 * A utility class which builds a manifest of the statics defined in all classes, along with their
4
 * access levels and values
5
 *
6
 * We use this to make the statics that the Config system uses as default values be truely immutable.
7
 *
8
 * It has the side effect of allowing Config to avoid private-level access restrictions, so we can
9
 * optionally catch attempts to modify the config statics (otherwise the modification will appear
10
 * to work, but won't actually have any effect - the equvilent of failing silently)
11
 *
12
 * @package framework
13
 * @subpackage manifest
14
 */
15
class SS_ConfigStaticManifest {
16
17
	protected $base;
18
	protected $tests;
19
20
	protected $cache;
21
	protected $key;
22
23
	protected $index;
24
	protected $statics;
25
26
	static protected $initial_classes = array(
27
		'Object', 'ViewableData', 'Injector', 'Director'
28
	);
29
30
	/**
31
	 * Constructs and initialises a new config static manifest, either loading the data
32
	 * from the cache or re-scanning for classes.
33
	 *
34
	 * @param string $base The manifest base path.
35
	 * @param bool   $includeTests Include the contents of "tests" directories.
36
	 * @param bool   $forceRegen Force the manifest to be regenerated.
37
	 * @param bool   $cache If the manifest is regenerated, cache it.
38
	 */
39
	public function __construct($base, $includeTests = false, $forceRegen = false, $cache = true) {
40
		$this->base  = $base;
41
		$this->tests = $includeTests;
42
43
		$cacheClass = defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE : 'ManifestCache_File';
44
45
		$this->cache = new $cacheClass('staticmanifest'.($includeTests ? '_tests' : ''));
46
		$this->key = sha1($base);
47
48
		if(!$forceRegen) {
49
			$this->index = $this->cache->load($this->key);
50
		}
51
52
		if($this->index) {
53
			$this->statics = $this->index['$statics'];
54
		}
55
		else {
56
			$this->regenerate($cache);
57
		}
58
	}
59
60
	public function get($class, $name, $default) {
61
		if (!isset($this->statics[$class])) {
62
			if (isset($this->index[$class])) {
63
				$info = $this->index[$class];
64
65
				if (isset($info['key']) && $details = $this->cache->load($this->key.'_'.$info['key'])) {
66
					$this->statics += $details;
67
				}
68
69
				if (!isset($this->statics[$class])) {
70
					$this->handleFile(null, $info['path'], null);
71
				}
72
			}
73
			else {
74
				$this->statics[$class] = false;
75
			}
76
		}
77
78
		if (isset($this->statics[$class][$name])) {
79
			$static = $this->statics[$class][$name];
80
81
			if ($static['access'] != T_PRIVATE) {
82
				Deprecation::notice('4.0', "Config static $class::\$$name must be marked as private",
83
					Deprecation::SCOPE_GLOBAL);
84
				// Don't warn more than once per static
85
				$this->statics[$class][$name]['access'] = T_PRIVATE;
86
			}
87
88
			return $static['value'];
89
		}
90
91
		return $default;
92
	}
93
94
	/**
95
	 * Completely regenerates the manifest file.
96
	 */
97
	public function regenerate($cache = true) {
98
		$this->index = array('$statics' => array());
99
		$this->statics = array();
100
101
		$finder = new ManifestFileFinder();
102
		$finder->setOptions(array(
103
			'name_regex'    => '/^([^_].*\.php)$/',
104
			'ignore_files'  => array('index.php', 'main.php', 'cli-script.php', 'SSTemplateParser.php'),
105
			'ignore_tests'  => !$this->tests,
106
			'file_callback' => array($this, 'handleFile')
107
		));
108
109
		$finder->find($this->base);
110
111
		if($cache) {
112
			$keysets = array();
113
114
			foreach ($this->statics as $class => $details) {
115
				if (in_array($class, self::$initial_classes)) {
116
					$this->index['$statics'][$class] = $details;
117
				}
118
				else {
119
					$key = sha1($class);
120
					$this->index[$class]['key'] = $key;
121
122
					$keysets[$key][$class] = $details;
123
				}
124
			}
125
126
			foreach ($keysets as $key => $details) {
127
				$this->cache->save($details, $this->key.'_'.$key);
128
			}
129
130
			$this->cache->save($this->index, $this->key);
131
		}
132
	}
133
134
	public function handleFile($basename, $pathname, $depth) {
0 ignored issues
show
Unused Code introduced by
The parameter $depth is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
135
		$parser = new SS_ConfigStaticManifest_Parser($pathname);
136
		$parser->parse();
137
138
		$this->index = array_merge($this->index, $parser->getInfo());
139
		$this->statics = array_merge($this->statics, $parser->getStatics());
140
	}
141
142
	public function getStatics() {
143
		return $this->statics;
144
	}
145
}
146
147
/**
148
 * A parser that processes a PHP file, using PHP's built in parser to get a string of tokens,
149
 * then processing them to find the static class variables, their access levels & values
150
 *
151
 * We can't do this using TokenisedRegularExpression because we need to keep track of state
152
 * as we process the token list (when we enter and leave a namespace or class, when we see
153
 * an access level keyword, etc)
154
 *
155
 * @package framework
156
 * @subpackage manifest
157
 */
158
class SS_ConfigStaticManifest_Parser {
159
160
	protected $info = array();
161
	protected $statics = array();
162
163
	protected $path;
164
	protected $tokens;
165
	protected $length;
166
	protected $pos;
167
168
	function __construct($path) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
169
		$this->path = $path;
170
		$file = file_get_contents($path);
171
172
		$this->tokens = token_get_all($file);
173
		$this->length = count($this->tokens);
174
		$this->pos = 0;
175
	}
176
177
	function getInfo() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
178
		return $this->info;
179
	}
180
181
	function getStatics() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
182
		return $this->statics;
183
	}
184
185
	/**
186
	 * Get the next token to process, incrementing the pointer
187
	 *
188
	 * @param bool $ignoreWhitespace - if true will skip any whitespace tokens & only return non-whitespace ones
189
	 * @return null | mixed - Either the next token or null if there isn't one
190
	 */
191
	protected function next($ignoreWhitespace = true) {
192
		do {
193
			if($this->pos >= $this->length) return null;
194
			$next = $this->tokens[$this->pos++];
195
		}
196
		while($ignoreWhitespace && is_array($next) && $next[0] == T_WHITESPACE);
197
198
		return $next;
199
	}
200
201
	/**
202
	 * Get the next set of tokens that form a string to process,
203
	 * incrementing the pointer
204
	 *
205
	 * @param bool $ignoreWhitespace - if true will skip any whitespace tokens
206
	 *             & only return non-whitespace ones
207
	 * @return null|string - Either the next string or null if there isn't one
208
	 */
209
	protected function nextString($ignoreWhitespace = true) {
210
		static $stop = array('{', '}', '(', ')', '[', ']');
211
212
		$string = '';
213
		while ($this->pos < $this->length) {
214
			$next = $this->tokens[$this->pos];
215
			if (is_string($next)) {
216
				if (!in_array($next, $stop)) {
217
					$string .= $next;
218
				} else {
219
					break;
220
				}
221
			} else if ($next[0] == T_STRING) {
222
				$string .= $next[1];
223
			} else if ($next[0] != T_WHITESPACE || !$ignoreWhitespace) {
224
				break;
225
			}
226
			$this->pos++;
227
		}
228
		if ($string === '') {
229
			return null;
230
		} else {
231
			return $string;
232
		}
233
	}
234
235
	/**
236
	 * Parse the given file to find the static variables declared in it, along with their access & values
237
	 */
238
	function parse() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
239
		$depth = 0; $namespace = null; $class = null; $clsdepth = null; $access = 0;
240
241
		while($token = $this->next()) {
242
			$type = ($token === (array)$token) ? $token[0] : $token;
243
244
			if($type == T_CLASS) {
245
				$next = $this->nextString();
246
				if($next === null) {
247
					user_error("Couldn\'t parse {$this->path} when building config static manifest", E_USER_ERROR);
248
				}
249
250
				$class = $next;
251
			}
252
			else if($type == T_NAMESPACE) {
253
				$namespace = '';
254
				while(true) {
255
					$next = $this->next();
256
257
					if($next == ';') {
258
						break;
259
					} elseif($next[0] == T_NS_SEPARATOR) {
260
						$namespace .= $next[1];
261
						$next = $this->next();
262
					}
263
264
					if(!is_string($next) && $next[0] != T_STRING) {
265
						user_error("Couldn\'t parse {$this->path} when building config static manifest", E_USER_ERROR);
266
					}
267
268
					$namespace .= is_string($next) ? $next : $next[1];
269
				}
270
			}
271
			else if($type == '{' || $type == T_CURLY_OPEN || $type == T_DOLLAR_OPEN_CURLY_BRACES){
272
				$depth += 1;
273
				if($class && !$clsdepth) $clsdepth = $depth;
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $clsdepth of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
274
			}
275
			else if($type == '}') {
276
				$depth -= 1;
277
				if($depth < $clsdepth) $class = $clsdepth = null;
278
				if($depth < 0) user_error("Hmm - depth calc wrong, hit negatives, see: ".$this->path, E_USER_ERROR);
279
			}
280
			else if($type == T_PUBLIC || $type == T_PRIVATE || $type == T_PROTECTED) {
281
				$access = $type;
282
			}
283
			else if($type == T_STATIC && $class && $depth == $clsdepth) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
284
				$this->parseStatic($access, $namespace ? $namespace.'\\'.$class : $class);
285
				$access = 0;
286
			}
287
			else {
288
				$access = 0;
289
			}
290
		}
291
	}
292
293
	/**
294
	 * During parsing we've found a "static" keyword. Parse out the variable names and value
295
	 * assignments that follow.
296
	 *
297
	 * Seperated out from parse partially so that we can recurse if there are multiple statics
298
	 * being declared in a comma seperated list
299
	 */
300
	function parseStatic($access, $class) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
301
		$variable = null;
302
		$value = '';
303
304
		while($token = $this->next()) {
305
			$type = ($token === (array)$token) ? $token[0] : $token;
306
307
			if($type == T_PUBLIC || $type == T_PRIVATE || $type == T_PROTECTED) {
308
				$access = $type;
309
			}
310
			else if($type == T_FUNCTION) {
311
				return;
312
			}
313
			else if($type == T_VARIABLE) {
314
				$variable = substr($token[1], 1); // Cut off initial "$"
315
			}
316
			else if($type == ';' || $type == ',' || $type == '=') {
317
				break;
318
			}
319
			else if($type == T_COMMENT || $type == T_DOC_COMMENT) {
320
				// NOP
321
			}
322
			else {
323
				user_error('Unexpected token ("' . token_name($type) . '") when building static manifest in class "'
324
					. $class . '": '.print_r($token, true), E_USER_ERROR);
325
			}
326
		}
327
328
		if($token == '=') {
329
			$depth = 0;
330
331
			while($token = ($this->pos >= $this->length) ? null : $this->tokens[$this->pos++]) {
332
				$type = ($token === (array)$token) ? $token[0] : $token;
333
334
				// Track array nesting depth
335
				if($type == T_ARRAY || $type == '[') {
336
					$depth += 1;
337
				} elseif($type == ')' || $type == ']') {
338
					$depth -= 1;
339
				}
340
341
				// Parse out the assignment side of a static declaration,
342
				// ending on either a ';' or a ',' outside an array
343
				if($type == T_WHITESPACE) {
344
					$value .= ' ';
345
				}
346
				else if($type == ';' || ($type == ',' && !$depth)) {
347
					break;
348
				}
349
				// Statics can reference class constants with self:: (and that won't work in eval)
350
				else if($type == T_STRING && $token[1] == 'self') {
351
					$value .= $class;
352
				}
353
				else {
354
					$value .= ($token === (array)$token) ? $token[1] : $token;
355
				}
356
			}
357
		}
358
359
		if (!isset($this->info[$class])) {
360
			$this->info[$class] = array(
361
				'path' => $this->path,
362
				'mtime' => filemtime($this->path),
363
			);
364
		}
365
366
		if(!isset($this->statics[$class])) {
367
			$this->statics[$class] = array();
368
		}
369
370
		$value = trim($value);
371
		if ($value) {
372
			$value = eval('static $temp = '.$value.";\n".'return $temp'.";\n");
373
		}
374
		else {
375
			$value = null;
376
		}
377
378
		$this->statics[$class][$variable] = array(
379
			'access' => $access,
380
			'value' => $value
381
		);
382
383
		if($token == ',') $this->parseStatic($access, $class);
384
	}
385
}
386