DocParser::PlainValue()   B
last analyzed

Complexity

Conditions 10
Paths 10

Size

Total Lines 41
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 27
c 1
b 0
f 0
nc 10
nop 0
dl 0
loc 41
rs 7.6666

How to fix   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
namespace Doctrine\Common\Annotations;
4
5
use Doctrine\Common\Annotations\Annotation\Attribute;
6
use Doctrine\Common\Annotations\Annotation\Enum;
7
use Doctrine\Common\Annotations\Annotation\Target;
8
use Doctrine\Common\Annotations\Annotation\Attributes;
9
use ReflectionClass;
10
use ReflectionException;
11
use ReflectionProperty;
12
use RuntimeException;
13
use stdClass;
14
15
16
/**
17
 * A parser for docblock annotations.
18
 *
19
 * It is strongly discouraged to change the default annotation parsing process.
20
 *
21
 * @author Benjamin Eberlei <[email protected]>
22
 * @author Guilherme Blanco <[email protected]>
23
 * @author Jonathan Wage <[email protected]>
24
 * @author Roman Borschel <[email protected]>
25
 * @author Johannes M. Schmitt <[email protected]>
26
 * @author Fabio B. Silva <[email protected]>
27
 */
28
final class DocParser
29
{
30
    /**
31
     * An array of all valid tokens for a class name.
32
     *
33
     * @var array
34
     */
35
    private static $classIdentifiers = [
36
        DocLexer::T_IDENTIFIER,
37
        DocLexer::T_TRUE,
38
        DocLexer::T_FALSE,
39
        DocLexer::T_NULL
40
    ];
41
42
    /**
43
     * The lexer.
44
     *
45
     * @var DocLexer
46
     */
47
    private $lexer;
48
49
    /**
50
     * Current target context.
51
     *
52
     * @var integer
53
     */
54
    private $target;
55
56
    /**
57
     * Doc parser used to collect annotation target.
58
     *
59
     * @var DocParser
60
     */
61
    private static $metadataParser;
62
63
    /**
64
     * Flag to control if the current annotation is nested or not.
65
     *
66
     * @var boolean
67
     */
68
    private $isNestedAnnotation = false;
69
70
    /**
71
     * Hashmap containing all use-statements that are to be used when parsing
72
     * the given doc block.
73
     *
74
     * @var array
75
     */
76
    private $imports = [];
77
78
    /**
79
     * This hashmap is used internally to cache results of class_exists()
80
     * look-ups.
81
     *
82
     * @var array
83
     */
84
    private $classExists = [];
85
86
    /**
87
     * Whether annotations that have not been imported should be ignored.
88
     *
89
     * @var boolean
90
     */
91
    private $ignoreNotImportedAnnotations = false;
92
93
    /**
94
     * An array of default namespaces if operating in simple mode.
95
     *
96
     * @var string[]
97
     */
98
    private $namespaces = [];
99
100
    /**
101
     * A list with annotations that are not causing exceptions when not resolved to an annotation class.
102
     *
103
     * The names must be the raw names as used in the class, not the fully qualified
104
     * class names.
105
     *
106
     * @var bool[] indexed by annotation name
107
     */
108
    private $ignoredAnnotationNames = [];
109
110
    /**
111
     * A list with annotations in namespaced format
112
     * that are not causing exceptions when not resolved to an annotation class.
113
     *
114
     * @var bool[] indexed by namespace name
115
     */
116
    private $ignoredAnnotationNamespaces = [];
117
118
    /**
119
     * @var string
120
     */
121
    private $context = '';
122
123
    /**
124
     * Hash-map for caching annotation metadata.
125
     *
126
     * @var array
127
     */
128
    private static $annotationMetadata = [
129
        Annotation\Target::class => [
130
            'is_annotation'    => true,
131
            'has_constructor'  => true,
132
            'properties'       => [],
133
            'targets_literal'  => 'ANNOTATION_CLASS',
134
            'targets'          => Target::TARGET_CLASS,
135
            'default_property' => 'value',
136
            'attribute_types'  => [
137
                'value'  => [
138
                    'required'  => false,
139
                    'type'      =>'array',
140
                    'array_type'=>'string',
141
                    'value'     =>'array<string>'
142
                ]
143
             ],
144
        ],
145
        Annotation\Attribute::class => [
146
            'is_annotation'    => true,
147
            'has_constructor'  => false,
148
            'targets_literal'  => 'ANNOTATION_ANNOTATION',
149
            'targets'          => Target::TARGET_ANNOTATION,
150
            'default_property' => 'name',
151
            'properties'       => [
152
                'name'      => 'name',
153
                'type'      => 'type',
154
                'required'  => 'required'
155
            ],
156
            'attribute_types'  => [
157
                'value'  => [
158
                    'required'  => true,
159
                    'type'      =>'string',
160
                    'value'     =>'string'
161
                ],
162
                'type'  => [
163
                    'required'  =>true,
164
                    'type'      =>'string',
165
                    'value'     =>'string'
166
                ],
167
                'required'  => [
168
                    'required'  =>false,
169
                    'type'      =>'boolean',
170
                    'value'     =>'boolean'
171
                ]
172
             ],
173
        ],
174
        Annotation\Attributes::class => [
175
            'is_annotation'    => true,
176
            'has_constructor'  => false,
177
            'targets_literal'  => 'ANNOTATION_CLASS',
178
            'targets'          => Target::TARGET_CLASS,
179
            'default_property' => 'value',
180
            'properties'       => [
181
                'value' => 'value'
182
            ],
183
            'attribute_types'  => [
184
                'value' => [
185
                    'type'      =>'array',
186
                    'required'  =>true,
187
                    'array_type'=>Annotation\Attribute::class,
188
                    'value'     =>'array<' . Annotation\Attribute::class . '>'
189
                ]
190
             ],
191
        ],
192
        Annotation\Enum::class => [
193
            'is_annotation'    => true,
194
            'has_constructor'  => true,
195
            'targets_literal'  => 'ANNOTATION_PROPERTY',
196
            'targets'          => Target::TARGET_PROPERTY,
197
            'default_property' => 'value',
198
            'properties'       => [
199
                'value' => 'value'
200
            ],
201
            'attribute_types'  => [
202
                'value' => [
203
                    'type'      => 'array',
204
                    'required'  => true,
205
                ],
206
                'literal' => [
207
                    'type'      => 'array',
208
                    'required'  => false,
209
                ],
210
             ],
211
        ],
212
    ];
213
214
    /**
215
     * Hash-map for handle types declaration.
216
     *
217
     * @var array
218
     */
219
    private static $typeMap = [
220
        'float'     => 'double',
221
        'bool'      => 'boolean',
222
        // allow uppercase Boolean in honor of George Boole
223
        'Boolean'   => 'boolean',
224
        'int'       => 'integer',
225
    ];
226
227
    /**
228
     * Constructs a new DocParser.
229
     */
230
    public function __construct()
231
    {
232
        $this->lexer = new DocLexer;
233
    }
234
235
    /**
236
     * Sets the annotation names that are ignored during the parsing process.
237
     *
238
     * The names are supposed to be the raw names as used in the class, not the
239
     * fully qualified class names.
240
     *
241
     * @param bool[] $names indexed by annotation name
242
     *
243
     * @return void
244
     */
245
    public function setIgnoredAnnotationNames(array $names)
246
    {
247
        $this->ignoredAnnotationNames = $names;
248
    }
249
250
    /**
251
     * Sets the annotation namespaces that are ignored during the parsing process.
252
     *
253
     * @param bool[] $ignoredAnnotationNamespaces indexed by annotation namespace name
254
     *
255
     * @return void
256
     */
257
    public function setIgnoredAnnotationNamespaces($ignoredAnnotationNamespaces)
258
    {
259
        $this->ignoredAnnotationNamespaces = $ignoredAnnotationNamespaces;
260
    }
261
262
    /**
263
     * Sets ignore on not-imported annotations.
264
     *
265
     * @param boolean $bool
266
     *
267
     * @return void
268
     */
269
    public function setIgnoreNotImportedAnnotations($bool)
270
    {
271
        $this->ignoreNotImportedAnnotations = (boolean) $bool;
272
    }
273
274
    /**
275
     * Sets the default namespaces.
276
     *
277
     * @param string $namespace
278
     *
279
     * @return void
280
     * @throws RuntimeException
281
     */
282
    public function addNamespace($namespace)
283
    {
284
        if ($this->imports) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->imports of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
285
            throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
286
        }
287
288
        $this->namespaces[] = $namespace;
289
    }
