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 20namespace Doctrine\Common\Annotations; 21 22use Doctrine\Common\Annotations\Annotation\IgnoreAnnotation; 23use Doctrine\Common\Annotations\Annotation\Target; 24use ReflectionClass; 25use ReflectionMethod; 26use ReflectionProperty; 27 28/** 29 * A reader for docblock annotations. 30 * 31 * @author Benjamin Eberlei <kontakt@beberlei.de> 32 * @author Guilherme Blanco <guilhermeblanco@hotmail.com> 33 * @author Jonathan Wage <jonwage@gmail.com> 34 * @author Roman Borschel <roman@code-factory.org> 35 * @author Johannes M. Schmitt <schmittjoh@gmail.com> 36 */ 37class AnnotationReader implements Reader 38{ 39 /** 40 * Global map for imports. 41 * 42 * @var array 43 */ 44 private static $globalImports = array( 45 'ignoreannotation' => 'Doctrine\Common\Annotations\Annotation\IgnoreAnnotation', 46 ); 47 48 /** 49 * A list with annotations that are not causing exceptions when not resolved to an annotation class. 50 * 51 * The names are case sensitive. 52 * 53 * @var array 54 */ 55 private static $globalIgnoredNames = array( 56 // Annotation tags 57 'Annotation' => true, 'Attribute' => true, 'Attributes' => true, 58 /* Can we enable this? 'Enum' => true, */ 59 'Required' => true, 60 'Target' => true, 61 // Widely used tags (but not existent in phpdoc) 62 'fix' => true , 'fixme' => true, 63 'override' => true, 64 // PHPDocumentor 1 tags 65 'abstract'=> true, 'access'=> true, 66 'code' => true, 67 'deprec'=> true, 68 'endcode' => true, 'exception'=> true, 69 'final'=> true, 70 'ingroup' => true, 'inheritdoc'=> true, 'inheritDoc'=> true, 71 'magic' => true, 72 'name'=> true, 73 'toc' => true, 'tutorial'=> true, 74 'private' => true, 75 'static'=> true, 'staticvar'=> true, 'staticVar'=> true, 76 'throw' => true, 77 // PHPDocumentor 2 tags. 78 'api' => true, 'author'=> true, 79 'category'=> true, 'copyright'=> true, 80 'deprecated'=> true, 81 'example'=> true, 82 'filesource'=> true, 83 'global'=> true, 84 'ignore'=> true, /* Can we enable this? 'index' => true, */ 'internal'=> true, 85 'license'=> true, 'link'=> true, 86 'method' => true, 87 'package'=> true, 'param'=> true, 'property' => true, 'property-read' => true, 'property-write' => true, 88 'return'=> true, 89 'see'=> true, 'since'=> true, 'source' => true, 'subpackage'=> true, 90 'throws'=> true, 'todo'=> true, 'TODO'=> true, 91 'usedby'=> true, 'uses' => true, 92 'var'=> true, 'version'=> true, 93 // PHPUnit tags 94 'codeCoverageIgnore' => true, 'codeCoverageIgnoreStart' => true, 'codeCoverageIgnoreEnd' => true, 95 // PHPCheckStyle 96 'SuppressWarnings' => true, 97 // PHPStorm 98 'noinspection' => true, 99 // PEAR 100 'package_version' => true, 101 // PlantUML 102 'startuml' => true, 'enduml' => true, 103 // Symfony 3.3 Cache Adapter 104 'experimental' => true 105 ); 106 107 /** 108 * A list with annotations that are not causing exceptions when not resolved to an annotation class. 109 * 110 * The names are case sensitive. 111 * 112 * @var array 113 */ 114 private static $globalIgnoredNamespaces = array(); 115 116 /** 117 * Add a new annotation to the globally ignored annotation names with regard to exception handling. 118 * 119 * @param string $name 120 */ 121 static public function addGlobalIgnoredName($name) 122 { 123 self::$globalIgnoredNames[$name] = true; 124 } 125 126 /** 127 * Add a new annotation to the globally ignored annotation namespaces with regard to exception handling. 128 * 129 * @param string $namespace 130 */ 131 static public function addGlobalIgnoredNamespace($namespace) 132 { 133 self::$globalIgnoredNamespaces[$namespace] = true; 134 } 135 136 /** 137 * Annotations parser. 138 * 139 * @var \Doctrine\Common\Annotations\DocParser 140 */ 141 private $parser; 142 143 /** 144 * Annotations parser used to collect parsing metadata. 145 * 146 * @var \Doctrine\Common\Annotations\DocParser 147 */ 148 private $preParser; 149 150 /** 151 * PHP parser used to collect imports. 152 * 153 * @var \Doctrine\Common\Annotations\PhpParser 154 */ 155 private $phpParser; 156 157 /** 158 * In-memory cache mechanism to store imported annotations per class. 159 * 160 * @var array 161 */ 162 private $imports = array(); 163 164 /** 165 * In-memory cache mechanism to store ignored annotations per class. 166 * 167 * @var array 168 */ 169 private $ignoredAnnotationNames = array(); 170 171 /** 172 * Constructor. 173 * 174 * Initializes a new AnnotationReader. 175 * 176 * @param DocParser $parser 177 * 178 * @throws AnnotationException 179 */ 180 public function __construct(DocParser $parser = null) 181 { 182 if (extension_loaded('Zend Optimizer+') && (ini_get('zend_optimizerplus.save_comments') === "0" || ini_get('opcache.save_comments') === "0")) { 183 throw AnnotationException::optimizerPlusSaveComments(); 184 } 185 186 if (extension_loaded('Zend OPcache') && ini_get('opcache.save_comments') == 0) { 187 throw AnnotationException::optimizerPlusSaveComments(); 188 } 189 190 if (PHP_VERSION_ID < 70000) { 191 if (extension_loaded('Zend Optimizer+') && (ini_get('zend_optimizerplus.load_comments') === "0" || ini_get('opcache.load_comments') === "0")) { 192 throw AnnotationException::optimizerPlusLoadComments(); 193 } 194 195 if (extension_loaded('Zend OPcache') && ini_get('opcache.load_comments') == 0) { 196 throw AnnotationException::optimizerPlusLoadComments(); 197 } 198 } 199 200 AnnotationRegistry::registerFile(__DIR__ . '/Annotation/IgnoreAnnotation.php'); 201 202 $this->parser = $parser ?: new DocParser(); 203 204 $this->preParser = new DocParser; 205 206 $this->preParser->setImports(self::$globalImports); 207 $this->preParser->setIgnoreNotImportedAnnotations(true); 208 209 $this->phpParser = new PhpParser; 210 } 211 212 /** 213 * {@inheritDoc} 214 */ 215 public function getClassAnnotations(ReflectionClass $class) 216 { 217 $this->parser->setTarget(Target::TARGET_CLASS); 218 $this->parser->setImports($this->getClassImports($class)); 219 $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); 220 $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 221 222 return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName()); 223 } 224 225 /** 226 * {@inheritDoc} 227 */ 228 public function getClassAnnotation(ReflectionClass $class, $annotationName) 229 { 230 $annotations = $this->getClassAnnotations($class); 231 232 foreach ($annotations as $annotation) { 233 if ($annotation instanceof $annotationName) { 234 return $annotation; 235 } 236 } 237 238 return null; 239 } 240 241 /** 242 * {@inheritDoc} 243 */ 244 public function getPropertyAnnotations(ReflectionProperty $property) 245 { 246 $class = $property->getDeclaringClass(); 247 $context = 'property ' . $class->getName() . "::\$" . $property->getName(); 248 249 $this->parser->setTarget(Target::TARGET_PROPERTY); 250 $this->parser->setImports($this->getPropertyImports($property)); 251 $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); 252 $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 253 254 return $this->parser->parse($property->getDocComment(), $context); 255 } 256 257 /** 258 * {@inheritDoc} 259 */ 260 public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) 261 { 262 $annotations = $this->getPropertyAnnotations($property); 263 264 foreach ($annotations as $annotation) { 265 if ($annotation instanceof $annotationName) { 266 return $annotation; 267 } 268 } 269 270 return null; 271 } 272 273 /** 274 * {@inheritDoc} 275 */ 276 public function getMethodAnnotations(ReflectionMethod $method) 277 { 278 $class = $method->getDeclaringClass(); 279 $context = 'method ' . $class->getName() . '::' . $method->getName() . '()'; 280 281 $this->parser->setTarget(Target::TARGET_METHOD); 282 $this->parser->setImports($this->getMethodImports($method)); 283 $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); 284 $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 285 286 return $this->parser->parse($method->getDocComment(), $context); 287 } 288 289 /** 290 * {@inheritDoc} 291 */ 292 public function getMethodAnnotation(ReflectionMethod $method, $annotationName) 293 { 294 $annotations = $this->getMethodAnnotations($method); 295 296 foreach ($annotations as $annotation) { 297 if ($annotation instanceof $annotationName) { 298 return $annotation; 299 } 300 } 301 302 return null; 303 } 304 305 /** 306 * Returns the ignored annotations for the given class. 307 * 308 * @param \ReflectionClass $class 309 * 310 * @return array 311 */ 312 private function getIgnoredAnnotationNames(ReflectionClass $class) 313 { 314 $name = $class->getName(); 315 if (isset($this->ignoredAnnotationNames[$name])) { 316 return $this->ignoredAnnotationNames[$name]; 317 } 318 319 $this->collectParsingMetadata($class); 320 321 return $this->ignoredAnnotationNames[$name]; 322 } 323 324 /** 325 * Retrieves imports. 326 * 327 * @param \ReflectionClass $class 328 * 329 * @return array 330 */ 331 private function getClassImports(ReflectionClass $class) 332 { 333 $name = $class->getName(); 334 if (isset($this->imports[$name])) { 335 return $this->imports[$name]; 336 } 337 338 $this->collectParsingMetadata($class); 339 340 return $this->imports[$name]; 341 } 342 343 /** 344 * Retrieves imports for methods. 345 * 346 * @param \ReflectionMethod $method 347 * 348 * @return array 349 */ 350 private function getMethodImports(ReflectionMethod $method) 351 { 352 $class = $method->getDeclaringClass(); 353 $classImports = $this->getClassImports($class); 354 if (!method_exists($class, 'getTraits')) { 355 return $classImports; 356 } 357 358 $traitImports = array(); 359 360 foreach ($class->getTraits() as $trait) { 361 if ($trait->hasMethod($method->getName()) 362 && $trait->getFileName() === $method->getFileName() 363 ) { 364 $traitImports = array_merge($traitImports, $this->phpParser->parseClass($trait)); 365 } 366 } 367 368 return array_merge($classImports, $traitImports); 369 } 370 371 /** 372 * Retrieves imports for properties. 373 * 374 * @param \ReflectionProperty $property 375 * 376 * @return array 377 */ 378 private function getPropertyImports(ReflectionProperty $property) 379 { 380 $class = $property->getDeclaringClass(); 381 $classImports = $this->getClassImports($class); 382 if (!method_exists($class, 'getTraits')) { 383 return $classImports; 384 } 385 386 $traitImports = array(); 387 388 foreach ($class->getTraits() as $trait) { 389 if ($trait->hasProperty($property->getName())) { 390 $traitImports = array_merge($traitImports, $this->phpParser->parseClass($trait)); 391 } 392 } 393 394 return array_merge($classImports, $traitImports); 395 } 396 397 /** 398 * Collects parsing metadata for a given class. 399 * 400 * @param \ReflectionClass $class 401 */ 402 private function collectParsingMetadata(ReflectionClass $class) 403 { 404 $ignoredAnnotationNames = self::$globalIgnoredNames; 405 $annotations = $this->preParser->parse($class->getDocComment(), 'class ' . $class->name); 406 407 foreach ($annotations as $annotation) { 408 if ($annotation instanceof IgnoreAnnotation) { 409 foreach ($annotation->names AS $annot) { 410 $ignoredAnnotationNames[$annot] = true; 411 } 412 } 413 } 414 415 $name = $class->getName(); 416 417 $this->imports[$name] = array_merge( 418 self::$globalImports, 419 $this->phpParser->parseClass($class), 420 array('__NAMESPACE__' => $class->getNamespaceName()) 421 ); 422 423 $this->ignoredAnnotationNames[$name] = $ignoredAnnotationNames; 424 } 425} 426