1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SilverStripe\Core\Manifest; |
4
|
|
|
|
5
|
|
|
use Exception; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* A utility class which builds a manifest of all classes, interfaces and some |
9
|
|
|
* additional items present in a directory, and caches it. |
10
|
|
|
* |
11
|
|
|
* It finds the following information: |
12
|
|
|
* - Class and interface names and paths. |
13
|
|
|
* - All direct and indirect descendants of a class. |
14
|
|
|
* - All implementors of an interface. |
15
|
|
|
* - All module configuration files. |
16
|
|
|
*/ |
17
|
|
|
class ClassManifest { |
18
|
|
|
|
19
|
|
|
const CONF_FILE = '_config.php'; |
20
|
|
|
const CONF_DIR = '_config'; |
21
|
|
|
|
22
|
|
|
protected $base; |
23
|
|
|
protected $tests; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var ManifestCache |
27
|
|
|
*/ |
28
|
|
|
protected $cache; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var string |
32
|
|
|
*/ |
33
|
|
|
protected $cacheKey; |
34
|
|
|
|
35
|
|
|
protected $classes = array(); |
36
|
|
|
protected $roots = array(); |
37
|
|
|
protected $children = array(); |
38
|
|
|
protected $descendants = array(); |
39
|
|
|
protected $interfaces = array(); |
40
|
|
|
protected $implementors = array(); |
41
|
|
|
protected $configs = array(); |
42
|
|
|
protected $configDirs = array(); |
43
|
|
|
protected $traits = array(); |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @return TokenisedRegularExpression |
47
|
|
|
*/ |
48
|
|
|
public static function get_class_parser() { |
49
|
|
|
return new TokenisedRegularExpression(array( |
50
|
|
|
0 => T_CLASS, |
51
|
|
|
1 => T_WHITESPACE, |
52
|
|
|
2 => array(T_STRING, 'can_jump_to' => array(7, 14), 'save_to' => 'className'), |
53
|
|
|
3 => T_WHITESPACE, |
54
|
|
|
4 => T_EXTENDS, |
55
|
|
|
5 => T_WHITESPACE, |
56
|
|
|
6 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => 14), |
57
|
|
|
7 => T_WHITESPACE, |
58
|
|
|
8 => T_IMPLEMENTS, |
59
|
|
|
9 => T_WHITESPACE, |
60
|
|
|
10 => array(T_STRING, 'can_jump_to' => 14, 'save_to' => 'interfaces[]'), |
61
|
|
|
11 => array(T_WHITESPACE, 'optional' => true), |
62
|
|
|
12 => array(',', 'can_jump_to' => 10, 'save_to' => 'interfaces[]'), |
63
|
|
|
13 => array(T_WHITESPACE, 'can_jump_to' => 10), |
64
|
|
|
14 => array(T_WHITESPACE, 'optional' => true), |
65
|
|
|
15 => '{', |
66
|
|
|
)); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @return TokenisedRegularExpression |
71
|
|
|
*/ |
72
|
|
|
public static function get_namespaced_class_parser() { |
73
|
|
|
return new TokenisedRegularExpression(array( |
74
|
|
|
0 => T_CLASS, |
75
|
|
|
1 => T_WHITESPACE, |
76
|
|
|
2 => array(T_STRING, 'can_jump_to' => array(8, 16), 'save_to' => 'className'), |
77
|
|
|
3 => T_WHITESPACE, |
78
|
|
|
4 => T_EXTENDS, |
79
|
|
|
5 => T_WHITESPACE, |
80
|
|
|
6 => array(T_NS_SEPARATOR, 'save_to' => 'extends[]', 'optional' => true), |
81
|
|
|
7 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => array(6, 16)), |
82
|
|
|
8 => T_WHITESPACE, |
83
|
|
|
9 => T_IMPLEMENTS, |
84
|
|
|
10 => T_WHITESPACE, |
85
|
|
|
11 => array(T_NS_SEPARATOR, 'save_to' => 'interfaces[]', 'optional' => true), |
86
|
|
|
12 => array(T_STRING, 'can_jump_to' => array(11, 16), 'save_to' => 'interfaces[]'), |
87
|
|
|
13 => array(T_WHITESPACE, 'optional' => true), |
88
|
|
|
14 => array(',', 'can_jump_to' => 11, 'save_to' => 'interfaces[]'), |
89
|
|
|
15 => array(T_WHITESPACE, 'can_jump_to' => 11), |
90
|
|
|
16 => array(T_WHITESPACE, 'optional' => true), |
91
|
|
|
17 => '{', |
92
|
|
|
)); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @return TokenisedRegularExpression |
97
|
|
|
*/ |
98
|
|
|
public static function get_trait_parser() { |
99
|
|
|
return new TokenisedRegularExpression(array( |
100
|
|
|
0 => T_TRAIT, |
101
|
|
|
1 => T_WHITESPACE, |
102
|
|
|
2 => array(T_STRING, 'save_to' => 'traitName') |
103
|
|
|
)); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* @return TokenisedRegularExpression |
108
|
|
|
*/ |
109
|
|
|
public static function get_namespace_parser() { |
110
|
|
|
return new TokenisedRegularExpression(array( |
111
|
|
|
0 => T_NAMESPACE, |
112
|
|
|
1 => T_WHITESPACE, |
113
|
|
|
2 => array(T_NS_SEPARATOR, 'save_to' => 'namespaceName[]', 'optional' => true), |
114
|
|
|
3 => array(T_STRING, 'save_to' => 'namespaceName[]', 'can_jump_to' => 2), |
115
|
|
|
4 => array(T_WHITESPACE, 'optional' => true), |
116
|
|
|
5 => ';', |
117
|
|
|
)); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* @return TokenisedRegularExpression |
122
|
|
|
*/ |
123
|
|
|
public static function get_interface_parser() { |
124
|
|
|
return new TokenisedRegularExpression(array( |
125
|
|
|
0 => T_INTERFACE, |
126
|
|
|
1 => T_WHITESPACE, |
127
|
|
|
2 => array(T_STRING, 'save_to' => 'interfaceName') |
128
|
|
|
)); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Create a {@link TokenisedRegularExpression} that extracts the namespaces imported with the 'use' keyword |
133
|
|
|
* |
134
|
|
|
* This searches symbols for a `use` followed by 1 or more namespaces which are optionally aliased using the `as` |
135
|
|
|
* keyword. The relevant matching tokens are added one-by-one into an array (using `save_to` param). |
136
|
|
|
* |
137
|
|
|
* eg: use Namespace\ClassName as Alias, OtherNamespace\ClassName; |
138
|
|
|
* |
139
|
|
|
* @return TokenisedRegularExpression |
140
|
|
|
*/ |
141
|
|
|
public static function get_imported_namespace_parser() { |
142
|
|
|
return new TokenisedRegularExpression(array( |
143
|
|
|
0 => T_USE, |
144
|
|
|
1 => T_WHITESPACE, |
145
|
|
|
2 => array(T_NS_SEPARATOR, 'save_to' => 'importString[]', 'optional' => true), |
146
|
|
|
3 => array(T_STRING, 'save_to' => 'importString[]', 'can_jump_to' => array(2, 8)), |
147
|
|
|
4 => array(T_WHITESPACE, 'save_to' => 'importString[]'), |
148
|
|
|
5 => array(T_AS, 'save_to' => 'importString[]'), |
149
|
|
|
6 => array(T_WHITESPACE, 'save_to' => 'importString[]'), |
150
|
|
|
7 => array(T_STRING, 'save_to' => 'importString[]'), |
151
|
|
|
8 => array(T_WHITESPACE, 'optional' => true), |
152
|
|
|
9 => array(',', 'save_to' => 'importString[]', 'optional' => true, 'can_jump_to' => 2), |
153
|
|
|
10 => array(T_WHITESPACE, 'optional' => true, 'can_jump_to' => 2), |
154
|
|
|
11 => ';', |
155
|
|
|
)); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Constructs and initialises a new class manifest, either loading the data |
160
|
|
|
* from the cache or re-scanning for classes. |
161
|
|
|
* |
162
|
|
|
* @param string $base The manifest base path. |
163
|
|
|
* @param bool $includeTests Include the contents of "tests" directories. |
164
|
|
|
* @param bool $forceRegen Force the manifest to be regenerated. |
165
|
|
|
* @param bool $cache If the manifest is regenerated, cache it. |
166
|
|
|
*/ |
167
|
|
|
public function __construct($base, $includeTests = false, $forceRegen = false, $cache = true) { |
168
|
|
|
$this->base = $base; |
169
|
|
|
$this->tests = $includeTests; |
170
|
|
|
|
171
|
|
|
$cacheClass = defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE : 'SilverStripe\\Core\\Manifest\\ManifestCache_File'; |
172
|
|
|
|
173
|
|
|
$this->cache = new $cacheClass('classmanifest'.($includeTests ? '_tests' : '')); |
174
|
|
|
$this->cacheKey = 'manifest'; |
175
|
|
|
|
176
|
|
|
if (!$forceRegen && $data = $this->cache->load($this->cacheKey)) { |
177
|
|
|
$this->classes = $data['classes']; |
178
|
|
|
$this->descendants = $data['descendants']; |
179
|
|
|
$this->interfaces = $data['interfaces']; |
180
|
|
|
$this->implementors = $data['implementors']; |
181
|
|
|
$this->configs = $data['configs']; |
182
|
|
|
$this->configDirs = $data['configDirs']; |
183
|
|
|
$this->traits = $data['traits']; |
184
|
|
|
} else { |
185
|
|
|
$this->regenerate($cache); |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Returns the file path to a class or interface if it exists in the |
191
|
|
|
* manifest. |
192
|
|
|
* |
193
|
|
|
* @param string $name |
194
|
|
|
* @return string|null |
195
|
|
|
*/ |
196
|
|
|
public function getItemPath($name) { |
197
|
|
|
$name = strtolower($name); |
198
|
|
|
|
199
|
|
|
if (isset($this->classes[$name])) { |
200
|
|
|
return $this->classes[$name]; |
201
|
|
|
} elseif (isset($this->interfaces[$name])) { |
202
|
|
|
return $this->interfaces[$name]; |
203
|
|
|
} elseif(isset($this->traits[$name])) { |
204
|
|
|
return $this->traits[$name]; |
205
|
|
|
} |
206
|
|
|
return null; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Returns a map of lowercased class names to file paths. |
211
|
|
|
* |
212
|
|
|
* @return array |
213
|
|
|
*/ |
214
|
|
|
public function getClasses() { |
215
|
|
|
return $this->classes; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Returns a lowercase array of all the class names in the manifest. |
220
|
|
|
* |
221
|
|
|
* @return array |
222
|
|
|
*/ |
223
|
|
|
public function getClassNames() { |
224
|
|
|
return array_keys($this->classes); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Returns a lowercase array of all trait names in the manifest |
229
|
|
|
* |
230
|
|
|
* @return array |
231
|
|
|
*/ |
232
|
|
|
public function getTraitNames() { |
233
|
|
|
return array_keys($this->traits); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* Returns an array of all the descendant data. |
238
|
|
|
* |
239
|
|
|
* @return array |
240
|
|
|
*/ |
241
|
|
|
public function getDescendants() { |
242
|
|
|
return $this->descendants; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Returns an array containing all the descendants (direct and indirect) |
247
|
|
|
* of a class. |
248
|
|
|
* |
249
|
|
|
* @param string|object $class |
250
|
|
|
* @return array |
251
|
|
|
*/ |
252
|
|
|
public function getDescendantsOf($class) { |
253
|
|
|
if (is_object($class)) { |
254
|
|
|
$class = get_class($class); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$lClass = strtolower($class); |
258
|
|
|
|
259
|
|
|
if (array_key_exists($lClass, $this->descendants)) { |
260
|
|
|
return $this->descendants[$lClass]; |
261
|
|
|
} else { |
262
|
|
|
return array(); |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Returns a map of lowercased interface names to file locations. |
268
|
|
|
* |
269
|
|
|
* @return array |
270
|
|
|
*/ |
271
|
|
|
public function getInterfaces() { |
272
|
|
|
return $this->interfaces; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Returns a map of lowercased interface names to the classes the implement |
277
|
|
|
* them. |
278
|
|
|
* |
279
|
|
|
* @return array |
280
|
|
|
*/ |
281
|
|
|
public function getImplementors() { |
282
|
|
|
return $this->implementors; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Returns an array containing the class names that implement a certain |
287
|
|
|
* interface. |
288
|
|
|
* |
289
|
|
|
* @param string $interface |
290
|
|
|
* @return array |
291
|
|
|
*/ |
292
|
|
|
public function getImplementorsOf($interface) { |
293
|
|
|
$interface = strtolower($interface); |
294
|
|
|
|
295
|
|
|
if (array_key_exists($interface, $this->implementors)) { |
296
|
|
|
return $this->implementors[$interface]; |
297
|
|
|
} else { |
298
|
|
|
return array(); |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* Returns an array of paths to module config files. |
304
|
|
|
* |
305
|
|
|
* @return array |
306
|
|
|
*/ |
307
|
|
|
public function getConfigs() { |
308
|
|
|
return $this->configs; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Returns an array of module names mapped to their paths. |
313
|
|
|
* |
314
|
|
|
* "Modules" in SilverStripe are simply directories with a _config.php |
315
|
|
|
* file. |
316
|
|
|
* |
317
|
|
|
* @return array |
318
|
|
|
*/ |
319
|
|
|
public function getModules() { |
320
|
|
|
$modules = array(); |
321
|
|
|
|
322
|
|
|
if($this->configs) { |
|
|
|
|
323
|
|
|
foreach($this->configs as $configPath) { |
324
|
|
|
$modules[basename(dirname($configPath))] = dirname($configPath); |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
if($this->configDirs) { |
|
|
|
|
329
|
|
|
foreach($this->configDirs as $configDir) { |
330
|
|
|
$path = preg_replace('/\/_config$/', '', dirname($configDir)); |
331
|
|
|
$modules[basename($path)] = $path; |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
return $modules; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Used to set up files that we want to exclude from parsing for performance reasons. |
340
|
|
|
*/ |
341
|
|
|
protected function setDefaults() |
342
|
|
|
{ |
343
|
|
|
$this->classes['sstemplateparser'] = FRAMEWORK_PATH.'/View/SSTemplateParser.php'; |
344
|
|
|
$this->classes['sstemplateparseexception'] = FRAMEWORK_PATH.'/View/SSTemplateParseException.php'; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* Completely regenerates the manifest file. |
349
|
|
|
* |
350
|
|
|
* @param bool $cache Cache the result. |
351
|
|
|
*/ |
352
|
|
|
public function regenerate($cache = true) { |
353
|
|
|
$resets = array( |
354
|
|
|
'classes', 'roots', 'children', 'descendants', 'interfaces', |
355
|
|
|
'implementors', 'configs', 'configDirs', 'traits' |
356
|
|
|
); |
357
|
|
|
|
358
|
|
|
// Reset the manifest so stale info doesn't cause errors. |
359
|
|
|
foreach ($resets as $reset) { |
360
|
|
|
$this->$reset = array(); |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
$this->setDefaults(); |
364
|
|
|
|
365
|
|
|
$finder = new ManifestFileFinder(); |
366
|
|
|
$finder->setOptions(array( |
367
|
|
|
'name_regex' => '/^(_config.php|[^_].*\.php)$/', |
368
|
|
|
'ignore_files' => array('index.php', 'main.php', 'cli-script.php', 'SSTemplateParser.php'), |
369
|
|
|
'ignore_tests' => !$this->tests, |
370
|
|
|
'file_callback' => array($this, 'handleFile'), |
371
|
|
|
'dir_callback' => array($this, 'handleDir') |
372
|
|
|
)); |
373
|
|
|
$finder->find($this->base); |
374
|
|
|
|
375
|
|
|
foreach ($this->roots as $root) { |
376
|
|
|
$this->coalesceDescendants($root); |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
if ($cache) { |
380
|
|
|
$data = array( |
381
|
|
|
'classes' => $this->classes, |
382
|
|
|
'descendants' => $this->descendants, |
383
|
|
|
'interfaces' => $this->interfaces, |
384
|
|
|
'implementors' => $this->implementors, |
385
|
|
|
'configs' => $this->configs, |
386
|
|
|
'configDirs' => $this->configDirs, |
387
|
|
|
'traits' => $this->traits, |
388
|
|
|
); |
389
|
|
|
$this->cache->save($data, $this->cacheKey); |
390
|
|
|
} |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
public function handleDir($basename, $pathname, $depth) { |
|
|
|
|
394
|
|
|
if ($basename == self::CONF_DIR) { |
395
|
|
|
$this->configDirs[] = $pathname; |
396
|
|
|
} |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* Find a the full namespaced declaration of a class (or interface) from a list of candidate imports |
401
|
|
|
* |
402
|
|
|
* This is typically used to determine the full class name in classes that have imported namesapced symbols (having |
403
|
|
|
* used the `use` keyword) |
404
|
|
|
* |
405
|
|
|
* NB: remember the '\\' is an escaped backslash and is interpreted as a single \ |
406
|
|
|
* |
407
|
|
|
* @param string $class The class (or interface) name to find in the candidate imports |
408
|
|
|
* @param string $namespace The namespace that was declared for the classes definition (if there was one) |
409
|
|
|
* @param array $imports The list of imported symbols (Classes or Interfaces) to test against |
410
|
|
|
* |
411
|
|
|
* @return string The fully namespaced class name |
412
|
|
|
*/ |
413
|
|
|
protected function findClassOrInterfaceFromCandidateImports($class, $namespace = '', $imports = array()) { |
414
|
|
|
|
415
|
|
|
//normalise the namespace |
416
|
|
|
$namespace = rtrim($namespace, '\\'); |
417
|
|
|
|
418
|
|
|
//by default we'll use the $class as our candidate |
419
|
|
|
$candidateClass = $class; |
420
|
|
|
|
421
|
|
|
if (!$class) { |
422
|
|
|
return $candidateClass; |
423
|
|
|
} |
424
|
|
|
//if the class starts with a \ then it is explicitly in the global namespace and we don't need to do |
425
|
|
|
// anything else |
426
|
|
|
if (substr($class, 0, 1) == '\\') { |
427
|
|
|
$candidateClass = substr($class, 1); |
428
|
|
|
return $candidateClass; |
429
|
|
|
} |
430
|
|
|
//if there's a namespace, starting assumption is the class is defined in that namespace |
431
|
|
|
if ($namespace) { |
432
|
|
|
$candidateClass = $namespace . '\\' . $class; |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
if (empty($imports)) { |
436
|
|
|
return $candidateClass; |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
//normalised class name (PHP is case insensitive for symbols/namespaces |
440
|
|
|
$lClass = strtolower($class); |
441
|
|
|
|
442
|
|
|
//go through all the imports and see if the class exists within one of them |
443
|
|
|
foreach ($imports as $alias => $import) { |
444
|
|
|
//normalise import |
445
|
|
|
$import = trim($import, '\\'); |
446
|
|
|
|
447
|
|
|
//if there is no string key, then there was no declared alias - we'll use the main declaration |
448
|
|
|
if (is_int($alias)) { |
449
|
|
|
$alias = strtolower($import); |
450
|
|
|
} else { |
451
|
|
|
$alias = strtolower($alias); |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
//exact match? Then it's a class in the global namespace that was imported OR it's an alias of |
455
|
|
|
// another namespace |
456
|
|
|
// or if it ends with the \ClassName then it's the class we are looking for |
457
|
|
|
if ($lClass == $alias |
458
|
|
|
|| substr_compare( |
459
|
|
|
$alias, |
460
|
|
|
'\\' . $lClass, |
461
|
|
|
strlen($alias) - strlen($lClass) - 1, |
462
|
|
|
// -1 because the $lClass length is 1 longer due to \ |
463
|
|
|
strlen($alias) |
464
|
|
|
) === 0 |
465
|
|
|
) { |
466
|
|
|
$candidateClass = $import; |
467
|
|
|
break; |
468
|
|
|
} |
469
|
|
|
} |
470
|
|
|
return $candidateClass; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
/** |
474
|
|
|
* Return an array of array($alias => $import) from tokenizer's tokens of a PHP file |
475
|
|
|
* |
476
|
|
|
* NB: If there is no alias we don't set a key to the array |
477
|
|
|
* |
478
|
|
|
* @param array $tokens The parsed tokens from tokenizer's parsing of a PHP file |
479
|
|
|
* |
480
|
|
|
* @return array The array of imports as (optional) $alias => $import |
481
|
|
|
*/ |
482
|
|
|
protected function getImportsFromTokens($tokens) { |
483
|
|
|
//parse out the imports |
484
|
|
|
$imports = self::get_imported_namespace_parser()->findAll($tokens); |
485
|
|
|
|
486
|
|
|
//if there are any imports, clean them up |
487
|
|
|
// imports come to us as array('importString' => array([array of matching tokens])) |
|
|
|
|
488
|
|
|
// we need to join this nested array into a string and split out the alias and the import |
489
|
|
|
if (!empty($imports)) { |
490
|
|
|
$cleanImports = array(); |
491
|
|
|
foreach ($imports as $import) { |
492
|
|
|
if (!empty($import['importString'])) { |
493
|
|
|
//join the array up into a string |
494
|
|
|
$importString = implode('', $import['importString']); |
495
|
|
|
//split at , to get each import declaration |
496
|
|
|
$importSet = explode(',', $importString); |
497
|
|
|
foreach ($importSet as $importDeclaration) { |
498
|
|
|
//split at ' as ' (any case) to see if we are aliasing the namespace |
499
|
|
|
$importDeclaration = preg_split('/\s+as\s+/i', $importDeclaration); |
500
|
|
|
//shift off the fully namespaced import |
501
|
|
|
$qualifiedImport = array_shift($importDeclaration); |
502
|
|
|
//if there are still items in the array, it's the alias |
503
|
|
|
if (!empty($importDeclaration)) { |
504
|
|
|
$cleanImports[array_shift($importDeclaration)] = $qualifiedImport; |
505
|
|
|
} |
506
|
|
|
else { |
507
|
|
|
$cleanImports[] = $qualifiedImport; |
508
|
|
|
} |
509
|
|
|
} |
510
|
|
|
} |
511
|
|
|
} |
512
|
|
|
$imports = $cleanImports; |
513
|
|
|
} |
514
|
|
|
return $imports; |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
public function handleFile($basename, $pathname, $depth) { |
|
|
|
|
518
|
|
|
if ($basename == self::CONF_FILE) { |
519
|
|
|
$this->configs[] = $pathname; |
520
|
|
|
return; |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
$classes = null; |
524
|
|
|
$interfaces = null; |
525
|
|
|
$namespace = null; |
526
|
|
|
$imports = null; |
527
|
|
|
$traits = null; |
528
|
|
|
|
529
|
|
|
// The results of individual file parses are cached, since only a few |
530
|
|
|
// files will have changed and TokenisedRegularExpression is quite |
531
|
|
|
// slow. A combination of the file name and file contents hash are used, |
532
|
|
|
// since just using the datetime lead to problems with upgrading. |
533
|
|
|
$key = preg_replace('/[^a-zA-Z0-9_]/', '_', $basename) . '_' . md5_file($pathname); |
534
|
|
|
|
535
|
|
|
$valid = false; |
536
|
|
|
if ($data = $this->cache->load($key)) { |
537
|
|
|
$valid = ( |
538
|
|
|
isset($data['classes']) && is_array($data['classes']) |
539
|
|
|
&& isset($data['interfaces']) && is_array($data['interfaces']) |
540
|
|
|
&& isset($data['namespace']) && is_string($data['namespace']) |
541
|
|
|
&& isset($data['imports']) && is_array($data['imports']) |
542
|
|
|
&& isset($data['traits']) && is_array($data['traits']) |
543
|
|
|
); |
544
|
|
|
|
545
|
|
|
if ($valid) { |
546
|
|
|
$classes = $data['classes']; |
547
|
|
|
$interfaces = $data['interfaces']; |
548
|
|
|
$namespace = $data['namespace']; |
549
|
|
|
$imports = $data['imports']; |
550
|
|
|
$traits = $data['traits']; |
551
|
|
|
} |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
if (!$valid) { |
555
|
|
|
$tokens = token_get_all(file_get_contents($pathname)); |
556
|
|
|
|
557
|
|
|
$classes = self::get_namespaced_class_parser()->findAll($tokens); |
558
|
|
|
$traits = self::get_trait_parser()->findAll($tokens); |
559
|
|
|
|
560
|
|
|
$namespace = self::get_namespace_parser()->findAll($tokens); |
561
|
|
|
|
562
|
|
|
if($namespace) { |
|
|
|
|
563
|
|
|
$namespace = implode('', $namespace[0]['namespaceName']); |
564
|
|
|
} else { |
565
|
|
|
$namespace = ''; |
566
|
|
|
} |
567
|
|
|
|
568
|
|
|
$imports = $this->getImportsFromTokens($tokens); |
569
|
|
|
|
570
|
|
|
$interfaces = self::get_interface_parser()->findAll($tokens); |
571
|
|
|
|
572
|
|
|
$cache = array( |
573
|
|
|
'classes' => $classes, |
574
|
|
|
'interfaces' => $interfaces, |
575
|
|
|
'namespace' => $namespace, |
576
|
|
|
'imports' => $imports, |
577
|
|
|
'traits' => $traits |
578
|
|
|
); |
579
|
|
|
$this->cache->save($cache, $key); |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
// Ensure namespace has no trailing slash, and namespaceBase does |
583
|
|
|
$namespaceBase = ''; |
584
|
|
|
if ($namespace) { |
585
|
|
|
$namespace = rtrim($namespace, '\\'); |
586
|
|
|
$namespaceBase = $namespace . '\\'; |
587
|
|
|
} |
588
|
|
|
|
589
|
|
|
foreach ($classes as $class) { |
590
|
|
|
$name = $namespaceBase . $class['className']; |
591
|
|
|
$extends = isset($class['extends']) ? implode('', $class['extends']) : null; |
592
|
|
|
$implements = isset($class['interfaces']) ? $class['interfaces'] : null; |
593
|
|
|
|
594
|
|
|
if ($extends) { |
|
|
|
|
595
|
|
|
$extends = $this->findClassOrInterfaceFromCandidateImports($extends, $namespace, $imports); |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
if (!empty($implements)) { |
599
|
|
|
//join all the tokens |
600
|
|
|
$implements = implode('', $implements); |
601
|
|
|
//split at comma |
602
|
|
|
$implements = explode(',', $implements); |
603
|
|
|
//normalise interfaces |
604
|
|
|
foreach ($implements as &$interface) { |
605
|
|
|
$interface = $this->findClassOrInterfaceFromCandidateImports($interface, $namespace, $imports); |
606
|
|
|
} |
607
|
|
|
//release the var name |
608
|
|
|
unset($interface); |
609
|
|
|
} |
610
|
|
|
|
611
|
|
|
$lowercaseName = strtolower($name); |
612
|
|
|
if (array_key_exists($lowercaseName, $this->classes)) { |
613
|
|
|
throw new Exception(sprintf( |
614
|
|
|
'There are two files containing the "%s" class: "%s" and "%s"', |
615
|
|
|
$name, $this->classes[$lowercaseName], $pathname |
616
|
|
|
)); |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
$this->classes[$lowercaseName] = $pathname; |
620
|
|
|
|
621
|
|
|
if ($extends) { |
|
|
|
|
622
|
|
|
$extends = strtolower($extends); |
623
|
|
|
|
624
|
|
|
if (!isset($this->children[$extends])) { |
625
|
|
|
$this->children[$extends] = array($name); |
626
|
|
|
} else { |
627
|
|
|
$this->children[$extends][] = $name; |
628
|
|
|
} |
629
|
|
|
} else { |
630
|
|
|
$this->roots[] = $name; |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
if ($implements) { |
634
|
|
|
foreach ($implements as $interface) { |
635
|
|
|
$interface = strtolower($interface); |
636
|
|
|
|
637
|
|
|
if (!isset($this->implementors[$interface])) { |
638
|
|
|
$this->implementors[$interface] = array($name); |
639
|
|
|
} else { |
640
|
|
|
$this->implementors[$interface][] = $name; |
641
|
|
|
} |
642
|
|
|
} |
643
|
|
|
} |
644
|
|
|
} |
645
|
|
|
|
646
|
|
|
foreach ($interfaces as $interface) { |
647
|
|
|
$this->interfaces[strtolower($namespaceBase . $interface['interfaceName'])] = $pathname; |
648
|
|
|
} |
649
|
|
|
foreach ($traits as $trait) { |
650
|
|
|
$this->traits[strtolower($namespaceBase . $trait['traitName'])] = $pathname; |
651
|
|
|
} |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
/** |
655
|
|
|
* Recursively coalesces direct child information into full descendant |
656
|
|
|
* information. |
657
|
|
|
* |
658
|
|
|
* @param string $class |
659
|
|
|
* @return array |
660
|
|
|
*/ |
661
|
|
|
protected function coalesceDescendants($class) { |
662
|
|
|
$lClass = strtolower($class); |
663
|
|
|
|
664
|
|
|
if (array_key_exists($lClass, $this->children)) { |
665
|
|
|
$this->descendants[$lClass] = array(); |
666
|
|
|
|
667
|
|
|
foreach ($this->children[$lClass] as $class) { |
668
|
|
|
$this->descendants[$lClass] = array_merge( |
669
|
|
|
$this->descendants[$lClass], |
670
|
|
|
array($class), |
671
|
|
|
$this->coalesceDescendants($class) |
672
|
|
|
); |
673
|
|
|
} |
674
|
|
|
|
675
|
|
|
return $this->descendants[$lClass]; |
676
|
|
|
} else { |
677
|
|
|
return array(); |
678
|
|
|
} |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
} |
682
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.