290
291
    /**
292
     * Sets the imports.
293
     *
294
     * @param array $imports
295
     *
296
     * @return void
297
     * @throws RuntimeException
298
     */
299
    public function setImports(array $imports)
300
    {
301
        if ($this->namespaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namespaces of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
302
            throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
303
        }
304
305
        $this->imports = $imports;
306
    }
307
308
    /**
309
     * Sets current target context as bitmask.
310
     *
311
     * @param integer $target
312
     *
313
     * @return void
314
     */
315
    public function setTarget($target)
316
    {
317
        $this->target = $target;
318
    }
319
320
    /**
321
     * Parses the given docblock string for annotations.
322
     *
323
     * @param string $input   The docblock string to parse.
324
     * @param string $context The parsing context.
325
     *
326
     * @return array Array of annotations. If no annotations are found, an empty array is returned.
327
     * @throws AnnotationException
328
     * @throws ReflectionException
329
     */
330
    public function parse($input, $context = '')
331
    {
332
        $pos = $this->findInitialTokenPosition($input);
333
        if ($pos === null) {
334
            return [];
335
        }
336
337
        $this->context = $context;
338
339
        $this->lexer->setInput(trim(substr($input, $pos), '* /'));
340
        $this->lexer->moveNext();
341
342
        return $this->Annotations();
343
    }
