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