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