344
345
    /**
346
     * Finds the first valid annotation
347
     *
348
     * @param string $input The docblock string to parse
349
     */
350
    private function findInitialTokenPosition($input): ?int
351
    {
352
        $pos = 0;
353
354
        // search for first valid annotation
355
        while (($pos = strpos($input, '@', $pos)) !== false) {
356
            $preceding = substr($input, $pos - 1, 1);
357
358
            // if the @ is preceded by a space, a tab or * it is valid
359
            if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") {
360
                return $pos;
361
            }
362
363
            $pos++;
364
        }
365
366
        return null;
367
    }
368
369
    /**
370
     * Attempts to match the given token with the current lookahead token.
371
     * If they match, updates the lookahead token; otherwise raises a syntax error.
372
     *
373
     * @param int $token Type of token.
374
     *
375
     * @return bool True if tokens match; false otherwise.
376
     * @throws AnnotationException
377
     */
378
    private function match(int $token): bool
379
    {
380
        if ( ! $this->lexer->isNextToken($token) ) {
381
            throw $this->syntaxError($this->lexer->getLiteral($token));
382
        }
383
384
        return $this->lexer->moveNext();
385
    }
386
387
    /**
388
     * Attempts to match the current lookahead token with any of the given tokens.
389
     *
390
     * If any of them matches, this method updates the lookahead token; otherwise
391
     * a syntax error is raised.
392
     *
393
     * @throws AnnotationException
394
     */
395
    private function matchAny(array $tokens): bool
396
    {
397
        if ( ! $this->lexer->isNextTokenAny($tokens)) {
398
            throw $this->syntaxError(implode(' or ', array_map([$this->lexer, 'getLiteral'], $tokens)));
399
        }
400
401
        return $this->lexer->moveNext();
402
    }
403
404
    /**
405
     * Generates a new syntax error.
406
     *
407
     * @param string     $expected Expected string.
408
     * @param array|null $token    Optional token.
409
     */
410
    private function syntaxError(string $expected, ?array $token = null): AnnotationException
411
    {
412
        if ($token === null) {
413
            $token = $this->lexer->lookahead;
414
        }
415
416
        $message  = sprintf('Expected %s, got ', $expected);
417
        $message .= ($this->lexer->lookahead === null)
418
            ? 'end of string'
419
            : sprintf("'%s' at position %s", $token['value'], $token['position']);
420
421
        if (strlen($this->context)) {
422
            $message .= ' in ' . $this->context;
423
        }
424
425
        $message .= '.';
426
427
        return AnnotationException::syntaxError($message);
428
    }
429
430
    /**
431
     * Attempts to check if a class exists or not. This never goes through the PHP autoloading mechanism
432
     * but uses the {@link AnnotationRegistry} to load classes.
433
     */
434
    private function classExists(string $fqcn): bool
435
    {
436
        if (isset($this->classExists[$fqcn])) {
437
            return $this->classExists[$fqcn];
438
        }
439
440
        // first check if the class already exists, maybe loaded through another AnnotationReader
441
        if (class_exists($fqcn, false)) {
442
            return $this->classExists[$fqcn] = true;
443
        }
444
445
        // final check, does this class exist?
446
        return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn);
447
    }
448
449
    /**
450
     * Collects parsing metadata for a given annotation class
451
     *
452
     * @param string $name The annotation name
453
     *
454
     * @throws AnnotationException
455
     * @throws ReflectionException
456
     */
457
    private function collectAnnotationMetadata(string $name): void
