source: pro-violet-viettel/sourcecode/application/libraries/Doctrine/Common/Annotations/DocParser.php @ 345

Last change on this file since 345 was 345, checked in by quyenla, 11 years ago

collaborator page

File size: 30.9 KB
Line 
1<?php
2/*
3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 *
15 * This software consists of voluntary contributions made by many individuals
16 * and is licensed under the LGPL. For more information, see
17 * <http://www.doctrine-project.org>.
18 */
19
20namespace Doctrine\Common\Annotations;
21
22use Closure;
23use ReflectionClass;
24use Doctrine\Common\Annotations\Annotation\Target;
25use Doctrine\Common\Annotations\Annotation\Attribute;
26use Doctrine\Common\Annotations\Annotation\Attributes;
27
28/**
29 * A parser for docblock annotations.
30 *
31 * It is strongly discouraged to change the default annotation parsing process.
32 *
33 * @author Benjamin Eberlei <kontakt@beberlei.de>
34 * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
35 * @author Jonathan Wage <jonwage@gmail.com>
36 * @author Roman Borschel <roman@code-factory.org>
37 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
38 * @author Fabio B. Silva <fabio.bat.silva@gmail.com>
39 */
40final class DocParser
41{
42    /**
43     * An array of all valid tokens for a class name.
44     *
45     * @var array
46     */
47    private static $classIdentifiers = array(DocLexer::T_IDENTIFIER, DocLexer::T_TRUE, DocLexer::T_FALSE, DocLexer::T_NULL);
48
49    /**
50     * The lexer.
51     *
52     * @var Doctrine\Common\Annotations\DocLexer
53     */
54    private $lexer;
55
56    /**
57     * Current target context
58     *
59     * @var string
60     */
61    private $target;
62
63    /**
64     * Doc Parser used to collect annotation target
65     *
66     * @var Doctrine\Common\Annotations\DocParser
67     */
68    private static $metadataParser;
69
70    /**
71     * Flag to control if the current annotation is nested or not.
72     *
73     * @var boolean
74     */
75    private $isNestedAnnotation = false;
76
77    /**
78     * Hashmap containing all use-statements that are to be used when parsing
79     * the given doc block.
80     *
81     * @var array
82     */
83    private $imports = array();
84
85    /**
86     * This hashmap is used internally to cache results of class_exists()
87     * look-ups.
88     *
89     * @var array
90     */
91    private $classExists = array();
92
93    /**
94     * Whether annotations that have not been imported should be ignored.
95     *
96     * @var boolean
97     */
98    private $ignoreNotImportedAnnotations = false;
99
100    /**
101     * An array of default namespaces if operating in simple mode.
102     *
103     * @var array
104     */
105    private $namespaces = array();
106
107    /**
108     * A list with annotations that are not causing exceptions when not resolved to an annotation class.
109     *
110     * The names must be the raw names as used in the class, not the fully qualified
111     * class names.
112     *
113     * @var array
114     */
115    private $ignoredAnnotationNames = array();
116
117    /**
118     * @var string
119     */
120    private $context = '';
121
122    /**
123     * Hash-map for caching annotation metadata
124     * @var array
125     */
126    private static $annotationMetadata = array(
127        'Doctrine\Common\Annotations\Annotation\Target' => array(
128            'is_annotation'    => true,
129            'has_constructor'  => true,
130            'properties'       => array(),
131            'targets_literal'  => 'ANNOTATION_CLASS',
132            'targets'          => Target::TARGET_CLASS,
133            'default_property' => 'value',
134            'attribute_types'  => array(
135                'value'  => array(
136                    'required'  => false,
137                    'type'      =>'array',
138                    'array_type'=>'string',
139                    'value'     =>'array<string>'
140                )
141             ),
142        ),
143        'Doctrine\Common\Annotations\Annotation\Attribute' => array(
144            'is_annotation'    => true,
145            'has_constructor'  => false,
146            'targets_literal'  => 'ANNOTATION_ANNOTATION',
147            'targets'          => Target::TARGET_ANNOTATION,
148            'default_property' => 'name',
149            'properties'       => array(
150                'name'      => 'name',
151                'type'      => 'type',
152                'required'  => 'required'
153            ),
154            'attribute_types'  => array(
155                'value'  => array(
156                    'required'  => true,
157                    'type'      =>'string',
158                    'value'     =>'string'
159                ),
160                'type'  => array(
161                    'required'  =>true,
162                    'type'      =>'string',
163                    'value'     =>'string'
164                ),
165                'required'  => array(
166                    'required'  =>false,
167                    'type'      =>'boolean',
168                    'value'     =>'boolean'
169                )
170             ),
171        ),
172        'Doctrine\Common\Annotations\Annotation\Attributes' => array(
173            'is_annotation'    => true,
174            'has_constructor'  => false,
175            'targets_literal'  => 'ANNOTATION_CLASS',
176            'targets'          => Target::TARGET_CLASS,
177            'default_property' => 'value',
178            'properties'       => array(
179                'value' => 'value'
180            ),
181            'attribute_types'  => array(
182                'value' => array(
183                    'type'      =>'array',
184                    'required'  =>true,
185                    'array_type'=>'Doctrine\Common\Annotations\Annotation\Attribute',
186                    'value'     =>'array<Doctrine\Common\Annotations\Annotation\Attribute>'
187                )
188             ),
189        ),
190    );
191
192    /**
193     * Hash-map for handle types declaration
194     *
195     * @var array
196     */
197    private static $typeMap = array(
198        'float'     => 'double',
199        'bool'      => 'boolean',
200        // allow uppercase Boolean in honor of George Boole
201        'Boolean'   => 'boolean',
202        'int'       => 'integer',
203    );
204
205    /**
206     * Constructs a new DocParser.
207     */
208    public function __construct()
209    {
210        $this->lexer = new DocLexer;
211    }
212
213    /**
214     * Sets the annotation names that are ignored during the parsing process.
215     *
216     * The names are supposed to be the raw names as used in the class, not the
217     * fully qualified class names.
218     *
219     * @param array $names
220     */
221    public function setIgnoredAnnotationNames(array $names)
222    {
223        $this->ignoredAnnotationNames = $names;
224    }
225
226    public function setIgnoreNotImportedAnnotations($bool)
227    {
228        $this->ignoreNotImportedAnnotations = (Boolean) $bool;
229    }
230
231    /**
232     * Sets the default namespaces.
233     * @param array $namespaces
234     */
235    public function addNamespace($namespace)
236    {
237        if ($this->imports) {
238            throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
239        }
240        $this->namespaces[] = $namespace;
241    }
242
243    public function setImports(array $imports)
244    {
245        if ($this->namespaces) {
246            throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
247        }
248        $this->imports = $imports;
249    }
250
251     /**
252     * Sets current target context as bitmask.
253     *
254     * @param integer $target
255     */
256    public function setTarget($target)
257    {
258        $this->target = $target;
259    }
260
261    /**
262     * Parses the given docblock string for annotations.
263     *
264     * @param string $input The docblock string to parse.
265     * @param string $context The parsing context.
266     * @return array Array of annotations. If no annotations are found, an empty array is returned.
267     */
268    public function parse($input, $context = '')
269    {
270        if (false === $pos = strpos($input, '@')) {
271            return array();
272        }
273
274        // also parse whatever character is before the @
275        if ($pos > 0) {
276            $pos -= 1;
277        }
278
279        $this->context = $context;
280        $this->lexer->setInput(trim(substr($input, $pos), '* /'));
281        $this->lexer->moveNext();
282
283        return $this->Annotations();
284    }
285
286    /**
287     * Attempts to match the given token with the current lookahead token.
288     * If they match, updates the lookahead token; otherwise raises a syntax error.
289     *
290     * @param int Token type.
291     * @return bool True if tokens match; false otherwise.
292     */
293    private function match($token)
294    {
295        if ( ! $this->lexer->isNextToken($token) ) {
296            $this->syntaxError($this->lexer->getLiteral($token));
297        }
298
299        return $this->lexer->moveNext();
300    }
301
302    /**
303     * Attempts to match the current lookahead token with any of the given tokens.
304     *
305     * If any of them matches, this method updates the lookahead token; otherwise
306     * a syntax error is raised.
307     *
308     * @param array $tokens
309     * @return bool
310     */
311    private function matchAny(array $tokens)
312    {
313        if ( ! $this->lexer->isNextTokenAny($tokens)) {
314            $this->syntaxError(implode(' or ', array_map(array($this->lexer, 'getLiteral'), $tokens)));
315        }
316
317        return $this->lexer->moveNext();
318    }
319
320    /**
321     * Generates a new syntax error.
322     *
323     * @param string $expected Expected string.
324     * @param array $token Optional token.
325     * @throws SyntaxException
326     */
327    private function syntaxError($expected, $token = null)
328    {
329        if ($token === null) {
330            $token = $this->lexer->lookahead;
331        }
332
333        $message =  "Expected {$expected}, got ";
334
335        if ($this->lexer->lookahead === null) {
336            $message .= 'end of string';
337        } else {
338            $message .= "'{$token['value']}' at position {$token['position']}";
339        }
340
341        if (strlen($this->context)) {
342            $message .= ' in ' . $this->context;
343        }
344
345        $message .= '.';
346
347        throw AnnotationException::syntaxError($message);
348    }
349
350    /**
351     * Attempt to check if a class exists or not. This never goes through the PHP autoloading mechanism
352     * but uses the {@link AnnotationRegistry} to load classes.
353     *
354     * @param string $fqcn
355     * @return boolean
356     */
357    private function classExists($fqcn)
358    {
359        if (isset($this->classExists[$fqcn])) {
360            return $this->classExists[$fqcn];
361        }
362
363        // first check if the class already exists, maybe loaded through another AnnotationReader
364        if (class_exists($fqcn, false)) {
365            return $this->classExists[$fqcn] = true;
366        }
367
368        // final check, does this class exist?
369        return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn);
370    }
371
372    /**
373     * Collects parsing metadata for a given annotation class
374     *
375     * @param   string $name        The annotation name
376     */
377    private function collectAnnotationMetadata($name)
378    {
379        if (self::$metadataParser == null){
380            self::$metadataParser = new self();
381            self::$metadataParser->setTarget(Target::TARGET_CLASS);
382            self::$metadataParser->setIgnoreNotImportedAnnotations(true);
383            self::$metadataParser->setImports(array(
384                'target'        => 'Doctrine\Common\Annotations\Annotation\Target',
385                'attribute'     => 'Doctrine\Common\Annotations\Annotation\Attribute',
386                'attributes'    => 'Doctrine\Common\Annotations\Annotation\Attributes'
387            ));
388            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Target.php');
389            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php');
390            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attributes.php');
391        }
392
393        $class      = new \ReflectionClass($name);
394        $docComment = $class->getDocComment();
395
396        // Sets default values for annotation metadata
397        $metadata = array(
398            'default_property' => null,
399            'has_constructor'  => (null !== $constructor = $class->getConstructor()) && $constructor->getNumberOfParameters() > 0,
400            'properties'       => array(),
401            'property_types'   => array(),
402            'attribute_types'  => array(),
403            'targets_literal'  => null,
404            'targets'          => Target::TARGET_ALL,
405            'is_annotation'    => false !== strpos($docComment, '@Annotation'),
406        );
407
408        // verify that the class is really meant to be an annotation
409        if ($metadata['is_annotation']) {
410            foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) {
411                if ($annotation instanceof Target) {
412                    $metadata['targets']         = $annotation->targets;
413                    $metadata['targets_literal'] = $annotation->literal;
414
415                } elseif ($annotation instanceof Attributes) {
416                    foreach ($annotation->value as $attrib) {
417                        // handle internal type declaration
418                        $type = isset(self::$typeMap[$attrib->type]) ? self::$typeMap[$attrib->type] : $attrib->type;
419
420                        // handle the case if the property type is mixed
421                        if ('mixed' !== $type) {
422                            // Checks if the property has array<type>
423                            if (false !== $pos = strpos($type, '<')) {
424                                $arrayType  = substr($type, $pos+1, -1);
425                                $type       = 'array';
426
427                                if (isset(self::$typeMap[$arrayType])) {
428                                    $arrayType = self::$typeMap[$arrayType];
429                                }
430
431                                $metadata['attribute_types'][$attrib->name]['array_type'] = $arrayType;
432                            }
433
434                            $metadata['attribute_types'][$attrib->name]['type']     = $type;
435                            $metadata['attribute_types'][$attrib->name]['value']    = $attrib->type;
436                            $metadata['attribute_types'][$attrib->name]['required'] = $attrib->required;
437                        }
438                    }
439                }
440            }
441
442            // if not has a constructor will inject values into public properties
443            if (false === $metadata['has_constructor']) {
444                // collect all public properties
445                foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
446                    $metadata['properties'][$property->name] = $property->name;
447
448                    // checks if the property has @var annotation
449                    if ((false !== $propertyComment = $property->getDocComment())
450                        && false !== strpos($propertyComment, '@var')
451                        && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches)) {
452                        // literal type declaration
453                        $value = $matches[1];
454
455                        // handle internal type declaration
456                        $type = isset(self::$typeMap[$value]) ? self::$typeMap[$value] : $value;
457
458                        // handle the case if the property type is mixed
459                        if ('mixed' !== $type) {
460                            // Checks if the property has @var array<type> annotation
461                            if (false !== $pos = strpos($type, '<')) {
462                                $arrayType = substr($type, $pos+1, -1);
463                                $type = 'array';
464
465                                if (isset(self::$typeMap[$arrayType])) {
466                                    $arrayType = self::$typeMap[$arrayType];
467                                }
468
469                                $metadata['attribute_types'][$property->name]['array_type'] = $arrayType;
470                            }
471
472                            $metadata['attribute_types'][$property->name]['type']       = $type;
473                            $metadata['attribute_types'][$property->name]['value']      = $value;
474                            $metadata['attribute_types'][$property->name]['required']   = false !== strpos($propertyComment, '@Required');
475                        }
476                    }
477                }
478
479                // choose the first property as default property
480                $metadata['default_property'] = reset($metadata['properties']);
481            }
482        }
483
484        self::$annotationMetadata[$name] = $metadata;
485    }
486
487    /**
488     * Annotations ::= Annotation {[ "*" ]* [Annotation]}*
489     *
490     * @return array
491     */
492    private function Annotations()
493    {
494        $annotations = array();
495
496        while (null !== $this->lexer->lookahead) {
497            if (DocLexer::T_AT !== $this->lexer->lookahead['type']) {
498                $this->lexer->moveNext();
499                continue;
500            }
501
502            // make sure the @ is preceded by non-catchable pattern
503            if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) {
504                $this->lexer->moveNext();
505                continue;
506            }
507
508            // make sure the @ is followed by either a namespace separator, or
509            // an identifier token
510            if ((null === $peek = $this->lexer->glimpse())
511                || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true))
512                || $peek['position'] !== $this->lexer->lookahead['position'] + 1) {
513                $this->lexer->moveNext();
514                continue;
515            }
516
517            $this->isNestedAnnotation = false;
518            if (false !== $annot = $this->Annotation()) {
519                $annotations[] = $annot;
520            }
521        }
522
523        return $annotations;
524    }
525
526    /**
527     * Annotation     ::= "@" AnnotationName ["(" [Values] ")"]
528     * AnnotationName ::= QualifiedName | SimpleName
529     * QualifiedName  ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName
530     * NameSpacePart  ::= identifier | null | false | true
531     * SimpleName     ::= identifier | null | false | true
532     *
533     * @return mixed False if it is not a valid annotation.
534     */
535    private function Annotation()
536    {
537        $this->match(DocLexer::T_AT);
538
539        // check if we have an annotation
540        if ($this->lexer->isNextTokenAny(self::$classIdentifiers)) {
541            $this->lexer->moveNext();
542            $name = $this->lexer->token['value'];
543        } else if ($this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) {
544            $name = '';
545        } else {
546            $this->syntaxError('namespace separator or identifier');
547        }
548
549        while ($this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value']) && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) {
550            $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
551            $this->matchAny(self::$classIdentifiers);
552            $name .= '\\'.$this->lexer->token['value'];
553        }
554
555        // only process names which are not fully qualified, yet
556        // fully qualified names must start with a \
557        $originalName = $name;
558        if ('\\' !== $name[0]) {
559            $alias = (false === $pos = strpos($name, '\\'))? $name : substr($name, 0, $pos);
560
561            $found = false;
562            if ($this->namespaces) {
563                foreach ($this->namespaces as $namespace) {
564                    if ($this->classExists($namespace.'\\'.$name)) {
565                        $name = $namespace.'\\'.$name;
566                        $found = true;
567                        break;
568                    }
569                }
570            } elseif (isset($this->imports[$loweredAlias = strtolower($alias)])) {
571                if (false !== $pos) {
572                    $name = $this->imports[$loweredAlias].substr($name, $pos);
573                } else {
574                    $name = $this->imports[$loweredAlias];
575                }
576                $found = true;
577            } elseif (isset($this->imports['__NAMESPACE__']) && $this->classExists($this->imports['__NAMESPACE__'].'\\'.$name)) {
578                 $name = $this->imports['__NAMESPACE__'].'\\'.$name;
579                 $found = true;
580            } elseif ($this->classExists($name)) {
581                $found = true;
582            }
583
584            if (!$found) {
585                if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
586                    return false;
587                }
588
589                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));
590            }
591        }
592
593        if (!$this->classExists($name)) {
594            throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context));
595        }
596
597        // at this point, $name contains the fully qualified class name of the
598        // annotation, and it is also guaranteed that this class exists, and
599        // that it is loaded
600
601
602        // collects the metadata annotation only if there is not yet
603        if (!isset(self::$annotationMetadata[$name])) {
604            $this->collectAnnotationMetadata($name);
605        }
606
607        // verify that the class is really meant to be an annotation and not just any ordinary class
608        if (self::$annotationMetadata[$name]['is_annotation'] === false) {
609            if (isset($this->ignoredAnnotationNames[$originalName])) {
610                return false;
611            }
612
613            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));
614        }
615
616        //if target is nested annotation
617        $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target;
618
619        // Next will be nested
620        $this->isNestedAnnotation = true;
621
622        //if annotation does not support current target
623        if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) {
624            throw AnnotationException::semanticalError(
625                sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.',
626                     $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal'])
627            );
628        }
629
630        $values = array();
631        if ($this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
632            $this->match(DocLexer::T_OPEN_PARENTHESIS);
633
634            if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
635                $values = $this->Values();
636            }
637
638            $this->match(DocLexer::T_CLOSE_PARENTHESIS);
639        }
640
641        // checks all declared attributes
642        foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) {
643            if ($property === self::$annotationMetadata[$name]['default_property']
644                && !isset($values[$property]) && isset($values['value'])) {
645                $property = 'value';
646            }
647
648            // handle a not given attribute or null value
649            if (!isset($values[$property])) {
650                if ($type['required']) {
651                    throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']);
652                }
653
654                continue;
655            }
656
657            if ($type['type'] === 'array') {
658                // handle the case of a single value
659                if (!is_array($values[$property])) {
660                    $values[$property] = array($values[$property]);
661                }
662
663                // checks if the attribute has array type declaration, such as "array<string>"
664                if (isset($type['array_type'])) {
665                    foreach ($values[$property] as $item) {
666                        if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) {
667                            throw AnnotationException::typeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item);
668                        }
669                    }
670                }
671            } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) {
672                throw AnnotationException::typeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]);
673            }
674        }
675
676        // check if the annotation expects values via the constructor,
677        // or directly injected into public properties
678        if (self::$annotationMetadata[$name]['has_constructor'] === true) {
679            return new $name($values);
680        }
681
682        $instance = new $name();
683        foreach ($values as $property => $value) {
684            if (!isset(self::$annotationMetadata[$name]['properties'][$property])) {
685                if ('value' !== $property) {
686                    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'])));
687                }
688
689                // handle the case if the property has no annotations
690                if (!$property = self::$annotationMetadata[$name]['default_property']) {
691                    throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not accept any values, but got %s.', $originalName, $this->context, json_encode($values)));
692                }
693            }
694
695            $instance->{$property} = $value;
696        }
697
698        return $instance;
699    }
700
701    /**
702     * Values ::= Array | Value {"," Value}*
703     *
704     * @return array
705     */
706    private function Values()
707    {
708        $values = array();
709
710        // Handle the case of a single array as value, i.e. @Foo({....})
711        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
712            $values['value'] = $this->Value();
713            return $values;
714        }
715
716        $values[] = $this->Value();
717
718        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
719            $this->match(DocLexer::T_COMMA);
720            $token = $this->lexer->lookahead;
721            $value = $this->Value();
722
723            if ( ! is_object($value) && ! is_array($value)) {
724                $this->syntaxError('Value', $token);
725            }
726
727            $values[] = $value;
728        }
729
730        foreach ($values as $k => $value) {
731            if (is_object($value) && $value instanceof \stdClass) {
732                $values[$value->name] = $value->value;
733            } else if ( ! isset($values['value'])){
734                $values['value'] = $value;
735            } else {
736                if ( ! is_array($values['value'])) {
737                    $values['value'] = array($values['value']);
738                }
739
740                $values['value'][] = $value;
741            }
742
743            unset($values[$k]);
744        }
745
746        return $values;
747    }
748
749    /**
750     * Value ::= PlainValue | FieldAssignment
751     *
752     * @return mixed
753     */
754    private function Value()
755    {
756        $peek = $this->lexer->glimpse();
757
758        if (DocLexer::T_EQUALS === $peek['type']) {
759            return $this->FieldAssignment();
760        }
761
762        return $this->PlainValue();
763    }
764
765    /**
766     * PlainValue ::= integer | string | float | boolean | Array | Annotation
767     *
768     * @return mixed
769     */
770    private function PlainValue()
771    {
772        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
773            return $this->Arrayx();
774        }
775
776        if ($this->lexer->isNextToken(DocLexer::T_AT)) {
777            return $this->Annotation();
778        }
779
780        switch ($this->lexer->lookahead['type']) {
781            case DocLexer::T_STRING:
782                $this->match(DocLexer::T_STRING);
783                return $this->lexer->token['value'];
784
785            case DocLexer::T_INTEGER:
786                $this->match(DocLexer::T_INTEGER);
787                return (int)$this->lexer->token['value'];
788
789            case DocLexer::T_FLOAT:
790                $this->match(DocLexer::T_FLOAT);
791                return (float)$this->lexer->token['value'];
792
793            case DocLexer::T_TRUE:
794                $this->match(DocLexer::T_TRUE);
795                return true;
796
797            case DocLexer::T_FALSE:
798                $this->match(DocLexer::T_FALSE);
799                return false;
800
801            case DocLexer::T_NULL:
802                $this->match(DocLexer::T_NULL);
803                return null;
804
805            default:
806                $this->syntaxError('PlainValue');
807        }
808    }
809
810    /**
811     * FieldAssignment ::= FieldName "=" PlainValue
812     * FieldName ::= identifier
813     *
814     * @return array
815     */
816    private function FieldAssignment()
817    {
818        $this->match(DocLexer::T_IDENTIFIER);
819        $fieldName = $this->lexer->token['value'];
820
821        $this->match(DocLexer::T_EQUALS);
822
823        $item = new \stdClass();
824        $item->name  = $fieldName;
825        $item->value = $this->PlainValue();
826
827        return $item;
828    }
829
830    /**
831     * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}"
832     *
833     * @return array
834     */
835    private function Arrayx()
836    {
837        $array = $values = array();
838
839        $this->match(DocLexer::T_OPEN_CURLY_BRACES);
840        $values[] = $this->ArrayEntry();
841
842        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
843            $this->match(DocLexer::T_COMMA);
844
845            // optional trailing comma
846            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
847                break;
848            }
849
850            $values[] = $this->ArrayEntry();
851        }
852
853        $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
854
855        foreach ($values as $value) {
856            list ($key, $val) = $value;
857
858            if ($key !== null) {
859                $array[$key] = $val;
860            } else {
861                $array[] = $val;
862            }
863        }
864
865        return $array;
866    }
867
868    /**
869     * ArrayEntry ::= Value | KeyValuePair
870     * KeyValuePair ::= Key ("=" | ":") PlainValue
871     * Key ::= string | integer
872     *
873     * @return array
874     */
875    private function ArrayEntry()
876    {
877        $peek = $this->lexer->glimpse();
878
879        if (DocLexer::T_EQUALS === $peek['type']
880                || DocLexer::T_COLON === $peek['type']) {
881            $this->matchAny(array(DocLexer::T_INTEGER, DocLexer::T_STRING));
882
883            $key = $this->lexer->token['value'];
884            $this->matchAny(array(DocLexer::T_EQUALS, DocLexer::T_COLON));
885
886            return array($key, $this->PlainValue());
887        }
888
889        return array(null, $this->Value());
890    }
891}
Note: See TracBrowser for help on using the repository browser.