Chris@0: . Chris@0: */ Chris@0: Chris@0: namespace Doctrine\Common\Annotations; Chris@0: Chris@0: use Doctrine\Common\Annotations\Annotation\IgnoreAnnotation; Chris@0: use Doctrine\Common\Annotations\Annotation\Target; Chris@0: use ReflectionClass; Chris@0: use ReflectionMethod; Chris@0: use ReflectionProperty; Chris@0: Chris@0: /** Chris@0: * A reader for docblock annotations. Chris@0: * Chris@0: * @author Benjamin Eberlei Chris@0: * @author Guilherme Blanco Chris@0: * @author Jonathan Wage Chris@0: * @author Roman Borschel Chris@0: * @author Johannes M. Schmitt Chris@0: */ Chris@0: class AnnotationReader implements Reader Chris@0: { Chris@0: /** Chris@0: * Global map for imports. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: private static $globalImports = array( Chris@0: 'ignoreannotation' => 'Doctrine\Common\Annotations\Annotation\IgnoreAnnotation', Chris@0: ); Chris@0: Chris@0: /** Chris@0: * A list with annotations that are not causing exceptions when not resolved to an annotation class. Chris@0: * Chris@0: * The names are case sensitive. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: private static $globalIgnoredNames = array( Chris@0: // Annotation tags Chris@0: 'Annotation' => true, 'Attribute' => true, 'Attributes' => true, Chris@0: /* Can we enable this? 'Enum' => true, */ Chris@0: 'Required' => true, Chris@0: 'Target' => true, Chris@0: // Widely used tags (but not existent in phpdoc) Chris@0: 'fix' => true , 'fixme' => true, Chris@0: 'override' => true, Chris@0: // PHPDocumentor 1 tags Chris@0: 'abstract'=> true, 'access'=> true, Chris@0: 'code' => true, Chris@0: 'deprec'=> true, Chris@0: 'endcode' => true, 'exception'=> true, Chris@0: 'final'=> true, Chris@0: 'ingroup' => true, 'inheritdoc'=> true, 'inheritDoc'=> true, Chris@0: 'magic' => true, Chris@0: 'name'=> true, Chris@0: 'toc' => true, 'tutorial'=> true, Chris@0: 'private' => true, Chris@0: 'static'=> true, 'staticvar'=> true, 'staticVar'=> true, Chris@0: 'throw' => true, Chris@0: // PHPDocumentor 2 tags. Chris@0: 'api' => true, 'author'=> true, Chris@0: 'category'=> true, 'copyright'=> true, Chris@0: 'deprecated'=> true, Chris@0: 'example'=> true, Chris@0: 'filesource'=> true, Chris@0: 'global'=> true, Chris@0: 'ignore'=> true, /* Can we enable this? 'index' => true, */ 'internal'=> true, Chris@0: 'license'=> true, 'link'=> true, Chris@0: 'method' => true, Chris@0: 'package'=> true, 'param'=> true, 'property' => true, 'property-read' => true, 'property-write' => true, Chris@0: 'return'=> true, Chris@0: 'see'=> true, 'since'=> true, 'source' => true, 'subpackage'=> true, Chris@0: 'throws'=> true, 'todo'=> true, 'TODO'=> true, Chris@0: 'usedby'=> true, 'uses' => true, Chris@0: 'var'=> true, 'version'=> true, Chris@0: // PHPUnit tags Chris@0: 'codeCoverageIgnore' => true, 'codeCoverageIgnoreStart' => true, 'codeCoverageIgnoreEnd' => true, Chris@0: // PHPCheckStyle Chris@0: 'SuppressWarnings' => true, Chris@0: // PHPStorm Chris@0: 'noinspection' => true, Chris@0: // PEAR Chris@0: 'package_version' => true, Chris@0: // PlantUML Chris@0: 'startuml' => true, 'enduml' => true, Chris@0: ); Chris@0: Chris@0: /** Chris@12: * A list with annotations that are not causing exceptions when not resolved to an annotation class. Chris@12: * Chris@12: * The names are case sensitive. Chris@12: * Chris@12: * @var array Chris@12: */ Chris@12: private static $globalIgnoredNamespaces = array(); Chris@12: Chris@12: /** Chris@0: * Add a new annotation to the globally ignored annotation names with regard to exception handling. Chris@0: * Chris@0: * @param string $name Chris@0: */ Chris@0: static public function addGlobalIgnoredName($name) Chris@0: { Chris@0: self::$globalIgnoredNames[$name] = true; Chris@0: } Chris@0: Chris@0: /** Chris@12: * Add a new annotation to the globally ignored annotation namespaces with regard to exception handling. Chris@12: * Chris@12: * @param string $namespace Chris@12: */ Chris@12: static public function addGlobalIgnoredNamespace($namespace) Chris@12: { Chris@12: self::$globalIgnoredNamespaces[$namespace] = true; Chris@12: } Chris@12: Chris@12: /** Chris@0: * Annotations parser. Chris@0: * Chris@0: * @var \Doctrine\Common\Annotations\DocParser Chris@0: */ Chris@0: private $parser; Chris@0: Chris@0: /** Chris@0: * Annotations parser used to collect parsing metadata. Chris@0: * Chris@0: * @var \Doctrine\Common\Annotations\DocParser Chris@0: */ Chris@0: private $preParser; Chris@0: Chris@0: /** Chris@0: * PHP parser used to collect imports. Chris@0: * Chris@0: * @var \Doctrine\Common\Annotations\PhpParser Chris@0: */ Chris@0: private $phpParser; Chris@0: Chris@0: /** Chris@0: * In-memory cache mechanism to store imported annotations per class. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: private $imports = array(); Chris@0: Chris@0: /** Chris@0: * In-memory cache mechanism to store ignored annotations per class. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: private $ignoredAnnotationNames = array(); Chris@0: Chris@0: /** Chris@0: * Constructor. Chris@0: * Chris@0: * Initializes a new AnnotationReader. Chris@12: * Chris@12: * @param DocParser $parser Chris@0: */ Chris@12: public function __construct(DocParser $parser = null) Chris@0: { Chris@0: if (extension_loaded('Zend Optimizer+') && (ini_get('zend_optimizerplus.save_comments') === "0" || ini_get('opcache.save_comments') === "0")) { Chris@0: throw AnnotationException::optimizerPlusSaveComments(); Chris@0: } Chris@0: Chris@0: if (extension_loaded('Zend OPcache') && ini_get('opcache.save_comments') == 0) { Chris@0: throw AnnotationException::optimizerPlusSaveComments(); Chris@0: } Chris@0: Chris@0: if (PHP_VERSION_ID < 70000) { Chris@0: if (extension_loaded('Zend Optimizer+') && (ini_get('zend_optimizerplus.load_comments') === "0" || ini_get('opcache.load_comments') === "0")) { Chris@0: throw AnnotationException::optimizerPlusLoadComments(); Chris@0: } Chris@0: Chris@0: if (extension_loaded('Zend OPcache') && ini_get('opcache.load_comments') == 0) { Chris@0: throw AnnotationException::optimizerPlusLoadComments(); Chris@0: } Chris@0: } Chris@0: Chris@0: AnnotationRegistry::registerFile(__DIR__ . '/Annotation/IgnoreAnnotation.php'); Chris@0: Chris@12: $this->parser = $parser ?: new DocParser(); Chris@12: Chris@0: $this->preParser = new DocParser; Chris@0: Chris@0: $this->preParser->setImports(self::$globalImports); Chris@0: $this->preParser->setIgnoreNotImportedAnnotations(true); Chris@0: Chris@0: $this->phpParser = new PhpParser; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritDoc} Chris@0: */ Chris@0: public function getClassAnnotations(ReflectionClass $class) Chris@0: { Chris@0: $this->parser->setTarget(Target::TARGET_CLASS); Chris@0: $this->parser->setImports($this->getClassImports($class)); Chris@0: $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); Chris@12: $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); Chris@0: Chris@0: return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritDoc} Chris@0: */ Chris@0: public function getClassAnnotation(ReflectionClass $class, $annotationName) Chris@0: { Chris@0: $annotations = $this->getClassAnnotations($class); Chris@0: Chris@0: foreach ($annotations as $annotation) { Chris@0: if ($annotation instanceof $annotationName) { Chris@0: return $annotation; Chris@0: } Chris@0: } Chris@0: Chris@0: return null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritDoc} Chris@0: */ Chris@0: public function getPropertyAnnotations(ReflectionProperty $property) Chris@0: { Chris@0: $class = $property->getDeclaringClass(); Chris@0: $context = 'property ' . $class->getName() . "::\$" . $property->getName(); Chris@0: Chris@0: $this->parser->setTarget(Target::TARGET_PROPERTY); Chris@0: $this->parser->setImports($this->getPropertyImports($property)); Chris@0: $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); Chris@12: $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); Chris@0: Chris@0: return $this->parser->parse($property->getDocComment(), $context); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritDoc} Chris@0: */ Chris@0: public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) Chris@0: { Chris@0: $annotations = $this->getPropertyAnnotations($property); Chris@0: Chris@0: foreach ($annotations as $annotation) { Chris@0: if ($annotation instanceof $annotationName) { Chris@0: return $annotation; Chris@0: } Chris@0: } Chris@0: Chris@0: return null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritDoc} Chris@0: */ Chris@0: public function getMethodAnnotations(ReflectionMethod $method) Chris@0: { Chris@0: $class = $method->getDeclaringClass(); Chris@0: $context = 'method ' . $class->getName() . '::' . $method->getName() . '()'; Chris@0: Chris@0: $this->parser->setTarget(Target::TARGET_METHOD); Chris@0: $this->parser->setImports($this->getMethodImports($method)); Chris@0: $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); Chris@12: $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); Chris@0: Chris@0: return $this->parser->parse($method->getDocComment(), $context); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritDoc} Chris@0: */ Chris@0: public function getMethodAnnotation(ReflectionMethod $method, $annotationName) Chris@0: { Chris@0: $annotations = $this->getMethodAnnotations($method); Chris@0: Chris@0: foreach ($annotations as $annotation) { Chris@0: if ($annotation instanceof $annotationName) { Chris@0: return $annotation; Chris@0: } Chris@0: } Chris@0: Chris@0: return null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the ignored annotations for the given class. Chris@0: * Chris@0: * @param \ReflectionClass $class Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function getIgnoredAnnotationNames(ReflectionClass $class) Chris@0: { Chris@12: $name = $class->getName(); Chris@12: if (isset($this->ignoredAnnotationNames[$name])) { Chris@0: return $this->ignoredAnnotationNames[$name]; Chris@0: } Chris@0: Chris@0: $this->collectParsingMetadata($class); Chris@0: Chris@0: return $this->ignoredAnnotationNames[$name]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Retrieves imports. Chris@0: * Chris@0: * @param \ReflectionClass $class Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function getClassImports(ReflectionClass $class) Chris@0: { Chris@12: $name = $class->getName(); Chris@12: if (isset($this->imports[$name])) { Chris@0: return $this->imports[$name]; Chris@0: } Chris@0: Chris@0: $this->collectParsingMetadata($class); Chris@0: Chris@0: return $this->imports[$name]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Retrieves imports for methods. Chris@0: * Chris@0: * @param \ReflectionMethod $method Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function getMethodImports(ReflectionMethod $method) Chris@0: { Chris@0: $class = $method->getDeclaringClass(); Chris@0: $classImports = $this->getClassImports($class); Chris@0: if (!method_exists($class, 'getTraits')) { Chris@0: return $classImports; Chris@0: } Chris@0: Chris@0: $traitImports = array(); Chris@0: Chris@0: foreach ($class->getTraits() as $trait) { Chris@0: if ($trait->hasMethod($method->getName()) Chris@0: && $trait->getFileName() === $method->getFileName() Chris@0: ) { Chris@0: $traitImports = array_merge($traitImports, $this->phpParser->parseClass($trait)); Chris@0: } Chris@0: } Chris@0: Chris@0: return array_merge($classImports, $traitImports); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Retrieves imports for properties. Chris@0: * Chris@0: * @param \ReflectionProperty $property Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function getPropertyImports(ReflectionProperty $property) Chris@0: { Chris@0: $class = $property->getDeclaringClass(); Chris@0: $classImports = $this->getClassImports($class); Chris@0: if (!method_exists($class, 'getTraits')) { Chris@0: return $classImports; Chris@0: } Chris@0: Chris@0: $traitImports = array(); Chris@0: Chris@0: foreach ($class->getTraits() as $trait) { Chris@0: if ($trait->hasProperty($property->getName())) { Chris@0: $traitImports = array_merge($traitImports, $this->phpParser->parseClass($trait)); Chris@0: } Chris@0: } Chris@0: Chris@0: return array_merge($classImports, $traitImports); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Collects parsing metadata for a given class. Chris@0: * Chris@0: * @param \ReflectionClass $class Chris@0: */ Chris@0: private function collectParsingMetadata(ReflectionClass $class) Chris@0: { Chris@0: $ignoredAnnotationNames = self::$globalIgnoredNames; Chris@0: $annotations = $this->preParser->parse($class->getDocComment(), 'class ' . $class->name); Chris@0: Chris@0: foreach ($annotations as $annotation) { Chris@0: if ($annotation instanceof IgnoreAnnotation) { Chris@0: foreach ($annotation->names AS $annot) { Chris@0: $ignoredAnnotationNames[$annot] = true; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $name = $class->getName(); Chris@0: Chris@0: $this->imports[$name] = array_merge( Chris@0: self::$globalImports, Chris@0: $this->phpParser->parseClass($class), Chris@0: array('__NAMESPACE__' => $class->getNamespaceName()) Chris@0: ); Chris@0: Chris@0: $this->ignoredAnnotationNames[$name] = $ignoredAnnotationNames; Chris@0: } Chris@0: }