DocParser.php 40 KB

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