458
    {
459
        if (self::$metadataParser === null) {
460
            self::$metadataParser = new self();
461
462
            self::$metadataParser->setIgnoreNotImportedAnnotations(true);
463
            self::$metadataParser->setIgnoredAnnotationNames($this->ignoredAnnotationNames);
464
            self::$metadataParser->setImports([
465
                'enum'          => Annotation\Enum::class,
466
                'target'        => Annotation\Target::class,
467
                'attribute'     => Annotation\Attribute::class,
468
                'attributes'    => Annotation\Attributes::class
469
            ]);
470
471
            // Make sure that annotations from metadata are loaded
472
            class_exists(Enum::class);
473
            class_exists(Target::class);
474
            class_exists(Attribute::class);
475
            class_exists(Attributes::class);
476
        }
477
478
        $class      = new ReflectionClass($name);
479
        $docComment = $class->getDocComment();
480
481
        // Sets default values for annotation metadata
482
        $metadata = [
483
            'default_property' => null,
484
            'has_constructor'  => (null !== $constructor = $class->getConstructor()) && $constructor->getNumberOfParameters() > 0,
485
            'properties'       => [],
486
            'property_types'   => [],
487
            'attribute_types'  => [],
488
            'targets_literal'  => null,
489
            'targets'          => Target::TARGET_ALL,
490
            'is_annotation'    => false !== strpos($docComment, '@Annotation'),
491
        ];
492
493
        // verify that the class is really meant to be an annotation
494
        if ($metadata['is_annotation']) {
495
            self::$metadataParser->setTarget(Target::TARGET_CLASS);
496
497
            foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) {
498
                if ($annotation instanceof Target) {
499
                    $metadata['targets']         = $annotation->targets;
500
                    $metadata['targets_literal'] = $annotation->literal;
501
502
                    continue;
503
                }
504
505
                if ($annotation instanceof Attributes) {
506
                    foreach ($annotation->value as $attribute) {
507
                        $this->collectAttributeTypeMetadata($metadata, $attribute);
508
                    }
509
                }
510
            }
511
512
            // if not has a constructor will inject values into public properties
513
            if (false === $metadata['has_constructor']) {
514
                // collect all public properties
515
                foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
516
                    $metadata['properties'][$property->name] = $property->name;
517
518
                    if (false === ($propertyComment = $property->getDocComment())) {
519
                        continue;
520
                    }
521
522
                    $attribute = new Attribute();
523
524
                    $attribute->required = (false !== strpos($propertyComment, '@Required'));
0 ignored issues
show
Bug introduced by
It seems like $propertyComment can also be of type true; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

524
                    $attribute->required = (false !== strpos(/** @scrutinizer ignore-type */ $propertyComment, '@Required'));
Loading history...
525
                    $attribute->name     = $property->name;
526
                    $attribute->type     = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches))
0 ignored issues
show
Bug introduced by
It seems like $propertyComment can also be of type true; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

526
                    $attribute->type     = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',/** @scrutinizer ignore-type */ $propertyComment, $matches))
Loading history...
527
                        ? $matches[1]
528
                        : 'mixed';
529
530
                    $this->collectAttributeTypeMetadata($metadata, $attribute);
531
532
                    // checks if the property has @Enum
533
                    if (false !== strpos($propertyComment, '@Enum')) {
534
                        $context = 'property ' . $class->name . "::\$" . $property->name;
535
536
                        self::$metadataParser->setTarget(Target::TARGET_PROPERTY);
537
538
                        foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) {
0 ignored issues
show
Bug introduced by
It seems like $propertyComment can also be of type true; however, parameter $input of Doctrine\Common\Annotations\DocParser::parse() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

538
                        foreach (self::$metadataParser->parse(/** @scrutinizer ignore-type */ $propertyComment, $context) as $annotation) {
Loading history...
539
                            if ( ! $annotation instanceof Enum) {
540
                                continue;
541
                            }
542
543
                            $metadata['enum'][$property->name]['value']   = $annotation->value;
544
                            $metadata['enum'][$property->name]['literal'] = ( ! empty($annotation->literal))
545
                                ? $annotation->literal
546
                                : $annotation->value;
547
                        }
548
                    }
549
                }
550
551
                // choose the first property as default property
552
                $metadata['default_property'] = reset($metadata['properties']);
553
            }
554
        }
555
556
        self::$annotationMetadata[$name] = $metadata;
557
    }
558
559
    /**
560
     * Collects parsing metadata for a given attribute.
561
     */
562
    private function collectAttributeTypeMetadata(array &$metadata, Attribute $attribute): void
563
    {
564
        // handle internal type declaration
565
        $type = self::$typeMap[$attribute->type] ?? $attribute->type;
566
567
        // handle the case if the property type is mixed
568
        if ('mixed' === $type) {
569
            return;
570
        }
571
572
        // Evaluate type
573
        switch (true) {
574
            // Checks if the property has array<type>
575
            case (false !== $pos = strpos($type, '<')):
576
                $arrayType  = substr($type, $pos + 1, -1);
577
                $type       = 'array';
578
579
                if (isset(self::$typeMap[$arrayType])) {
580
                    $arrayType = self::$typeMap[$arrayType];
581
                }
582
583
                $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType;
584
                break;
585
586
            // Checks if the property has type[]
587
            case (false !== $pos = strrpos($type, '[')):
588
                $arrayType  = substr($type, 0, $pos);
589
                $type       = 'array';
590
591
                if (isset(self::$typeMap[$arrayType])) {
592
                    $arrayType = self::$typeMap[$arrayType];
593
                }
594
595
                $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType;
596
                break;
597
        }
598
599
        $metadata['attribute_types'][$attribute->name]['type']     = $type;
600
        $metadata['attribute_types'][$attribute->name]['value']    = $attribute->type;
601
        $metadata['attribute_types'][$attribute->name]['required'] = $attribute->required;
602
    }
