AnnotationReader.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <?php
  2. namespace Doctrine\Common\Annotations;
  3. use Doctrine\Common\Annotations\Annotation\IgnoreAnnotation;
  4. use Doctrine\Common\Annotations\Annotation\Target;
  5. use ReflectionClass;
  6. use ReflectionFunction;
  7. use ReflectionMethod;
  8. use ReflectionProperty;
  9. use function array_merge;
  10. use function class_exists;
  11. use function extension_loaded;
  12. use function ini_get;
  13. /**
  14. * A reader for docblock annotations.
  15. */
  16. class AnnotationReader implements Reader
  17. {
  18. /**
  19. * Global map for imports.
  20. *
  21. * @var array<string, class-string>
  22. */
  23. private static $globalImports = [
  24. 'ignoreannotation' => Annotation\IgnoreAnnotation::class,
  25. ];
  26. /**
  27. * A list with annotations that are not causing exceptions when not resolved to an annotation class.
  28. *
  29. * The names are case sensitive.
  30. *
  31. * @var array<string, true>
  32. */
  33. private static $globalIgnoredNames = ImplicitlyIgnoredAnnotationNames::LIST;
  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<string, true>
  40. */
  41. private static $globalIgnoredNamespaces = [];
  42. /**
  43. * Add a new annotation to the globally ignored annotation names with regard to exception handling.
  44. *
  45. * @param string $name
  46. */
  47. public static function addGlobalIgnoredName($name)
  48. {
  49. self::$globalIgnoredNames[$name] = true;
  50. }
  51. /**
  52. * Add a new annotation to the globally ignored annotation namespaces with regard to exception handling.
  53. *
  54. * @param string $namespace
  55. */
  56. public static function addGlobalIgnoredNamespace($namespace)
  57. {
  58. self::$globalIgnoredNamespaces[$namespace] = true;
  59. }
  60. /**
  61. * Annotations parser.
  62. *
  63. * @var DocParser
  64. */
  65. private $parser;
  66. /**
  67. * Annotations parser used to collect parsing metadata.
  68. *
  69. * @var DocParser
  70. */
  71. private $preParser;
  72. /**
  73. * PHP parser used to collect imports.
  74. *
  75. * @var PhpParser
  76. */
  77. private $phpParser;
  78. /**
  79. * In-memory cache mechanism to store imported annotations per class.
  80. *
  81. * @psalm-var array<'class'|'function', array<string, array<string, class-string>>>
  82. */
  83. private $imports = [];
  84. /**
  85. * In-memory cache mechanism to store ignored annotations per class.
  86. *
  87. * @psalm-var array<'class'|'function', array<string, array<string, true>>>
  88. */
  89. private $ignoredAnnotationNames = [];
  90. /**
  91. * Initializes a new AnnotationReader.
  92. *
  93. * @throws AnnotationException
  94. */
  95. public function __construct(?DocParser $parser = null)
  96. {
  97. if (
  98. extension_loaded('Zend Optimizer+') && (ini_get('zend_optimizerplus.save_comments') === '0' ||
  99. ini_get('opcache.save_comments') === '0')
  100. ) {
  101. throw AnnotationException::optimizerPlusSaveComments();
  102. }
  103. if (extension_loaded('Zend OPcache') && ini_get('opcache.save_comments') === 0) {
  104. throw AnnotationException::optimizerPlusSaveComments();
  105. }
  106. // Make sure that the IgnoreAnnotation annotation is loaded
  107. class_exists(IgnoreAnnotation::class);
  108. $this->parser = $parser ?: new DocParser();
  109. $this->preParser = new DocParser();
  110. $this->preParser->setImports(self::$globalImports);
  111. $this->preParser->setIgnoreNotImportedAnnotations(true);
  112. $this->preParser->setIgnoredAnnotationNames(self::$globalIgnoredNames);
  113. $this->phpParser = new PhpParser();
  114. }
  115. /**
  116. * {@inheritDoc}
  117. */
  118. public function getClassAnnotations(ReflectionClass $class)
  119. {
  120. $this->parser->setTarget(Target::TARGET_CLASS);
  121. $this->parser->setImports($this->getImports($class));
  122. $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class));
  123. $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);
  124. return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName());
  125. }
  126. /**
  127. * {@inheritDoc}
  128. */
  129. public function getClassAnnotation(ReflectionClass $class, $annotationName)
  130. {
  131. $annotations = $this->getClassAnnotations($class);
  132. foreach ($annotations as $annotation) {
  133. if ($annotation instanceof $annotationName) {
  134. return $annotation;
  135. }
  136. }
  137. return null;
  138. }
  139. /**
  140. * {@inheritDoc}
  141. */
  142. public function getPropertyAnnotations(ReflectionProperty $property)
  143. {
  144. $class = $property->getDeclaringClass();
  145. $context = 'property ' . $class->getName() . '::$' . $property->getName();
  146. $this->parser->setTarget(Target::TARGET_PROPERTY);
  147. $this->parser->setImports($this->getPropertyImports($property));
  148. $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class));
  149. $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);
  150. return $this->parser->parse($property->getDocComment(), $context);
  151. }
  152. /**
  153. * {@inheritDoc}
  154. */
  155. public function getPropertyAnnotation(ReflectionProperty $property, $annotationName)
  156. {
  157. $annotations = $this->getPropertyAnnotations($property);
  158. foreach ($annotations as $annotation) {
  159. if ($annotation instanceof $annotationName) {
  160. return $annotation;
  161. }
  162. }
  163. return null;
  164. }
  165. /**
  166. * {@inheritDoc}
  167. */
  168. public function getMethodAnnotations(ReflectionMethod $method)
  169. {
  170. $class = $method->getDeclaringClass();
  171. $context = 'method ' . $class->getName() . '::' . $method->getName() . '()';
  172. $this->parser->setTarget(Target::TARGET_METHOD);
  173. $this->parser->setImports($this->getMethodImports($method));
  174. $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class));
  175. $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);
  176. return $this->parser->parse($method->getDocComment(), $context);
  177. }
  178. /**
  179. * {@inheritDoc}
  180. */
  181. public function getMethodAnnotation(ReflectionMethod $method, $annotationName)
  182. {
  183. $annotations = $this->getMethodAnnotations($method);
  184. foreach ($annotations as $annotation) {
  185. if ($annotation instanceof $annotationName) {
  186. return $annotation;
  187. }
  188. }
  189. return null;
  190. }
  191. /**
  192. * Gets the annotations applied to a function.
  193. *
  194. * @phpstan-return list<object> An array of Annotations.
  195. */
  196. public function getFunctionAnnotations(ReflectionFunction $function): array
  197. {
  198. $context = 'function ' . $function->getName();
  199. $this->parser->setTarget(Target::TARGET_FUNCTION);
  200. $this->parser->setImports($this->getImports($function));
  201. $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($function));
  202. $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);
  203. return $this->parser->parse($function->getDocComment(), $context);
  204. }
  205. /**
  206. * Gets a function annotation.
  207. *
  208. * @return object|null The Annotation or NULL, if the requested annotation does not exist.
  209. */
  210. public function getFunctionAnnotation(ReflectionFunction $function, string $annotationName)
  211. {
  212. $annotations = $this->getFunctionAnnotations($function);
  213. foreach ($annotations as $annotation) {
  214. if ($annotation instanceof $annotationName) {
  215. return $annotation;
  216. }
  217. }
  218. return null;
  219. }
  220. /**
  221. * Returns the ignored annotations for the given class or function.
  222. *
  223. * @param ReflectionClass|ReflectionFunction $reflection
  224. *
  225. * @return array<string, true>
  226. */
  227. private function getIgnoredAnnotationNames($reflection): array
  228. {
  229. $type = $reflection instanceof ReflectionClass ? 'class' : 'function';
  230. $name = $reflection->getName();
  231. if (isset($this->ignoredAnnotationNames[$type][$name])) {
  232. return $this->ignoredAnnotationNames[$type][$name];
  233. }
  234. $this->collectParsingMetadata($reflection);
  235. return $this->ignoredAnnotationNames[$type][$name];
  236. }
  237. /**
  238. * Retrieves imports for a class or a function.
  239. *
  240. * @param ReflectionClass|ReflectionFunction $reflection
  241. *
  242. * @return array<string, class-string>
  243. */
  244. private function getImports($reflection): array
  245. {
  246. $type = $reflection instanceof ReflectionClass ? 'class' : 'function';
  247. $name = $reflection->getName();
  248. if (isset($this->imports[$type][$name])) {
  249. return $this->imports[$type][$name];
  250. }
  251. $this->collectParsingMetadata($reflection);
  252. return $this->imports[$type][$name];
  253. }
  254. /**
  255. * Retrieves imports for methods.
  256. *
  257. * @return array<string, class-string>
  258. */
  259. private function getMethodImports(ReflectionMethod $method)
  260. {
  261. $class = $method->getDeclaringClass();
  262. $classImports = $this->getImports($class);
  263. $traitImports = [];
  264. foreach ($class->getTraits() as $trait) {
  265. if (
  266. ! $trait->hasMethod($method->getName())
  267. || $trait->getFileName() !== $method->getFileName()
  268. ) {
  269. continue;
  270. }
  271. $traitImports = array_merge($traitImports, $this->phpParser->parseUseStatements($trait));
  272. }
  273. return array_merge($classImports, $traitImports);
  274. }
  275. /**
  276. * Retrieves imports for properties.
  277. *
  278. * @return array<string, class-string>
  279. */
  280. private function getPropertyImports(ReflectionProperty $property)
  281. {
  282. $class = $property->getDeclaringClass();
  283. $classImports = $this->getImports($class);
  284. $traitImports = [];
  285. foreach ($class->getTraits() as $trait) {
  286. if (! $trait->hasProperty($property->getName())) {
  287. continue;
  288. }
  289. $traitImports = array_merge($traitImports, $this->phpParser->parseUseStatements($trait));
  290. }
  291. return array_merge($classImports, $traitImports);
  292. }
  293. /**
  294. * Collects parsing metadata for a given class or function.
  295. *
  296. * @param ReflectionClass|ReflectionFunction $reflection
  297. */
  298. private function collectParsingMetadata($reflection): void
  299. {
  300. $type = $reflection instanceof ReflectionClass ? 'class' : 'function';
  301. $name = $reflection->getName();
  302. $ignoredAnnotationNames = self::$globalIgnoredNames;
  303. $annotations = $this->preParser->parse($reflection->getDocComment(), $type . ' ' . $name);
  304. foreach ($annotations as $annotation) {
  305. if (! ($annotation instanceof IgnoreAnnotation)) {
  306. continue;
  307. }
  308. foreach ($annotation->names as $annot) {
  309. $ignoredAnnotationNames[$annot] = true;
  310. }
  311. }
  312. $this->imports[$type][$name] = array_merge(
  313. self::$globalImports,
  314. $this->phpParser->parseUseStatements($reflection),
  315. [
  316. '__NAMESPACE__' => $reflection->getNamespaceName(),
  317. 'self' => $name,
  318. ]
  319. );
  320. $this->ignoredAnnotationNames[$type][$name] = $ignoredAnnotationNames;
  321. }
  322. }