Completed
Push — master ( 5446fb...885a47 )
by Alexander
07:02
created

AnnotationManager::classHasMember()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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

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...
501 8
        ) {
502 1
            if (interface_exists($class, $this->autoload)) {
503
                throw new AnnotationException("Reading annotations from interface '{$class}' is not supported");
504
            }
505 7
506 4
            throw new AnnotationException("Unable to read annotations from an undefined class/trait '{$class}'");
507
        }
508 3
509
        if ($type === null) {
510
            return $this->getAnnotations($class);
511
        } else {
512
            return $this->filterAnnotations($this->getAnnotations($class), $type);
513
        }
514
    }
515
516
    /**
517
     * Inspects Annotations applied to a given method
518
     *
519
     * @param string|object|\ReflectionClass|\ReflectionMethod $class A class name, an object, a ReflectionClass, or a ReflectionMethod instance
520
     * @param string $method The name of a method of the given class (or null, if the first parameter is a ReflectionMethod)
521
     * @param string $type An optional annotation class/interface name - if specified, only annotations of the given type are returned.
522
     *                     Alternatively, prefixing with "@" invokes name-resolution (allowing you to query by annotation name.)
523
     *
524 11
     * @throws AnnotationException for undefined method or class-name
525
     * @return IAnnotation[] list of Annotation objects
526 11
     */
527 1 View Code Duplication
    public function getMethodAnnotations($class, $method = null, $type = null)
528 11
    {
529 1
        if ($class instanceof \ReflectionClass) {
530 1
            $class = $class->getName();
531 11
        } elseif ($class instanceof \ReflectionMethod) {
532 1
            $method = $class->name;
533 1
            $class = $class->class;
534 11
        } elseif (is_object($class)) {
535
            $class = get_class($class);
536
        } else {
537 11
            $class = ltrim($class, '\\');
538 1
        }
539
540
        if (!class_exists($class, $this->autoload)) {
541 10
            throw new AnnotationException("Unable to read annotations from an undefined class '{$class}'");
542 1
        }
543
544
        if (!method_exists($class, $method)) {
545 9
            throw new AnnotationException("Unable to read annotations from an undefined method {$class}::{$method}()");
546 4
        }
547
548 5
        if ($type === null) {
549
            return $this->getAnnotations($class, self::MEMBER_METHOD, $method);
550
        } else {
551
            return $this->filterAnnotations($this->getAnnotations($class, self::MEMBER_METHOD, $method), $type);
552 1
        }
553
    }
554
555
    /**
556
     * Inspects Annotations applied to a given property
557
     *
558
     * @param string|object|\ReflectionClass|\ReflectionProperty $class A class name, an object, a ReflectionClass, or a ReflectionProperty instance
559
     * @param string $property The name of a defined property of the given class (or null, if the first parameter is a ReflectionProperty)
560
     * @param string $type An optional annotation class/interface name - if specified, only annotations of the given type are returned.
561
     *                     Alternatively, prefixing with "@" invokes name-resolution (allowing you to query by annotation name.)
562
     *
563
     * @return IAnnotation[] list of Annotation objects
564
     *
565
     * @throws AnnotationException for undefined class-name
566
     */
567 View Code Duplication
    public function getPropertyAnnotations($class, $property = null, $type = null)
568
    {
569
        if ($class instanceof \ReflectionClass) {
570
            $class = $class->getName();
571
        } elseif ($class instanceof \ReflectionProperty) {
572
            $property = $class->name;
573
            $class = $class->class;
574
        } elseif (is_object($class)) {
575
            $class = get_class($class);
576
        } else {
577
            $class = ltrim($class, '\\');
578
        }
579
580
        if (!class_exists($class, $this->autoload)) {
581
            throw new AnnotationException("Unable to read annotations from an undefined class '{$class}'");
582
        }
583
584
        if (!property_exists($class, $property)) {
585
            throw new AnnotationException("Unable to read annotations from an undefined property {$class}::\${$property}");
586
        }
587
588
        if ($type === null) {
589
            return $this->getAnnotations($class, self::MEMBER_PROPERTY, '$' . $property);
590
        } else {
591
            return $this->filterAnnotations($this->getAnnotations($class, self::MEMBER_PROPERTY, '$' . $property), $type);
592
        }
593
    }
594
}
595