603
604
    /**
605
     * Annotations ::= Annotation {[ "*" ]* [Annotation]}*
606
     *
607
     * @throws AnnotationException
608
     * @throws ReflectionException
609
     */
610
    private function Annotations(): array
611
    {
612
        $annotations = [];
613
614
        while (null !== $this->lexer->lookahead) {
615
            if (DocLexer::T_AT !== $this->lexer->lookahead['type']) {
616
                $this->lexer->moveNext();
617
                continue;
618
            }
619
620
            // make sure the @ is preceded by non-catchable pattern
621
            if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) {
622
                $this->lexer->moveNext();
623
                continue;
624
            }
625
626
            // make sure the @ is followed by either a namespace separator, or
627
            // an identifier token
628
            if ((null === $peek = $this->lexer->glimpse())
629
                || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true))
630
                || $peek['position'] !== $this->lexer->lookahead['position'] + 1) {
631
                $this->lexer->moveNext();
632
                continue;
633
            }
634
635
            $this->isNestedAnnotation = false;
636
            if (false !== $annot = $this->Annotation()) {
637
                $annotations[] = $annot;
638
            }
639
        }
640
641
        return $annotations;
642
    }
643
644
    /**
645
     * Annotation     ::= "@" AnnotationName MethodCall
646
     * AnnotationName ::= QualifiedName | SimpleName
647
     * QualifiedName  ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName
648
     * NameSpacePart  ::= identifier | null | false | true
649
     * SimpleName     ::= identifier | null | false | true
650
     *
651
     * @return mixed False if it is not a valid annotation.
652
     *
653
     * @throws AnnotationException
654
     * @throws ReflectionException
655
     */
656
    private function Annotation()
657
    {
658
        $this->match(DocLexer::T_AT);
659
660
        // check if we have an annotation
661
        $name = $this->Identifier();
662
663
        if ($this->lexer->isNextToken(DocLexer::T_MINUS)
664
            && $this->lexer->nextTokenIsAdjacent()
665
        ) {
666
            // Annotations with dashes, such as "@foo-" or "@foo-bar", are to be discarded
667
            return false;
668
        }
669
670
        // only process names which are not fully qualified, yet
671
        // fully qualified names must start with a \
672
        $originalName = $name;
673
674
        if ('\\' !== $name[0]) {
675
            $pos = strpos($name, '\\');
676
            $alias = (false === $pos)? $name : substr($name, 0, $pos);
677
            $found = false;
678
            $loweredAlias = strtolower($alias);
679
680
            if ($this->namespaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namespaces of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
681
                foreach ($this->namespaces as $namespace) {
682
                    if ($this->classExists($namespace.'\\'.$name)) {
683
                        $name = $namespace.'\\'.$name;
684
                        $found = true;
685
                        break;
686
                    }
687
                }
688
            } elseif (isset($this->imports[$loweredAlias])) {
689
                $namespace = ltrim($this->imports[$loweredAlias], '\\');
690
                $name = (false !== $pos)
691
                    ? $namespace . substr($name, $pos)
692
                    : $namespace;
693
                $found = $this->classExists($name);
694
            } elseif ( ! isset($this->ignoredAnnotationNames[$name])
695
                && isset($this->imports['__NAMESPACE__'])
696
                && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name)
697
            ) {
698
                $name  = $this->imports['__NAMESPACE__'].'\\'.$name;
699
                $found = true;
700
            } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) {
701
                $found = true;
702
            }
703
704
            if ( ! $found) {
705
                if ($this->isIgnoredAnnotation($name)) {
706
                    return false;
707
                }
708
709
                throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s was never imported. Did you maybe forget to add a "use" statement for this annotation?', $name, $this->context));
710
            }
711
        }
712
713
        $name = ltrim($name,'\\');
714
715
        if ( ! $this->classExists($name)) {
716
            throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context));
717
        }
718
719
        // at this point, $name contains the fully qualified class name of the
720
        // annotation, and it is also guaranteed that this class exists, and
721
        // that it is loaded
722
723
724
        // collects the metadata annotation only if there is not yet
