-
-
Notifications
You must be signed in to change notification settings - Fork 188
/
Compiler.php
389 lines (350 loc) · 14 KB
/
Compiler.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
<?php
namespace Neos\Flow\ObjectManagement\Proxy;
/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/
use Neos\Flow\Annotations as Flow;
use Neos\Cache\Frontend\PhpFrontend;
use Neos\Flow\ObjectManagement\CompileTimeObjectManager;
use Neos\Flow\Reflection\ReflectionService;
use Neos\Flow\Tests\BaseTestCase;
/**
* Builder for proxy classes which are used to implement Dependency Injection and
* Aspect-Oriented Programming
*
* @Flow\Scope("singleton")
* @Flow\Proxy(false)
*/
class Compiler
{
/**
* @var string
*/
const ORIGINAL_CLASSNAME_SUFFIX = '_Original';
/**
* @var CompileTimeObjectManager
*/
protected $objectManager;
/**
* @var PhpFrontend
*/
protected $classesCache;
/**
* @var ReflectionService
*/
protected $reflectionService;
/**
* @var array
*/
protected $proxyClasses = [];
/**
* Hardcoded list of Flow sub packages which must be immune proxying for security, technical or conceptual reasons.
* @var array
*/
protected $excludedSubPackages = ['Neos\Flow\Aop', 'Neos\Flow\Cor', 'Neos\Flow\Obj', 'Neos\Flow\Pac', 'Neos\Flow\Ref', 'Neos\Flow\Uti'];
/**
* Length of the prefix that will be checked for exclusion of proxy building.
* See above.
*
* @var integer
*/
protected $excludedSubPackagesLength;
/**
* The final map of proxy classes that end up in the cache.
*
* @var array
*/
protected $storedProxyClasses = [];
/**
* Compiler constructor.
*/
public function __construct()
{
$this->excludedSubPackagesLength = strlen('Neos\Flow') + 4;
}
/**
* @param CompileTimeObjectManager $objectManager
* @return void
*/
public function injectObjectManager(CompileTimeObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* Injects the cache for storing the renamed original classes and proxy classes
*
* @param PhpFrontend $classesCache
* @return void
* @Flow\Autowiring(false)
*/
public function injectClassesCache(PhpFrontend $classesCache)
{
$this->classesCache = $classesCache;
}
/**
* @param ReflectionService $reflectionService
* @return void
*/
public function injectReflectionService(ReflectionService $reflectionService)
{
$this->reflectionService = $reflectionService;
}
/**
* Returns a proxy class object for the specified original class.
*
* If no such proxy class has been created yet by this renderer,
* this function will create one and register it for later use.
*
* If the class is not proxyable, or is not a real class at all,
* false will be returned
*
* @param string $fullClassName Name of the original class
* @return ProxyClass|boolean
*/
public function getProxyClass(string $fullClassName)
{
if (interface_exists($fullClassName) || in_array(BaseTestCase::class, class_parents($fullClassName))) {
return false;
}
if (class_exists($fullClassName) === false) {
return false;
}
$classReflection = new \ReflectionClass($fullClassName);
if ($classReflection->isInternal() === true) {
return false;
}
if (method_exists($classReflection, 'isEnum') && $classReflection->isEnum()) {
return false;
}
$proxyAnnotation = $this->reflectionService->getClassAnnotation($fullClassName, Flow\Proxy::class);
if ($proxyAnnotation !== null && $proxyAnnotation->enabled === false) {
return false;
}
if (in_array(substr($fullClassName, 0, $this->excludedSubPackagesLength), $this->excludedSubPackages)) {
return false;
}
// Annotation classes (like \Neos\Flow\Annotations\Entity) must never be proxied because that would break the Doctrine AnnotationParser
if ($classReflection->isFinal() && preg_match('/^\s?\*\s?\@Annotation\s/m', $classReflection->getDocComment()) === 1) {
return false;
}
if (!isset($this->proxyClasses[$fullClassName])) {
$this->proxyClasses[$fullClassName] = new ProxyClass($fullClassName);
$this->proxyClasses[$fullClassName]->injectReflectionService($this->reflectionService);
}
return $this->proxyClasses[$fullClassName];
}
/**
* Checks if the specified class still exists in the code cache. If that is the case, it means that obviously
* the proxy class doesn't have to be rebuilt because otherwise the cache would have been flushed by the file
* monitor or some other mechanism.
*
* @param string $fullClassName Name of the original class
* @return boolean true if a cache entry exists
*/
public function hasCacheEntryForClass($fullClassName)
{
if (isset($this->proxyClasses[$fullClassName])) {
return false;
}
return $this->classesCache->has(str_replace('\\', '_', $fullClassName));
}
/**
* Compiles the configured proxy classes and methods as static PHP code and stores it in the proxy class code cache.
* Also builds the static object container which acts as a registry for non-prototype objects during runtime.
*
* @return integer Number of classes which have been compiled
*/
public function compile()
{
$compiledClasses = [];
foreach ($this->objectManager->getRegisteredClassNames() as $fullOriginalClassNames) {
foreach ($fullOriginalClassNames as $fullOriginalClassName) {
if (isset($this->proxyClasses[$fullOriginalClassName])) {
$proxyClassCode = $this->proxyClasses[$fullOriginalClassName]->render();
if ($proxyClassCode !== '') {
$class = new \ReflectionClass($fullOriginalClassName);
$classPathAndFilename = $class->getFileName();
$this->cacheOriginalClassFileAndProxyCode($fullOriginalClassName, $classPathAndFilename, $proxyClassCode);
$this->storedProxyClasses[str_replace('\\', '_', $fullOriginalClassName)] = true;
$compiledClasses[] = $fullOriginalClassName;
}
} else {
if ($this->classesCache->has(str_replace('\\', '_', $fullOriginalClassName))) {
$this->storedProxyClasses[str_replace('\\', '_', $fullOriginalClassName)] = true;
}
}
}
}
$this->emitCompiledClasses($compiledClasses);
return count($compiledClasses);
}
/**
* @param array<string> $classNames
* @Flow\Signal
*/
public function emitCompiledClasses(array $classNames)
{
}
/**
* @return string
*/
public function getStoredProxyClassMap()
{
$return = '<?php
/**
* This is a cached list of all proxy classes. Only classes in this array will
* actually be loaded from the proxy class cache in the ClassLoader.
*/
return ' . var_export($this->storedProxyClasses, true) . ';';
return $return;
}
/**
* Reads the specified class file, appends ORIGINAL_CLASSNAME_SUFFIX to its
* class name and stores the result in the proxy classes cache.
*
* @param string $className Short class name of the class to copy
* @param string $pathAndFilename Full path and filename of the original class file
* @param string $proxyClassCode The code that makes up the proxy class
* @return void
*
* @throws Exception If the original class filename doesn't match the actual class name inside the file.
*/
protected function cacheOriginalClassFileAndProxyCode($className, $pathAndFilename, $proxyClassCode)
{
$classCode = file_get_contents($pathAndFilename);
$classCode = $this->stripOpeningPhpTag($classCode);
$classNameSuffix = self::ORIGINAL_CLASSNAME_SUFFIX;
$classCode = preg_replace_callback('/^([a-z\s]*?)(final\s+)?(interface|class)\s+([a-zA-Z0-9_]+)/m', static function ($matches) use ($pathAndFilename, $classNameSuffix, $proxyClassCode) {
$classNameAccordingToFileName = basename($pathAndFilename, '.php');
if ($matches[4] !== $classNameAccordingToFileName) {
throw new Exception('The name of the class "' . $matches[4] . '" is not the same as the filename which is "' . basename($pathAndFilename) . '". Path: ' . $pathAndFilename, 1398356897);
}
return $matches[1] . $matches[3] . ' ' . $matches[4] . $classNameSuffix;
}, $classCode);
// comment out "final" keyword, if the method is final and if it is advised (= part of the $proxyClassCode)
// Note: Method name regex according to http://php.net/manual/en/language.oop5.basic.php
$classCode = preg_replace_callback('/^(\s*)((public|protected)\s+)?final(\s+(public|protected))?(\s+function\s+)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]+\s*\()/m', static function ($matches) use ($proxyClassCode) {
// the method is not advised => don't remove the final keyword
if (strpos($proxyClassCode, $matches[0]) === false) {
return $matches[0];
}
return $matches[1] . $matches[2] . '/*final*/' . $matches[4] . $matches[6] . $matches[7];
}, $classCode);
$classCode = preg_replace('/\\?>[\n\s\r]*$/', '', $classCode);
$proxyClassCode .= PHP_EOL . '# PathAndFilename: ' . $pathAndFilename;
$separator =
PHP_EOL . '#' .
PHP_EOL . '# Start of Flow generated Proxy code' .
PHP_EOL . '#' . PHP_EOL;
$this->classesCache->set(str_replace('\\', '_', $className), $classCode . $separator . $proxyClassCode);
}
/**
* Removes the first opening php tag ("<?php") from the given $classCode if there is any
*
* @param string $classCode
* @return string the original class code without opening php tag
*/
protected function stripOpeningPhpTag($classCode)
{
return preg_replace('/^\s*\\<\\?php(.*\n|.*)/', '$1', $classCode, 1);
}
/**
* Render the source (string) form of a PHP Attribute.
* @param \ReflectionAttribute $attribute
* @return string
*/
public static function renderAttribute($attribute): string
{
$attributeAsString = '\\' . $attribute->getName();
if (count($attribute->getArguments()) > 0) {
$argumentsAsString = [];
foreach ($attribute->getArguments() as $argumentName => $argumentValue) {
$renderedArgumentValue = var_export($argumentValue, true);
if (is_numeric($argumentName)) {
$argumentsAsString[] = $renderedArgumentValue;
} else {
$argumentsAsString[] = "$argumentName: $renderedArgumentValue";
}
}
$attributeAsString .= '(' . implode(', ', $argumentsAsString) . ')';
}
return "#[$attributeAsString]";
}
/**
* Render the source (string) form of an Annotation instance.
*
* @param \Doctrine\Common\Annotations\Annotation $annotation
* @return string
*/
public static function renderAnnotation($annotation)
{
$annotationAsString = '@\\' . get_class($annotation);
$optionDefaults = get_class_vars(get_class($annotation));
$optionValues = get_object_vars($annotation);
$optionsAsStrings = [];
foreach ($optionValues as $optionName => $optionValue) {
// FIXME: This is a workaround for https://github.com/neos/flow-development-collection/issues/2387
if ($optionName[0] === '_') {
continue;
}
$optionValueAsString = '';
if (is_object($optionValue)) {
$optionValueAsString = self::renderAnnotation($optionValue);
} elseif (is_string($optionValue)) {
$optionValueAsString = '"' . $optionValue . '"';
} elseif (is_bool($optionValue)) {
$optionValueAsString = $optionValue ? 'true' : 'false';
} elseif (is_scalar($optionValue)) {
$optionValueAsString = $optionValue;
} elseif (is_array($optionValue)) {
$optionValueAsString = self::renderOptionArrayValueAsString($optionValue);
}
switch ($optionName) {
case 'value':
$optionsAsStrings[] = $optionValueAsString;
break;
default:
if ($optionValue === $optionDefaults[$optionName]) {
break;
}
$optionsAsStrings[] = $optionName . '=' . $optionValueAsString;
}
}
return $annotationAsString . ($optionsAsStrings !== [] ? '(' . implode(', ', $optionsAsStrings) . ')' : '');
}
/**
* Render an array value as string for an annotation.
*
* @param array $optionValue
* @return string
*/
protected static function renderOptionArrayValueAsString(array $optionValue)
{
$values = [];
foreach ($optionValue as $k => $v) {
$value = '';
if (is_string($k)) {
$value .= '"' . $k . '"=';
}
if (is_object($v)) {
$value .= self::renderAnnotation($v);
} elseif (is_array($v)) {
$value .= self::renderOptionArrayValueAsString($v);
} elseif (is_string($v)) {
$value .= '"' . $v . '"';
} elseif (is_bool($v)) {
$value .= $v ? 'true' : 'false';
} elseif (is_scalar($v)) {
$value .= $v;
}
$values[] = $value;
}
return '{ ' . implode(', ', $values) . ' }';
}
}