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