725
        if ( ! isset(self::$annotationMetadata[$name])) {
726
            $this->collectAnnotationMetadata($name);
727
        }
728
729
        // verify that the class is really meant to be an annotation and not just any ordinary class
730
        if (self::$annotationMetadata[$name]['is_annotation'] === false) {
731
            if ($this->isIgnoredAnnotation($originalName) || $this->isIgnoredAnnotation($name)) {
732
                return false;
733
            }
734
735
            throw AnnotationException::semanticalError(sprintf('The class "%s" is not annotated with @Annotation. Are you sure this class can be used as annotation? If so, then you need to add @Annotation to the _class_ doc comment of "%s". If it is indeed no annotation, then you need to add @IgnoreAnnotation("%s") to the _class_ doc comment of %s.', $name, $name, $originalName, $this->context));
736
        }
737
738
        //if target is nested annotation
739
        $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target;
740
741
        // Next will be nested
742
        $this->isNestedAnnotation = true;
743
744
        //if annotation does not support current target
745
        if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) {
746
            throw AnnotationException::semanticalError(
747
                sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.',
748
                     $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal'])
749
            );
750
        }
751
752
        $values = $this->MethodCall();
753
754
        if (isset(self::$annotationMetadata[$name]['enum'])) {
755
            // checks all declared attributes
756
            foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) {
757
                // checks if the attribute is a valid enumerator
758
                if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) {
759
                    throw AnnotationException::enumeratorError($property, $name, $this->context, $enum['literal'], $values[$property]);
760
                }
761
            }
762
        }
763
764
        // checks all declared attributes
765
        foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) {
766
            if ($property === self::$annotationMetadata[$name]['default_property']
767
                && !isset($values[$property]) && isset($values['value'])) {
768
                $property = 'value';
769
            }
770
771
            // handle a not given attribute or null value
772
            if (!isset($values[$property])) {
773
                if ($type['required']) {
774
                    throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']);
775
                }
776
777
                continue;
778
            }
779
780
            if ($type['type'] === 'array') {
781
                // handle the case of a single value
782
                if ( ! is_array($values[$property])) {
783
                    $values[$property] = [$values[$property]];
784
                }
785
786
                // checks if the attribute has array type declaration, such as "array<string>"
787
                if (isset($type['array_type'])) {
788
                    foreach ($values[$property] as $item) {
789
                        if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) {
790
                            throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item);
791
                        }
792
                    }
793
                }
794
            } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) {
795
                throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]);
796
            }
797
        }
798
799
        // check if the annotation expects values via the constructor,
800
        // or directly injected into public properties
801
        if (self::$annotationMetadata[$name]['has_constructor'] === true) {
802
            return new $name($values);
803
        }
804
805
        $instance = new $name();
806
807
        foreach ($values as $property => $value) {
808
            if (!isset(self::$annotationMetadata[$name]['properties'][$property])) {
809
                if ('value' !== $property) {
810
                    throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not have a property named "%s". Available properties: %s', $originalName, $this->context, $property, implode(', ', self::$annotationMetadata[$name]['properties'])));
811
                }
812
813
                // handle the case if the property has no annotations
814
                if ( ! $property = self::$annotationMetadata[$name]['default_property']) {
815
                    throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not accept any values, but got %s.', $originalName, $this->context, json_encode($values)));
816
                }
817
            }
818
819
            $instance->{$property} = $value;
820
        }
821
822
        return $instance;
823
    }
824
825
    /**
826
     * MethodCall ::= ["(" [Values] ")"]
827
     *
828
     * @throws AnnotationException
829
     * @throws ReflectionException
830
     */
831
    private function MethodCall(): array
832
    {
833
        $values = [];
834
835
        if ( ! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
836
            return $values;
837
        }
838
839
        $this->match(DocLexer::T_OPEN_PARENTHESIS);
840
841
        if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
842
            $values = $this->Values();
843
        }
844
845
        $this->match(DocLexer::T_CLOSE_PARENTHESIS);
846
847
        return $values;
848
    }
849
850
    /**
851
     * Values ::= Array | Value {"," Value}* [","]
852
     *
853
     * @throws AnnotationException
854
     * @throws ReflectionException
855
     */
856
    private function Values(): array
857
    {
858
        $values = [$this->Value()];
859
860
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
861
            $this->match(DocLexer::T_COMMA);
862
863
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
864
                break;
865
            }
866
867
            $token = $this->lexer->lookahead;
868
            $value = $this->Value();
869
870
            if ( ! is_object($value) && ! is_array($value)) {
871
                throw $this->syntaxError('Value', $token);
872
            }
873
874
            $values[] = $value;
875
        }
