Completed
Push — master ( 41892d...85d060 )
by Alexander
13s
created

AnnotationManager::getAnnotations()   D

Complexity

Conditions 25
Paths 210

Size

Total Lines 96
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 66
CRAP Score 25.002

Importance

Changes 0
Metric Value
dl 0
loc 96
ccs 66
cts 67
cp 0.9851
rs 4.1653
c 0
b 0
f 0
cc 25
eloc 52
nc 210
nop 3
crap 25.002

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of the php-annotation framework.
5
 *
6
 * (c) Rasmus Schultz <[email protected]>
7
 *
8
 * This software is licensed under the GNU LGPL license
9
 * for more information, please see:
10
 *
11
 * <https://github.com/mindplay-dk/php-annotations>
12
 */
13
14
namespace mindplay\annotations;
15
16
/**
17
 * This class manages the retrieval of Annotations from source code files
18
 */
19
class AnnotationManager
20
{
21
    const CACHE_FORMAT_VERSION = 3;
22
23
    const MEMBER_CLASS = 'class';
24
25
    const MEMBER_PROPERTY = 'property';
26
27
    const MEMBER_METHOD = 'method';
28
29
    /**
30
     * @var boolean Enable PHP autoloader when searching for annotation classes (defaults to true)
31
     */
32
    public $autoload = true;
33
34
    /**
35
     * @var string The class-name suffix for Annotation classes.
36
     */
37
    public $suffix = 'Annotation';
38
39
    /**
40
     * @var string The default namespace for annotations with no namespace qualifier.
41
     */
42
    public $namespace = '';
43
44
    /**
45
     * @var AnnotationCache|bool a cache-provider used to store annotation-data after parsing; or false to disable caching
46
     * @see getAnnotationData()
47
     */
48
    public $cache;
49
50
    /**
51
     * @var array List of registered annotation aliases.
52
     */
53
    public $registry = array(
54
        'api'            => false,
55
        'abstract'       => false,
56
        'access'         => false,
57
        'author'         => false,
58
        'category'       => false,
59
        'copyright'      => false,
60
        'deprecated'     => false,
61
        'example'        => false,
62
        'filesource'     => false,
63
        'final'          => false,
64
        'global'         => false,
65
        'ignore'         => false,
66
        'internal'       => false,
67
        'license'        => false,
68
        'link'           => false,
69
        'method'         => 'mindplay\annotations\standard\MethodAnnotation',
70
        'name'           => false,
71
        'package'        => false,
72
        'param'          => 'mindplay\annotations\standard\ParamAnnotation',
73
        'property'       => 'mindplay\annotations\standard\PropertyAnnotation',
74
        'property-read'  => 'mindplay\annotations\standard\PropertyReadAnnotation',
75
        'property-write' => 'mindplay\annotations\standard\PropertyWriteAnnotation',
76
        'return'         => 'mindplay\annotations\standard\ReturnAnnotation',
77
        'see'            => false,
78
        'since'          => false,
79
        'source'         => false,
80
        'static'         => false,
81
        'staticvar'      => false,
82
        'subpackage'     => false,
83
        'todo'           => false,
84
        'tutorial'       => false,
85
        'throws'         => false,
86
        'type'           => 'mindplay\annotations\standard\TypeAnnotation',
87
        'usage'          => 'mindplay\annotations\UsageAnnotation',
88
        'stop'           => 'mindplay\annotations\StopAnnotation',
89
        'uses'           => false,
90
        'var'            => 'mindplay\annotations\standard\VarAnnotation',
91
        'version'        => false,
92
    );
93
94
    /**
95
     * @var boolean $debug Set to TRUE to enable HTML output for debugging
96
     */
97
    public $debug = false;
98
99
    /**
100
     * @var AnnotationParser
101
     */
102
    protected $parser;
103
104
    /**
105
     * An internal cache for annotation-data loaded from source-code files
106
     *
107
     * @var AnnotationFile[] hash where absolute path to php source-file => AnnotationFile instance
108
     */
109
    protected $files = array();
110
111
    /**
112
     * @var array[] An internal cache for Annotation instances
113
     * @see getAnnotations()
114
     */
115
    protected $annotations = array();
116
117
    /**
118
     * @var bool[] An array of flags indicating which annotation sets have been initialized
119
     * @see getAnnotations()
120
     */
121
    protected $initialized = array();
122
123
    /**
124
     * @var UsageAnnotation[] An internal cache for UsageAnnotation instances
125
     */
126
    protected $usage = array();
127
128
    /**
129
     * @var UsageAnnotation The standard UsageAnnotation
130
     */
131
    private $_usageAnnotation;
132
133
    /**
134
     * @var string a seed for caching - used when generating cache keys, to prevent collisions
135
     * when using more than one AnnotationManager in the same application.
136
     */
137
    private $_cacheSeed = '';
138
139
    /**
140
     * Whether this version of PHP has support for traits.
141
     */
142
    private $_traitsSupported;
143
144
    /**
145
     * Initialize the Annotation Manager
146
     *
147
     * @param string $cacheSeed only needed if using more than one AnnotationManager in the same application
148
     */
149 8
    public function __construct($cacheSeed = '')
150
    {
151 8
        $this->_cacheSeed = $cacheSeed;
152 8
        $this->_usageAnnotation = new UsageAnnotation();
153 8
        $this->_usageAnnotation->class = true;
154 8
        $this->_usageAnnotation->inherited = true;
155 8
        $this->_traitsSupported = \version_compare(PHP_VERSION, '5.4.0', '>=');
156 8
    }
157
158
    /**
159
     * Creates and returns the AnnotationParser instance
160
     * @return AnnotationParser
161
     */
162 13
    public function getParser()
163
    {
164 13
        if (!isset($this->parser)) {
165 6
            $this->parser = new AnnotationParser($this);
166 6
            $this->parser->debug = $this->debug;
167 6
            $this->parser->autoload = $this->autoload;
168 6
        }
169
170 13
        return $this->parser;
171
    }
172
173
    /**
174
     * Retrieves annotation-data from a given source-code file.
175
     *
176
     * Member-names in the returned array have the following format: Class, Class::method or Class::$member
177
     *
178
     * @param string $path the path of the source-code file from which to obtain annotation-data.
179
     * @return AnnotationFile
180
     *
181
     * @throws AnnotationException if cache is not configured
182
     *
183
     * @see $files
184
     * @see $cache
185
     */
186 36
    protected function getAnnotationFile($path)
187
    {
188 36
        if (!isset($this->files[$path])) {
189 13
            if ($this->cache === null) {
190 1
                throw new AnnotationException("AnnotationManager::\$cache is not configured");
191
            }
192
193 12
            if ($this->cache === false) {
194
                # caching is disabled
195 4
                $code = $this->getParser()->parseFile($path);
196 4
                $data = eval($code);
197 4
            } else {
198 8
                $checksum = \crc32($path . ':' . $this->_cacheSeed . ':' . self::CACHE_FORMAT_VERSION);
199 8
                $key = \basename($path) . '-' . \sprintf('%x', $checksum);
200
201 8
                if (($this->cache->exists($key) === false) || (\filemtime($path) > $this->cache->getTimestamp($key))) {
202 8
                    $code = $this->getParser()->parseFile($path);
203 8
                    $this->cache->store($key, $code);
204 8
                }
205
206 8
                $data = $this->cache->fetch($key);
207
            }
208
209 12
            $this->files[$path] = new AnnotationFile($path, $data);
210 12
        }
211
212 35
        return $this->files[$path];
213
    }
214
215
    /**
216
     * Resolves a name, using built-in annotation name resolution rules, and the registry.
217
     *
218
     * @param string $name the annotation-name
219
     *
220
     * @return string|bool The fully qualified annotation class-name, or false if the
221
     * requested annotation has been disabled (set to false) in the registry.
222
     *
223
     * @see $registry
224
     */
225 21
    public function resolveName($name)
226
    {
227 21
        if (\strpos($name, '\\') !== false) {
228 2
            return $name . $this->suffix; // annotation class-name is fully qualified
229
        }
230
231 20
        $type = \lcfirst($name);
232
233 20
        if (isset($this->registry[$type])) {
234 10
            return $this->registry[$type]; // type-name is registered
235
        }
236
237 16
        $type = \ucfirst(\strtr($name, '-', '_')) . $this->suffix;
238
239 16
        return \strlen($this->namespace)
240 16
            ? $this->namespace . '\\' . $type
241 16
            : $type;
242
    }
243
244
    /**
245
     * Constructs, initializes and returns IAnnotation objects
246
     *
247
     * @param string $class_name The name of the class from which to obtain Annotations
248
     * @param string $member_type The type of member, e.g. "class", "property" or "method"
249
     * @param string $member_name Optional member name, e.g. "method" or "$property"
250
     *
251
     * @return IAnnotation[] array of IAnnotation objects for the given class/member/name
252
     * @throws AnnotationException for bad annotations
253
     */
254 44
    protected function getAnnotations($class_name, $member_type = self::MEMBER_CLASS, $member_name = null)
255
    {
256 44
        $key = $class_name . ($member_name ? '::' . $member_name : '');
257
258 44
        if (!isset($this->initialized[$key])) {
259 35
            $annotations = array();
260 35
            $classAnnotations = array();
261
262 35
            if ($member_type !== self::MEMBER_CLASS) {
263 17
                $classAnnotations = $this->getAnnotations($class_name, self::MEMBER_CLASS);
264 17
            }
265
266 35
            $reflection = new \ReflectionClass($class_name);
267
268 35
            if ($reflection->getFileName() && !$reflection->isInternal()) {
269 35
                $file = $this->getAnnotationFile($reflection->getFileName());
270 34
            }
271
272 34
            $inherit = true; // inherit parent annotations unless directed not to
273
274 34
            if (isset($file)) {
275 34
                if (isset($file->data[$key])) {
276 32
                    foreach ($file->data[$key] as $spec) {
277 32
                        $name = $spec['#name']; // currently unused
278 32
                        $type = $spec['#type'];
279
280 32
                        unset($spec['#name'], $spec['#type']);
281
282 32
                        if (!\class_exists($type, $this->autoload)) {
283
                            throw new AnnotationException("Annotation type '{$type}' does not exist");
284
                        }
285
286 32
                        $annotation = new $type;
287
288 32
                        if (!($annotation instanceof IAnnotation)) {
289 1
                            throw new AnnotationException("Annotation type '{$type}' does not implement the mandatory IAnnotation interface");
290
                        }
291
292 31
                        if ($annotation instanceof IAnnotationFileAware) {
293 3
                            $annotation->setAnnotationFile($file);
294 3
                        }
295
296 31
                        $annotation->initAnnotation($spec);
297
298 30
                        $annotations[] = $annotation;
299 30
                    }
300
301 30
                    if ($member_type === self::MEMBER_CLASS) {
302 19
                        $classAnnotations = $annotations;
303 19
                    }
304 33
                } else if ($this->_traitsSupported && $member_name !== null) {
305 8
                    $traitAnnotations = array();
306
307 8
                    if (isset($file->traitMethodOverrides[$class_name][$member_name])) {
308 2
                        list($traitName, $originalMemberName) = $file->traitMethodOverrides[$class_name][$member_name];
309 2
                        $traitAnnotations = $this->getAnnotations($traitName, $member_type, $originalMemberName);
310 2
                    } else {
311 7
                        foreach ($reflection->getTraitNames() as $traitName) {
312 5
                            if ($this->classHasMember($traitName, $member_type, $member_name)) {
313 5
                                $traitAnnotations = $this->getAnnotations($traitName, $member_type, $member_name);
314 5
                                break;
315
                            }
316 7
                        }
317
                    }
318
319 8
                    $annotations = \array_merge($traitAnnotations, $annotations);
320 8
                }
321 33
            }
322
323 33
            foreach ($classAnnotations as $classAnnotation) {
324 26
                if ($classAnnotation instanceof StopAnnotation) {
325 3
                    $inherit = false; // do not inherit parent annotations
326 3
                }
327 33
            }
328
329 33
            if ($inherit && $parent = get_parent_class($class_name)) {
330 26
                $parent_annotations = array();
331
332 26
                if ($parent !== __NAMESPACE__ . '\Annotation') {
333 19
                    foreach ($this->getAnnotations($parent, $member_type, $member_name) as $annotation) {
334 16
                        if ($this->getUsage(\get_class($annotation))->inherited) {
335 16
                            $parent_annotations[] = $annotation;
336 16
                        }
337 18
                    }
338 18
                }
339
340 26
                $annotations = \array_merge($parent_annotations, $annotations);
341 26
            }
342
343 33
            $this->annotations[$key] = $this->applyConstraints($annotations, $member_type);
344
345 33
            $this->initialized[$key] = true;
346 33
        }
347
348 42
        return $this->annotations[$key];
349
    }
350
351
    /**
352
     * Determines whether a class or trait has the specified member.
353
     *
354
     * @param string $className The name of the class or trait to check
355
     * @param string $memberType The type of member, e.g. "property" or "method"
356
     * @param string $memberName The member name, e.g. "method" or "$property"
357
     *
358
     * @return bool whether class or trait has the specified member
359
     */
360 5
    protected function classHasMember($className, $memberType, $memberName)
361
    {
362 5
        if ($memberType === self::MEMBER_METHOD) {
363 3
            return \method_exists($className, $memberName);
364 2
        } else if ($memberType === self::MEMBER_PROPERTY) {
365 2
            return \property_exists($className, \ltrim($memberName, '$'));
366
        }
367
        return false;
368
    }
369
370
    /**
371
     * Validates the constraints (as defined by the UsageAnnotation of each annotation) of a
372
     * list of annotations for a given type of member.
373
     *
374
     * @param IAnnotation[] $annotations An array of IAnnotation objects to be validated (sorted with inherited annotations on top).
375
     * @param string        $member      The type of member to validate against (e.g. "class", "property" or "method").
376
     *
377
     * @return IAnnotation[] validated and filtered list of IAnnotations objects
378
     *
379
     * @throws AnnotationException if a constraint is violated.
380
     */
381 36
    protected function applyConstraints(array $annotations, $member)
382
    {
383 36
        $result = array();
384 36
        $annotationCount = \count($annotations);
385
386 36
        foreach ($annotations as $outerIndex => $annotation) {
387 33
            $type = \get_class($annotation);
388 33
            $usage = $this->getUsage($type);
389
390
            // Checks, that annotation can be applied to given class/method/property according to it's @usage annotation.
391 33
            if (!$usage->$member) {
392 3
                throw new AnnotationException("Annotation type '{$type}' cannot be applied to a {$member}");
393
            }
394
395 31
            if (!$usage->multiple) {
396
                // Process annotation coming after current (in the outer loop) and of same type.
397 16
                for ($innerIndex = $outerIndex + 1; $innerIndex < $annotationCount; $innerIndex += 1) {
398 5
                    if (!$annotations[$innerIndex] instanceof $type) {
399 3
                        continue;
400
                    }
401
402 2
                    if ($usage->inherited) {
403 1
                        continue 2; // Another annotation (in inner loop) overrides this one (in outer loop) - skip it.
404
                    }
405
406 1
                    throw new AnnotationException("Only one annotation of '{$type}' type may be applied to the same {$member}");
407
                }
408 16
            }
409
410 31
            $result[] = $annotation;
411 34
        }
412
413 34
        return $result;
414
    }
415
416
    /**
417
     * Filters annotations by class name
418
     *
419
     * @param IAnnotation[] $annotations An array of annotation objects
420
     * @param string $type The class-name by which to filter annotation objects; or annotation
421
     * type-name with a leading "@", e.g. "@var", which will be resolved through the registry
422
     *
423
     * @return array The filtered array of annotation objects - may return an empty array
424
     */
425 17
    protected function filterAnnotations(array $annotations, $type)
426
    {
427 17
        if (\substr($type, 0, 1) === '@') {
428 10
            $type = $this->resolveName(\substr($type, 1));
429 10
        }
430
431 17
        if ($type === false) {
432 1
            return array();
433
        }
434
435 16
        $result = array();
436
437 16
        foreach ($annotations as $annotation) {
438 16
            if ($annotation instanceof $type) {
439 16
                $result[] = $annotation;
440 16
            }
441 16
        }
442
443 16
        return $result;
444
    }
445
446
    /**
447
     * Obtain the UsageAnnotation for a given Annotation class
448
     *
449
     * @param string $class The Annotation type class-name
450
     * @return UsageAnnotation
451
     * @throws AnnotationException if the given class-name is invalid; if the annotation-type has no defined usage
452
     */
453 35
    public function getUsage($class)
454
    {
455 35
        if ($class === $this->registry['usage']) {
456 13
            return $this->_usageAnnotation;
457
        }
458
459 35
        if (!isset($this->usage[$class])) {
460 17
            if (!\class_exists($class, $this->autoload)) {
461 1
                throw new AnnotationException("Annotation type '{$class}' does not exist");
462
            }
463
464 16
            $usage = $this->getAnnotations($class);
465
466 16
            if (\count($usage) === 0) {
467 1
                throw new AnnotationException("The class '{$class}' must have exactly one UsageAnnotation");
468
            } else {
469 15
                if (\count($usage) !== 1 || !($usage[0] instanceof UsageAnnotation)) {
470 2
                    throw new AnnotationException("The class '{$class}' must have exactly one UsageAnnotation (no other Annotations are allowed)");
471
                } else {
472 14
                    $usage = $usage[0];
473
                }
474
            }
475
476 14
            $this->usage[$class] = $usage;
477 14
        }
478
479 33
        return $this->usage[$class];
480
    }
481
482
    /**
483
     * Inspects Annotations applied to a given class
484
     *
485
     * @param string|object|\ReflectionClass $class A class name, an object, or a ReflectionClass instance
486
     * @param string $type An optional annotation class/interface name - if specified, only annotations of the given type are returned.
487
     *                     Alternatively, prefixing with "@" invokes name-resolution (allowing you to query by annotation name.)
488
     *
489
     * @return Annotation[] Annotation instances
490
     * @throws AnnotationException if a given class-name is undefined
491
     */
492 20
    public function getClassAnnotations($class, $type = null)
493
    {
494 20
        if ($class instanceof \ReflectionClass) {
495 1
            $class = $class->getName();
496 20
        } elseif (\is_object($class)) {
497 1
            $class = \get_class($class);
498 1
        } else {
499 20
            $class = \ltrim($class, '\\');
500
        }
501
502 20
        if (!\class_exists($class, $this->autoload) &&
503 3
            !(\function_exists('trait_exists') && \trait_exists($class, $this->autoload))
504 20
        ) {
505 2
            if (\interface_exists($class, $this->autoload)) {
506 1
                throw new AnnotationException("Reading annotations from interface '{$class}' is not supported");
507
            }
508
509 1
            throw new AnnotationException("Unable to read annotations from an undefined class/trait '{$class}'");
510
        }
511
512 18
        if ($type === null) {
513 7
            return $this->getAnnotations($class);
514
        } else {
515 11
            return $this->filterAnnotations($this->getAnnotations($class), $type);
516
        }
517
    }
518
519
    /**
520
     * Inspects Annotations applied to a given method
521
     *
522
     * @param string|object|\ReflectionClass|\ReflectionMethod $class A class name, an object, a ReflectionClass, or a ReflectionMethod instance
523
     * @param string $method The name of a method of the given class (or null, if the first parameter is a ReflectionMethod)
524
     * @param string $type An optional annotation class/interface name - if specified, only annotations of the given type are returned.
525
     *                     Alternatively, prefixing with "@" invokes name-resolution (allowing you to query by annotation name.)
526
     *
527
     * @throws AnnotationException for undefined method or class-name
528
     * @return IAnnotation[] list of Annotation objects
529
     */
530 13 View Code Duplication
    public function getMethodAnnotations($class, $method = null, $type = null)
531
    {
532 13
        if ($class instanceof \ReflectionClass) {
533 1
            $class = $class->getName();
534 13
        } elseif ($class instanceof \ReflectionMethod) {
535 1
            $method = $class->name;
536 1
            $class = $class->class;
537 13
        } elseif (\is_object($class)) {
538 1
            $class = \get_class($class);
539 1
        } else {
540 13
            $class = \ltrim($class, '\\');
541
        }
542
543 13
        if (!\class_exists($class, $this->autoload)) {
544 1
            throw new AnnotationException("Unable to read annotations from an undefined class '{$class}'");
545
        }
546
547 12
        if (!\method_exists($class, $method)) {
548 1
            throw new AnnotationException("Unable to read annotations from an undefined method {$class}::{$method}()");
549
        }
550
551 11
        if ($type === null) {
552 8
            return $this->getAnnotations($class, self::MEMBER_METHOD, $method);
553
        } else {
554 3
            return $this->filterAnnotations($this->getAnnotations($class, self::MEMBER_METHOD, $method), $type);
555
        }
556
    }
557
558
    /**
559
     * Inspects Annotations applied to a given property
560
     *
561
     * @param string|object|\ReflectionClass|\ReflectionProperty $class A class name, an object, a ReflectionClass, or a ReflectionProperty instance
562
     * @param string $property The name of a defined property of the given class (or null, if the first parameter is a ReflectionProperty)
563
     * @param string $type An optional annotation class/interface name - if specified, only annotations of the given type are returned.
564
     *                     Alternatively, prefixing with "@" invokes name-resolution (allowing you to query by annotation name.)
565
     *
566
     * @return IAnnotation[] list of Annotation objects
567
     *
568
     * @throws AnnotationException for undefined class-name
569
     */
570 13 View Code Duplication
    public function getPropertyAnnotations($class, $property = null, $type = null)
571
    {
572 13
        if ($class instanceof \ReflectionClass) {
573 1
            $class = $class->getName();
574 13
        } elseif ($class instanceof \ReflectionProperty) {
575 1
            $property = $class->name;
576 1
            $class = $class->class;
577 13
        } elseif (\is_object($class)) {
578 1
            $class = \get_class($class);
579 1
        } else {
580 13
            $class = \ltrim($class, '\\');
581
        }
582
583 13
        if (!\class_exists($class, $this->autoload)) {
584 1
            throw new AnnotationException("Unable to read annotations from an undefined class '{$class}'");
585
        }
586
587 12
        if (!\property_exists($class, $property)) {
588 1
            throw new AnnotationException("Unable to read annotations from an undefined property {$class}::\${$property}");
589
        }
590
591 11
        if ($type === null) {
592 6
            return $this->getAnnotations($class, self::MEMBER_PROPERTY, '$' . $property);
593
        } else {
594 5
            return $this->filterAnnotations($this->getAnnotations($class, self::MEMBER_PROPERTY, '$' . $property), $type);
595
        }
596
    }
597
}
598