├── LICENSE ├── README.md ├── UPGRADE.md ├── composer.json ├── docs └── en │ ├── annotations.rst │ ├── custom.rst │ ├── index.rst │ └── sidebar.rst └── lib └── Doctrine └── Common └── Annotations ├── Annotation.php ├── Annotation ├── Attribute.php ├── Attributes.php ├── Enum.php ├── IgnoreAnnotation.php ├── NamedArgumentConstructor.php ├── Required.php └── Target.php ├── AnnotationException.php ├── AnnotationReader.php ├── AnnotationRegistry.php ├── DocLexer.php ├── DocParser.php ├── ImplicitlyIgnoredAnnotationNames.php ├── IndexedReader.php ├── PhpParser.php ├── PsrCachedReader.php ├── Reader.php └── TokenParser.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2013 Doctrine Project 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ PHP 8 introduced 2 | [attributes](https://www.php.net/manual/en/language.attributes.overview.php), 3 | which are a native replacement for annotations. As such, this library is 4 | considered feature complete, and should receive exclusively bugfixes and 5 | security fixes. 6 | 7 | We do not recommend using this library in new projects and encourage authors 8 | of downstream libraries to offer support for attributes as an alternative to 9 | Doctrine Annotations. 10 | 11 | Have a look at [our blog](https://www.doctrine-project.org/2022/11/04/annotations-to-attributes.html) 12 | to learn more. 13 | 14 | # Doctrine Annotations 15 | 16 | [![Build Status](https://github.com/doctrine/annotations/workflows/Continuous%20Integration/badge.svg?label=build)](https://github.com/doctrine/persistence/actions) 17 | [![Dependency Status](https://www.versioneye.com/package/php--doctrine--annotations/badge.png)](https://www.versioneye.com/package/php--doctrine--annotations) 18 | [![Reference Status](https://www.versioneye.com/php/doctrine:annotations/reference_badge.svg)](https://www.versioneye.com/php/doctrine:annotations/references) 19 | [![Total Downloads](https://poser.pugx.org/doctrine/annotations/downloads.png)](https://packagist.org/packages/doctrine/annotations) 20 | [![Latest Stable Version](https://img.shields.io/packagist/v/doctrine/annotations.svg?label=stable)](https://packagist.org/packages/doctrine/annotations) 21 | 22 | Docblock Annotations Parser library (extracted from [Doctrine Common](https://github.com/doctrine/common)). 23 | 24 | ## Documentation 25 | 26 | See the [doctrine-project website](https://www.doctrine-project.org/projects/doctrine-annotations/en/stable/index.html). 27 | 28 | ## Contributing 29 | 30 | When making a pull request, make sure your changes follow the 31 | [Coding Standard Guidelines](https://www.doctrine-project.org/projects/doctrine-coding-standard/en/current/reference/index.html#introduction). 32 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade from 1.0.x to 2.0.x 2 | 3 | - The `NamedArgumentConstructorAnnotation` has been removed. Use the `@NamedArgumentConstructor` 4 | annotation instead. 5 | - `SimpleAnnotationReader` has been removed. 6 | - `DocLexer::peek()` and `DocLexer::glimpse` now return 7 | `Doctrine\Common\Lexer\Token` objects. When using `doctrine/lexer` 2, these 8 | implement `ArrayAccess` as a way for you to still be able to treat them as 9 | arrays in some ways. 10 | - `CachedReader` and `FileCacheReader` have been removed use `PsrCachedReader` instead. 11 | - `AnnotationRegistry` methods related to registering annotations instead of 12 | using autoloading have been removed. 13 | - Parameter type declarations have been added to all methods of all classes. If 14 | you have classes inheriting from classes inside this package, you should add 15 | parameter and return type declarations. 16 | - Support for PHP < 7.2 has been removed 17 | - `PhpParser::parseClass()` has been removed. Use 18 | `PhpParser::parseUseStatements()` instead. 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doctrine/annotations", 3 | "description": "Docblock Annotations Parser", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "annotations", 8 | "docblock", 9 | "parser" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Guilherme Blanco", 14 | "email": "guilhermeblanco@gmail.com" 15 | }, 16 | { 17 | "name": "Roman Borschel", 18 | "email": "roman@code-factory.org" 19 | }, 20 | { 21 | "name": "Benjamin Eberlei", 22 | "email": "kontakt@beberlei.de" 23 | }, 24 | { 25 | "name": "Jonathan Wage", 26 | "email": "jonwage@gmail.com" 27 | }, 28 | { 29 | "name": "Johannes Schmitt", 30 | "email": "schmittjoh@gmail.com" 31 | } 32 | ], 33 | "homepage": "https://www.doctrine-project.org/projects/annotations.html", 34 | "require": { 35 | "php": "^7.2 || ^8.0", 36 | "ext-tokenizer": "*", 37 | "doctrine/lexer": "^2 || ^3", 38 | "psr/cache": "^1 || ^2 || ^3" 39 | }, 40 | "require-dev": { 41 | "doctrine/cache": "^2.0", 42 | "doctrine/coding-standard": "^10", 43 | "phpstan/phpstan": "^1.10.28", 44 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", 45 | "symfony/cache": "^5.4 || ^6.4 || ^7", 46 | "vimeo/psalm": "^4.30 || ^5.14" 47 | }, 48 | "suggest": { 49 | "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" 50 | }, 51 | "autoload": { 52 | "psr-4": { 53 | "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" 54 | } 55 | }, 56 | "autoload-dev": { 57 | "psr-4": { 58 | "Doctrine\\Performance\\Common\\Annotations\\": "tests/Doctrine/Performance/Common/Annotations", 59 | "Doctrine\\Tests\\Common\\Annotations\\": "tests/Doctrine/Tests/Common/Annotations" 60 | }, 61 | "files": [ 62 | "tests/Doctrine/Tests/Common/Annotations/Fixtures/functions.php", 63 | "tests/Doctrine/Tests/Common/Annotations/Fixtures/SingleClassLOC1000.php" 64 | ] 65 | }, 66 | "config": { 67 | "allow-plugins": { 68 | "dealerdirect/phpcodesniffer-composer-installer": true 69 | }, 70 | "sort-packages": true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /docs/en/annotations.rst: -------------------------------------------------------------------------------- 1 | Handling Annotations 2 | ==================== 3 | 4 | There are several different approaches to handling annotations in PHP. 5 | Doctrine Annotations maps docblock annotations to PHP classes. Because 6 | not all docblock annotations are used for metadata purposes a filter is 7 | applied to ignore or skip classes that are not Doctrine annotations. 8 | 9 | Take a look at the following code snippet: 10 | 11 | .. code-block:: php 12 | 13 | namespace MyProject\Entities; 14 | 15 | use Doctrine\ORM\Mapping AS ORM; 16 | use Symfony\Component\Validator\Constraints AS Assert; 17 | 18 | /** 19 | * @author Benjamin Eberlei 20 | * @ORM\Entity 21 | * @MyProject\Annotations\Foobarable 22 | */ 23 | class User 24 | { 25 | /** 26 | * @ORM\Id @ORM\Column @ORM\GeneratedValue 27 | * @dummy 28 | * @var int 29 | */ 30 | private $id; 31 | 32 | /** 33 | * @ORM\Column(type="string") 34 | * @Assert\NotEmpty 35 | * @Assert\Email 36 | * @var string 37 | */ 38 | private $email; 39 | } 40 | 41 | In this snippet you can see a variety of different docblock annotations: 42 | 43 | - Documentation annotations such as ``@var`` and ``@author``. These 44 | annotations are ignored and never considered for throwing an 45 | exception due to wrongly used annotations. 46 | - Annotations imported through use statements. The statement ``use 47 | Doctrine\ORM\Mapping AS ORM`` makes all classes under that namespace 48 | available as ``@ORM\ClassName``. Same goes for the import of 49 | ``@Assert``. 50 | - The ``@dummy`` annotation. It is not a documentation annotation and 51 | not ignored. For Doctrine Annotations it is not entirely clear how 52 | to handle this annotation. Depending on the configuration an exception 53 | (unknown annotation) will be thrown when parsing this annotation. 54 | - The fully qualified annotation ``@MyProject\Annotations\Foobarable``. 55 | This is transformed directly into the given class name. 56 | 57 | How are these annotations loaded? From looking at the code you could 58 | guess that the ORM Mapping, Assert Validation and the fully qualified 59 | annotation can just be loaded using 60 | the defined PHP autoloaders. This is not the case however: For error 61 | handling reasons every check for class existence inside the 62 | ``AnnotationReader`` sets the second parameter $autoload 63 | of ``class_exists($name, $autoload)`` to false. To work flawlessly the 64 | ``AnnotationReader`` requires silent autoloaders which many autoloaders are 65 | not. Silent autoloading is NOT part of the `PSR-0 specification 66 | `_ 67 | for autoloading. 68 | 69 | This is why Doctrine Annotations uses its own autoloading mechanism 70 | through a global registry. If you are wondering about the annotation 71 | registry being global, there is no other way to solve the architectural 72 | problems of autoloading annotation classes in a straightforward fashion. 73 | Additionally if you think about PHP autoloading then you recognize it is 74 | a global as well. 75 | 76 | To anticipate the configuration section, making the above PHP class work 77 | with Doctrine Annotations requires this setup: 78 | 79 | .. code-block:: php 80 | 81 | use Doctrine\Common\Annotations\AnnotationReader; 82 | 83 | $reader = new AnnotationReader(); 84 | AnnotationReader::addGlobalIgnoredName('dummy'); 85 | 86 | We create the actual ``AnnotationReader`` instance. 87 | Note that we also add ``dummy`` to the global list of ignored 88 | annotations for which we do not throw exceptions. Setting this is 89 | necessary in our example case, otherwise ``@dummy`` would trigger an 90 | exception to be thrown during the parsing of the docblock of 91 | ``MyProject\Entities\User#id``. 92 | 93 | Setup and Configuration 94 | ----------------------- 95 | 96 | To use the annotations library is simple, you just need to create a new 97 | ``AnnotationReader`` instance: 98 | 99 | .. code-block:: php 100 | 101 | $reader = new \Doctrine\Common\Annotations\AnnotationReader(); 102 | 103 | This creates a simple annotation reader with no caching other than in 104 | memory (in php arrays). Since parsing docblocks can be expensive you 105 | should cache this process by using a caching reader. 106 | 107 | To cache annotations, you can create a ``Doctrine\Common\Annotations\PsrCachedReader``. 108 | This reader decorates the original reader and stores all annotations in a PSR-6 109 | cache: 110 | 111 | .. code-block:: php 112 | 113 | use Doctrine\Common\Annotations\AnnotationReader; 114 | use Doctrine\Common\Annotations\PsrCachedReader; 115 | 116 | $cache = ... // instantiate a PSR-6 Cache pool 117 | 118 | $reader = new PsrCachedReader( 119 | new AnnotationReader(), 120 | $cache, 121 | $debug = true 122 | ); 123 | 124 | The ``debug`` flag is used here as well to invalidate the cache files 125 | when the PHP class with annotations changed and should be used during 126 | development. 127 | 128 | .. warning :: 129 | 130 | The ``AnnotationReader`` works and caches under the 131 | assumption that all annotations of a doc-block are processed at 132 | once. That means that annotation classes that do not exist and 133 | aren't loaded and cannot be autoloaded (using the 134 | AnnotationRegistry) would never be visible and not accessible if a 135 | cache is used unless the cache is cleared and the annotations 136 | requested again, this time with all annotations defined. 137 | 138 | By default the annotation reader returns a list of annotations with 139 | numeric indexes. If you want your annotations to be indexed by their 140 | class name you can wrap the reader in an ``IndexedReader``: 141 | 142 | .. code-block:: php 143 | 144 | use Doctrine\Common\Annotations\AnnotationReader; 145 | use Doctrine\Common\Annotations\IndexedReader; 146 | 147 | $reader = new IndexedReader(new AnnotationReader()); 148 | 149 | .. warning:: 150 | 151 | You should never wrap the indexed reader inside a cached reader, 152 | only the other way around. This way you can re-use the cache with 153 | indexed or numeric keys, otherwise your code may experience failures 154 | due to caching in a numerical or indexed format. 155 | 156 | Ignoring missing exceptions 157 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 158 | 159 | By default an exception is thrown from the ``AnnotationReader`` if an 160 | annotation was found that: 161 | 162 | - is not part of the list of ignored "documentation annotations"; 163 | - was not imported through a use statement; 164 | - is not a fully qualified class that exists. 165 | 166 | You can disable this behavior for specific names if your docblocks do 167 | not follow strict requirements: 168 | 169 | .. code-block:: php 170 | 171 | $reader = new \Doctrine\Common\Annotations\AnnotationReader(); 172 | AnnotationReader::addGlobalIgnoredName('foo'); 173 | 174 | PHP Imports 175 | ~~~~~~~~~~~ 176 | 177 | By default the annotation reader parses the use-statement of a php file 178 | to gain access to the import rules and register them for the annotation 179 | processing. Only if you are using PHP Imports can you validate the 180 | correct usage of annotations and throw exceptions if you misspelled an 181 | annotation. This mechanism is enabled by default. 182 | 183 | To ease the upgrade path, we still allow you to disable this mechanism. 184 | Note however that we will remove this in future versions: 185 | 186 | .. code-block:: php 187 | 188 | $reader = new \Doctrine\Common\Annotations\AnnotationReader(); 189 | $reader->setEnabledPhpImports(false); 190 | -------------------------------------------------------------------------------- /docs/en/custom.rst: -------------------------------------------------------------------------------- 1 | Custom Annotation Classes 2 | ========================= 3 | 4 | If you want to define your own annotations, you just have to group them 5 | in a namespace. 6 | Annotation classes have to contain a class-level docblock with the text 7 | ``@Annotation``: 8 | 9 | .. code-block:: php 10 | 11 | namespace MyCompany\Annotations; 12 | 13 | /** @Annotation */ 14 | class Bar 15 | { 16 | // some code 17 | } 18 | 19 | Inject annotation values 20 | ------------------------ 21 | 22 | The annotation parser checks if the annotation constructor has arguments, 23 | if so then it will pass the value array, otherwise it will try to inject 24 | values into public properties directly: 25 | 26 | 27 | .. code-block:: php 28 | 29 | namespace MyCompany\Annotations; 30 | 31 | /** 32 | * @Annotation 33 | * 34 | * Some Annotation using a constructor 35 | */ 36 | class Bar 37 | { 38 | private $foo; 39 | 40 | public function __construct(array $values) 41 | { 42 | $this->foo = $values['foo']; 43 | } 44 | } 45 | 46 | /** 47 | * @Annotation 48 | * 49 | * Some Annotation without a constructor 50 | */ 51 | class Foo 52 | { 53 | public $bar; 54 | } 55 | 56 | Optional: Constructors with Named Parameters 57 | -------------------------------------------- 58 | 59 | Starting with Annotations v1.11 a new annotation instantiation strategy 60 | is available that aims at compatibility of Annotation classes with the PHP 8 61 | attribute feature. You need to declare a constructor with regular parameter 62 | names that match the named arguments in the annotation syntax. 63 | 64 | To enable this feature, you can tag your annotation class with 65 | ``@NamedArgumentConstructor`` (available from v1.12) or implement the 66 | ``Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation`` interface 67 | (available from v1.11 and deprecated as of v1.12). 68 | When using the ``@NamedArgumentConstructor`` tag, the first argument of the 69 | constructor is considered as the default one. 70 | 71 | 72 | Usage with the ``@NamedArgumentConstructor`` tag 73 | 74 | .. code-block:: php 75 | 76 | namespace MyCompany\Annotations; 77 | 78 | /** 79 | * @Annotation 80 | * @NamedArgumentConstructor 81 | */ 82 | class Bar implements NamedArgumentConstructorAnnotation 83 | { 84 | private $foo; 85 | 86 | public function __construct(string $foo) 87 | { 88 | $this->foo = $foo; 89 | } 90 | } 91 | 92 | /** Usable with @Bar(foo="baz") */ 93 | /** Usable with @Bar("baz") */ 94 | 95 | In combination with PHP 8's constructor property promotion feature 96 | you can simplify this to: 97 | 98 | .. code-block:: php 99 | 100 | namespace MyCompany\Annotations; 101 | 102 | /** 103 | * @Annotation 104 | * @NamedArgumentConstructor 105 | */ 106 | class Bar implements NamedArgumentConstructorAnnotation 107 | { 108 | public function __construct(private string $foo) {} 109 | } 110 | 111 | 112 | Usage with the 113 | ``Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation`` 114 | interface (v1.11, deprecated as of v1.12): 115 | .. code-block:: php 116 | 117 | namespace MyCompany\Annotations; 118 | 119 | use Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation; 120 | 121 | /** @Annotation */ 122 | class Bar implements NamedArgumentConstructorAnnotation 123 | { 124 | private $foo; 125 | 126 | public function __construct(private string $foo) {} 127 | } 128 | 129 | /** Usable with @Bar(foo="baz") */ 130 | 131 | Annotation Target 132 | ----------------- 133 | 134 | ``@Target`` indicates the kinds of class elements to which an annotation 135 | type is applicable. Then you could define one or more targets: 136 | 137 | - ``CLASS`` Allowed in class docblocks 138 | - ``PROPERTY`` Allowed in property docblocks 139 | - ``METHOD`` Allowed in the method docblocks 140 | - ``FUNCTION`` Allowed in function dockblocks 141 | - ``ALL`` Allowed in class, property, method and function docblocks 142 | - ``ANNOTATION`` Allowed inside other annotations 143 | 144 | If the annotations is not allowed in the current context, an 145 | ``AnnotationException`` is thrown. 146 | 147 | .. code-block:: php 148 | 149 | namespace MyCompany\Annotations; 150 | 151 | /** 152 | * @Annotation 153 | * @Target({"METHOD","PROPERTY"}) 154 | */ 155 | class Bar 156 | { 157 | // some code 158 | } 159 | 160 | /** 161 | * @Annotation 162 | * @Target("CLASS") 163 | */ 164 | class Foo 165 | { 166 | // some code 167 | } 168 | 169 | Attribute types 170 | --------------- 171 | 172 | The annotation parser checks the given parameters using the phpdoc 173 | annotation ``@var``, The data type could be validated using the ``@var`` 174 | annotation on the annotation properties or using the ``@Attributes`` and 175 | ``@Attribute`` annotations. 176 | 177 | If the data type does not match you get an ``AnnotationException`` 178 | 179 | .. code-block:: php 180 | 181 | namespace MyCompany\Annotations; 182 | 183 | /** 184 | * @Annotation 185 | * @Target({"METHOD","PROPERTY"}) 186 | */ 187 | class Bar 188 | { 189 | /** @var mixed */ 190 | public $mixed; 191 | 192 | /** @var boolean */ 193 | public $boolean; 194 | 195 | /** @var bool */ 196 | public $bool; 197 | 198 | /** @var float */ 199 | public $float; 200 | 201 | /** @var string */ 202 | public $string; 203 | 204 | /** @var integer */ 205 | public $integer; 206 | 207 | /** @var array */ 208 | public $array; 209 | 210 | /** @var SomeAnnotationClass */ 211 | public $annotation; 212 | 213 | /** @var array */ 214 | public $arrayOfIntegers; 215 | 216 | /** @var array */ 217 | public $arrayOfAnnotations; 218 | } 219 | 220 | /** 221 | * @Annotation 222 | * @Target({"METHOD","PROPERTY"}) 223 | * @Attributes({ 224 | * @Attribute("stringProperty", type = "string"), 225 | * @Attribute("annotProperty", type = "SomeAnnotationClass"), 226 | * }) 227 | */ 228 | class Foo 229 | { 230 | public function __construct(array $values) 231 | { 232 | $this->stringProperty = $values['stringProperty']; 233 | $this->annotProperty = $values['annotProperty']; 234 | } 235 | 236 | // some code 237 | } 238 | 239 | Annotation Required 240 | ------------------- 241 | 242 | ``@Required`` indicates that the field must be specified when the 243 | annotation is used. If it is not used you get an ``AnnotationException`` 244 | stating that this value can not be null. 245 | 246 | Declaring a required field: 247 | 248 | .. code-block:: php 249 | 250 | /** 251 | * @Annotation 252 | * @Target("ALL") 253 | */ 254 | class Foo 255 | { 256 | /** @Required */ 257 | public $requiredField; 258 | } 259 | 260 | Usage: 261 | 262 | .. code-block:: php 263 | 264 | /** @Foo(requiredField="value") */ 265 | public $direction; // Valid 266 | 267 | /** @Foo */ 268 | public $direction; // Required field missing, throws an AnnotationException 269 | 270 | 271 | Enumerated values 272 | ----------------- 273 | 274 | - An annotation property marked with ``@Enum`` is a field that accepts a 275 | fixed set of scalar values. 276 | - You should use ``@Enum`` fields any time you need to represent fixed 277 | values. 278 | - The annotation parser checks the given value and throws an 279 | ``AnnotationException`` if the value does not match. 280 | 281 | 282 | Declaring an enumerated property: 283 | 284 | .. code-block:: php 285 | 286 | /** 287 | * @Annotation 288 | * @Target("ALL") 289 | */ 290 | class Direction 291 | { 292 | /** 293 | * @Enum({"NORTH", "SOUTH", "EAST", "WEST"}) 294 | */ 295 | public $value; 296 | } 297 | 298 | Annotation usage: 299 | 300 | .. code-block:: php 301 | 302 | /** @Direction("NORTH") */ 303 | public $direction; // Valid value 304 | 305 | /** @Direction("NORTHEAST") */ 306 | public $direction; // Invalid value, throws an AnnotationException 307 | 308 | 309 | Constants 310 | --------- 311 | 312 | The use of constants and class constants is available on the annotations 313 | parser. 314 | 315 | The following usages are allowed: 316 | 317 | .. code-block:: php 318 | 319 | namespace MyCompany\Entity; 320 | 321 | use MyCompany\Annotations\Foo; 322 | use MyCompany\Annotations\Bar; 323 | use MyCompany\Entity\SomeClass; 324 | 325 | /** 326 | * @Foo(PHP_EOL) 327 | * @Bar(Bar::FOO) 328 | * @Foo({SomeClass::FOO, SomeClass::BAR}) 329 | * @Bar({SomeClass::FOO_KEY = SomeClass::BAR_VALUE}) 330 | */ 331 | class User 332 | { 333 | } 334 | 335 | 336 | Be careful with constants and the cache ! 337 | 338 | .. note:: 339 | 340 | The cached reader will not re-evaluate each time an annotation is 341 | loaded from cache. When a constant is changed the cache must be 342 | cleaned. 343 | 344 | 345 | Usage 346 | ----- 347 | 348 | Using the library API is simple. Using the annotations described in the 349 | previous section, you can now annotate other classes with your 350 | annotations: 351 | 352 | .. code-block:: php 353 | 354 | namespace MyCompany\Entity; 355 | 356 | use MyCompany\Annotations\Foo; 357 | use MyCompany\Annotations\Bar; 358 | 359 | /** 360 | * @Foo(bar="foo") 361 | * @Bar(foo="bar") 362 | */ 363 | class User 364 | { 365 | } 366 | 367 | Now we can write a script to get the annotations above: 368 | 369 | .. code-block:: php 370 | 371 | $reflClass = new ReflectionClass('MyCompany\Entity\User'); 372 | $classAnnotations = $reader->getClassAnnotations($reflClass); 373 | 374 | foreach ($classAnnotations AS $annot) { 375 | if ($annot instanceof \MyCompany\Annotations\Foo) { 376 | echo $annot->bar; // prints "foo"; 377 | } else if ($annot instanceof \MyCompany\Annotations\Bar) { 378 | echo $annot->foo; // prints "bar"; 379 | } 380 | } 381 | 382 | You have a complete API for retrieving annotation class instances from a 383 | class, property or method docblock: 384 | 385 | 386 | Reader API 387 | ~~~~~~~~~~ 388 | 389 | Access all annotations of a class 390 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 391 | 392 | .. code-block:: php 393 | 394 | public function getClassAnnotations(\ReflectionClass $class); 395 | 396 | Access one annotation of a class 397 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 398 | 399 | .. code-block:: php 400 | 401 | public function getClassAnnotation(\ReflectionClass $class, $annotationName); 402 | 403 | Access all annotations of a method 404 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 405 | 406 | .. code-block:: php 407 | 408 | public function getMethodAnnotations(\ReflectionMethod $method); 409 | 410 | Access one annotation of a method 411 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 412 | 413 | .. code-block:: php 414 | 415 | public function getMethodAnnotation(\ReflectionMethod $method, $annotationName); 416 | 417 | Access all annotations of a property 418 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 419 | 420 | .. code-block:: php 421 | 422 | public function getPropertyAnnotations(\ReflectionProperty $property); 423 | 424 | Access one annotation of a property 425 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 426 | 427 | .. code-block:: php 428 | 429 | public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName); 430 | 431 | Access all annotations of a function 432 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 433 | 434 | .. code-block:: php 435 | 436 | public function getFunctionAnnotations(\ReflectionFunction $property); 437 | 438 | Access one annotation of a function 439 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 440 | 441 | .. code-block:: php 442 | 443 | public function getFunctionAnnotation(\ReflectionFunction $property, $annotationName); 444 | -------------------------------------------------------------------------------- /docs/en/index.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Deprecation notice 5 | ------------------ 6 | 7 | PHP 8 introduced `attributes 8 | `_, 9 | which are a native replacement for annotations. As such, this library is 10 | considered feature complete, and should receive exclusively bugfixes and 11 | security fixes. 12 | 13 | We do not recommend using this library in new projects and encourage authors 14 | of downstream libraries to offer support for attributes as an alternative to 15 | Doctrine Annotations. 16 | 17 | Have a look at `our blog `_ 18 | to learn more. 19 | 20 | Introduction 21 | ------------ 22 | 23 | Doctrine Annotations allows to implement custom annotation 24 | functionality for PHP classes and functions. 25 | 26 | .. code-block:: php 27 | 28 | class Foo 29 | { 30 | /** 31 | * @MyAnnotation(myProperty="value") 32 | */ 33 | private $bar; 34 | } 35 | 36 | Annotations aren't implemented in PHP itself which is why this component 37 | offers a way to use the PHP doc-blocks as a place for the well known 38 | annotation syntax using the ``@`` char. 39 | 40 | Annotations in Doctrine are used for the ORM configuration to build the 41 | class mapping, but it can be used in other projects for other purposes 42 | too. 43 | 44 | Installation 45 | ------------ 46 | 47 | You can install the Annotation component with composer: 48 | 49 | .. code-block:: 50 | 51 | $ composer require doctrine/annotations 52 | 53 | Create an annotation class 54 | -------------------------- 55 | 56 | An annotation class is a representation of the later used annotation 57 | configuration in classes. The annotation class of the previous example 58 | looks like this: 59 | 60 | .. code-block:: php 61 | 62 | /** 63 | * @Annotation 64 | */ 65 | final class MyAnnotation 66 | { 67 | public $myProperty; 68 | } 69 | 70 | The annotation class is declared as an annotation by ``@Annotation``. 71 | 72 | :doc:`Read more about custom annotations. ` 73 | 74 | Reading annotations 75 | ------------------- 76 | 77 | The access to the annotations happens by reflection of the class or function 78 | containing them. There are multiple reader-classes implementing the 79 | ``Doctrine\Common\Annotations\Reader`` interface, that can access the 80 | annotations of a class. A common one is 81 | ``Doctrine\Common\Annotations\AnnotationReader``: 82 | 83 | .. code-block:: php 84 | 85 | use Doctrine\Common\Annotations\AnnotationReader; 86 | 87 | $reflectionClass = new ReflectionClass(Foo::class); 88 | $property = $reflectionClass->getProperty('bar'); 89 | 90 | $reader = new AnnotationReader(); 91 | $myAnnotation = $reader->getPropertyAnnotation( 92 | $property, 93 | MyAnnotation::class 94 | ); 95 | 96 | echo $myAnnotation->myProperty; // result: "value" 97 | 98 | A reader has multiple methods to access the annotations of a class or 99 | function. 100 | 101 | :doc:`Read more about handling annotations. ` 102 | 103 | IDE Support 104 | ^^^^^^^^^^^ 105 | 106 | Some IDEs already provide support for annotations: 107 | 108 | - Eclipse via the `Symfony2 Plugin `_ 109 | - PhpStorm via the `PHP Annotations Plugin `_ or the `Symfony Plugin `_ 110 | -------------------------------------------------------------------------------- /docs/en/sidebar.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. toctree:: 4 | :depth: 3 5 | 6 | index 7 | annotations 8 | custom 9 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/Annotation.php: -------------------------------------------------------------------------------- 1 | $data Key-value for properties to be defined in this class. */ 22 | final public function __construct(array $data) 23 | { 24 | foreach ($data as $key => $value) { 25 | $this->$key = $value; 26 | } 27 | } 28 | 29 | /** 30 | * Error handler for unknown property accessor in Annotation class. 31 | * 32 | * @throws BadMethodCallException 33 | */ 34 | public function __get(string $name) 35 | { 36 | throw new BadMethodCallException( 37 | sprintf("Unknown property '%s' on annotation '%s'.", $name, static::class) 38 | ); 39 | } 40 | 41 | /** 42 | * Error handler for unknown property mutator in Annotation class. 43 | * 44 | * @param mixed $value Property value. 45 | * 46 | * @throws BadMethodCallException 47 | */ 48 | public function __set(string $name, $value) 49 | { 50 | throw new BadMethodCallException( 51 | sprintf("Unknown property '%s' on annotation '%s'.", $name, static::class) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/Annotation/Attribute.php: -------------------------------------------------------------------------------- 1 | */ 14 | public $value; 15 | } 16 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/Annotation/Enum.php: -------------------------------------------------------------------------------- 1 | */ 27 | public $value; 28 | 29 | /** 30 | * Literal target declaration. 31 | * 32 | * @var mixed[] 33 | */ 34 | public $literal; 35 | 36 | /** 37 | * @phpstan-param array{literal?: mixed[], value: list} $values 38 | * 39 | * @throws InvalidArgumentException 40 | */ 41 | public function __construct(array $values) 42 | { 43 | if (! isset($values['literal'])) { 44 | $values['literal'] = []; 45 | } 46 | 47 | foreach ($values['value'] as $var) { 48 | if (! is_scalar($var)) { 49 | throw new InvalidArgumentException(sprintf( 50 | '@Enum supports only scalar values "%s" given.', 51 | is_object($var) ? get_class($var) : gettype($var) 52 | )); 53 | } 54 | } 55 | 56 | foreach ($values['literal'] as $key => $var) { 57 | if (! in_array($key, $values['value'])) { 58 | throw new InvalidArgumentException(sprintf( 59 | 'Undefined enumerator value "%s" for literal "%s".', 60 | $key, 61 | $var 62 | )); 63 | } 64 | } 65 | 66 | $this->value = $values['value']; 67 | $this->literal = $values['literal']; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php: -------------------------------------------------------------------------------- 1 | */ 21 | public $names; 22 | 23 | /** 24 | * @phpstan-param array{value: string|list} $values 25 | * 26 | * @throws RuntimeException 27 | */ 28 | public function __construct(array $values) 29 | { 30 | if (is_string($values['value'])) { 31 | $values['value'] = [$values['value']]; 32 | } 33 | 34 | if (! is_array($values['value'])) { 35 | throw new RuntimeException(sprintf( 36 | '@IgnoreAnnotation expects either a string name, or an array of strings, but got %s.', 37 | json_encode($values['value']) 38 | )); 39 | } 40 | 41 | $this->names = $values['value']; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/Annotation/NamedArgumentConstructor.php: -------------------------------------------------------------------------------- 1 | */ 32 | private static $map = [ 33 | 'ALL' => self::TARGET_ALL, 34 | 'CLASS' => self::TARGET_CLASS, 35 | 'METHOD' => self::TARGET_METHOD, 36 | 'PROPERTY' => self::TARGET_PROPERTY, 37 | 'FUNCTION' => self::TARGET_FUNCTION, 38 | 'ANNOTATION' => self::TARGET_ANNOTATION, 39 | ]; 40 | 41 | /** @phpstan-var list */ 42 | public $value; 43 | 44 | /** 45 | * Targets as bitmask. 46 | * 47 | * @var int 48 | */ 49 | public $targets; 50 | 51 | /** 52 | * Literal target declaration. 53 | * 54 | * @var string 55 | */ 56 | public $literal; 57 | 58 | /** 59 | * @phpstan-param array{value?: string|list} $values 60 | * 61 | * @throws InvalidArgumentException 62 | */ 63 | public function __construct(array $values) 64 | { 65 | if (! isset($values['value'])) { 66 | $values['value'] = null; 67 | } 68 | 69 | if (is_string($values['value'])) { 70 | $values['value'] = [$values['value']]; 71 | } 72 | 73 | if (! is_array($values['value'])) { 74 | throw new InvalidArgumentException( 75 | sprintf( 76 | '@Target expects either a string value, or an array of strings, "%s" given.', 77 | is_object($values['value']) ? get_class($values['value']) : gettype($values['value']) 78 | ) 79 | ); 80 | } 81 | 82 | $bitmask = 0; 83 | foreach ($values['value'] as $literal) { 84 | if (! isset(self::$map[$literal])) { 85 | throw new InvalidArgumentException( 86 | sprintf( 87 | 'Invalid Target "%s". Available targets: [%s]', 88 | $literal, 89 | implode(', ', array_keys(self::$map)) 90 | ) 91 | ); 92 | } 93 | 94 | $bitmask |= self::$map[$literal]; 95 | } 96 | 97 | $this->targets = $bitmask; 98 | $this->value = $values['value']; 99 | $this->literal = implode(', ', $this->value); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/AnnotationException.php: -------------------------------------------------------------------------------- 1 | $available 123 | * 124 | * @return AnnotationException 125 | */ 126 | public static function enumeratorError( 127 | string $attributeName, 128 | string $annotationName, 129 | string $context, 130 | array $available, 131 | $given 132 | ) { 133 | return new self(sprintf( 134 | '[Enum Error] Attribute "%s" of @%s declared on %s accepts only [%s], but got %s.', 135 | $attributeName, 136 | $annotationName, 137 | $context, 138 | implode(', ', $available), 139 | is_object($given) ? get_class($given) : $given 140 | )); 141 | } 142 | 143 | /** @return AnnotationException */ 144 | public static function optimizerPlusSaveComments() 145 | { 146 | return new self( 147 | 'You have to enable opcache.save_comments=1 or zend_optimizerplus.save_comments=1.' 148 | ); 149 | } 150 | 151 | /** @return AnnotationException */ 152 | public static function optimizerPlusLoadComments() 153 | { 154 | return new self( 155 | 'You have to enable opcache.load_comments=1 or zend_optimizerplus.load_comments=1.' 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/AnnotationReader.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | private static $globalImports = [ 31 | 'ignoreannotation' => Annotation\IgnoreAnnotation::class, 32 | ]; 33 | 34 | /** 35 | * A list with annotations that are not causing exceptions when not resolved to an annotation class. 36 | * 37 | * The names are case sensitive. 38 | * 39 | * @var array 40 | */ 41 | private static $globalIgnoredNames = ImplicitlyIgnoredAnnotationNames::LIST; 42 | 43 | /** 44 | * A list with annotations that are not causing exceptions when not resolved to an annotation class. 45 | * 46 | * The names are case sensitive. 47 | * 48 | * @var array 49 | */ 50 | private static $globalIgnoredNamespaces = []; 51 | 52 | /** 53 | * Add a new annotation to the globally ignored annotation names with regard to exception handling. 54 | */ 55 | public static function addGlobalIgnoredName(string $name) 56 | { 57 | self::$globalIgnoredNames[$name] = true; 58 | } 59 | 60 | /** 61 | * Add a new annotation to the globally ignored annotation namespaces with regard to exception handling. 62 | */ 63 | public static function addGlobalIgnoredNamespace(string $namespace) 64 | { 65 | self::$globalIgnoredNamespaces[$namespace] = true; 66 | } 67 | 68 | /** 69 | * Annotations parser. 70 | * 71 | * @var DocParser 72 | */ 73 | private $parser; 74 | 75 | /** 76 | * Annotations parser used to collect parsing metadata. 77 | * 78 | * @var DocParser 79 | */ 80 | private $preParser; 81 | 82 | /** 83 | * PHP parser used to collect imports. 84 | * 85 | * @var PhpParser 86 | */ 87 | private $phpParser; 88 | 89 | /** 90 | * In-memory cache mechanism to store imported annotations per class. 91 | * 92 | * @psalm-var array<'class'|'function', array>> 93 | */ 94 | private $imports = []; 95 | 96 | /** 97 | * In-memory cache mechanism to store ignored annotations per class. 98 | * 99 | * @psalm-var array<'class'|'function', array>> 100 | */ 101 | private $ignoredAnnotationNames = []; 102 | 103 | /** 104 | * Initializes a new AnnotationReader. 105 | * 106 | * @throws AnnotationException 107 | */ 108 | public function __construct(?DocParser $parser = null) 109 | { 110 | if ( 111 | extension_loaded('Zend Optimizer+') && 112 | (filter_var(ini_get('zend_optimizerplus.save_comments'), FILTER_VALIDATE_BOOLEAN) === false || 113 | filter_var(ini_get('opcache.save_comments'), FILTER_VALIDATE_BOOLEAN) === false) 114 | ) { 115 | throw AnnotationException::optimizerPlusSaveComments(); 116 | } 117 | 118 | if ( 119 | extension_loaded('Zend OPcache') && 120 | filter_var(ini_get('opcache.save_comments'), FILTER_VALIDATE_BOOLEAN) === false 121 | ) { 122 | throw AnnotationException::optimizerPlusSaveComments(); 123 | } 124 | 125 | // Make sure that the IgnoreAnnotation annotation is loaded 126 | class_exists(IgnoreAnnotation::class); 127 | 128 | $this->parser = $parser ?: new DocParser(); 129 | 130 | $this->preParser = new DocParser(); 131 | 132 | $this->preParser->setImports(self::$globalImports); 133 | $this->preParser->setIgnoreNotImportedAnnotations(true); 134 | $this->preParser->setIgnoredAnnotationNames(self::$globalIgnoredNames); 135 | 136 | $this->phpParser = new PhpParser(); 137 | } 138 | 139 | /** 140 | * {@inheritDoc} 141 | */ 142 | public function getClassAnnotations(ReflectionClass $class) 143 | { 144 | $this->parser->setTarget(Target::TARGET_CLASS); 145 | $this->parser->setImports($this->getImports($class)); 146 | $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); 147 | $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 148 | 149 | return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName()); 150 | } 151 | 152 | /** 153 | * {@inheritDoc} 154 | */ 155 | public function getClassAnnotation(ReflectionClass $class, $annotationName) 156 | { 157 | $annotations = $this->getClassAnnotations($class); 158 | 159 | foreach ($annotations as $annotation) { 160 | if ($annotation instanceof $annotationName) { 161 | return $annotation; 162 | } 163 | } 164 | 165 | return null; 166 | } 167 | 168 | /** 169 | * {@inheritDoc} 170 | */ 171 | public function getPropertyAnnotations(ReflectionProperty $property) 172 | { 173 | $class = $property->getDeclaringClass(); 174 | $context = 'property ' . $class->getName() . '::$' . $property->getName(); 175 | 176 | $this->parser->setTarget(Target::TARGET_PROPERTY); 177 | $this->parser->setImports($this->getPropertyImports($property)); 178 | $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); 179 | $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 180 | 181 | return $this->parser->parse($property->getDocComment(), $context); 182 | } 183 | 184 | /** 185 | * {@inheritDoc} 186 | */ 187 | public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) 188 | { 189 | $annotations = $this->getPropertyAnnotations($property); 190 | 191 | foreach ($annotations as $annotation) { 192 | if ($annotation instanceof $annotationName) { 193 | return $annotation; 194 | } 195 | } 196 | 197 | return null; 198 | } 199 | 200 | /** 201 | * {@inheritDoc} 202 | */ 203 | public function getMethodAnnotations(ReflectionMethod $method) 204 | { 205 | $class = $method->getDeclaringClass(); 206 | $context = 'method ' . $class->getName() . '::' . $method->getName() . '()'; 207 | 208 | $this->parser->setTarget(Target::TARGET_METHOD); 209 | $this->parser->setImports($this->getMethodImports($method)); 210 | $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); 211 | $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 212 | 213 | return $this->parser->parse($method->getDocComment(), $context); 214 | } 215 | 216 | /** 217 | * {@inheritDoc} 218 | */ 219 | public function getMethodAnnotation(ReflectionMethod $method, $annotationName) 220 | { 221 | $annotations = $this->getMethodAnnotations($method); 222 | 223 | foreach ($annotations as $annotation) { 224 | if ($annotation instanceof $annotationName) { 225 | return $annotation; 226 | } 227 | } 228 | 229 | return null; 230 | } 231 | 232 | /** 233 | * Gets the annotations applied to a function. 234 | * 235 | * @phpstan-return list An array of Annotations. 236 | */ 237 | public function getFunctionAnnotations(ReflectionFunction $function): array 238 | { 239 | $context = 'function ' . $function->getName(); 240 | 241 | $this->parser->setTarget(Target::TARGET_FUNCTION); 242 | $this->parser->setImports($this->getImports($function)); 243 | $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($function)); 244 | $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 245 | 246 | return $this->parser->parse($function->getDocComment(), $context); 247 | } 248 | 249 | /** 250 | * Gets a function annotation. 251 | * 252 | * @return object|null The Annotation or NULL, if the requested annotation does not exist. 253 | */ 254 | public function getFunctionAnnotation(ReflectionFunction $function, string $annotationName) 255 | { 256 | $annotations = $this->getFunctionAnnotations($function); 257 | 258 | foreach ($annotations as $annotation) { 259 | if ($annotation instanceof $annotationName) { 260 | return $annotation; 261 | } 262 | } 263 | 264 | return null; 265 | } 266 | 267 | /** 268 | * Returns the ignored annotations for the given class or function. 269 | * 270 | * @param ReflectionClass|ReflectionFunction $reflection 271 | * 272 | * @return array 273 | */ 274 | private function getIgnoredAnnotationNames($reflection): array 275 | { 276 | $type = $reflection instanceof ReflectionClass ? 'class' : 'function'; 277 | $name = $reflection->getName(); 278 | 279 | if (isset($this->ignoredAnnotationNames[$type][$name])) { 280 | return $this->ignoredAnnotationNames[$type][$name]; 281 | } 282 | 283 | $this->collectParsingMetadata($reflection); 284 | 285 | return $this->ignoredAnnotationNames[$type][$name]; 286 | } 287 | 288 | /** 289 | * Retrieves imports for a class or a function. 290 | * 291 | * @param ReflectionClass|ReflectionFunction $reflection 292 | * 293 | * @return array 294 | */ 295 | private function getImports($reflection): array 296 | { 297 | $type = $reflection instanceof ReflectionClass ? 'class' : 'function'; 298 | $name = $reflection->getName(); 299 | 300 | if (isset($this->imports[$type][$name])) { 301 | return $this->imports[$type][$name]; 302 | } 303 | 304 | $this->collectParsingMetadata($reflection); 305 | 306 | return $this->imports[$type][$name]; 307 | } 308 | 309 | /** 310 | * Retrieves imports for methods. 311 | * 312 | * @return array 313 | */ 314 | private function getMethodImports(ReflectionMethod $method) 315 | { 316 | $class = $method->getDeclaringClass(); 317 | $classImports = $this->getImports($class); 318 | 319 | $traitImports = []; 320 | 321 | foreach ($class->getTraits() as $trait) { 322 | if ( 323 | ! $trait->hasMethod($method->getName()) 324 | || $trait->getFileName() !== $method->getFileName() 325 | ) { 326 | continue; 327 | } 328 | 329 | $traitImports = array_merge($traitImports, $this->phpParser->parseUseStatements($trait)); 330 | } 331 | 332 | return array_merge($classImports, $traitImports); 333 | } 334 | 335 | /** 336 | * Retrieves imports for properties. 337 | * 338 | * @return array 339 | */ 340 | private function getPropertyImports(ReflectionProperty $property) 341 | { 342 | $class = $property->getDeclaringClass(); 343 | $classImports = $this->getImports($class); 344 | 345 | $traitImports = []; 346 | 347 | foreach ($class->getTraits() as $trait) { 348 | if (! $trait->hasProperty($property->getName())) { 349 | continue; 350 | } 351 | 352 | $traitImports = array_merge($traitImports, $this->phpParser->parseUseStatements($trait)); 353 | } 354 | 355 | return array_merge($classImports, $traitImports); 356 | } 357 | 358 | /** 359 | * Collects parsing metadata for a given class or function. 360 | * 361 | * @param ReflectionClass|ReflectionFunction $reflection 362 | */ 363 | private function collectParsingMetadata($reflection): void 364 | { 365 | $type = $reflection instanceof ReflectionClass ? 'class' : 'function'; 366 | $name = $reflection->getName(); 367 | 368 | $ignoredAnnotationNames = self::$globalIgnoredNames; 369 | $annotations = $this->preParser->parse($reflection->getDocComment(), $type . ' ' . $name); 370 | 371 | foreach ($annotations as $annotation) { 372 | if (! ($annotation instanceof IgnoreAnnotation)) { 373 | continue; 374 | } 375 | 376 | foreach ($annotation->names as $annot) { 377 | $ignoredAnnotationNames[$annot] = true; 378 | } 379 | } 380 | 381 | $this->imports[$type][$name] = array_merge( 382 | self::$globalImports, 383 | $this->phpParser->parseUseStatements($reflection), 384 | [ 385 | '__NAMESPACE__' => $reflection->getNamespaceName(), 386 | 'self' => $name, 387 | ] 388 | ); 389 | 390 | $this->ignoredAnnotationNames[$type][$name] = $ignoredAnnotationNames; 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/AnnotationRegistry.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class DocLexer extends AbstractLexer 22 | { 23 | public const T_NONE = 1; 24 | public const T_INTEGER = 2; 25 | public const T_STRING = 3; 26 | public const T_FLOAT = 4; 27 | 28 | // All tokens that are also identifiers should be >= 100 29 | public const T_IDENTIFIER = 100; 30 | public const T_AT = 101; 31 | public const T_CLOSE_CURLY_BRACES = 102; 32 | public const T_CLOSE_PARENTHESIS = 103; 33 | public const T_COMMA = 104; 34 | public const T_EQUALS = 105; 35 | public const T_FALSE = 106; 36 | public const T_NAMESPACE_SEPARATOR = 107; 37 | public const T_OPEN_CURLY_BRACES = 108; 38 | public const T_OPEN_PARENTHESIS = 109; 39 | public const T_TRUE = 110; 40 | public const T_NULL = 111; 41 | public const T_COLON = 112; 42 | public const T_MINUS = 113; 43 | 44 | /** @var array */ 45 | protected $noCase = [ 46 | '@' => self::T_AT, 47 | ',' => self::T_COMMA, 48 | '(' => self::T_OPEN_PARENTHESIS, 49 | ')' => self::T_CLOSE_PARENTHESIS, 50 | '{' => self::T_OPEN_CURLY_BRACES, 51 | '}' => self::T_CLOSE_CURLY_BRACES, 52 | '=' => self::T_EQUALS, 53 | ':' => self::T_COLON, 54 | '-' => self::T_MINUS, 55 | '\\' => self::T_NAMESPACE_SEPARATOR, 56 | ]; 57 | 58 | /** @var array */ 59 | protected $withCase = [ 60 | 'true' => self::T_TRUE, 61 | 'false' => self::T_FALSE, 62 | 'null' => self::T_NULL, 63 | ]; 64 | 65 | /** 66 | * Whether the next token starts immediately, or if there were 67 | * non-captured symbols before that 68 | */ 69 | public function nextTokenIsAdjacent(): bool 70 | { 71 | return $this->token === null 72 | || ($this->lookahead !== null 73 | && ($this->lookahead->position - $this->token->position) === strlen($this->token->value)); 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | protected function getCatchablePatterns() 80 | { 81 | return [ 82 | '[a-z_\\\][a-z0-9_\:\\\]*[a-z_][a-z0-9_]*', 83 | '(?:[+-]?[0-9]+(?:[\.][0-9]+)*)(?:[eE][+-]?[0-9]+)?', 84 | '"(?:""|[^"])*+"', 85 | ]; 86 | } 87 | 88 | /** 89 | * {@inheritDoc} 90 | */ 91 | protected function getNonCatchablePatterns() 92 | { 93 | return ['\s+', '\*+', '(.)']; 94 | } 95 | 96 | /** 97 | * {@inheritDoc} 98 | */ 99 | protected function getType(&$value) 100 | { 101 | $type = self::T_NONE; 102 | 103 | if ($value[0] === '"') { 104 | $value = str_replace('""', '"', substr($value, 1, strlen($value) - 2)); 105 | 106 | return self::T_STRING; 107 | } 108 | 109 | if (isset($this->noCase[$value])) { 110 | return $this->noCase[$value]; 111 | } 112 | 113 | if ($value[0] === '_' || $value[0] === '\\' || ctype_alpha($value[0])) { 114 | return self::T_IDENTIFIER; 115 | } 116 | 117 | $lowerValue = strtolower($value); 118 | 119 | if (isset($this->withCase[$lowerValue])) { 120 | return $this->withCase[$lowerValue]; 121 | } 122 | 123 | // Checking numeric value 124 | if (is_numeric($value)) { 125 | return strpos($value, '.') !== false || stripos($value, 'e') !== false 126 | ? self::T_FLOAT : self::T_INTEGER; 127 | } 128 | 129 | return $type; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/DocParser.php: -------------------------------------------------------------------------------- 1 | , named_arguments?: array} 54 | */ 55 | final class DocParser 56 | { 57 | /** 58 | * An array of all valid tokens for a class name. 59 | * 60 | * @phpstan-var list 61 | */ 62 | private static $classIdentifiers = [ 63 | DocLexer::T_IDENTIFIER, 64 | DocLexer::T_TRUE, 65 | DocLexer::T_FALSE, 66 | DocLexer::T_NULL, 67 | ]; 68 | 69 | /** 70 | * The lexer. 71 | * 72 | * @var DocLexer 73 | */ 74 | private $lexer; 75 | 76 | /** 77 | * Current target context. 78 | * 79 | * @var int 80 | */ 81 | private $target; 82 | 83 | /** 84 | * Doc parser used to collect annotation target. 85 | * 86 | * @var DocParser 87 | */ 88 | private static $metadataParser; 89 | 90 | /** 91 | * Flag to control if the current annotation is nested or not. 92 | * 93 | * @var bool 94 | */ 95 | private $isNestedAnnotation = false; 96 | 97 | /** 98 | * Hashmap containing all use-statements that are to be used when parsing 99 | * the given doc block. 100 | * 101 | * @var array 102 | */ 103 | private $imports = []; 104 | 105 | /** 106 | * This hashmap is used internally to cache results of class_exists() 107 | * look-ups. 108 | * 109 | * @var array 110 | */ 111 | private $classExists = []; 112 | 113 | /** 114 | * Whether annotations that have not been imported should be ignored. 115 | * 116 | * @var bool 117 | */ 118 | private $ignoreNotImportedAnnotations = false; 119 | 120 | /** 121 | * An array of default namespaces if operating in simple mode. 122 | * 123 | * @var string[] 124 | */ 125 | private $namespaces = []; 126 | 127 | /** 128 | * A list with annotations that are not causing exceptions when not resolved to an annotation class. 129 | * 130 | * The names must be the raw names as used in the class, not the fully qualified 131 | * 132 | * @var bool[] indexed by annotation name 133 | */ 134 | private $ignoredAnnotationNames = []; 135 | 136 | /** 137 | * A list with annotations in namespaced format 138 | * that are not causing exceptions when not resolved to an annotation class. 139 | * 140 | * @var bool[] indexed by namespace name 141 | */ 142 | private $ignoredAnnotationNamespaces = []; 143 | 144 | /** @var string */ 145 | private $context = ''; 146 | 147 | /** 148 | * Hash-map for caching annotation metadata. 149 | * 150 | * @var array 151 | */ 152 | private static $annotationMetadata = [ 153 | Annotation\Target::class => [ 154 | 'is_annotation' => true, 155 | 'has_constructor' => true, 156 | 'has_named_argument_constructor' => false, 157 | 'properties' => [], 158 | 'targets_literal' => 'ANNOTATION_CLASS', 159 | 'targets' => Target::TARGET_CLASS, 160 | 'default_property' => 'value', 161 | 'attribute_types' => [ 162 | 'value' => [ 163 | 'required' => false, 164 | 'type' => 'array', 165 | 'array_type' => 'string', 166 | 'value' => 'array', 167 | ], 168 | ], 169 | ], 170 | Annotation\Attribute::class => [ 171 | 'is_annotation' => true, 172 | 'has_constructor' => false, 173 | 'has_named_argument_constructor' => false, 174 | 'targets_literal' => 'ANNOTATION_ANNOTATION', 175 | 'targets' => Target::TARGET_ANNOTATION, 176 | 'default_property' => 'name', 177 | 'properties' => [ 178 | 'name' => 'name', 179 | 'type' => 'type', 180 | 'required' => 'required', 181 | ], 182 | 'attribute_types' => [ 183 | 'value' => [ 184 | 'required' => true, 185 | 'type' => 'string', 186 | 'value' => 'string', 187 | ], 188 | 'type' => [ 189 | 'required' => true, 190 | 'type' => 'string', 191 | 'value' => 'string', 192 | ], 193 | 'required' => [ 194 | 'required' => false, 195 | 'type' => 'boolean', 196 | 'value' => 'boolean', 197 | ], 198 | ], 199 | ], 200 | Annotation\Attributes::class => [ 201 | 'is_annotation' => true, 202 | 'has_constructor' => false, 203 | 'has_named_argument_constructor' => false, 204 | 'targets_literal' => 'ANNOTATION_CLASS', 205 | 'targets' => Target::TARGET_CLASS, 206 | 'default_property' => 'value', 207 | 'properties' => ['value' => 'value'], 208 | 'attribute_types' => [ 209 | 'value' => [ 210 | 'type' => 'array', 211 | 'required' => true, 212 | 'array_type' => Annotation\Attribute::class, 213 | 'value' => 'array<' . Annotation\Attribute::class . '>', 214 | ], 215 | ], 216 | ], 217 | Annotation\Enum::class => [ 218 | 'is_annotation' => true, 219 | 'has_constructor' => true, 220 | 'has_named_argument_constructor' => false, 221 | 'targets_literal' => 'ANNOTATION_PROPERTY', 222 | 'targets' => Target::TARGET_PROPERTY, 223 | 'default_property' => 'value', 224 | 'properties' => ['value' => 'value'], 225 | 'attribute_types' => [ 226 | 'value' => [ 227 | 'type' => 'array', 228 | 'required' => true, 229 | ], 230 | 'literal' => [ 231 | 'type' => 'array', 232 | 'required' => false, 233 | ], 234 | ], 235 | ], 236 | Annotation\NamedArgumentConstructor::class => [ 237 | 'is_annotation' => true, 238 | 'has_constructor' => false, 239 | 'has_named_argument_constructor' => false, 240 | 'targets_literal' => 'ANNOTATION_CLASS', 241 | 'targets' => Target::TARGET_CLASS, 242 | 'default_property' => null, 243 | 'properties' => [], 244 | 'attribute_types' => [], 245 | ], 246 | ]; 247 | 248 | /** 249 | * Hash-map for handle types declaration. 250 | * 251 | * @var array 252 | */ 253 | private static $typeMap = [ 254 | 'float' => 'double', 255 | 'bool' => 'boolean', 256 | // allow uppercase Boolean in honor of George Boole 257 | 'Boolean' => 'boolean', 258 | 'int' => 'integer', 259 | ]; 260 | 261 | /** 262 | * Constructs a new DocParser. 263 | */ 264 | public function __construct() 265 | { 266 | $this->lexer = new DocLexer(); 267 | } 268 | 269 | /** 270 | * Sets the annotation names that are ignored during the parsing process. 271 | * 272 | * The names are supposed to be the raw names as used in the class, not the 273 | * fully qualified class names. 274 | * 275 | * @param bool[] $names indexed by annotation name 276 | * 277 | * @return void 278 | */ 279 | public function setIgnoredAnnotationNames(array $names) 280 | { 281 | $this->ignoredAnnotationNames = $names; 282 | } 283 | 284 | /** 285 | * Sets the annotation namespaces that are ignored during the parsing process. 286 | * 287 | * @param bool[] $ignoredAnnotationNamespaces indexed by annotation namespace name 288 | * 289 | * @return void 290 | */ 291 | public function setIgnoredAnnotationNamespaces(array $ignoredAnnotationNamespaces) 292 | { 293 | $this->ignoredAnnotationNamespaces = $ignoredAnnotationNamespaces; 294 | } 295 | 296 | /** 297 | * Sets ignore on not-imported annotations. 298 | * 299 | * @return void 300 | */ 301 | public function setIgnoreNotImportedAnnotations(bool $bool) 302 | { 303 | $this->ignoreNotImportedAnnotations = $bool; 304 | } 305 | 306 | /** 307 | * Sets the default namespaces. 308 | * 309 | * @return void 310 | * 311 | * @throws RuntimeException 312 | */ 313 | public function addNamespace(string $namespace) 314 | { 315 | if ($this->imports) { 316 | throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); 317 | } 318 | 319 | $this->namespaces[] = $namespace; 320 | } 321 | 322 | /** 323 | * Sets the imports. 324 | * 325 | * @param array $imports 326 | * 327 | * @return void 328 | * 329 | * @throws RuntimeException 330 | */ 331 | public function setImports(array $imports) 332 | { 333 | if ($this->namespaces) { 334 | throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); 335 | } 336 | 337 | $this->imports = $imports; 338 | } 339 | 340 | /** 341 | * Sets current target context as bitmask. 342 | * 343 | * @return void 344 | */ 345 | public function setTarget(int $target) 346 | { 347 | $this->target = $target; 348 | } 349 | 350 | /** 351 | * Parses the given docblock string for annotations. 352 | * 353 | * @phpstan-return list Array of annotations. If no annotations are found, an empty array is returned. 354 | * 355 | * @throws AnnotationException 356 | * @throws ReflectionException 357 | */ 358 | public function parse(string $input, string $context = '') 359 | { 360 | $pos = $this->findInitialTokenPosition($input); 361 | if ($pos === null) { 362 | return []; 363 | } 364 | 365 | $this->context = $context; 366 | 367 | $this->lexer->setInput(trim(substr($input, $pos), '* /')); 368 | $this->lexer->moveNext(); 369 | 370 | return $this->Annotations(); 371 | } 372 | 373 | /** 374 | * Finds the first valid annotation 375 | */ 376 | private function findInitialTokenPosition(string $input): ?int 377 | { 378 | $pos = 0; 379 | 380 | // search for first valid annotation 381 | while (($pos = strpos($input, '@', $pos)) !== false) { 382 | $preceding = substr($input, $pos - 1, 1); 383 | 384 | // if the @ is preceded by a space, a tab or * it is valid 385 | if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") { 386 | return $pos; 387 | } 388 | 389 | $pos++; 390 | } 391 | 392 | return null; 393 | } 394 | 395 | /** 396 | * Attempts to match the given token with the current lookahead token. 397 | * If they match, updates the lookahead token; otherwise raises a syntax error. 398 | * 399 | * @param int $token Type of token. 400 | * 401 | * @return bool True if tokens match; false otherwise. 402 | * 403 | * @throws AnnotationException 404 | */ 405 | private function match(int $token): bool 406 | { 407 | if (! $this->lexer->isNextToken($token)) { 408 | throw $this->syntaxError($this->lexer->getLiteral($token)); 409 | } 410 | 411 | return $this->lexer->moveNext(); 412 | } 413 | 414 | /** 415 | * Attempts to match the current lookahead token with any of the given tokens. 416 | * 417 | * If any of them matches, this method updates the lookahead token; otherwise 418 | * a syntax error is raised. 419 | * 420 | * @phpstan-param list $tokens 421 | * 422 | * @throws AnnotationException 423 | */ 424 | private function matchAny(array $tokens): bool 425 | { 426 | if (! $this->lexer->isNextTokenAny($tokens)) { 427 | throw $this->syntaxError(implode(' or ', array_map([$this->lexer, 'getLiteral'], $tokens))); 428 | } 429 | 430 | return $this->lexer->moveNext(); 431 | } 432 | 433 | /** 434 | * Generates a new syntax error. 435 | * 436 | * @param string $expected Expected string. 437 | * @param mixed[]|null $token Optional token. 438 | */ 439 | private function syntaxError(string $expected, ?array $token = null): AnnotationException 440 | { 441 | if ($token === null) { 442 | $token = $this->lexer->lookahead; 443 | } 444 | 445 | $message = sprintf('Expected %s, got ', $expected); 446 | $message .= $this->lexer->lookahead === null 447 | ? 'end of string' 448 | : sprintf("'%s' at position %s", $token->value, $token->position); 449 | 450 | if (strlen($this->context)) { 451 | $message .= ' in ' . $this->context; 452 | } 453 | 454 | $message .= '.'; 455 | 456 | return AnnotationException::syntaxError($message); 457 | } 458 | 459 | /** 460 | * Attempts to check if a class exists or not. This never goes through the PHP autoloading mechanism 461 | * but uses the {@link AnnotationRegistry} to load classes. 462 | * 463 | * @param class-string $fqcn 464 | */ 465 | private function classExists(string $fqcn): bool 466 | { 467 | if (isset($this->classExists[$fqcn])) { 468 | return $this->classExists[$fqcn]; 469 | } 470 | 471 | // first check if the class already exists, maybe loaded through another AnnotationReader 472 | if (class_exists($fqcn, false)) { 473 | return $this->classExists[$fqcn] = true; 474 | } 475 | 476 | // final check, does this class exist? 477 | return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn); 478 | } 479 | 480 | /** 481 | * Collects parsing metadata for a given annotation class 482 | * 483 | * @param class-string $name The annotation name 484 | * 485 | * @throws AnnotationException 486 | * @throws ReflectionException 487 | */ 488 | private function collectAnnotationMetadata(string $name): void 489 | { 490 | if (self::$metadataParser === null) { 491 | self::$metadataParser = new self(); 492 | 493 | self::$metadataParser->setIgnoreNotImportedAnnotations(true); 494 | self::$metadataParser->setIgnoredAnnotationNames($this->ignoredAnnotationNames); 495 | self::$metadataParser->setImports([ 496 | 'enum' => Enum::class, 497 | 'target' => Target::class, 498 | 'attribute' => Attribute::class, 499 | 'attributes' => Attributes::class, 500 | 'namedargumentconstructor' => NamedArgumentConstructor::class, 501 | ]); 502 | 503 | // Make sure that annotations from metadata are loaded 504 | class_exists(Enum::class); 505 | class_exists(Target::class); 506 | class_exists(Attribute::class); 507 | class_exists(Attributes::class); 508 | class_exists(NamedArgumentConstructor::class); 509 | } 510 | 511 | $class = new ReflectionClass($name); 512 | $docComment = $class->getDocComment(); 513 | 514 | // Sets default values for annotation metadata 515 | $constructor = $class->getConstructor(); 516 | $metadata = [ 517 | 'default_property' => null, 518 | 'has_constructor' => $constructor !== null && $constructor->getNumberOfParameters() > 0, 519 | 'constructor_args' => [], 520 | 'properties' => [], 521 | 'property_types' => [], 522 | 'attribute_types' => [], 523 | 'targets_literal' => null, 524 | 'targets' => Target::TARGET_ALL, 525 | 'is_annotation' => strpos($docComment, '@Annotation') !== false, 526 | ]; 527 | 528 | $metadata['has_named_argument_constructor'] = false; 529 | 530 | // verify that the class is really meant to be an annotation 531 | if ($metadata['is_annotation']) { 532 | self::$metadataParser->setTarget(Target::TARGET_CLASS); 533 | 534 | foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) { 535 | if ($annotation instanceof Target) { 536 | $metadata['targets'] = $annotation->targets; 537 | $metadata['targets_literal'] = $annotation->literal; 538 | 539 | continue; 540 | } 541 | 542 | if ($annotation instanceof NamedArgumentConstructor) { 543 | $metadata['has_named_argument_constructor'] = $metadata['has_constructor']; 544 | if ($metadata['has_named_argument_constructor']) { 545 | // choose the first argument as the default property 546 | $metadata['default_property'] = $constructor->getParameters()[0]->getName(); 547 | } 548 | } 549 | 550 | if (! ($annotation instanceof Attributes)) { 551 | continue; 552 | } 553 | 554 | foreach ($annotation->value as $attribute) { 555 | $this->collectAttributeTypeMetadata($metadata, $attribute); 556 | } 557 | } 558 | 559 | // if not has a constructor will inject values into public properties 560 | if ($metadata['has_constructor'] === false) { 561 | // collect all public properties 562 | foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { 563 | $metadata['properties'][$property->name] = $property->name; 564 | 565 | $propertyComment = $property->getDocComment(); 566 | if ($propertyComment === false) { 567 | continue; 568 | } 569 | 570 | $attribute = new Attribute(); 571 | 572 | $attribute->required = (strpos($propertyComment, '@Required') !== false); 573 | $attribute->name = $property->name; 574 | $attribute->type = (strpos($propertyComment, '@var') !== false && 575 | preg_match('/@var\s+([^\s]+)/', $propertyComment, $matches)) 576 | ? $matches[1] 577 | : 'mixed'; 578 | 579 | $this->collectAttributeTypeMetadata($metadata, $attribute); 580 | 581 | // checks if the property has @Enum 582 | if (strpos($propertyComment, '@Enum') === false) { 583 | continue; 584 | } 585 | 586 | $context = 'property ' . $class->name . '::$' . $property->name; 587 | 588 | self::$metadataParser->setTarget(Target::TARGET_PROPERTY); 589 | 590 | foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) { 591 | if (! $annotation instanceof Enum) { 592 | continue; 593 | } 594 | 595 | $metadata['enum'][$property->name]['value'] = $annotation->value; 596 | $metadata['enum'][$property->name]['literal'] = (! empty($annotation->literal)) 597 | ? $annotation->literal 598 | : $annotation->value; 599 | } 600 | } 601 | 602 | // choose the first property as default property 603 | $metadata['default_property'] = reset($metadata['properties']); 604 | } elseif ($metadata['has_named_argument_constructor']) { 605 | foreach ($constructor->getParameters() as $parameter) { 606 | if ($parameter->isVariadic()) { 607 | break; 608 | } 609 | 610 | $metadata['constructor_args'][$parameter->getName()] = [ 611 | 'position' => $parameter->getPosition(), 612 | 'default' => $parameter->isOptional() ? $parameter->getDefaultValue() : null, 613 | ]; 614 | } 615 | } 616 | } 617 | 618 | self::$annotationMetadata[$name] = $metadata; 619 | } 620 | 621 | /** 622 | * Collects parsing metadata for a given attribute. 623 | * 624 | * @param mixed[] $metadata 625 | */ 626 | private function collectAttributeTypeMetadata(array &$metadata, Attribute $attribute): void 627 | { 628 | // handle internal type declaration 629 | $type = self::$typeMap[$attribute->type] ?? $attribute->type; 630 | 631 | // handle the case if the property type is mixed 632 | if ($type === 'mixed') { 633 | return; 634 | } 635 | 636 | // Evaluate type 637 | $pos = strpos($type, '<'); 638 | if ($pos !== false) { 639 | // Checks if the property has array 640 | $arrayType = substr($type, $pos + 1, -1); 641 | $type = 'array'; 642 | 643 | if (isset(self::$typeMap[$arrayType])) { 644 | $arrayType = self::$typeMap[$arrayType]; 645 | } 646 | 647 | $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; 648 | } else { 649 | // Checks if the property has type[] 650 | $pos = strrpos($type, '['); 651 | if ($pos !== false) { 652 | $arrayType = substr($type, 0, $pos); 653 | $type = 'array'; 654 | 655 | if (isset(self::$typeMap[$arrayType])) { 656 | $arrayType = self::$typeMap[$arrayType]; 657 | } 658 | 659 | $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; 660 | } 661 | } 662 | 663 | $metadata['attribute_types'][$attribute->name]['type'] = $type; 664 | $metadata['attribute_types'][$attribute->name]['value'] = $attribute->type; 665 | $metadata['attribute_types'][$attribute->name]['required'] = $attribute->required; 666 | } 667 | 668 | /** 669 | * Annotations ::= Annotation {[ "*" ]* [Annotation]}* 670 | * 671 | * @phpstan-return list 672 | * 673 | * @throws AnnotationException 674 | * @throws ReflectionException 675 | */ 676 | private function Annotations(): array 677 | { 678 | $annotations = []; 679 | 680 | while ($this->lexer->lookahead !== null) { 681 | if ($this->lexer->lookahead->type !== DocLexer::T_AT) { 682 | $this->lexer->moveNext(); 683 | continue; 684 | } 685 | 686 | // make sure the @ is preceded by non-catchable pattern 687 | if ( 688 | $this->lexer->token !== null && 689 | $this->lexer->lookahead->position === $this->lexer->token->position + strlen( 690 | $this->lexer->token->value 691 | ) 692 | ) { 693 | $this->lexer->moveNext(); 694 | continue; 695 | } 696 | 697 | // make sure the @ is followed by either a namespace separator, or 698 | // an identifier token 699 | $peek = $this->lexer->glimpse(); 700 | if ( 701 | ($peek === null) 702 | || ($peek->type !== DocLexer::T_NAMESPACE_SEPARATOR && ! in_array( 703 | $peek->type, 704 | self::$classIdentifiers, 705 | true 706 | )) 707 | || $peek->position !== $this->lexer->lookahead->position + 1 708 | ) { 709 | $this->lexer->moveNext(); 710 | continue; 711 | } 712 | 713 | $this->isNestedAnnotation = false; 714 | $annot = $this->Annotation(); 715 | if ($annot === false) { 716 | continue; 717 | } 718 | 719 | $annotations[] = $annot; 720 | } 721 | 722 | return $annotations; 723 | } 724 | 725 | /** 726 | * Annotation ::= "@" AnnotationName MethodCall 727 | * AnnotationName ::= QualifiedName | SimpleName 728 | * QualifiedName ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName 729 | * NameSpacePart ::= identifier | null | false | true 730 | * SimpleName ::= identifier | null | false | true 731 | * 732 | * @return object|false False if it is not a valid annotation. 733 | * 734 | * @throws AnnotationException 735 | * @throws ReflectionException 736 | */ 737 | private function Annotation() 738 | { 739 | $this->match(DocLexer::T_AT); 740 | 741 | // check if we have an annotation 742 | $name = $this->Identifier(); 743 | 744 | if ( 745 | $this->lexer->isNextToken(DocLexer::T_MINUS) 746 | && $this->lexer->nextTokenIsAdjacent() 747 | ) { 748 | // Annotations with dashes, such as "@foo-" or "@foo-bar", are to be discarded 749 | return false; 750 | } 751 | 752 | // only process names which are not fully qualified, yet 753 | // fully qualified names must start with a \ 754 | $originalName = $name; 755 | 756 | if ($name[0] !== '\\') { 757 | $pos = strpos($name, '\\'); 758 | $alias = ($pos === false) ? $name : substr($name, 0, $pos); 759 | $found = false; 760 | $loweredAlias = strtolower($alias); 761 | 762 | if ($this->namespaces) { 763 | foreach ($this->namespaces as $namespace) { 764 | if ($this->classExists($namespace . '\\' . $name)) { 765 | $name = $namespace . '\\' . $name; 766 | $found = true; 767 | break; 768 | } 769 | } 770 | } elseif (isset($this->imports[$loweredAlias])) { 771 | $namespace = ltrim($this->imports[$loweredAlias], '\\'); 772 | $name = ($pos !== false) 773 | ? $namespace . substr($name, $pos) 774 | : $namespace; 775 | $found = $this->classExists($name); 776 | } elseif ( 777 | ! isset($this->ignoredAnnotationNames[$name]) 778 | && isset($this->imports['__NAMESPACE__']) 779 | && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name) 780 | ) { 781 | $name = $this->imports['__NAMESPACE__'] . '\\' . $name; 782 | $found = true; 783 | } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) { 784 | $found = true; 785 | } 786 | 787 | if (! $found) { 788 | if ($this->isIgnoredAnnotation($name)) { 789 | return false; 790 | } 791 | 792 | throw AnnotationException::semanticalError(sprintf( 793 | <<<'EXCEPTION' 794 | The annotation "@%s" in %s was never imported. Did you maybe forget to add a "use" statement for this annotation? 795 | EXCEPTION 796 | , 797 | $name, 798 | $this->context 799 | )); 800 | } 801 | } 802 | 803 | $name = ltrim($name, '\\'); 804 | 805 | if (! $this->classExists($name)) { 806 | throw AnnotationException::semanticalError(sprintf( 807 | 'The annotation "@%s" in %s does not exist, or could not be auto-loaded.', 808 | $name, 809 | $this->context 810 | )); 811 | } 812 | 813 | // at this point, $name contains the fully qualified class name of the 814 | // annotation, and it is also guaranteed that this class exists, and 815 | // that it is loaded 816 | 817 | // collects the metadata annotation only if there is not yet 818 | if (! isset(self::$annotationMetadata[$name])) { 819 | $this->collectAnnotationMetadata($name); 820 | } 821 | 822 | // verify that the class is really meant to be an annotation and not just any ordinary class 823 | if (self::$annotationMetadata[$name]['is_annotation'] === false) { 824 | if ($this->isIgnoredAnnotation($originalName) || $this->isIgnoredAnnotation($name)) { 825 | return false; 826 | } 827 | 828 | throw AnnotationException::semanticalError(sprintf( 829 | <<<'EXCEPTION' 830 | The class "%s" is not annotated with @Annotation. 831 | Are you sure this class can be used as annotation? 832 | If so, then you need to add @Annotation to the _class_ doc comment of "%s". 833 | If it is indeed no annotation, then you need to add @IgnoreAnnotation("%s") to the _class_ doc comment of %s. 834 | EXCEPTION 835 | , 836 | $name, 837 | $name, 838 | $originalName, 839 | $this->context 840 | )); 841 | } 842 | 843 | //if target is nested annotation 844 | $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target; 845 | 846 | // Next will be nested 847 | $this->isNestedAnnotation = true; 848 | 849 | //if annotation does not support current target 850 | if ((self::$annotationMetadata[$name]['targets'] & $target) === 0 && $target) { 851 | throw AnnotationException::semanticalError( 852 | sprintf( 853 | <<<'EXCEPTION' 854 | Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s. 855 | EXCEPTION 856 | , 857 | $originalName, 858 | $this->context, 859 | self::$annotationMetadata[$name]['targets_literal'] 860 | ) 861 | ); 862 | } 863 | 864 | $arguments = $this->MethodCall(); 865 | $values = $this->resolvePositionalValues($arguments, $name); 866 | 867 | if (isset(self::$annotationMetadata[$name]['enum'])) { 868 | // checks all declared attributes 869 | foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) { 870 | // checks if the attribute is a valid enumerator 871 | if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) { 872 | throw AnnotationException::enumeratorError( 873 | $property, 874 | $name, 875 | $this->context, 876 | $enum['literal'], 877 | $values[$property] 878 | ); 879 | } 880 | } 881 | } 882 | 883 | // checks all declared attributes 884 | foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) { 885 | if ( 886 | $property === self::$annotationMetadata[$name]['default_property'] 887 | && ! isset($values[$property]) && isset($values['value']) 888 | ) { 889 | $property = 'value'; 890 | } 891 | 892 | // handle a not given attribute or null value 893 | if (! isset($values[$property])) { 894 | if ($type['required']) { 895 | throw AnnotationException::requiredError( 896 | $property, 897 | $originalName, 898 | $this->context, 899 | 'a(n) ' . $type['value'] 900 | ); 901 | } 902 | 903 | continue; 904 | } 905 | 906 | if ($type['type'] === 'array') { 907 | // handle the case of a single value 908 | if (! is_array($values[$property])) { 909 | $values[$property] = [$values[$property]]; 910 | } 911 | 912 | // checks if the attribute has array type declaration, such as "array" 913 | if (isset($type['array_type'])) { 914 | foreach ($values[$property] as $item) { 915 | if (gettype($item) !== $type['array_type'] && ! $item instanceof $type['array_type']) { 916 | throw AnnotationException::attributeTypeError( 917 | $property, 918 | $originalName, 919 | $this->context, 920 | 'either a(n) ' . $type['array_type'] . ', or an array of ' . $type['array_type'] . 's', 921 | $item 922 | ); 923 | } 924 | } 925 | } 926 | } elseif (gettype($values[$property]) !== $type['type'] && ! $values[$property] instanceof $type['type']) { 927 | throw AnnotationException::attributeTypeError( 928 | $property, 929 | $originalName, 930 | $this->context, 931 | 'a(n) ' . $type['value'], 932 | $values[$property] 933 | ); 934 | } 935 | } 936 | 937 | if (self::$annotationMetadata[$name]['has_named_argument_constructor']) { 938 | if (PHP_VERSION_ID >= 80000) { 939 | foreach ($values as $property => $value) { 940 | if (! isset(self::$annotationMetadata[$name]['constructor_args'][$property])) { 941 | throw AnnotationException::creationError(sprintf( 942 | <<<'EXCEPTION' 943 | The annotation @%s declared on %s does not have a property named "%s" 944 | that can be set through its named arguments constructor. 945 | Available named arguments: %s 946 | EXCEPTION 947 | , 948 | $originalName, 949 | $this->context, 950 | $property, 951 | implode(', ', array_keys(self::$annotationMetadata[$name]['constructor_args'])) 952 | )); 953 | } 954 | } 955 | 956 | return $this->instantiateAnnotiation($originalName, $this->context, $name, $values); 957 | } 958 | 959 | $positionalValues = []; 960 | foreach (self::$annotationMetadata[$name]['constructor_args'] as $property => $parameter) { 961 | $positionalValues[$parameter['position']] = $parameter['default']; 962 | } 963 | 964 | foreach ($values as $property => $value) { 965 | if (! isset(self::$annotationMetadata[$name]['constructor_args'][$property])) { 966 | throw AnnotationException::creationError(sprintf( 967 | <<<'EXCEPTION' 968 | The annotation @%s declared on %s does not have a property named "%s" 969 | that can be set through its named arguments constructor. 970 | Available named arguments: %s 971 | EXCEPTION 972 | , 973 | $originalName, 974 | $this->context, 975 | $property, 976 | implode(', ', array_keys(self::$annotationMetadata[$name]['constructor_args'])) 977 | )); 978 | } 979 | 980 | $positionalValues[self::$annotationMetadata[$name]['constructor_args'][$property]['position']] = $value; 981 | } 982 | 983 | return $this->instantiateAnnotiation($originalName, $this->context, $name, $positionalValues); 984 | } 985 | 986 | // check if the annotation expects values via the constructor, 987 | // or directly injected into public properties 988 | if (self::$annotationMetadata[$name]['has_constructor'] === true) { 989 | return $this->instantiateAnnotiation($originalName, $this->context, $name, [$values]); 990 | } 991 | 992 | $instance = $this->instantiateAnnotiation($originalName, $this->context, $name, []); 993 | 994 | foreach ($values as $property => $value) { 995 | if (! isset(self::$annotationMetadata[$name]['properties'][$property])) { 996 | if ($property !== 'value') { 997 | throw AnnotationException::creationError(sprintf( 998 | <<<'EXCEPTION' 999 | The annotation @%s declared on %s does not have a property named "%s". 1000 | Available properties: %s 1001 | EXCEPTION 1002 | , 1003 | $originalName, 1004 | $this->context, 1005 | $property, 1006 | implode(', ', self::$annotationMetadata[$name]['properties']) 1007 | )); 1008 | } 1009 | 1010 | // handle the case if the property has no annotations 1011 | $property = self::$annotationMetadata[$name]['default_property']; 1012 | if (! $property) { 1013 | throw AnnotationException::creationError(sprintf( 1014 | 'The annotation @%s declared on %s does not accept any values, but got %s.', 1015 | $originalName, 1016 | $this->context, 1017 | json_encode($values) 1018 | )); 1019 | } 1020 | } 1021 | 1022 | $instance->{$property} = $value; 1023 | } 1024 | 1025 | return $instance; 1026 | } 1027 | 1028 | /** 1029 | * MethodCall ::= ["(" [Values] ")"] 1030 | * 1031 | * @psalm-return Arguments 1032 | * 1033 | * @throws AnnotationException 1034 | * @throws ReflectionException 1035 | */ 1036 | private function MethodCall(): array 1037 | { 1038 | $values = []; 1039 | 1040 | if (! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) { 1041 | return $values; 1042 | } 1043 | 1044 | $this->match(DocLexer::T_OPEN_PARENTHESIS); 1045 | 1046 | if (! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { 1047 | $values = $this->Values(); 1048 | } 1049 | 1050 | $this->match(DocLexer::T_CLOSE_PARENTHESIS); 1051 | 1052 | return $values; 1053 | } 1054 | 1055 | /** 1056 | * Values ::= Array | Value {"," Value}* [","] 1057 | * 1058 | * @psalm-return Arguments 1059 | * 1060 | * @throws AnnotationException 1061 | * @throws ReflectionException 1062 | */ 1063 | private function Values(): array 1064 | { 1065 | $values = [$this->Value()]; 1066 | 1067 | while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { 1068 | $this->match(DocLexer::T_COMMA); 1069 | 1070 | if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { 1071 | break; 1072 | } 1073 | 1074 | $token = $this->lexer->lookahead; 1075 | $value = $this->Value(); 1076 | 1077 | $values[] = $value; 1078 | } 1079 | 1080 | $namedArguments = []; 1081 | $positionalArguments = []; 1082 | foreach ($values as $k => $value) { 1083 | if (is_object($value) && $value instanceof stdClass) { 1084 | $namedArguments[$value->name] = $value->value; 1085 | } else { 1086 | $positionalArguments[$k] = $value; 1087 | } 1088 | } 1089 | 1090 | return ['named_arguments' => $namedArguments, 'positional_arguments' => $positionalArguments]; 1091 | } 1092 | 1093 | /** 1094 | * Constant ::= integer | string | float | boolean 1095 | * 1096 | * @return mixed 1097 | * 1098 | * @throws AnnotationException 1099 | */ 1100 | private function Constant() 1101 | { 1102 | $identifier = $this->Identifier(); 1103 | 1104 | if (! defined($identifier) && strpos($identifier, '::') !== false && $identifier[0] !== '\\') { 1105 | [$className, $const] = explode('::', $identifier); 1106 | 1107 | $pos = strpos($className, '\\'); 1108 | $alias = ($pos === false) ? $className : substr($className, 0, $pos); 1109 | $found = false; 1110 | $loweredAlias = strtolower($alias); 1111 | 1112 | switch (true) { 1113 | case ! empty($this->namespaces): 1114 | foreach ($this->namespaces as $ns) { 1115 | if (class_exists($ns . '\\' . $className) || interface_exists($ns . '\\' . $className)) { 1116 | $className = $ns . '\\' . $className; 1117 | $found = true; 1118 | break; 1119 | } 1120 | } 1121 | 1122 | break; 1123 | 1124 | case isset($this->imports[$loweredAlias]): 1125 | $found = true; 1126 | $className = ($pos !== false) 1127 | ? $this->imports[$loweredAlias] . substr($className, $pos) 1128 | : $this->imports[$loweredAlias]; 1129 | break; 1130 | 1131 | default: 1132 | if (isset($this->imports['__NAMESPACE__'])) { 1133 | $ns = $this->imports['__NAMESPACE__']; 1134 | 1135 | if (class_exists($ns . '\\' . $className) || interface_exists($ns . '\\' . $className)) { 1136 | $className = $ns . '\\' . $className; 1137 | $found = true; 1138 | } 1139 | } 1140 | 1141 | break; 1142 | } 1143 | 1144 | if ($found) { 1145 | $identifier = $className . '::' . $const; 1146 | } 1147 | } 1148 | 1149 | /** 1150 | * Checks if identifier ends with ::class and remove the leading backslash if it exists. 1151 | */ 1152 | if ( 1153 | $this->identifierEndsWithClassConstant($identifier) && 1154 | ! $this->identifierStartsWithBackslash($identifier) 1155 | ) { 1156 | return substr($identifier, 0, $this->getClassConstantPositionInIdentifier($identifier)); 1157 | } 1158 | 1159 | if ($this->identifierEndsWithClassConstant($identifier) && $this->identifierStartsWithBackslash($identifier)) { 1160 | return substr($identifier, 1, $this->getClassConstantPositionInIdentifier($identifier) - 1); 1161 | } 1162 | 1163 | if (! defined($identifier)) { 1164 | throw AnnotationException::semanticalErrorConstants($identifier, $this->context); 1165 | } 1166 | 1167 | return constant($identifier); 1168 | } 1169 | 1170 | private function identifierStartsWithBackslash(string $identifier): bool 1171 | { 1172 | return $identifier[0] === '\\'; 1173 | } 1174 | 1175 | private function identifierEndsWithClassConstant(string $identifier): bool 1176 | { 1177 | return $this->getClassConstantPositionInIdentifier($identifier) === strlen($identifier) - strlen('::class'); 1178 | } 1179 | 1180 | /** @return int|false */ 1181 | private function getClassConstantPositionInIdentifier(string $identifier) 1182 | { 1183 | return stripos($identifier, '::class'); 1184 | } 1185 | 1186 | /** 1187 | * Identifier ::= string 1188 | * 1189 | * @throws AnnotationException 1190 | */ 1191 | private function Identifier(): string 1192 | { 1193 | // check if we have an annotation 1194 | if (! $this->lexer->isNextTokenAny(self::$classIdentifiers)) { 1195 | throw $this->syntaxError('namespace separator or identifier'); 1196 | } 1197 | 1198 | $this->lexer->moveNext(); 1199 | 1200 | $className = $this->lexer->token->value; 1201 | 1202 | while ( 1203 | $this->lexer->lookahead !== null && 1204 | $this->lexer->lookahead->position === ($this->lexer->token->position + 1205 | strlen($this->lexer->token->value)) && 1206 | $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR) 1207 | ) { 1208 | $this->match(DocLexer::T_NAMESPACE_SEPARATOR); 1209 | $this->matchAny(self::$classIdentifiers); 1210 | 1211 | $className .= '\\' . $this->lexer->token->value; 1212 | } 1213 | 1214 | return $className; 1215 | } 1216 | 1217 | /** 1218 | * Value ::= PlainValue | FieldAssignment 1219 | * 1220 | * @return mixed 1221 | * 1222 | * @throws AnnotationException 1223 | * @throws ReflectionException 1224 | */ 1225 | private function Value() 1226 | { 1227 | $peek = $this->lexer->glimpse(); 1228 | 1229 | if ($peek->type === DocLexer::T_EQUALS) { 1230 | return $this->FieldAssignment(); 1231 | } 1232 | 1233 | return $this->PlainValue(); 1234 | } 1235 | 1236 | /** 1237 | * PlainValue ::= integer | string | float | boolean | Array | Annotation 1238 | * 1239 | * @return mixed 1240 | * 1241 | * @throws AnnotationException 1242 | * @throws ReflectionException 1243 | */ 1244 | private function PlainValue() 1245 | { 1246 | if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { 1247 | return $this->Arrayx(); 1248 | } 1249 | 1250 | if ($this->lexer->isNextToken(DocLexer::T_AT)) { 1251 | return $this->Annotation(); 1252 | } 1253 | 1254 | if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { 1255 | return $this->Constant(); 1256 | } 1257 | 1258 | switch ($this->lexer->lookahead->type) { 1259 | case DocLexer::T_STRING: 1260 | $this->match(DocLexer::T_STRING); 1261 | 1262 | return $this->lexer->token->value; 1263 | 1264 | case DocLexer::T_INTEGER: 1265 | $this->match(DocLexer::T_INTEGER); 1266 | 1267 | return (int) $this->lexer->token->value; 1268 | 1269 | case DocLexer::T_FLOAT: 1270 | $this->match(DocLexer::T_FLOAT); 1271 | 1272 | return (float) $this->lexer->token->value; 1273 | 1274 | case DocLexer::T_TRUE: 1275 | $this->match(DocLexer::T_TRUE); 1276 | 1277 | return true; 1278 | 1279 | case DocLexer::T_FALSE: 1280 | $this->match(DocLexer::T_FALSE); 1281 | 1282 | return false; 1283 | 1284 | case DocLexer::T_NULL: 1285 | $this->match(DocLexer::T_NULL); 1286 | 1287 | return null; 1288 | 1289 | default: 1290 | throw $this->syntaxError('PlainValue'); 1291 | } 1292 | } 1293 | 1294 | /** 1295 | * FieldAssignment ::= FieldName "=" PlainValue 1296 | * FieldName ::= identifier 1297 | * 1298 | * @throws AnnotationException 1299 | * @throws ReflectionException 1300 | */ 1301 | private function FieldAssignment(): stdClass 1302 | { 1303 | $this->match(DocLexer::T_IDENTIFIER); 1304 | $fieldName = $this->lexer->token->value; 1305 | 1306 | $this->match(DocLexer::T_EQUALS); 1307 | 1308 | $item = new stdClass(); 1309 | $item->name = $fieldName; 1310 | $item->value = $this->PlainValue(); 1311 | 1312 | return $item; 1313 | } 1314 | 1315 | /** 1316 | * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}" 1317 | * 1318 | * @return mixed[] 1319 | * 1320 | * @throws AnnotationException 1321 | * @throws ReflectionException 1322 | */ 1323 | private function Arrayx(): array 1324 | { 1325 | $array = $values = []; 1326 | 1327 | $this->match(DocLexer::T_OPEN_CURLY_BRACES); 1328 | 1329 | // If the array is empty, stop parsing and return. 1330 | if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) { 1331 | $this->match(DocLexer::T_CLOSE_CURLY_BRACES); 1332 | 1333 | return $array; 1334 | } 1335 | 1336 | $values[] = $this->ArrayEntry(); 1337 | 1338 | while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { 1339 | $this->match(DocLexer::T_COMMA); 1340 | 1341 | // optional trailing comma 1342 | if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) { 1343 | break; 1344 | } 1345 | 1346 | $values[] = $this->ArrayEntry(); 1347 | } 1348 | 1349 | $this->match(DocLexer::T_CLOSE_CURLY_BRACES); 1350 | 1351 | foreach ($values as $value) { 1352 | [$key, $val] = $value; 1353 | 1354 | if ($key !== null) { 1355 | $array[$key] = $val; 1356 | } else { 1357 | $array[] = $val; 1358 | } 1359 | } 1360 | 1361 | return $array; 1362 | } 1363 | 1364 | /** 1365 | * ArrayEntry ::= Value | KeyValuePair 1366 | * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant 1367 | * Key ::= string | integer | Constant 1368 | * 1369 | * @phpstan-return array{mixed, mixed} 1370 | * 1371 | * @throws AnnotationException 1372 | * @throws ReflectionException 1373 | */ 1374 | private function ArrayEntry(): array 1375 | { 1376 | $peek = $this->lexer->glimpse(); 1377 | 1378 | if ( 1379 | $peek->type === DocLexer::T_EQUALS 1380 | || $peek->type === DocLexer::T_COLON 1381 | ) { 1382 | if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { 1383 | $key = $this->Constant(); 1384 | } else { 1385 | $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]); 1386 | $key = $this->lexer->token->value; 1387 | } 1388 | 1389 | $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]); 1390 | 1391 | return [$key, $this->PlainValue()]; 1392 | } 1393 | 1394 | return [null, $this->Value()]; 1395 | } 1396 | 1397 | /** 1398 | * Checks whether the given $name matches any ignored annotation name or namespace 1399 | */ 1400 | private function isIgnoredAnnotation(string $name): bool 1401 | { 1402 | if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) { 1403 | return true; 1404 | } 1405 | 1406 | foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) { 1407 | $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\'; 1408 | 1409 | if (stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace) === 0) { 1410 | return true; 1411 | } 1412 | } 1413 | 1414 | return false; 1415 | } 1416 | 1417 | /** 1418 | * Resolve positional arguments (without name) to named ones 1419 | * 1420 | * @psalm-param Arguments $arguments 1421 | * 1422 | * @return array 1423 | */ 1424 | private function resolvePositionalValues(array $arguments, string $name): array 1425 | { 1426 | $positionalArguments = $arguments['positional_arguments'] ?? []; 1427 | $values = $arguments['named_arguments'] ?? []; 1428 | 1429 | if ( 1430 | self::$annotationMetadata[$name]['has_named_argument_constructor'] 1431 | && self::$annotationMetadata[$name]['default_property'] !== null 1432 | ) { 1433 | // We must ensure that we don't have positional arguments after named ones 1434 | $positions = array_keys($positionalArguments); 1435 | $lastPosition = null; 1436 | foreach ($positions as $position) { 1437 | if ( 1438 | ($lastPosition === null && $position !== 0) || 1439 | ($lastPosition !== null && $position !== $lastPosition + 1) 1440 | ) { 1441 | throw $this->syntaxError('Positional arguments after named arguments is not allowed'); 1442 | } 1443 | 1444 | $lastPosition = $position; 1445 | } 1446 | 1447 | foreach (self::$annotationMetadata[$name]['constructor_args'] as $property => $parameter) { 1448 | $position = $parameter['position']; 1449 | if (isset($values[$property]) || ! isset($positionalArguments[$position])) { 1450 | continue; 1451 | } 1452 | 1453 | $values[$property] = $positionalArguments[$position]; 1454 | } 1455 | } else { 1456 | if (count($positionalArguments) > 0 && ! isset($values['value'])) { 1457 | if (count($positionalArguments) === 1) { 1458 | $value = array_pop($positionalArguments); 1459 | } else { 1460 | $value = array_values($positionalArguments); 1461 | } 1462 | 1463 | $values['value'] = $value; 1464 | } 1465 | } 1466 | 1467 | return $values; 1468 | } 1469 | 1470 | /** 1471 | * Try to instantiate the annotation and catch and process any exceptions related to failure 1472 | * 1473 | * @param class-string $name 1474 | * @param array $arguments 1475 | * 1476 | * @return object 1477 | * 1478 | * @throws AnnotationException 1479 | */ 1480 | private function instantiateAnnotiation(string $originalName, string $context, string $name, array $arguments) 1481 | { 1482 | try { 1483 | return new $name(...$arguments); 1484 | } catch (Throwable $exception) { 1485 | throw AnnotationException::creationError( 1486 | sprintf( 1487 | 'An error occurred while instantiating the annotation @%s declared on %s: "%s".', 1488 | $originalName, 1489 | $context, 1490 | $exception->getMessage() 1491 | ), 1492 | $exception 1493 | ); 1494 | } 1495 | } 1496 | } 1497 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php: -------------------------------------------------------------------------------- 1 | true, 16 | 'Attribute' => true, 17 | 'Attributes' => true, 18 | /* Can we enable this? 'Enum' => true, */ 19 | 'Required' => true, 20 | 'Target' => true, 21 | 'NamedArgumentConstructor' => true, 22 | ]; 23 | 24 | private const WidelyUsedNonStandard = [ 25 | 'fix' => true, 26 | 'fixme' => true, 27 | 'override' => true, 28 | ]; 29 | 30 | private const PhpDocumentor1 = [ 31 | 'abstract' => true, 32 | 'access' => true, 33 | 'code' => true, 34 | 'deprec' => true, 35 | 'endcode' => true, 36 | 'exception' => true, 37 | 'final' => true, 38 | 'ingroup' => true, 39 | 'inheritdoc' => true, 40 | 'inheritDoc' => true, 41 | 'magic' => true, 42 | 'name' => true, 43 | 'private' => true, 44 | 'static' => true, 45 | 'staticvar' => true, 46 | 'staticVar' => true, 47 | 'toc' => true, 48 | 'tutorial' => true, 49 | 'throw' => true, 50 | ]; 51 | 52 | private const PhpDocumentor2 = [ 53 | 'api' => true, 54 | 'author' => true, 55 | 'category' => true, 56 | 'copyright' => true, 57 | 'deprecated' => true, 58 | 'example' => true, 59 | 'filesource' => true, 60 | 'global' => true, 61 | 'ignore' => true, 62 | /* Can we enable this? 'index' => true, */ 63 | 'internal' => true, 64 | 'license' => true, 65 | 'link' => true, 66 | 'method' => true, 67 | 'package' => true, 68 | 'param' => true, 69 | 'property' => true, 70 | 'property-read' => true, 71 | 'property-write' => true, 72 | 'return' => true, 73 | 'see' => true, 74 | 'since' => true, 75 | 'source' => true, 76 | 'subpackage' => true, 77 | 'throws' => true, 78 | 'todo' => true, 79 | 'TODO' => true, 80 | 'usedby' => true, 81 | 'uses' => true, 82 | 'var' => true, 83 | 'version' => true, 84 | ]; 85 | 86 | private const PHPUnit = [ 87 | 'author' => true, 88 | 'after' => true, 89 | 'afterClass' => true, 90 | 'backupGlobals' => true, 91 | 'backupStaticAttributes' => true, 92 | 'before' => true, 93 | 'beforeClass' => true, 94 | 'codeCoverageIgnore' => true, 95 | 'codeCoverageIgnoreStart' => true, 96 | 'codeCoverageIgnoreEnd' => true, 97 | 'covers' => true, 98 | 'coversDefaultClass' => true, 99 | 'coversNothing' => true, 100 | 'dataProvider' => true, 101 | 'depends' => true, 102 | 'doesNotPerformAssertions' => true, 103 | 'expectedException' => true, 104 | 'expectedExceptionCode' => true, 105 | 'expectedExceptionMessage' => true, 106 | 'expectedExceptionMessageRegExp' => true, 107 | 'group' => true, 108 | 'large' => true, 109 | 'medium' => true, 110 | 'preserveGlobalState' => true, 111 | 'requires' => true, 112 | 'runTestsInSeparateProcesses' => true, 113 | 'runInSeparateProcess' => true, 114 | 'small' => true, 115 | 'test' => true, 116 | 'testdox' => true, 117 | 'testWith' => true, 118 | 'ticket' => true, 119 | 'uses' => true, 120 | ]; 121 | 122 | private const PhpCheckStyle = ['SuppressWarnings' => true]; 123 | 124 | private const PhpStorm = ['noinspection' => true]; 125 | 126 | private const PEAR = ['package_version' => true]; 127 | 128 | private const PlainUML = [ 129 | 'startuml' => true, 130 | 'enduml' => true, 131 | ]; 132 | 133 | private const Symfony = ['experimental' => true]; 134 | 135 | private const PhpCodeSniffer = [ 136 | 'codingStandardsIgnoreStart' => true, 137 | 'codingStandardsIgnoreEnd' => true, 138 | ]; 139 | 140 | private const SlevomatCodingStandard = ['phpcsSuppress' => true]; 141 | 142 | private const Phan = ['suppress' => true]; 143 | 144 | private const Rector = ['noRector' => true]; 145 | 146 | private const StaticAnalysis = [ 147 | // PHPStan, Psalm 148 | 'extends' => true, 149 | 'implements' => true, 150 | 'readonly' => true, 151 | 'template' => true, 152 | 'use' => true, 153 | 154 | // Psalm 155 | 'pure' => true, 156 | 'immutable' => true, 157 | ]; 158 | 159 | public const LIST = self::Reserved 160 | + self::WidelyUsedNonStandard 161 | + self::PhpDocumentor1 162 | + self::PhpDocumentor2 163 | + self::PHPUnit 164 | + self::PhpCheckStyle 165 | + self::PhpStorm 166 | + self::PEAR 167 | + self::PlainUML 168 | + self::Symfony 169 | + self::SlevomatCodingStandard 170 | + self::PhpCodeSniffer 171 | + self::Phan 172 | + self::Rector 173 | + self::StaticAnalysis; 174 | 175 | private function __construct() 176 | { 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/IndexedReader.php: -------------------------------------------------------------------------------- 1 | delegate = $reader; 23 | } 24 | 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function getClassAnnotations(ReflectionClass $class) 29 | { 30 | $annotations = []; 31 | foreach ($this->delegate->getClassAnnotations($class) as $annot) { 32 | $annotations[get_class($annot)] = $annot; 33 | } 34 | 35 | return $annotations; 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | public function getClassAnnotation(ReflectionClass $class, $annotationName) 42 | { 43 | return $this->delegate->getClassAnnotation($class, $annotationName); 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | public function getMethodAnnotations(ReflectionMethod $method) 50 | { 51 | $annotations = []; 52 | foreach ($this->delegate->getMethodAnnotations($method) as $annot) { 53 | $annotations[get_class($annot)] = $annot; 54 | } 55 | 56 | return $annotations; 57 | } 58 | 59 | /** 60 | * {@inheritDoc} 61 | */ 62 | public function getMethodAnnotation(ReflectionMethod $method, $annotationName) 63 | { 64 | return $this->delegate->getMethodAnnotation($method, $annotationName); 65 | } 66 | 67 | /** 68 | * {@inheritDoc} 69 | */ 70 | public function getPropertyAnnotations(ReflectionProperty $property) 71 | { 72 | $annotations = []; 73 | foreach ($this->delegate->getPropertyAnnotations($property) as $annot) { 74 | $annotations[get_class($annot)] = $annot; 75 | } 76 | 77 | return $annotations; 78 | } 79 | 80 | /** 81 | * {@inheritDoc} 82 | */ 83 | public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) 84 | { 85 | return $this->delegate->getPropertyAnnotation($property, $annotationName); 86 | } 87 | 88 | /** 89 | * Proxies all methods to the delegate. 90 | * 91 | * @param mixed[] $args 92 | * 93 | * @return mixed 94 | */ 95 | public function __call(string $method, array $args) 96 | { 97 | return call_user_func_array([$this->delegate, $method], $args); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/PhpParser.php: -------------------------------------------------------------------------------- 1 | a list with use statements in the form (Alias => FQN). 25 | */ 26 | public function parseUseStatements($reflection): array 27 | { 28 | if (method_exists($reflection, 'getUseStatements')) { 29 | return $reflection->getUseStatements(); 30 | } 31 | 32 | $filename = $reflection->getFileName(); 33 | 34 | if ($filename === false) { 35 | return []; 36 | } 37 | 38 | $content = $this->getFileContent($filename, $reflection->getStartLine()); 39 | 40 | if ($content === null) { 41 | return []; 42 | } 43 | 44 | $namespace = preg_quote($reflection->getNamespaceName()); 45 | $content = preg_replace('/^.*?(\bnamespace\s+' . $namespace . '\s*[;{].*)$/s', '\\1', $content); 46 | $tokenizer = new TokenParser('parseUseStatements($reflection->getNamespaceName()); 49 | } 50 | 51 | /** 52 | * Gets the content of the file right up to the given line number. 53 | * 54 | * @param string $filename The name of the file to load. 55 | * @param int $lineNumber The number of lines to read from file. 56 | * 57 | * @return string|null The content of the file or null if the file does not exist. 58 | */ 59 | private function getFileContent(string $filename, $lineNumber) 60 | { 61 | if (! is_file($filename)) { 62 | return null; 63 | } 64 | 65 | $content = ''; 66 | $lineCnt = 0; 67 | $file = new SplFileObject($filename); 68 | while (! $file->eof()) { 69 | if ($lineCnt++ === $lineNumber) { 70 | break; 71 | } 72 | 73 | $content .= $file->fgets(); 74 | } 75 | 76 | return $content; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/PsrCachedReader.php: -------------------------------------------------------------------------------- 1 | > */ 35 | private $loadedAnnotations = []; 36 | 37 | /** @var int[] */ 38 | private $loadedFilemtimes = []; 39 | 40 | public function __construct(Reader $reader, CacheItemPoolInterface $cache, bool $debug = false) 41 | { 42 | $this->delegate = $reader; 43 | $this->cache = $cache; 44 | $this->debug = (bool) $debug; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function getClassAnnotations(ReflectionClass $class) 51 | { 52 | $cacheKey = $class->getName(); 53 | 54 | if (isset($this->loadedAnnotations[$cacheKey])) { 55 | return $this->loadedAnnotations[$cacheKey]; 56 | } 57 | 58 | $annots = $this->fetchFromCache($cacheKey, $class, 'getClassAnnotations', $class); 59 | 60 | return $this->loadedAnnotations[$cacheKey] = $annots; 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | */ 66 | public function getClassAnnotation(ReflectionClass $class, $annotationName) 67 | { 68 | foreach ($this->getClassAnnotations($class) as $annot) { 69 | if ($annot instanceof $annotationName) { 70 | return $annot; 71 | } 72 | } 73 | 74 | return null; 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | public function getPropertyAnnotations(ReflectionProperty $property) 81 | { 82 | $class = $property->getDeclaringClass(); 83 | $cacheKey = $class->getName() . '$' . $property->getName(); 84 | 85 | if (isset($this->loadedAnnotations[$cacheKey])) { 86 | return $this->loadedAnnotations[$cacheKey]; 87 | } 88 | 89 | $annots = $this->fetchFromCache($cacheKey, $class, 'getPropertyAnnotations', $property); 90 | 91 | return $this->loadedAnnotations[$cacheKey] = $annots; 92 | } 93 | 94 | /** 95 | * {@inheritDoc} 96 | */ 97 | public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) 98 | { 99 | foreach ($this->getPropertyAnnotations($property) as $annot) { 100 | if ($annot instanceof $annotationName) { 101 | return $annot; 102 | } 103 | } 104 | 105 | return null; 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | public function getMethodAnnotations(ReflectionMethod $method) 112 | { 113 | $class = $method->getDeclaringClass(); 114 | $cacheKey = $class->getName() . '#' . $method->getName(); 115 | 116 | if (isset($this->loadedAnnotations[$cacheKey])) { 117 | return $this->loadedAnnotations[$cacheKey]; 118 | } 119 | 120 | $annots = $this->fetchFromCache($cacheKey, $class, 'getMethodAnnotations', $method); 121 | 122 | return $this->loadedAnnotations[$cacheKey] = $annots; 123 | } 124 | 125 | /** 126 | * {@inheritDoc} 127 | */ 128 | public function getMethodAnnotation(ReflectionMethod $method, $annotationName) 129 | { 130 | foreach ($this->getMethodAnnotations($method) as $annot) { 131 | if ($annot instanceof $annotationName) { 132 | return $annot; 133 | } 134 | } 135 | 136 | return null; 137 | } 138 | 139 | public function clearLoadedAnnotations(): void 140 | { 141 | $this->loadedAnnotations = []; 142 | $this->loadedFilemtimes = []; 143 | } 144 | 145 | /** @return mixed[] */ 146 | private function fetchFromCache( 147 | string $cacheKey, 148 | ReflectionClass $class, 149 | string $method, 150 | Reflector $reflector 151 | ): array { 152 | $cacheKey = rawurlencode($cacheKey); 153 | 154 | $item = $this->cache->getItem($cacheKey); 155 | if (($this->debug && ! $this->refresh($cacheKey, $class)) || ! $item->isHit()) { 156 | $this->cache->save($item->set($this->delegate->{$method}($reflector))); 157 | } 158 | 159 | return $item->get(); 160 | } 161 | 162 | /** 163 | * Used in debug mode to check if the cache is fresh. 164 | * 165 | * @return bool Returns true if the cache was fresh, or false if the class 166 | * being read was modified since writing to the cache. 167 | */ 168 | private function refresh(string $cacheKey, ReflectionClass $class): bool 169 | { 170 | $lastModification = $this->getLastModification($class); 171 | if ($lastModification === 0) { 172 | return true; 173 | } 174 | 175 | $item = $this->cache->getItem('[C]' . $cacheKey); 176 | if ($item->isHit() && $item->get() >= $lastModification) { 177 | return true; 178 | } 179 | 180 | $this->cache->save($item->set(time())); 181 | 182 | return false; 183 | } 184 | 185 | /** 186 | * Returns the time the class was last modified, testing traits and parents 187 | */ 188 | private function getLastModification(ReflectionClass $class): int 189 | { 190 | $filename = $class->getFileName(); 191 | 192 | if (isset($this->loadedFilemtimes[$filename])) { 193 | return $this->loadedFilemtimes[$filename]; 194 | } 195 | 196 | $parent = $class->getParentClass(); 197 | 198 | $lastModification = max(array_merge( 199 | [$filename !== false && is_file($filename) ? filemtime($filename) : 0], 200 | array_map(function (ReflectionClass $reflectionTrait): int { 201 | return $this->getTraitLastModificationTime($reflectionTrait); 202 | }, $class->getTraits()), 203 | array_map(function (ReflectionClass $class): int { 204 | return $this->getLastModification($class); 205 | }, $class->getInterfaces()), 206 | $parent ? [$this->getLastModification($parent)] : [] 207 | )); 208 | 209 | assert($lastModification !== false); 210 | 211 | return $this->loadedFilemtimes[$filename] = $lastModification; 212 | } 213 | 214 | private function getTraitLastModificationTime(ReflectionClass $reflectionTrait): int 215 | { 216 | $fileName = $reflectionTrait->getFileName(); 217 | 218 | if (isset($this->loadedFilemtimes[$fileName])) { 219 | return $this->loadedFilemtimes[$fileName]; 220 | } 221 | 222 | $lastModificationTime = max(array_merge( 223 | [$fileName !== false && is_file($fileName) ? filemtime($fileName) : 0], 224 | array_map(function (ReflectionClass $reflectionTrait): int { 225 | return $this->getTraitLastModificationTime($reflectionTrait); 226 | }, $reflectionTrait->getTraits()) 227 | )); 228 | 229 | assert($lastModificationTime !== false); 230 | 231 | return $this->loadedFilemtimes[$fileName] = $lastModificationTime; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/Reader.php: -------------------------------------------------------------------------------- 1 | An array of Annotations. 21 | */ 22 | public function getClassAnnotations(ReflectionClass $class); 23 | 24 | /** 25 | * Gets a class annotation. 26 | * 27 | * @param ReflectionClass $class The ReflectionClass of the class from which 28 | * the class annotations should be read. 29 | * @param class-string $annotationName The name of the annotation. 30 | * 31 | * @return T|null The Annotation or NULL, if the requested annotation does not exist. 32 | * 33 | * @template T 34 | */ 35 | public function getClassAnnotation(ReflectionClass $class, $annotationName); 36 | 37 | /** 38 | * Gets the annotations applied to a method. 39 | * 40 | * @param ReflectionMethod $method The ReflectionMethod of the method from which 41 | * the annotations should be read. 42 | * 43 | * @return array An array of Annotations. 44 | */ 45 | public function getMethodAnnotations(ReflectionMethod $method); 46 | 47 | /** 48 | * Gets a method annotation. 49 | * 50 | * @param ReflectionMethod $method The ReflectionMethod to read the annotations from. 51 | * @param class-string $annotationName The name of the annotation. 52 | * 53 | * @return T|null The Annotation or NULL, if the requested annotation does not exist. 54 | * 55 | * @template T 56 | */ 57 | public function getMethodAnnotation(ReflectionMethod $method, $annotationName); 58 | 59 | /** 60 | * Gets the annotations applied to a property. 61 | * 62 | * @param ReflectionProperty $property The ReflectionProperty of the property 63 | * from which the annotations should be read. 64 | * 65 | * @return array An array of Annotations. 66 | */ 67 | public function getPropertyAnnotations(ReflectionProperty $property); 68 | 69 | /** 70 | * Gets a property annotation. 71 | * 72 | * @param ReflectionProperty $property The ReflectionProperty to read the annotations from. 73 | * @param class-string $annotationName The name of the annotation. 74 | * 75 | * @return T|null The Annotation or NULL, if the requested annotation does not exist. 76 | * 77 | * @template T 78 | */ 79 | public function getPropertyAnnotation(ReflectionProperty $property, $annotationName); 80 | } 81 | -------------------------------------------------------------------------------- /lib/Doctrine/Common/Annotations/TokenParser.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | private $tokens; 34 | 35 | /** 36 | * The number of tokens. 37 | * 38 | * @var int 39 | */ 40 | private $numTokens; 41 | 42 | /** 43 | * The current array pointer. 44 | * 45 | * @var int 46 | */ 47 | private $pointer = 0; 48 | 49 | public function __construct(string $contents) 50 | { 51 | $this->tokens = token_get_all($contents); 52 | 53 | // The PHP parser sets internal compiler globals for certain things. Annoyingly, the last docblock comment it 54 | // saw gets stored in doc_comment. When it comes to compile the next thing to be include()d this stored 55 | // doc_comment becomes owned by the first thing the compiler sees in the file that it considers might have a 56 | // docblock. If the first thing in the file is a class without a doc block this would cause calls to 57 | // getDocBlock() on said class to return our long lost doc_comment. Argh. 58 | // To workaround, cause the parser to parse an empty docblock. Sure getDocBlock() will return this, but at least 59 | // it's harmless to us. 60 | token_get_all("numTokens = count($this->tokens); 63 | } 64 | 65 | /** 66 | * Gets the next non whitespace and non comment token. 67 | * 68 | * @param bool $docCommentIsComment If TRUE then a doc comment is considered a comment and skipped. 69 | * If FALSE then only whitespace and normal comments are skipped. 70 | * 71 | * @return mixed[]|string|null The token if exists, null otherwise. 72 | */ 73 | public function next(bool $docCommentIsComment = true) 74 | { 75 | for ($i = $this->pointer; $i < $this->numTokens; $i++) { 76 | $this->pointer++; 77 | if ( 78 | $this->tokens[$i][0] === T_WHITESPACE || 79 | $this->tokens[$i][0] === T_COMMENT || 80 | ($docCommentIsComment && $this->tokens[$i][0] === T_DOC_COMMENT) 81 | ) { 82 | continue; 83 | } 84 | 85 | return $this->tokens[$i]; 86 | } 87 | 88 | return null; 89 | } 90 | 91 | /** 92 | * Parses a single use statement. 93 | * 94 | * @return array A list with all found class names for a use statement. 95 | */ 96 | public function parseUseStatement() 97 | { 98 | $groupRoot = ''; 99 | $class = ''; 100 | $alias = ''; 101 | $statements = []; 102 | $explicitAlias = false; 103 | while (($token = $this->next())) { 104 | if (! $explicitAlias && $token[0] === T_STRING) { 105 | $class .= $token[1]; 106 | $alias = $token[1]; 107 | } elseif ($explicitAlias && $token[0] === T_STRING) { 108 | $alias = $token[1]; 109 | } elseif ( 110 | PHP_VERSION_ID >= 80000 && 111 | ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED) 112 | ) { 113 | $class .= $token[1]; 114 | 115 | $classSplit = explode('\\', $token[1]); 116 | $alias = $classSplit[count($classSplit) - 1]; 117 | } elseif ($token[0] === T_NS_SEPARATOR) { 118 | $class .= '\\'; 119 | $alias = ''; 120 | } elseif ($token[0] === T_AS) { 121 | $explicitAlias = true; 122 | $alias = ''; 123 | } elseif ($token === ',') { 124 | $statements[strtolower($alias)] = $groupRoot . $class; 125 | $class = ''; 126 | $alias = ''; 127 | $explicitAlias = false; 128 | } elseif ($token === ';') { 129 | $statements[strtolower($alias)] = $groupRoot . $class; 130 | break; 131 | } elseif ($token === '{') { 132 | $groupRoot = $class; 133 | $class = ''; 134 | } elseif ($token === '}') { 135 | continue; 136 | } else { 137 | break; 138 | } 139 | } 140 | 141 | return $statements; 142 | } 143 | 144 | /** 145 | * Gets all use statements. 146 | * 147 | * @param string $namespaceName The namespace name of the reflected class. 148 | * 149 | * @return array A list with all found use statements. 150 | */ 151 | public function parseUseStatements(string $namespaceName) 152 | { 153 | $statements = []; 154 | while (($token = $this->next())) { 155 | if ($token[0] === T_USE) { 156 | $statements = array_merge($statements, $this->parseUseStatement()); 157 | continue; 158 | } 159 | 160 | if ($token[0] !== T_NAMESPACE || $this->parseNamespace() !== $namespaceName) { 161 | continue; 162 | } 163 | 164 | // Get fresh array for new namespace. This is to prevent the parser to collect the use statements 165 | // for a previous namespace with the same name. This is the case if a namespace is defined twice 166 | // or if a namespace with the same name is commented out. 167 | $statements = []; 168 | } 169 | 170 | return $statements; 171 | } 172 | 173 | /** 174 | * Gets the namespace. 175 | * 176 | * @return string The found namespace. 177 | */ 178 | public function parseNamespace() 179 | { 180 | $name = ''; 181 | while ( 182 | ($token = $this->next()) && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR || ( 183 | PHP_VERSION_ID >= 80000 && 184 | ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED) 185 | )) 186 | ) { 187 | $name .= $token[1]; 188 | } 189 | 190 | return $name; 191 | } 192 | 193 | /** 194 | * Gets the class name. 195 | * 196 | * @return string The found class name. 197 | */ 198 | public function parseClass() 199 | { 200 | // Namespaces and class names are tokenized the same: T_STRINGs 201 | // separated by T_NS_SEPARATOR so we can use one function to provide 202 | // both. 203 | return $this->parseNamespace(); 204 | } 205 | } 206 | --------------------------------------------------------------------------------