876
877
        foreach ($values as $k => $value) {
878
            if (is_object($value) && $value instanceof stdClass) {
879
                $values[$value->name] = $value->value;
880
            } else if ( ! isset($values['value'])){
881
                $values['value'] = $value;
882
            } else {
883
                if ( ! is_array($values['value'])) {
884
                    $values['value'] = [$values['value']];
885
                }
886
887
                $values['value'][] = $value;
888
            }
889
890
            unset($values[$k]);
891
        }
892
893
        return $values;
894
    }
895
896
    /**
897
     * Constant ::= integer | string | float | boolean
898
     *
899
     * @return mixed
900
     *
901
     * @throws AnnotationException
902
     */
903
    private function Constant()
904
    {
905
        $identifier = $this->Identifier();
906
907
        if ( ! defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) {
908
            list($className, $const) = explode('::', $identifier);
909
910
            $pos = strpos($className, '\\');
911
            $alias = (false === $pos) ? $className : substr($className, 0, $pos);
912
            $found = false;
913
            $loweredAlias = strtolower($alias);
914
915
            switch (true) {
916
                case !empty ($this->namespaces):
917
                    foreach ($this->namespaces as $ns) {
918
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
919
                             $className = $ns.'\\'.$className;
920
                             $found = true;
921
                             break;
922
                        }
923
                    }
924
                    break;
925
926
                case isset($this->imports[$loweredAlias]):
927
                    $found     = true;
928
                    $className = (false !== $pos)
929
                        ? $this->imports[$loweredAlias] . substr($className, $pos)
930
                        : $this->imports[$loweredAlias];
931
                    break;
932
933
                default:
934
                    if(isset($this->imports['__NAMESPACE__'])) {
935
                        $ns = $this->imports['__NAMESPACE__'];
936
937
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
938
                            $className = $ns.'\\'.$className;
939
                            $found = true;
940
                        }
941
                    }
942
                    break;
943
            }
944
945
            if ($found) {
946
                 $identifier = $className . '::' . $const;
947
            }
948
        }
949
950
        /**
951
         * Checks if identifier ends with ::class and remove the leading backslash if it exists.
952
         */
953
        if ($this->identifierEndsWithClassConstant($identifier) && ! $this->identifierStartsWithBackslash($identifier)) {
954
            return substr($identifier, 0, $this->getClassConstantPositionInIdentifier($identifier));
955
        }
956
        if ($this->identifierEndsWithClassConstant($identifier) && $this->identifierStartsWithBackslash($identifier)) {
957
            return substr($identifier, 1, $this->getClassConstantPositionInIdentifier($identifier) - 1);
958
        }
959
960
        if (!defined($identifier)) {
961
            throw AnnotationException::semanticalErrorConstants($identifier, $this->context);
962
        }
963
964
        return constant($identifier);
965
    }
966
967
    private function identifierStartsWithBackslash(string $identifier) : bool
968
    {
969
        return '\\' === $identifier[0];
970
    }
971
972
    private function identifierEndsWithClassConstant(string $identifier) : bool
973
    {
974
        return $this->getClassConstantPositionInIdentifier($identifier) === strlen($identifier) - strlen('::class');
975
    }
976
977
    /**
978
     * @return int|false
979
     */
980
    private function getClassConstantPositionInIdentifier(string $identifier)
981
    {
982
        return stripos($identifier, '::class');
983
    }
984
985
    /**
986
     * Identifier ::= string
987
     *
988
     * @throws AnnotationException
989
     */
990
    private function Identifier(): string
991
    {
992
        // check if we have an annotation
993
        if ( ! $this->lexer->isNextTokenAny(self::$classIdentifiers)) {
994
            throw $this->syntaxError('namespace separator or identifier');
995
        }
996
997
        $this->lexer->moveNext();
998
999
        $className = $this->lexer->token['value'];
1000
1001
        while (
1002
            null !== $this->lexer->lookahead &&
1003
            $this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value'])) &&
1004
            $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)
1005
        ) {
1006
            $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
1007
            $this->matchAny(self::$classIdentifiers);
1008
1009
            $className .= '\\' . $this->lexer->token['value'];
1010
        }
1011
1012
        return $className;
1013
    }
1014
1015
    /**
1016
     * Value ::= PlainValue | FieldAssignment
1017
     *
1018
     * @return mixed
1019
     * @throws AnnotationException
1020
     * @throws ReflectionException
1021
     */
1022
    private function Value()
1023
    {
1024
        $peek = $this->lexer->glimpse();
1025
1026
        if (DocLexer::T_EQUALS === $peek['type']) {
1027
            return $this->FieldAssignment();
1028
        }
1029
1030
        return $this->PlainValue();
1031
    }
