Completed
Push — compatibility-v9 ( 6c12d0...62deca )
by Romain
01:58
created

ReflectionService::hasMethod()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 4
nc 5
nop 2
1
<?php
2
namespace Romm\ConfigurationObject\Legacy\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 Exception;
18
use TYPO3\CMS\Core\Utility\ClassNamingUtility;
19
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
20
21
/**
22
 * A backport of the TYPO3.Flow reflection service for acquiring reflection based information.
23
 * Most of the code is based on the TYPO3.Flow reflection service.
24
 *
25
 * @api
26
 */
27
class ReflectionService implements \TYPO3\CMS\Core\SingletonInterface
28
{
29
    /**
30
     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
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 = [];
60
61
    /**
62
     * Array of tags and the names of classes which are tagged with them.
63
     *
64
     * @var array
65
     */
66
    protected $taggedClasses = [];
67
68
    /**
69
     * Array of class names and their tags and values.
70
     *
71
     * @var array
72
     */
73
    protected $classTagsValues = [];
74
75
    /**
76
     * Array of class names, method names and their tags and values.
77
     *
78
     * @var array
79
     */
80
    protected $methodTagsValues = [];
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 = [];
89
90
    /**
91
     * Array of class names and names of their properties.
92
     *
93
     * @var array
94
     */
95
    protected $classPropertyNames = [];
96
97
    /**
98
     * Array of class names and names of their methods.
99
     *
100
     * @var array
101
     */
102
    protected $classMethodNames = [];
103
104
    /**
105
     * Array of class names, property names and their tags and values.
106
     *
107
     * @var array
108
     */
109
    protected $propertyTagsValues = [];
110
111
    /**
112
     * List of tags which are ignored while reflecting class and method annotations.
113
     *
114
     * @var array
115
     */
116
    protected $ignoredTags = ['package', 'subpackage', 'license', 'copyright', 'author', 'version', 'const'];
117
118
    /**
119
     * Indicates whether the Reflection cache needs to be updated.
120
     *
121
     * This flag needs to be set as soon as new Reflection information was
122
     * created.
123
     *
124
     * @see reflectClass()
125
     * @see getMethodReflection()
126
     * @var bool
127
     */
128
    protected $dataCacheNeedsUpdate = false;
129
130
    /**
131
     * Local cache for Class schemata
132
     *
133
     * @var array
134
     */
135
    protected $classSchemata = [];
136
137
    /**
138
     * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
139
     */
140
    protected $configurationManager;
141
142
    /**
143
     * @var string
144
     */
145
    protected $cacheIdentifier;
146
147
    /**
148
     * Internal runtime cache of method reflection objects
149
     *
150
     * @var array
151
     */
152
    protected $methodReflections = [];
153
154
    /**
155
     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
156
     */
157
    public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
158
    {
159
        $this->objectManager = $objectManager;
160
    }
161
162
    /**
163
     * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
164
     */
165
    public function injectConfigurationManager(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager)
166
    {
167
        $this->configurationManager = $configurationManager;
168
    }
169
170
    /**
171
     * Sets the data cache.
172
     *
173
     * The cache must be set before initializing the Reflection Service.
174
     *
175
     * @param \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend $dataCache Cache for the Reflection service
176
     */
177
    public function setDataCache(\TYPO3\CMS\Core\Cache\Frontend\VariableFrontend $dataCache)
178
    {
179
        $this->dataCache = $dataCache;
180
    }
181
182
    /**
183
     * Initializes this service
184
     *
185
     * @throws Exception
186
     */
187
    public function initialize()
188
    {
189
        if ($this->initialized) {
190
            throw new Exception('The Reflection Service can only be initialized once.', 1232044696);
191
        }
192
        $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
193
        $this->cacheIdentifier = 'ReflectionData_' . $frameworkConfiguration['extensionName'];
194
        $this->loadFromCache();
195
        $this->initialized = true;
196
    }
197
198
    /**
199
     * Returns whether the Reflection Service is initialized.
200
     *
201
     * @return bool true if the Reflection Service is initialized, otherwise false
202
     */
203
    public function isInitialized()
204
    {
205
        return $this->initialized;
206
    }
207
208
    /**
209
     * Shuts the Reflection Service down.
210
     */
211
    public function shutdown()
212
    {
213
        if ($this->dataCacheNeedsUpdate) {
214
            $this->saveToCache();
215
        }
216
        $this->initialized = false;
217
    }
218
219
    /**
220
     * Returns all tags and their values the specified class is tagged with
221
     *
222
     * @param string $className Name of the class
223
     * @return array An array of tags and their values or an empty array if no tags were found
224
     */
225
    public function getClassTagsValues($className)
226
    {
227
        if (!isset($this->reflectedClassNames[$className])) {
228
            $this->reflectClass($className);
229
        }
230
        if (!isset($this->classTagsValues[$className])) {
231
            return [];
232
        }
233
        return isset($this->classTagsValues[$className]) ? $this->classTagsValues[$className] : [];
234
    }
235
236
    /**
237
     * Returns the values of the specified class tag
238
     *
239
     * @param string $className Name of the class containing the property
240
     * @param string $tag Tag to return the values of
241
     * @return array An array of values or an empty array if the tag was not found
242
     */
243
    public function getClassTagValues($className, $tag)
244
    {
245
        if (!isset($this->reflectedClassNames[$className])) {
246
            $this->reflectClass($className);
247
        }
248
        if (!isset($this->classTagsValues[$className])) {
249
            return [];
250
        }
251
        return isset($this->classTagsValues[$className][$tag]) ? $this->classTagsValues[$className][$tag] : [];
252
    }
253
254
    /**
255
     * Returns the names of all properties of the specified class
256
     *
257
     * @param string $className Name of the class to return the property names of
258
     * @return array An array of property names or an empty array if none exist
259
     */
260
    public function getClassPropertyNames($className)
261
    {
262
        if (!isset($this->reflectedClassNames[$className])) {
263
            $this->reflectClass($className);
264
        }
265
        return isset($this->classPropertyNames[$className]) ? $this->classPropertyNames[$className] : [];
266
    }
267
268
    /**
269
     * Returns the class schema for the given class
270
     *
271
     * @param mixed $classNameOrObject The class name or an object
272
     * @return ClassSchema
273
     */
274
    public function getClassSchema($classNameOrObject)
275
    {
276
        $className = is_object($classNameOrObject) ? get_class($classNameOrObject) : $classNameOrObject;
277
        if (isset($this->classSchemata[$className])) {
278
            return $this->classSchemata[$className];
279
        }
280
        return $this->buildClassSchema($className);
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);
0 ignored issues
show
Bug introduced by
Consider using $parameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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();
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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);
0 ignored issues
show
Bug introduced by
Consider using $parameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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
481
     * @return ClassSchema The class schema
482
     */
483
    protected function buildClassSchema($className)
484
    {
485
        if (!class_exists($className)) {
486
            throw new Exception('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);
0 ignored issues
show
Unused Code introduced by
The call to ObjectManagerInterface::get() has too many arguments starting with $className.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
489
        if (is_subclass_of($className, \TYPO3\CMS\Extbase\DomainObject\AbstractEntity::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \TYPO3\CMS\Extbase\Domai...t\AbstractEntity::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
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)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \TYPO3\CMS\Extbase\Domai...tractValueObject::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
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;
0 ignored issues
show
Bug introduced by
Consider using $parameterClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
534
        if ($parameter->isDefaultValueAvailable()) {
535
            $parameterInformation['defaultValue'] = $parameter->getDefaultValue();
536
        }
537
        if ($parameterClass !== null) {
538
            $parameterInformation['type'] = $parameterClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $parameterClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
539
        } elseif ($method !== null) {
540
            $methodTagsAndValues = $this->getMethodTagsValues($method->getDeclaringClass()->getName(), $method->getName());
0 ignored issues
show
introduced by
Consider using $method->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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
}
619