Revert "[BUGFIX] Reflection Cache does not save methodReflections"
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Reflection / ReflectionService.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Reflection;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\ClassNamingUtility;
18 use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
19
20 /**
21 * A backport of the TYPO3.Flow reflection service for aquiring reflection based information.
22 * Most of the code is based on the TYPO3.Flow reflection service.
23 *
24 * @api
25 */
26 class ReflectionService implements \TYPO3\CMS\Core\SingletonInterface
27 {
28 /**
29 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
30 */
31 protected $objectManager;
32
33 /**
34 * Whether this service has been initialized.
35 *
36 * @var bool
37 */
38 protected $initialized = false;
39
40 /**
41 * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
42 */
43 protected $dataCache;
44
45 /**
46 * Whether class alterations should be detected on each initialization.
47 *
48 * @var bool
49 */
50 protected $detectClassChanges = false;
51
52 /**
53 * All available class names to consider. Class name = key, value is the
54 * UNIX timestamp the class was reflected.
55 *
56 * @var array
57 */
58 protected $reflectedClassNames = [];
59
60 /**
61 * Array of tags and the names of classes which are tagged with them.
62 *
63 * @var array
64 */
65 protected $taggedClasses = [];
66
67 /**
68 * Array of class names and their tags and values.
69 *
70 * @var array
71 */
72 protected $classTagsValues = [];
73
74 /**
75 * Array of class names, method names and their tags and values.
76 *
77 * @var array
78 */
79 protected $methodTagsValues = [];
80
81 /**
82 * Array of class names, method names, their parameters and additional
83 * information about the parameters.
84 *
85 * @var array
86 */
87 protected $methodParameters = [];
88
89 /**
90 * Array of class names and names of their properties.
91 *
92 * @var array
93 */
94 protected $classPropertyNames = [];
95
96 /**
97 * Array of class names, property names and their tags and values.
98 *
99 * @var array
100 */
101 protected $propertyTagsValues = [];
102
103 /**
104 * List of tags which are ignored while reflecting class and method annotations.
105 *
106 * @var array
107 */
108 protected $ignoredTags = ['package', 'subpackage', 'license', 'copyright', 'author', 'version', 'const'];
109
110 /**
111 * Indicates whether the Reflection cache needs to be updated.
112 *
113 * This flag needs to be set as soon as new Reflection information was
114 * created.
115 *
116 * @see reflectClass()
117 * @see getMethodReflection()
118 * @var bool
119 */
120 protected $dataCacheNeedsUpdate = false;
121
122 /**
123 * Local cache for Class schemata
124 *
125 * @var array
126 */
127 protected $classSchemata = [];
128
129 /**
130 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
131 */
132 protected $configurationManager;
133
134 /**
135 * @var string
136 */
137 protected $cacheIdentifier;
138
139 /**
140 * @var array
141 */
142 protected $methodReflections = [];
143
144 /**
145 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
146 */
147 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
148 {
149 $this->objectManager = $objectManager;
150 }
151
152 /**
153 * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
154 */
155 public function injectConfigurationManager(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager)
156 {
157 $this->configurationManager = $configurationManager;
158 }
159
160 /**
161 * Sets the data cache.
162 *
163 * The cache must be set before initializing the Reflection Service.
164 *
165 * @param \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend $dataCache Cache for the Reflection service
166 * @return void
167 */
168 public function setDataCache(\TYPO3\CMS\Core\Cache\Frontend\VariableFrontend $dataCache)
169 {
170 $this->dataCache = $dataCache;
171 }
172
173 /**
174 * Initializes this service
175 *
176 * @throws Exception
177 * @return void
178 */
179 public function initialize()
180 {
181 if ($this->initialized) {
182 throw new Exception('The Reflection Service can only be initialized once.', 1232044696);
183 }
184 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
185 $this->cacheIdentifier = 'ReflectionData_' . $frameworkConfiguration['extensionName'];
186 $this->loadFromCache();
187 $this->initialized = true;
188 }
189
190 /**
191 * Returns whether the Reflection Service is initialized.
192 *
193 * @return bool true if the Reflection Service is initialized, otherwise false
194 */
195 public function isInitialized()
196 {
197 return $this->initialized;
198 }
199
200 /**
201 * Shuts the Reflection Service down.
202 *
203 * @return void
204 */
205 public function shutdown()
206 {
207 if ($this->dataCacheNeedsUpdate) {
208 $this->saveToCache();
209 }
210 $this->initialized = false;
211 }
212
213 /**
214 * Returns all tags and their values the specified class is tagged with
215 *
216 * @param string $className Name of the class
217 * @return array An array of tags and their values or an empty array if no tags were found
218 */
219 public function getClassTagsValues($className)
220 {
221 if (!isset($this->reflectedClassNames[$className])) {
222 $this->reflectClass($className);
223 }
224 if (!isset($this->classTagsValues[$className])) {
225 return [];
226 }
227 return isset($this->classTagsValues[$className]) ? $this->classTagsValues[$className] : [];
228 }
229
230 /**
231 * Returns the values of the specified class tag
232 *
233 * @param string $className Name of the class containing the property
234 * @param string $tag Tag to return the values of
235 * @return array An array of values or an empty array if the tag was not found
236 */
237 public function getClassTagValues($className, $tag)
238 {
239 if (!isset($this->reflectedClassNames[$className])) {
240 $this->reflectClass($className);
241 }
242 if (!isset($this->classTagsValues[$className])) {
243 return [];
244 }
245 return isset($this->classTagsValues[$className][$tag]) ? $this->classTagsValues[$className][$tag] : [];
246 }
247
248 /**
249 * Returns the names of all properties of the specified class
250 *
251 * @param string $className Name of the class to return the property names of
252 * @return array An array of property names or an empty array if none exist
253 */
254 public function getClassPropertyNames($className)
255 {
256 if (!isset($this->reflectedClassNames[$className])) {
257 $this->reflectClass($className);
258 }
259 return isset($this->classPropertyNames[$className]) ? $this->classPropertyNames[$className] : [];
260 }
261
262 /**
263 * Returns the class schema for the given class
264 *
265 * @param mixed $classNameOrObject The class name or an object
266 * @return ClassSchema
267 */
268 public function getClassSchema($classNameOrObject)
269 {
270 $className = is_object($classNameOrObject) ? get_class($classNameOrObject) : $classNameOrObject;
271 if (isset($this->classSchemata[$className])) {
272 return $this->classSchemata[$className];
273 } else {
274 return $this->buildClassSchema($className);
275 }
276 }
277
278 /**
279 * Wrapper for method_exists() which tells if the given method exists.
280 *
281 * @param string $className Name of the class containing the method
282 * @param string $methodName Name of the method
283 * @return bool
284 */
285 public function hasMethod($className, $methodName)
286 {
287 try {
288 if (!array_key_exists($className, $this->methodReflections) || !array_key_exists($methodName, $this->methodReflections[$className])) {
289 $this->methodReflections[$className][$methodName] = new MethodReflection($className, $methodName);
290 $this->dataCacheNeedsUpdate = true;
291 }
292 } catch (\ReflectionException $e) {
293 // Method does not exist. Store this information in cache.
294 $this->methodReflections[$className][$methodName] = null;
295 }
296 return isset($this->methodReflections[$className][$methodName]);
297 }
298
299 /**
300 * Returns all tags and their values the specified method is tagged with
301 *
302 * @param string $className Name of the class containing the method
303 * @param string $methodName Name of the method to return the tags and values of
304 * @return array An array of tags and their values or an empty array of no tags were found
305 */
306 public function getMethodTagsValues($className, $methodName)
307 {
308 if (!isset($this->methodTagsValues[$className][$methodName])) {
309 $this->methodTagsValues[$className][$methodName] = [];
310 $method = $this->getMethodReflection($className, $methodName);
311 foreach ($method->getTagsValues() as $tag => $values) {
312 if (array_search($tag, $this->ignoredTags) === false) {
313 $this->methodTagsValues[$className][$methodName][$tag] = $values;
314 }
315 }
316 }
317 return $this->methodTagsValues[$className][$methodName];
318 }
319
320 /**
321 * Returns an array of parameters of the given method. Each entry contains
322 * additional information about the parameter position, type hint etc.
323 *
324 * @param string $className Name of the class containing the method
325 * @param string $methodName Name of the method to return parameter information of
326 * @return array An array of parameter names and additional information or an empty array of no parameters were found
327 */
328 public function getMethodParameters($className, $methodName)
329 {
330 if (!isset($this->methodParameters[$className][$methodName])) {
331 $method = $this->getMethodReflection($className, $methodName);
332 $this->methodParameters[$className][$methodName] = [];
333 foreach ($method->getParameters() as $parameterPosition => $parameter) {
334 $this->methodParameters[$className][$methodName][$parameter->getName()] = $this->convertParameterReflectionToArray($parameter, $parameterPosition, $method);
335 }
336 }
337 return $this->methodParameters[$className][$methodName];
338 }
339
340 /**
341 * Returns all tags and their values the specified class property is tagged with
342 *
343 * @param string $className Name of the class containing the property
344 * @param string $propertyName Name of the property to return the tags and values of
345 * @return array An array of tags and their values or an empty array of no tags were found
346 */
347 public function getPropertyTagsValues($className, $propertyName)
348 {
349 if (!isset($this->reflectedClassNames[$className])) {
350 $this->reflectClass($className);
351 }
352 if (!isset($this->propertyTagsValues[$className])) {
353 return [];
354 }
355 return isset($this->propertyTagsValues[$className][$propertyName]) ? $this->propertyTagsValues[$className][$propertyName] : [];
356 }
357
358 /**
359 * Returns the values of the specified class property tag
360 *
361 * @param string $className Name of the class containing the property
362 * @param string $propertyName Name of the tagged property
363 * @param string $tag Tag to return the values of
364 * @return array An array of values or an empty array if the tag was not found
365 */
366 public function getPropertyTagValues($className, $propertyName, $tag)
367 {
368 if (!isset($this->reflectedClassNames[$className])) {
369 $this->reflectClass($className);
370 }
371 if (!isset($this->propertyTagsValues[$className][$propertyName])) {
372 return [];
373 }
374 return isset($this->propertyTagsValues[$className][$propertyName][$tag]) ? $this->propertyTagsValues[$className][$propertyName][$tag] : [];
375 }
376
377 /**
378 * Tells if the specified class is known to this reflection service and
379 * reflection information is available.
380 *
381 * @param string $className Name of the class
382 * @return bool If the class is reflected by this service
383 */
384 public function isClassReflected($className)
385 {
386 return isset($this->reflectedClassNames[$className]);
387 }
388
389 /**
390 * Tells if the specified class is tagged with the given tag
391 *
392 * @param string $className Name of the class
393 * @param string $tag Tag to check for
394 * @return bool TRUE if the class is tagged with $tag, otherwise FALSE
395 */
396 public function isClassTaggedWith($className, $tag)
397 {
398 if ($this->initialized === false) {
399 return false;
400 }
401 if (!isset($this->reflectedClassNames[$className])) {
402 $this->reflectClass($className);
403 }
404 if (!isset($this->classTagsValues[$className])) {
405 return false;
406 }
407 return isset($this->classTagsValues[$className][$tag]);
408 }
409
410 /**
411 * Tells if the specified class property is tagged with the given tag
412 *
413 * @param string $className Name of the class
414 * @param string $propertyName Name of the property
415 * @param string $tag Tag to check for
416 * @return bool TRUE if the class property is tagged with $tag, otherwise FALSE
417 */
418 public function isPropertyTaggedWith($className, $propertyName, $tag)
419 {
420 if (!isset($this->reflectedClassNames[$className])) {
421 $this->reflectClass($className);
422 }
423 if (!isset($this->propertyTagsValues[$className])) {
424 return false;
425 }
426 if (!isset($this->propertyTagsValues[$className][$propertyName])) {
427 return false;
428 }
429 return isset($this->propertyTagsValues[$className][$propertyName][$tag]);
430 }
431
432 /**
433 * Reflects the given class and stores the results in this service's properties.
434 *
435 * @param string $className Full qualified name of the class to reflect
436 * @return void
437 */
438 protected function reflectClass($className)
439 {
440 $class = new ClassReflection($className);
441 $this->reflectedClassNames[$className] = time();
442 foreach ($class->getTagsValues() as $tag => $values) {
443 if (array_search($tag, $this->ignoredTags) === false) {
444 $this->taggedClasses[$tag][] = $className;
445 $this->classTagsValues[$className][$tag] = $values;
446 }
447 }
448 foreach ($class->getProperties() as $property) {
449 $propertyName = $property->getName();
450 $this->classPropertyNames[$className][] = $propertyName;
451 foreach ($property->getTagsValues() as $tag => $values) {
452 if (array_search($tag, $this->ignoredTags) === false) {
453 $this->propertyTagsValues[$className][$propertyName][$tag] = $values;
454 }
455 }
456 }
457 foreach ($class->getMethods() as $method) {
458 $methodName = $method->getName();
459 foreach ($method->getTagsValues() as $tag => $values) {
460 if (array_search($tag, $this->ignoredTags) === false) {
461 $this->methodTagsValues[$className][$methodName][$tag] = $values;
462 }
463 }
464 foreach ($method->getParameters() as $parameterPosition => $parameter) {
465 $this->methodParameters[$className][$methodName][$parameter->getName()] = $this->convertParameterReflectionToArray($parameter, $parameterPosition, $method);
466 }
467 }
468 ksort($this->reflectedClassNames);
469 $this->dataCacheNeedsUpdate = true;
470 }
471
472 /**
473 * Builds class schemata from classes annotated as entities or value objects
474 *
475 * @param string $className
476 * @throws Exception\UnknownClassException
477 * @return ClassSchema The class schema
478 */
479 protected function buildClassSchema($className)
480 {
481 if (!class_exists($className)) {
482 throw new Exception\UnknownClassException('The classname "' . $className . '" was not found and thus can not be reflected.', 1278450972);
483 }
484 $classSchema = $this->objectManager->get(\TYPO3\CMS\Extbase\Reflection\ClassSchema::class, $className);
485 if (is_subclass_of($className, \TYPO3\CMS\Extbase\DomainObject\AbstractEntity::class)) {
486 $classSchema->setModelType(ClassSchema::MODELTYPE_ENTITY);
487 $possibleRepositoryClassName = ClassNamingUtility::translateModelNameToRepositoryName($className);
488 if (class_exists($possibleRepositoryClassName)) {
489 $classSchema->setAggregateRoot(true);
490 }
491 } elseif (is_subclass_of($className, \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject::class)) {
492 $classSchema->setModelType(ClassSchema::MODELTYPE_VALUEOBJECT);
493 }
494 foreach ($this->getClassPropertyNames($className) as $propertyName) {
495 if (!$this->isPropertyTaggedWith($className, $propertyName, 'transient') && $this->isPropertyTaggedWith($className, $propertyName, 'var')) {
496 $cascadeTagValues = $this->getPropertyTagValues($className, $propertyName, 'cascade');
497 $classSchema->addProperty($propertyName, implode(' ', $this->getPropertyTagValues($className, $propertyName, 'var')), $this->isPropertyTaggedWith($className, $propertyName, 'lazy'), $cascadeTagValues[0]);
498 }
499 if ($this->isPropertyTaggedWith($className, $propertyName, 'uuid')) {
500 $classSchema->setUuidPropertyName($propertyName);
501 }
502 if ($this->isPropertyTaggedWith($className, $propertyName, 'identity')) {
503 $classSchema->markAsIdentityProperty($propertyName);
504 }
505 }
506 $this->classSchemata[$className] = $classSchema;
507 $this->dataCacheNeedsUpdate = true;
508 return $classSchema;
509 }
510
511 /**
512 * Converts the given parameter reflection into an information array
513 *
514 * @param ParameterReflection $parameter The parameter to reflect
515 * @param int $parameterPosition
516 * @param MethodReflection|NULL $method
517 * @return array Parameter information array
518 */
519 protected function convertParameterReflectionToArray(ParameterReflection $parameter, $parameterPosition, MethodReflection $method = null)
520 {
521 $parameterInformation = [
522 'position' => $parameterPosition,
523 'byReference' => $parameter->isPassedByReference(),
524 'array' => $parameter->isArray(),
525 'optional' => $parameter->isOptional(),
526 'allowsNull' => $parameter->allowsNull()
527 ];
528 $parameterClass = $parameter->getClass();
529 $parameterInformation['class'] = $parameterClass !== null ? $parameterClass->getName() : null;
530 if ($parameter->isDefaultValueAvailable()) {
531 $parameterInformation['defaultValue'] = $parameter->getDefaultValue();
532 }
533 if ($parameterClass !== null) {
534 $parameterInformation['type'] = $parameterClass->getName();
535 } elseif ($method !== null) {
536 $methodTagsAndValues = $this->getMethodTagsValues($method->getDeclaringClass()->getName(), $method->getName());
537 if (isset($methodTagsAndValues['param']) && isset($methodTagsAndValues['param'][$parameterPosition])) {
538 $explodedParameters = explode(' ', $methodTagsAndValues['param'][$parameterPosition]);
539 if (count($explodedParameters) >= 2) {
540 if (TypeHandlingUtility::isSimpleType($explodedParameters[0])) {
541 // ensure that short names of simple types are resolved correctly to the long form
542 // this is important for all kinds of type checks later on
543 $typeInfo = TypeHandlingUtility::parseType($explodedParameters[0]);
544 $parameterInformation['type'] = $typeInfo['type'];
545 } else {
546 $parameterInformation['type'] = $explodedParameters[0];
547 }
548 }
549 }
550 }
551 if (isset($parameterInformation['type']) && $parameterInformation['type'][0] === '\\') {
552 $parameterInformation['type'] = substr($parameterInformation['type'], 1);
553 }
554 return $parameterInformation;
555 }
556
557 /**
558 * Returns the Reflection of a method.
559 *
560 * @param string $className Name of the class containing the method
561 * @param string $methodName Name of the method to return the Reflection for
562 * @return MethodReflection the method Reflection object
563 */
564 protected function getMethodReflection($className, $methodName)
565 {
566 if (!isset($this->methodReflections[$className][$methodName])) {
567 $this->methodReflections[$className][$methodName] = new MethodReflection($className, $methodName);
568 $this->dataCacheNeedsUpdate = true;
569 }
570 return $this->methodReflections[$className][$methodName];
571 }
572
573 /**
574 * Tries to load the reflection data from this service's cache.
575 *
576 * @return void
577 */
578 protected function loadFromCache()
579 {
580 $data = $this->dataCache->get($this->cacheIdentifier);
581 if ($data !== false) {
582 foreach ($data as $propertyName => $propertyValue) {
583 $this->{$propertyName} = $propertyValue;
584 }
585 }
586 }
587
588 /**
589 * Exports the internal reflection data into the ReflectionData cache.
590 *
591 * @throws Exception
592 * @return void
593 */
594 protected function saveToCache()
595 {
596 if (!is_object($this->dataCache)) {
597 throw new Exception('A cache must be injected before initializing the Reflection Service.', 1232044697);
598 }
599 $data = [];
600 $propertyNames = [
601 'reflectedClassNames',
602 'classPropertyNames',
603 'classTagsValues',
604 'methodTagsValues',
605 'methodParameters',
606 'propertyTagsValues',
607 'taggedClasses',
608 'classSchemata'
609 ];
610 foreach ($propertyNames as $propertyName) {
611 $data[$propertyName] = $this->{$propertyName};
612 }
613 $this->dataCache->set($this->cacheIdentifier, $data);
614 }
615 }