1032
1033
    /**
1034
     * PlainValue ::= integer | string | float | boolean | Array | Annotation
1035
     *
1036
     * @return mixed
1037
     * @throws AnnotationException
1038
     * @throws ReflectionException
1039
     */
1040
    private function PlainValue()
1041
    {
1042
        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
1043
            return $this->Arrayx();
1044
        }
1045
1046
        if ($this->lexer->isNextToken(DocLexer::T_AT)) {
1047
            return $this->Annotation();
1048
        }
1049
1050
        if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1051
            return $this->Constant();
1052
        }
1053
1054
        switch ($this->lexer->lookahead['type']) {
1055
            case DocLexer::T_STRING:
1056
                $this->match(DocLexer::T_STRING);
1057
                return $this->lexer->token['value'];
1058
1059
            case DocLexer::T_INTEGER:
1060
                $this->match(DocLexer::T_INTEGER);
1061
                return (int)$this->lexer->token['value'];
1062
1063
            case DocLexer::T_FLOAT:
1064
                $this->match(DocLexer::T_FLOAT);
1065
                return (float)$this->lexer->token['value'];
1066
1067
            case DocLexer::T_TRUE:
1068
                $this->match(DocLexer::T_TRUE);
1069
                return true;
1070
1071
            case DocLexer::T_FALSE:
1072
                $this->match(DocLexer::T_FALSE);
1073
                return false;
1074
1075
            case DocLexer::T_NULL:
1076
                $this->match(DocLexer::T_NULL);
1077
                return null;
1078
1079
            default:
1080
                throw $this->syntaxError('PlainValue');
1081
        }
1082
    }
1083
1084
    /**
1085
     * FieldAssignment ::= FieldName "=" PlainValue
1086
     * FieldName ::= identifier
1087
     * @throws AnnotationException
1088
     * @throws ReflectionException
1089
     */
1090
    private function FieldAssignment(): stdClass
1091
    {
1092
        $this->match(DocLexer::T_IDENTIFIER);
1093
        $fieldName = $this->lexer->token['value'];
1094
1095
        $this->match(DocLexer::T_EQUALS);
1096
1097
        $item = new stdClass();
1098
        $item->name  = $fieldName;
1099
        $item->value = $this->PlainValue();
1100
1101
        return $item;
1102
    }
1103
1104
    /**
1105
     * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}"
1106
     * @throws AnnotationException
1107
     * @throws ReflectionException
1108
     */
1109
    private function Arrayx(): array
1110
    {
1111
        $array = $values = [];
1112
1113
        $this->match(DocLexer::T_OPEN_CURLY_BRACES);
1114
1115
        // If the array is empty, stop parsing and return.
1116
        if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1117
            $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1118
1119
            return $array;
1120
        }
1121
1122
        $values[] = $this->ArrayEntry();
1123
1124
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
1125
            $this->match(DocLexer::T_COMMA);
1126
1127
            // optional trailing comma
1128
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1129
                break;
1130
            }
1131
1132
            $values[] = $this->ArrayEntry();
1133
        }
1134
1135
        $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1136
1137
        foreach ($values as $value) {
1138
            list ($key, $val) = $value;
1139
1140
            if ($key !== null) {
1141
                $array[$key] = $val;
1142
            } else {
1143
                $array[] = $val;
1144
            }
1145
        }
1146
1147
        return $array;
1148
    }
1149
1150
    /**
1151
     * ArrayEntry ::= Value | KeyValuePair
1152
     * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant
1153
     * Key ::= string | integer | Constant
1154
     *
1155
     * @throws AnnotationException
1156
     * @throws ReflectionException
1157
     */
1158
    private function ArrayEntry(): array
1159
    {
1160
        $peek = $this->lexer->glimpse();
1161
1162
        if (DocLexer::T_EQUALS === $peek['type']
1163
                || DocLexer::T_COLON === $peek['type']) {
1164
1165
            if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1166
                $key = $this->Constant();
1167
            } else {
1168
                $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]);
1169
                $key = $this->lexer->token['value'];
1170
            }
1171
1172
            $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]);
1173
1174
            return [$key, $this->PlainValue()];
1175
        }
1176
1177
        return [null, $this->Value()];
1178
    }
1179
1180
    /**
1181
     * Checks whether the given $name matches any ignored annotation name or namespace
1182
     */
1183
    private function isIgnoredAnnotation(string $name): bool
1184
    {
1185
        if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
1186
            return true;
1187
        }
1188
1189
        foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) {
1190
            $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\';
1191
1192
            if (0 === stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace)) {
1193
                return true;
1194
            }
1195
        }
1196
1197
        return false;
1198
    }
1199
}
1200