1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This software package is licensed under `AGPL, Commercial` license[s]. |
5
|
|
|
* |
6
|
|
|
* @package maslosoft/signals |
7
|
|
|
* @license AGPL, Commercial |
8
|
|
|
* |
9
|
|
|
* @copyright Copyright (c) Peter Maselkowski <[email protected]> |
10
|
|
|
* @link https://maslosoft.com/signals/ |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace Maslosoft\Signals\Builder; |
14
|
|
|
|
15
|
|
|
use Exception; |
16
|
|
|
use Maslosoft\Addendum\Exceptions\ParseException; |
17
|
|
|
use Maslosoft\Addendum\Interfaces\AnnotatedInterface; |
18
|
|
|
use Maslosoft\Addendum\Utilities\AnnotationUtility; |
19
|
|
|
use Maslosoft\Addendum\Utilities\ClassChecker; |
20
|
|
|
use Maslosoft\Addendum\Utilities\FileWalker; |
21
|
|
|
use Maslosoft\Addendum\Utilities\NameNormalizer; |
22
|
|
|
use Maslosoft\Signals\Exceptions\ClassNotFoundException; |
23
|
|
|
use Maslosoft\Signals\Helpers\DataSorter; |
24
|
|
|
use Maslosoft\Signals\Interfaces\ExtractorInterface; |
25
|
|
|
use Maslosoft\Signals\Meta\DocumentMethodMeta; |
26
|
|
|
use Maslosoft\Signals\Meta\DocumentPropertyMeta; |
27
|
|
|
use Maslosoft\Signals\Meta\DocumentTypeMeta; |
28
|
|
|
use Maslosoft\Signals\Meta\SignalsMeta; |
29
|
|
|
use Maslosoft\Signals\Signal; |
30
|
|
|
use ParseError; |
31
|
|
|
use Psr\Log\LoggerInterface; |
32
|
|
|
use ReflectionException; |
33
|
|
|
use UnexpectedValueException; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Addendum extractor |
37
|
|
|
* @codeCoverageIgnore |
38
|
|
|
* @author Piotr Maselkowski <pmaselkowski at gmail.com> |
39
|
|
|
*/ |
40
|
|
|
class Addendum implements ExtractorInterface |
41
|
|
|
{ |
42
|
|
|
|
43
|
|
|
// Data keys for annotations extraction |
44
|
|
|
const SlotFor = 'SlotFor'; |
45
|
|
|
const SignalFor = 'SignalFor'; |
46
|
|
|
// Default annotation names |
47
|
|
|
const SlotName = 'SlotFor'; |
48
|
|
|
const SignalName = 'SignalFor'; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Signal instance |
52
|
|
|
* @var Signal |
53
|
|
|
*/ |
54
|
|
|
private $signal = null; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Signals and slots data |
58
|
|
|
* @var mixed |
59
|
|
|
*/ |
60
|
|
|
private $data = [ |
61
|
|
|
Signal::Slots => [ |
62
|
|
|
], |
63
|
|
|
Signal::Signals => [ |
64
|
|
|
] |
65
|
|
|
]; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Scanned file paths |
69
|
|
|
* @var string[] |
70
|
|
|
*/ |
71
|
|
|
private $paths = []; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Annotations matching patterns |
75
|
|
|
* @var string[] |
76
|
|
|
*/ |
77
|
|
|
private $patterns = []; |
78
|
|
|
private static $file = ''; |
79
|
|
|
|
80
|
|
|
public function __construct() |
81
|
|
|
{ |
82
|
|
|
$annotations = [ |
83
|
|
|
self::SlotFor, |
84
|
|
|
self::SignalFor |
85
|
|
|
]; |
86
|
|
|
foreach ($annotations as $annotation) |
87
|
|
|
{ |
88
|
|
|
$annotation = preg_replace('~^@~', '', $annotation); |
89
|
|
|
$this->patterns[] = sprintf('~@%s~', $annotation); |
90
|
|
|
} |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Handler for class not found errors |
95
|
|
|
* |
96
|
|
|
* @internal This must be public, but should not be used anywhere else |
97
|
|
|
* @param string $className |
98
|
|
|
*/ |
99
|
|
|
public static function autoloadHandler($className) |
100
|
|
|
{ |
101
|
|
|
// These are loaded in some other way... |
102
|
|
|
if ($className === 'PHP_Invoker') |
103
|
|
|
{ |
104
|
|
|
return false; |
105
|
|
|
} |
106
|
|
|
if (stripos($className, 'phpunit') !== false) |
107
|
|
|
{ |
108
|
|
|
return false; |
109
|
|
|
} |
110
|
|
|
if (!ClassChecker::exists($className)) |
111
|
|
|
{ |
112
|
|
|
throw new ClassNotFoundException("Class $className not found when processing " . self::$file); |
113
|
|
|
} |
114
|
|
|
return false; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Get signals and slots data |
119
|
|
|
* @return mixed |
120
|
|
|
*/ |
121
|
|
|
public function getData() |
122
|
|
|
{ |
123
|
|
|
(new FileWalker([], [$this, 'processFile'], $this->signal->paths, $this->signal->ignoreDirs))->walk(); |
124
|
|
|
DataSorter::sort($this->data); |
125
|
|
|
return $this->data; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Get scanned paths. This is available only after getData call. |
130
|
|
|
* @return string[] |
131
|
|
|
*/ |
132
|
|
|
public function getPaths() |
133
|
|
|
{ |
134
|
|
|
return $this->paths; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Set signal instance |
139
|
|
|
* @param Signal $signal |
140
|
|
|
*/ |
141
|
|
|
public function setSignal(Signal $signal) |
142
|
|
|
{ |
143
|
|
|
$this->signal = $signal; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Get logger |
148
|
|
|
* @return LoggerInterface |
149
|
|
|
*/ |
150
|
|
|
public function getLogger() |
151
|
|
|
{ |
152
|
|
|
return $this->signal->getLogger(); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* @param string $file |
157
|
|
|
*/ |
158
|
|
|
public function processFile($file, $contents) |
159
|
|
|
{ |
160
|
|
|
$this->getLogger()->debug("Processing `$file`"); |
161
|
|
|
$file = realpath($file); |
162
|
|
|
|
163
|
|
|
self::$file = $file; |
164
|
|
|
|
165
|
|
|
$ignoreFile = sprintf('%s/.signalignore', dirname($file)); |
166
|
|
|
|
167
|
|
|
if (file_exists($ignoreFile)) |
168
|
|
|
{ |
169
|
|
|
$this->getLogger()->notice("Skipping `$file` because of `$ignoreFile`" . PHP_EOL); |
170
|
|
|
return; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
$this->paths[] = $file; |
174
|
|
|
// Remove initial `\` from namespace |
175
|
|
|
try |
176
|
|
|
{ |
177
|
|
|
$annotated = AnnotationUtility::rawAnnotate($file); |
178
|
|
|
} |
179
|
|
|
catch (ClassNotFoundException $e) |
180
|
|
|
{ |
181
|
|
|
$this->log($e, $file); |
182
|
|
|
return; |
183
|
|
|
} |
184
|
|
|
catch (ParseException $e) |
185
|
|
|
{ |
186
|
|
|
$this->err($e, $file); |
187
|
|
|
return; |
188
|
|
|
} |
189
|
|
|
catch (UnexpectedValueException $e) |
190
|
|
|
{ |
191
|
|
|
$this->err($e, $file); |
192
|
|
|
return; |
193
|
|
|
} |
194
|
|
|
catch (Exception $e) |
195
|
|
|
{ |
196
|
|
|
$this->err($e, $file); |
197
|
|
|
return; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
$namespace = preg_replace('~^\\\\+~', '', $annotated['namespace']); |
201
|
|
|
$className = $annotated['className']; |
202
|
|
|
|
203
|
|
|
|
204
|
|
|
// Use fully qualified name, class must autoload |
205
|
|
|
$fqn = $namespace . '\\' . $className; |
206
|
|
|
NameNormalizer::normalize($fqn); |
207
|
|
|
|
208
|
|
|
try |
209
|
|
|
{ |
210
|
|
|
// NOTE: This autloader must be registered on ReflectionClass |
211
|
|
|
// creation ONLY! That's why register/unregister. |
212
|
|
|
// This will detect not found depending classes |
213
|
|
|
// (base classes,interfaces,traits etc.) |
214
|
|
|
$autoload = static::class . '::autoloadHandler'; |
215
|
|
|
spl_autoload_register($autoload); |
216
|
|
|
eval('$info = new ReflectionClass($fqn);'); |
|
|
|
|
217
|
|
|
spl_autoload_unregister($autoload); |
218
|
|
|
} |
219
|
|
|
catch (ParseError $e) |
220
|
|
|
{ |
221
|
|
|
$this->err($e, $file); |
222
|
|
|
return; |
223
|
|
|
} |
224
|
|
|
catch (ClassNotFoundException $e) |
225
|
|
|
{ |
226
|
|
|
$this->log($e, $file); |
227
|
|
|
return; |
228
|
|
|
} |
229
|
|
|
catch (ReflectionException $e) |
230
|
|
|
{ |
231
|
|
|
$this->err($e, $file); |
232
|
|
|
return; |
233
|
|
|
} |
234
|
|
|
$isAnnotated = $info->implementsInterface(AnnotatedInterface::class); |
|
|
|
|
235
|
|
|
$hasSignals = $this->hasSignals($contents); |
236
|
|
|
$isAbstract = $info->isAbstract() || $info->isInterface(); |
237
|
|
|
|
238
|
|
|
if ($isAnnotated) |
239
|
|
|
{ |
240
|
|
|
$this->getLogger()->debug("Annotated: $info->name"); |
241
|
|
|
} |
242
|
|
|
else |
243
|
|
|
{ |
244
|
|
|
$this->getLogger()->debug("Not annotated: $info->name"); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
// Old classes must now implement interface |
248
|
|
|
// Brake BC! |
249
|
|
|
if ($hasSignals && !$isAnnotated && !$isAbstract) |
250
|
|
|
{ |
251
|
|
|
throw new UnexpectedValueException(sprintf('Class %s must implement %s to use signals', $fqn, AnnotatedInterface::class)); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
// Skip not annotated class |
255
|
|
|
if (!$isAnnotated) |
256
|
|
|
{ |
257
|
|
|
return; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
// Skip abstract classes |
261
|
|
|
if ($isAbstract) |
262
|
|
|
{ |
263
|
|
|
return; |
264
|
|
|
} |
265
|
|
|
try |
266
|
|
|
{ |
267
|
|
|
// Discard notices (might be the case when outdated cache?) |
268
|
|
|
$level = error_reporting(); |
269
|
|
|
error_reporting(E_WARNING); |
270
|
|
|
$meta = SignalsMeta::create($fqn); |
271
|
|
|
error_reporting($level); |
272
|
|
|
} |
273
|
|
|
catch (ParseException $e) |
274
|
|
|
{ |
275
|
|
|
$this->err($e, $file); |
276
|
|
|
return; |
277
|
|
|
} |
278
|
|
|
catch (ClassNotFoundException $e) |
279
|
|
|
{ |
280
|
|
|
$this->log($e, $file); |
281
|
|
|
return; |
282
|
|
|
} |
283
|
|
|
catch (UnexpectedValueException $e) |
284
|
|
|
{ |
285
|
|
|
$this->err($e, $file); |
286
|
|
|
return; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/* @var $typeMeta DocumentTypeMeta */ |
290
|
|
|
$typeMeta = $meta->type(); |
291
|
|
|
|
292
|
|
|
// Signals |
293
|
|
|
foreach ($typeMeta->signalFor as $slot) |
294
|
|
|
{ |
295
|
|
|
$this->getLogger()->debug("Signal: $slot:$fqn"); |
296
|
|
|
$this->data[Signal::Slots][$slot][$fqn] = true; |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
// Slots |
300
|
|
|
// For constructor injection |
301
|
|
|
foreach ($typeMeta->slotFor as $slot) |
302
|
|
|
{ |
303
|
|
|
$key = implode('::', [$fqn, '__construct']) . '()'; |
304
|
|
|
$this->getLogger()->debug("Slot: $slot:$fqn$key"); |
305
|
|
|
$this->data[Signal::Signals][$slot][$fqn][$key] = true; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
// For method injection |
309
|
|
|
foreach ($meta->methods() as $methodName => $method) |
310
|
|
|
{ |
311
|
|
|
/* @var $method DocumentMethodMeta */ |
312
|
|
|
foreach ($method->slotFor as $slot) |
313
|
|
|
{ |
314
|
|
|
$key = implode('::', [$fqn, $methodName]) . '()'; |
315
|
|
|
$this->getLogger()->debug("Slot: $slot:$fqn$key"); |
316
|
|
|
$this->data[Signal::Signals][$slot][$fqn][$key] = sprintf('%s()', $methodName); |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
// For property injection |
321
|
|
|
foreach ($meta->fields() as $fieldName => $field) |
322
|
|
|
{ |
323
|
|
|
/* @var $field DocumentPropertyMeta */ |
324
|
|
|
foreach ($field->slotFor as $slot) |
325
|
|
|
{ |
326
|
|
|
$key = implode('::$', [$fqn, $fieldName]); |
327
|
|
|
$this->getLogger()->debug("Slot: $slot:$fqn$key"); |
328
|
|
|
$this->data[Signal::Signals][$slot][$fqn][$key] = sprintf('%s', $fieldName); |
329
|
|
|
} |
330
|
|
|
} |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
private function hasSignals($contents) |
334
|
|
|
{ |
335
|
|
|
foreach ($this->patterns as $pattern) |
336
|
|
|
{ |
337
|
|
|
if (preg_match($pattern, $contents)) |
338
|
|
|
{ |
339
|
|
|
return true; |
340
|
|
|
} |
341
|
|
|
} |
342
|
|
|
return false; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
private function log(Exception $e, $file) |
346
|
|
|
{ |
347
|
|
|
$msg = sprintf('Warning: %s while scanning file `%s`', $e->getMessage(), $file); |
348
|
|
|
$msg = $msg . PHP_EOL; |
349
|
|
|
$this->signal->getLogger()->warning($msg); |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
private function err(Exception $e, $file) |
353
|
|
|
{ |
354
|
|
|
$msg = sprintf('Error: %s while scanning file `%s`', $e->getMessage(), $file); |
355
|
|
|
$msg = $msg . PHP_EOL; |
356
|
|
|
$this->signal->getLogger()->error($msg); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
} |
360
|
|
|
|
On one hand,
eval
might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM,eval
prevents some optimization that they perform.