diff --git a/lib/Doctrine/Annotations/Annotation/Target.php b/lib/Doctrine/Annotations/Annotation/Target.php index 502910dec..3e5b7371a 100644 --- a/lib/Doctrine/Annotations/Annotation/Target.php +++ b/lib/Doctrine/Annotations/Annotation/Target.php @@ -2,6 +2,8 @@ namespace Doctrine\Annotations\Annotation; +use Doctrine\Annotations\Metadata\AnnotationTarget; + /** * Annotation that can be used to signal to the parser * to check the annotation target during the parsing process. @@ -12,11 +14,11 @@ */ final class Target { - const TARGET_CLASS = 1; - const TARGET_METHOD = 2; - const TARGET_PROPERTY = 4; - const TARGET_ANNOTATION = 8; - const TARGET_ALL = 15; + public const TARGET_CLASS = AnnotationTarget::TARGET_CLASS; + public const TARGET_METHOD = AnnotationTarget::TARGET_METHOD; + public const TARGET_PROPERTY = AnnotationTarget::TARGET_PROPERTY; + public const TARGET_ANNOTATION = AnnotationTarget::TARGET_ANNOTATION; + public const TARGET_ALL = AnnotationTarget::TARGET_ALL; /** * @var array diff --git a/lib/Doctrine/Annotations/DocParser.php b/lib/Doctrine/Annotations/DocParser.php index 62f719270..445d2d043 100644 --- a/lib/Doctrine/Annotations/DocParser.php +++ b/lib/Doctrine/Annotations/DocParser.php @@ -3,10 +3,18 @@ namespace Doctrine\Annotations; use Doctrine\Annotations\Annotation\Attribute; +use Doctrine\Annotations\Metadata\AnnotationTarget; +use Doctrine\Annotations\Metadata\Builder\AnnotationMetadataBuilder; +use Doctrine\Annotations\Metadata\Builder\PropertyMetadataBuilder; +use Doctrine\Annotations\Metadata\InternalAnnotations; +use Doctrine\Annotations\Metadata\MetadataCollection; +use Doctrine\Annotations\Metadata\TransientMetadataCollection; use ReflectionClass; use Doctrine\Annotations\Annotation\Enum; use Doctrine\Annotations\Annotation\Target; use Doctrine\Annotations\Annotation\Attributes; +use function array_key_exists; +use function array_keys; /** * A parser for docblock annotations. @@ -118,93 +126,12 @@ final class DocParser /** * Hash-map for caching annotation metadata. * - * @var array + * @var MetadataCollection */ - private static $annotationMetadata = [ - 'Doctrine\Annotations\Annotation\Target' => [ - 'is_annotation' => true, - 'has_constructor' => true, - 'properties' => [], - 'targets_literal' => 'ANNOTATION_CLASS', - 'targets' => Target::TARGET_CLASS, - 'default_property' => 'value', - 'attribute_types' => [ - 'value' => [ - 'required' => false, - 'type' =>'array', - 'array_type'=>'string', - 'value' =>'array' - ] - ], - ], - 'Doctrine\Annotations\Annotation\Attribute' => [ - 'is_annotation' => true, - 'has_constructor' => false, - 'targets_literal' => 'ANNOTATION_ANNOTATION', - 'targets' => Target::TARGET_ANNOTATION, - 'default_property' => 'name', - 'properties' => [ - 'name' => 'name', - 'type' => 'type', - 'required' => 'required' - ], - 'attribute_types' => [ - 'value' => [ - 'required' => true, - 'type' =>'string', - 'value' =>'string' - ], - 'type' => [ - 'required' =>true, - 'type' =>'string', - 'value' =>'string' - ], - 'required' => [ - 'required' =>false, - 'type' =>'boolean', - 'value' =>'boolean' - ] - ], - ], - 'Doctrine\Annotations\Annotation\Attributes' => [ - 'is_annotation' => true, - 'has_constructor' => false, - 'targets_literal' => 'ANNOTATION_CLASS', - 'targets' => Target::TARGET_CLASS, - 'default_property' => 'value', - 'properties' => [ - 'value' => 'value' - ], - 'attribute_types' => [ - 'value' => [ - 'type' =>'array', - 'required' =>true, - 'array_type'=>'Doctrine\Annotations\Annotation\Attribute', - 'value' =>'array' - ] - ], - ], - 'Doctrine\Annotations\Annotation\Enum' => [ - 'is_annotation' => true, - 'has_constructor' => true, - 'targets_literal' => 'ANNOTATION_PROPERTY', - 'targets' => Target::TARGET_PROPERTY, - 'default_property' => 'value', - 'properties' => [ - 'value' => 'value' - ], - 'attribute_types' => [ - 'value' => [ - 'type' => 'array', - 'required' => true, - ], - 'literal' => [ - 'type' => 'array', - 'required' => false, - ], - ], - ], - ]; + private $metadata; + + /** @var array */ + private $nonAnnotationClasses = []; /** * Hash-map for handle types declaration. @@ -224,7 +151,8 @@ final class DocParser */ public function __construct() { - $this->lexer = new DocLexer; + $this->lexer = new DocLexer; + $this->metadata = InternalAnnotations::createMetadata(); } /** @@ -468,85 +396,99 @@ private function collectAnnotationMetadata($name) ]); } - $class = new \ReflectionClass($name); + $class = new ReflectionClass($name); $docComment = $class->getDocComment(); - // Sets default values for annotation metadata - $metadata = [ - 'default_property' => null, - 'has_constructor' => (null !== $constructor = $class->getConstructor()) && $constructor->getNumberOfParameters() > 0, - 'properties' => [], - 'property_types' => [], - 'attribute_types' => [], - 'targets_literal' => null, - 'targets' => Target::TARGET_ALL, - 'is_annotation' => false !== strpos($docComment, '@Annotation'), - ]; - // verify that the class is really meant to be an annotation - if ($metadata['is_annotation']) { - self::$metadataParser->setTarget(Target::TARGET_CLASS); + if (strpos($docComment, '@Annotation') === false) { + $this->nonAnnotationClasses[$name] = true; + return; + } - foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) { - if ($annotation instanceof Target) { - $metadata['targets'] = $annotation->targets; - $metadata['targets_literal'] = $annotation->literal; + $constructor = $class->getConstructor(); + $useConstructor = $constructor !== null && $constructor->getNumberOfParameters() > 0; + $annotationBuilder = new AnnotationMetadataBuilder($name); - continue; - } + if ($useConstructor) { + $annotationBuilder->withUsingConstructor(); + } - if ($annotation instanceof Attributes) { - foreach ($annotation->value as $attribute) { - $this->collectAttributeTypeMetadata($metadata, $attribute); - } + self::$metadataParser->setTarget(Target::TARGET_CLASS); + + foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) { + if ($annotation instanceof Target) { + $annotationBuilder->withTarget(AnnotationTarget::fromAnnotation($annotation)); + + continue; + } + + if ($annotation instanceof Attributes) { + foreach ($annotation->value as $attribute) { + $propertyBuilder = new PropertyMetadataBuilder($attribute->name); + + $this->collectAttributeTypeMetadata($propertyBuilder, $attribute); + $annotationBuilder->withProperty($propertyBuilder->build()); } } + } - // if not has a constructor will inject values into public properties - if (false === $metadata['has_constructor']) { - // collect all public properties - foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { - $metadata['properties'][$property->name] = $property->name; + if ($useConstructor) { + $this->metadata->add($annotationBuilder->build()); - if (false === ($propertyComment = $property->getDocComment())) { - continue; - } + return; + } - $attribute = new Attribute(); + // if there is no constructor we will inject values into public properties - $attribute->required = (false !== strpos($propertyComment, '@Required')); - $attribute->name = $property->name; - $attribute->type = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches)) - ? $matches[1] - : 'mixed'; + // collect all public properties + foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $i => $property) { + $propertyBuilder = new PropertyMetadataBuilder($property->getName()); + $propertyComment = $property->getDocComment(); - $this->collectAttributeTypeMetadata($metadata, $attribute); + if ($i === 0) { + $propertyBuilder->withBeingDefault(); + } - // checks if the property has @Enum - if (false !== strpos($propertyComment, '@Enum')) { - $context = 'property ' . $class->name . "::\$" . $property->name; - self::$metadataParser->setTarget(Target::TARGET_PROPERTY); + if ($propertyComment === false) { + $annotationBuilder->withProperty($propertyBuilder->build()); - foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) { - if ( ! $annotation instanceof Enum) { - continue; - } + continue; + } - $metadata['enum'][$property->name]['value'] = $annotation->value; - $metadata['enum'][$property->name]['literal'] = ( ! empty($annotation->literal)) - ? $annotation->literal - : $annotation->value; - } + $attribute = new Attribute(); + $attribute->required = (false !== strpos($propertyComment, '@Required')); + $attribute->name = $property->name; + $attribute->type = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches)) + ? $matches[1] + : 'mixed'; + + $this->collectAttributeTypeMetadata($propertyBuilder, $attribute); + + // checks if the property has @Enum + if (false !== strpos($propertyComment, '@Enum')) { + $context = 'property ' . $class->name . "::\$" . $property->name; + + self::$metadataParser->setTarget(Target::TARGET_PROPERTY); + + foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) { + if ( ! $annotation instanceof Enum) { + continue; } - } - // choose the first property as default property - $metadata['default_property'] = reset($metadata['properties']); + $propertyBuilder = $propertyBuilder->withEnum([ + 'value' => $annotation->value, + 'literal' => ( ! empty($annotation->literal)) + ? $annotation->literal + : $annotation->value, + ]); + } } + + $annotationBuilder->withProperty($propertyBuilder->build()); } - self::$annotationMetadata[$name] = $metadata; + $this->metadata->add($annotationBuilder->build()); } /** @@ -554,11 +496,11 @@ private function collectAnnotationMetadata($name) * * @param array $metadata * @param Attribute $attribute - * - * @return void */ - private function collectAttributeTypeMetadata(&$metadata, Attribute $attribute) - { + private function collectAttributeTypeMetadata( + PropertyMetadataBuilder $metadata, + Attribute $attribute + ) : void { // handle internal type declaration $type = self::$typeMap[$attribute->type] ?? $attribute->type; @@ -567,36 +509,40 @@ private function collectAttributeTypeMetadata(&$metadata, Attribute $attribute) return; } + if ($attribute->required) { + $metadata->withBeingRequired(); + } + // Evaluate type - switch (true) { - // Checks if the property has array - case (false !== $pos = strpos($type, '<')): - $arrayType = substr($type, $pos + 1, -1); - $type = 'array'; - - if (isset(self::$typeMap[$arrayType])) { - $arrayType = self::$typeMap[$arrayType]; - } - $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; - break; + // Checks if the property has array + if (false !== $pos = strpos($type, '<')) { + $arrayType = substr($type, $pos + 1, -1); + + $metadata->withType([ + 'type' => 'array', + 'array_type' => self::$typeMap[$arrayType] ?? $arrayType, + ]); - // Checks if the property has type[] - case (false !== $pos = strrpos($type, '[')): - $arrayType = substr($type, 0, $pos); - $type = 'array'; + return; + } - if (isset(self::$typeMap[$arrayType])) { - $arrayType = self::$typeMap[$arrayType]; - } + // Checks if the property has type[] + if (false !== $pos = strrpos($type, '[')) { + $arrayType = substr($type, 0, $pos); - $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; - break; + $metadata->withType([ + 'type' => 'array', + 'array_type' => self::$typeMap[$arrayType] ?? $arrayType, + ]); + + return; } - $metadata['attribute_types'][$attribute->name]['type'] = $type; - $metadata['attribute_types'][$attribute->name]['value'] = $attribute->type; - $metadata['attribute_types'][$attribute->name]['required'] = $attribute->required; + $metadata->withType([ + 'type' => $type, + 'value' => $attribute->type, + ]); } /** @@ -710,12 +656,12 @@ private function Annotation() // collects the metadata annotation only if there is not yet - if ( ! isset(self::$annotationMetadata[$name])) { + if (! $this->metadata->has($name) && ! array_key_exists($name, $this->nonAnnotationClasses)) { $this->collectAnnotationMetadata($name); } // verify that the class is really meant to be an annotation and not just any ordinary class - if (self::$annotationMetadata[$name]['is_annotation'] === false) { + if (array_key_exists($name, $this->nonAnnotationClasses)) { if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$originalName])) { return false; } @@ -728,80 +674,97 @@ private function Annotation() // Next will be nested $this->isNestedAnnotation = true; + $metadata = $this->metadata->get($name); //if annotation does not support current target - if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) { + if (($metadata->getTarget()->unwrap() & $target) === 0 && $target) { throw AnnotationException::semanticalError( sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.', - $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal']) + $originalName, $this->context, $metadata->getTarget()->describe()) ); } $values = $this->MethodCall(); - if (isset(self::$annotationMetadata[$name]['enum'])) { - // checks all declared attributes - foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) { - // checks if the attribute is a valid enumerator - if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) { - throw AnnotationException::enumeratorError($property, $name, $this->context, $enum['literal'], $values[$property]); - } + // checks all declared attributes for enums + foreach ($metadata->getProperties() as $property) { + $propertyName = $property->getName(); + $enum = $property->getEnum(); + + // checks if the attribute is a valid enumerator + if ($enum !== null && isset($values[$propertyName]) && ! in_array($values[$propertyName], $enum['value'])) { + throw AnnotationException::enumeratorError($propertyName, $name, $this->context, $enum['literal'], $values[$propertyName]); } } // checks all declared attributes - foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) { - if ($property === self::$annotationMetadata[$name]['default_property'] - && !isset($values[$property]) && isset($values['value'])) { - $property = 'value'; + foreach ($metadata->getProperties() as $property) { + $propertyName = $property->getName(); + $valueName = $propertyName; + $type = $property->getType(); + + if ($property->isDefault() && !isset($values[$propertyName]) && isset($values['value'])) { + $valueName = 'value'; } // handle a not given attribute or null value - if (!isset($values[$property])) { - if ($type['required']) { - throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']); + if (! isset($values[$valueName])) { + if ($property->isRequired()) { + throw AnnotationException::requiredError($propertyName, $originalName, $this->context, 'a(n) ' . $type['value']); } continue; } - if ($type['type'] === 'array') { + if ($type !== null && $type['type'] === 'array') { // handle the case of a single value - if ( ! is_array($values[$property])) { - $values[$property] = [$values[$property]]; + if ( ! is_array($values[$valueName])) { + $values[$valueName] = [$values[$valueName]]; } // checks if the attribute has array type declaration, such as "array" if (isset($type['array_type'])) { - foreach ($values[$property] as $item) { + foreach ($values[$valueName] as $item) { if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) { - throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item); + throw AnnotationException::attributeTypeError($propertyName, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item); } } } - } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) { - throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]); + } elseif ($type !== null && gettype($values[$valueName]) !== $type['type'] && !$values[$valueName] instanceof $type['type']) { + throw AnnotationException::attributeTypeError($propertyName, $originalName, $this->context, 'a(n) '.$type['value'], $values[$valueName]); } } // check if the annotation expects values via the constructor, // or directly injected into public properties - if (self::$annotationMetadata[$name]['has_constructor'] === true) { + if ($metadata->usesConstructor()) { return new $name($values); } $instance = new $name(); foreach ($values as $property => $value) { - if (!isset(self::$annotationMetadata[$name]['properties'][$property])) { + if (! isset($metadata->getProperties()[$property])) { if ('value' !== $property) { - 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']))); + 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(', ', array_keys($metadata->getProperties())) + ) + ); } + $defaultProperty = $metadata->getDefaultProperty(); + // handle the case if the property has no annotations - if ( ! $property = self::$annotationMetadata[$name]['default_property']) { + if ($defaultProperty === null) { throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not accept any values, but got %s.', $originalName, $this->context, json_encode($values))); } + + $property = $defaultProperty->getName(); } $instance->{$property} = $value; diff --git a/lib/Doctrine/Annotations/Metadata/AnnotationMetadata.php b/lib/Doctrine/Annotations/Metadata/AnnotationMetadata.php new file mode 100644 index 000000000..ceae9f8bf --- /dev/null +++ b/lib/Doctrine/Annotations/Metadata/AnnotationMetadata.php @@ -0,0 +1,98 @@ + */ + private $properties; + + /** @var PropertyMetadata|null */ + private $defaultProperty; + + /** + * @param PropertyMetadata[] $properties + */ + public function __construct( + string $name, + AnnotationTarget $target, + bool $hasConstructor, + PropertyMetadata ...$properties + ) { + $this->name = $name; + $this->target = $target; + $this->usesConstructor = $hasConstructor; + $this->properties = array_combine( + array_map( + static function (PropertyMetadata $property) : string { + return $property->getName(); + }, + $properties + ), + $properties + ); + + $defaultProperties = array_filter( + $properties, + static function (PropertyMetadata $property) : bool { + return $property->isDefault(); + } + ); + + if (count($defaultProperties) > 1) { + throw TooManyDefaultProperties::new($name, ...$defaultProperties); + } + + $this->defaultProperty = array_values($defaultProperties)[0] ?? null; + } + + public function getName() : string + { + return $this->name; + } + + public function getTarget() : AnnotationTarget + { + return $this->target; + } + + public function usesConstructor() : bool + { + return $this->usesConstructor; + } + + /** + * @return array + */ + public function getProperties() : array + { + return $this->properties; + } + + public function getDefaultProperty() : ?PropertyMetadata + { + return $this->defaultProperty; + } +} diff --git a/lib/Doctrine/Annotations/Metadata/AnnotationTarget.php b/lib/Doctrine/Annotations/Metadata/AnnotationTarget.php new file mode 100644 index 000000000..fd837358f --- /dev/null +++ b/lib/Doctrine/Annotations/Metadata/AnnotationTarget.php @@ -0,0 +1,122 @@ + 'CLASS', + self::TARGET_METHOD => 'METHOD', + self::TARGET_PROPERTY => 'PROPERTY', + self::TARGET_ANNOTATION => 'ANNOTATION', + self::TARGET_ALL => 'ALL', + ]; + + /** @var int */ + private $target; + + /** + * @throws InvalidAnnotationTarget + */ + public function __construct(int $target) + { + if ($target < 0 || $target > self::TARGET_ALL) { + throw InvalidAnnotationTarget::fromInvalidBitmask($target); + } + + $this->target = $target; + } + + public static function class() : self + { + return new self(self::TARGET_CLASS); + } + + public static function method() : self + { + return new self(self::TARGET_METHOD); + } + + public static function property() : self + { + return new self(self::TARGET_PROPERTY); + } + + public static function annotation() : self + { + return new self(self::TARGET_ANNOTATION); + } + + public static function all() : self + { + return new self(self::TARGET_ALL); + } + + public static function fromAnnotation(Target $annotation) : self + { + return new self($annotation->targets); + } + + public function unwrap() : int + { + return $this->target; + } + + public function targetsClass() : bool + { + return ($this->target & self::TARGET_CLASS) === self::TARGET_CLASS; + } + + public function targetsMethod() : bool + { + return ($this->target & self::TARGET_METHOD) === self::TARGET_METHOD; + } + + public function targetsProperty() : bool + { + return ($this->target & self::TARGET_PROPERTY) === self::TARGET_PROPERTY; + } + + public function targetsAnnotation() : bool + { + return ($this->target & self::TARGET_ANNOTATION) === self::TARGET_ANNOTATION; + } + + public function describe() : string + { + if ($this->target === self::TARGET_ALL) { + return self::LABELS[self::TARGET_ALL]; + } + + return implode( + ', ', + array_filter( + self::LABELS, + function (int $target) : bool { + return ($this->target & $target) === $target; + }, + ARRAY_FILTER_USE_KEY + ) + ); + } +} diff --git a/lib/Doctrine/Annotations/Metadata/Builder/AnnotationMetadataBuilder.php b/lib/Doctrine/Annotations/Metadata/Builder/AnnotationMetadataBuilder.php new file mode 100644 index 000000000..c37332763 --- /dev/null +++ b/lib/Doctrine/Annotations/Metadata/Builder/AnnotationMetadataBuilder.php @@ -0,0 +1,64 @@ +name = $name; + $this->target = AnnotationTarget::all(); + } + + public function withTarget(AnnotationTarget $target) : self + { + $this->target = $target; + + return $this; + } + + public function withUsingConstructor() : self + { + $this->usesConstructor = true; + + return $this; + } + + public function withProperty(PropertyMetadata $property) : self + { + $this->properties[] = $property; + + return $this; + } + + public function build() : AnnotationMetadata + { + return new AnnotationMetadata( + $this->name, + $this->target, + $this->usesConstructor, + ...$this->properties + ); + } +} diff --git a/lib/Doctrine/Annotations/Metadata/Builder/PropertyMetadataBuilder.php b/lib/Doctrine/Annotations/Metadata/Builder/PropertyMetadataBuilder.php new file mode 100644 index 000000000..a0df6c6ec --- /dev/null +++ b/lib/Doctrine/Annotations/Metadata/Builder/PropertyMetadataBuilder.php @@ -0,0 +1,78 @@ +|null */ + private $enum; + + public function __construct(string $name) + { + $this->name = $name; + } + + /** + * @param string[] $type + */ + public function withType(array $type) : self + { + $this->type = $type; + + return $this; + } + + public function withBeingRequired() : self + { + $this->required = true; + + return $this; + } + + public function withBeingDefault() : self + { + $this->default = true; + + return $this; + } + + /** + * @param array|null $enum + */ + public function withEnum(array $enum) : self + { + $this->enum = $enum; + + return $this; + } + + public function build() : PropertyMetadata + { + return new PropertyMetadata( + $this->name, + $this->type, + $this->required, + $this->default, + $this->enum + ); + } +} diff --git a/lib/Doctrine/Annotations/Metadata/Exception/InvalidAnnotationTarget.php b/lib/Doctrine/Annotations/Metadata/Exception/InvalidAnnotationTarget.php new file mode 100644 index 000000000..cdc32fed2 --- /dev/null +++ b/lib/Doctrine/Annotations/Metadata/Exception/InvalidAnnotationTarget.php @@ -0,0 +1,16 @@ +getName())); + } +} diff --git a/lib/Doctrine/Annotations/Metadata/Exception/MetadataDoesNotExist.php b/lib/Doctrine/Annotations/Metadata/Exception/MetadataDoesNotExist.php new file mode 100644 index 000000000..874d724e3 --- /dev/null +++ b/lib/Doctrine/Annotations/Metadata/Exception/MetadataDoesNotExist.php @@ -0,0 +1,16 @@ +getName(); + }, + $properties + ) + ) + ) + ); + } +} diff --git a/lib/Doctrine/Annotations/Metadata/InternalAnnotations.php b/lib/Doctrine/Annotations/Metadata/InternalAnnotations.php new file mode 100644 index 000000000..3a930e6f4 --- /dev/null +++ b/lib/Doctrine/Annotations/Metadata/InternalAnnotations.php @@ -0,0 +1,89 @@ + 'string'], + true, + true + ), + new PropertyMetadata( + 'type', + ['type' => 'string'], + true + ), + new PropertyMetadata( + 'required', + ['type' => 'boolean'] + ) + ), + new AnnotationMetadata( + Attributes::class, + AnnotationTarget::class(), + false, + new PropertyMetadata( + 'value', + [ + 'type' => 'array', + 'array_type' =>Attribute::class, + 'value' =>'array<' . Attribute::class . '>', + ], + true, + true + ) + ), + new AnnotationMetadata( + Enum::class, + AnnotationTarget::property(), + true, + new PropertyMetadata( + 'value', + ['type' => 'array'], + true, + true + ), + new PropertyMetadata( + 'literal', + ['type' => 'array'] + ) + ), + new AnnotationMetadata( + Target::class, + AnnotationTarget::class(), + true, + new PropertyMetadata( + 'value', + [ + 'type' =>'array', + 'array_type'=>'string', + 'value' =>'array', + ], + false, + true + ) + ) + ); + } +} diff --git a/lib/Doctrine/Annotations/Metadata/MetadataCollection.php b/lib/Doctrine/Annotations/Metadata/MetadataCollection.php new file mode 100644 index 000000000..0de4406f5 --- /dev/null +++ b/lib/Doctrine/Annotations/Metadata/MetadataCollection.php @@ -0,0 +1,38 @@ + + */ + public function getIterator() : Traversable; + + public function count() : int; +} diff --git a/lib/Doctrine/Annotations/Metadata/PropertyMetadata.php b/lib/Doctrine/Annotations/Metadata/PropertyMetadata.php new file mode 100644 index 000000000..c17023151 --- /dev/null +++ b/lib/Doctrine/Annotations/Metadata/PropertyMetadata.php @@ -0,0 +1,76 @@ +|null */ + private $type; + + /** @var bool */ + private $required; + + /** @var bool */ + private $default; + + /** @var array|null */ + private $enum; + + /** + * @param array $type + * @param array|null $enum + */ + public function __construct( + string $name, + ?array $type, + bool $required = false, + bool $default = false, + ?array $enum = null + ) { + $this->name = $name; + $this->type = $type; + $this->required = $required; + $this->default = $default; + $this->enum = $enum; + } + + public function getName() : string + { + return $this->name; + } + + public function isRequired() : bool + { + return $this->required; + } + + /** + * @return array|null + */ + public function getType() : ?array + { + return $this->type; + } + + public function isDefault() : bool + { + return $this->default; + } + + /** + * @return array|null + */ + public function getEnum() : ?array + { + return $this->enum; + } +} diff --git a/lib/Doctrine/Annotations/Metadata/TransientMetadataCollection.php b/lib/Doctrine/Annotations/Metadata/TransientMetadataCollection.php new file mode 100644 index 000000000..5a69c56fa --- /dev/null +++ b/lib/Doctrine/Annotations/Metadata/TransientMetadataCollection.php @@ -0,0 +1,77 @@ + */ + private $metadata = []; + + public function __construct(AnnotationMetadata ...$metadatas) + { + $this->add(...$metadatas); + } + + /** + * @throws MetadataAlreadyExists + */ + public function add(AnnotationMetadata ...$metadatas) : void + { + foreach ($metadatas as $metadata) { + if (isset($this->metadata[$metadata->getName()])) { + throw MetadataAlreadyExists::new($metadata); + } + + $this->metadata[$metadata->getName()] = $metadata; + } + } + + public function include(MetadataCollection $other) : void + { + $this->add(...$other); + } + + /** + * @throws MetadataDoesNotExist + */ + public function get(string $name) : AnnotationMetadata + { + if (! isset($this->metadata[$name])) { + throw MetadataDoesNotExist::new($name); + } + + return $this->metadata[$name]; + } + + public function has(string $name) : bool + { + return array_key_exists($name, $this->metadata); + } + + /** + * @return Traversable + */ + public function getIterator() : Traversable + { + yield from array_values($this->metadata); + } + + public function count() : int + { + return count($this->metadata); + } +} diff --git a/tests/Doctrine/Tests/Annotations/Metadata/AnnotationTargetTest.php b/tests/Doctrine/Tests/Annotations/Metadata/AnnotationTargetTest.php new file mode 100644 index 000000000..df9e0d859 --- /dev/null +++ b/tests/Doctrine/Tests/Annotations/Metadata/AnnotationTargetTest.php @@ -0,0 +1,102 @@ +unwrap()); + } + + public function testAllIncludesEverything() : void + { + self::assertTrue((AnnotationTarget::TARGET_ALL & AnnotationTarget::TARGET_CLASS) !== 0, 'class in all'); + self::assertTrue((AnnotationTarget::TARGET_ALL & AnnotationTarget::TARGET_METHOD) !== 0, 'method in all'); + self::assertTrue((AnnotationTarget::TARGET_ALL & AnnotationTarget::TARGET_PROPERTY) !== 0, 'property in all'); + self::assertTrue((AnnotationTarget::TARGET_ALL & AnnotationTarget::TARGET_ANNOTATION) !== 0, 'annotation in all'); + } + + public function testStaticFactories() : void + { + self::assertSame(AnnotationTarget::TARGET_CLASS, AnnotationTarget::class()->unwrap()); + self::assertSame(AnnotationTarget::TARGET_METHOD, AnnotationTarget::method()->unwrap()); + self::assertSame(AnnotationTarget::TARGET_PROPERTY, AnnotationTarget::property()->unwrap()); + self::assertSame(AnnotationTarget::TARGET_ANNOTATION, AnnotationTarget::annotation()->unwrap()); + self::assertSame(AnnotationTarget::TARGET_ALL, AnnotationTarget::all()->unwrap()); + } + + /** + * @dataProvider accessorsProvider() + */ + public function testAccessors(AnnotationTarget $target, int $raw) : void + { + self::assertSame(($raw & AnnotationTarget::TARGET_CLASS) !== 0, $target->targetsClass()); + self::assertSame(($raw & AnnotationTarget::TARGET_METHOD) !== 0, $target->targetsMethod()); + self::assertSame(($raw & AnnotationTarget::TARGET_PROPERTY) !== 0, $target->targetsProperty()); + self::assertSame(($raw & AnnotationTarget::TARGET_ANNOTATION) !== 0, $target->targetsAnnotation()); + } + + /** + * @dataProvider describeProvider() + */ + public function testDescribe(AnnotationTarget $target, string $described) : void + { + self::assertSame($described, $target->describe()); + } + + /** + * @dataProvider invalidTargetsProvider() + */ + public function testInvalidTargetBitmask(int $target) : void + { + $this->expectException(InvalidAnnotationTarget::class); + $this->expectExceptionMessage(sprintf('Annotation target "%d" is not valid bitmask of allowed targets.', $target)); + + new AnnotationTarget($target); + } + + /** + * @return (AnnotationTarget|int)[][] + */ + public function accessorsProvider() : iterable + { + yield [AnnotationTarget::class(), AnnotationTarget::TARGET_CLASS]; + yield [AnnotationTarget::method(), AnnotationTarget::TARGET_METHOD]; + yield [AnnotationTarget::property(), AnnotationTarget::TARGET_PROPERTY]; + yield [AnnotationTarget::annotation(), AnnotationTarget::TARGET_ANNOTATION]; + yield [AnnotationTarget::all(), AnnotationTarget::TARGET_ALL]; + } + + /** + * @return (AnnotationTarget|int)[][] + */ + public function describeProvider() : iterable + { + yield [AnnotationTarget::class(), 'CLASS']; + yield [AnnotationTarget::method(), 'METHOD']; + yield [AnnotationTarget::property(), 'PROPERTY']; + yield [AnnotationTarget::annotation(), 'ANNOTATION']; + yield [AnnotationTarget::all(), 'ALL']; + yield [ + new AnnotationTarget(AnnotationTarget::TARGET_CLASS | AnnotationTarget::TARGET_ANNOTATION), + 'CLASS, ANNOTATION', + ]; + } + + /** + * @return int[][] + */ + public function invalidTargetsProvider() : iterable + { + yield [-1]; + yield [AnnotationTarget::TARGET_ALL + 1]; + } +} diff --git a/tests/Doctrine/Tests/Annotations/Metadata/Builder/AnnotationMetadataBuilderTest.php b/tests/Doctrine/Tests/Annotations/Metadata/Builder/AnnotationMetadataBuilderTest.php new file mode 100644 index 000000000..80d1569a1 --- /dev/null +++ b/tests/Doctrine/Tests/Annotations/Metadata/Builder/AnnotationMetadataBuilderTest.php @@ -0,0 +1,43 @@ +build(); + + self::assertSame('Foo', $metadata->getName()); + self::assertSame(AnnotationTarget::TARGET_ALL, $metadata->getTarget()->unwrap()); + self::assertFalse($metadata->usesConstructor()); + self::assertSame([], $metadata->getProperties()); + self::assertNull($metadata->getDefaultProperty()); + } + + public function testBuilding() : void + { + $propertyA = new PropertyMetadata('a', ['type' => 'string'], true, true); + $propertyB = new PropertyMetadata('b', ['type' => 'boolean']); + + $metadata = (new AnnotationMetadataBuilder('Foo')) + ->withTarget(AnnotationTarget::class()) + ->withUsingConstructor() + ->withProperty($propertyA) + ->withProperty($propertyB) + ->build(); + + self::assertSame('Foo', $metadata->getName()); + self::assertSame(AnnotationTarget::TARGET_CLASS, $metadata->getTarget()->unwrap()); + self::assertTrue($metadata->usesConstructor()); + self::assertSame(['a' => $propertyA, 'b' => $propertyB], $metadata->getProperties()); + self::assertSame($propertyA, $metadata->getDefaultProperty()); + } +} diff --git a/tests/Doctrine/Tests/Annotations/Metadata/Builder/AnnotationMetadataTest.php b/tests/Doctrine/Tests/Annotations/Metadata/Builder/AnnotationMetadataTest.php new file mode 100644 index 000000000..b9db58d75 --- /dev/null +++ b/tests/Doctrine/Tests/Annotations/Metadata/Builder/AnnotationMetadataTest.php @@ -0,0 +1,40 @@ +expectException(TooManyDefaultProperties::class); + $this->expectExceptionMessage( + 'The annotation "Foo" can only have at most one default property, currently has 2: "a", "b".' + ); + + new AnnotationMetadata( + 'Foo', + AnnotationTarget::all(), + false, + new PropertyMetadata( + 'a', + ['type' => 'string'], + true, + true + ), + new PropertyMetadata( + 'b', + ['type' => 'string'], + true, + true + ) + ); + } +} diff --git a/tests/Doctrine/Tests/Annotations/Metadata/Builder/PropertyMetadataBuilderTest.php b/tests/Doctrine/Tests/Annotations/Metadata/Builder/PropertyMetadataBuilderTest.php new file mode 100644 index 000000000..234b38dde --- /dev/null +++ b/tests/Doctrine/Tests/Annotations/Metadata/Builder/PropertyMetadataBuilderTest.php @@ -0,0 +1,38 @@ +build(); + + self::assertSame('foo', $metadata->getName()); + self::assertFalse($metadata->isRequired()); + self::assertNull($metadata->getType()); + self::assertFalse($metadata->isDefault()); + self::assertNull($metadata->getEnum()); + } + + public function testBuilding() : void + { + $metadata = (new PropertyMetadataBuilder('foo')) + ->withBeingDefault() + ->withBeingRequired() + ->withType(['type' => 'string']) + ->withEnum(['value' => [1, 2, 3], 'literal' => '1, 2, 3']) + ->build(); + + self::assertSame('foo', $metadata->getName()); + self::assertTrue($metadata->isRequired()); + self::assertSame(['type' => 'string'], $metadata->getType()); + self::assertTrue($metadata->isDefault()); + self::assertSame(['value' => [1, 2, 3], 'literal' => '1, 2, 3'], $metadata->getEnum()); + } +} diff --git a/tests/Doctrine/Tests/Annotations/Metadata/TransientMetadataCollectionTest.php b/tests/Doctrine/Tests/Annotations/Metadata/TransientMetadataCollectionTest.php new file mode 100644 index 000000000..e61fffee6 --- /dev/null +++ b/tests/Doctrine/Tests/Annotations/Metadata/TransientMetadataCollectionTest.php @@ -0,0 +1,98 @@ +createDummyMetadata('Foo'); + $bar = $this->createDummyMetadata('Bar'); + $baz = $this->createDummyMetadata('Baz'); + + $collection = new TransientMetadataCollection(); + + self::assertCount(0, $collection); + self::assertFalse($collection->has('Foo')); + self::assertFalse($collection->has('Bar')); + self::assertFalse($collection->has('Baz')); + self::assertSame([], iterator_to_array($collection)); + + $collection->add($foo, $bar); + + self::assertCount(2, $collection); + self::assertTrue($collection->has('Foo')); + self::assertSame($foo, $collection->get('Foo')); + self::assertTrue($collection->has('Bar')); + self::assertSame($bar, $collection->get('Bar')); + self::assertSame([$foo, $bar], iterator_to_array($collection)); + + $collection->add($baz); + + self::assertCount(3, $collection); + self::assertTrue($collection->has('Baz')); + self::assertSame($baz, $collection->get('Baz')); + self::assertSame([$foo, $bar, $baz], iterator_to_array($collection)); + } + + public function testInclude() : void + { + $foo = $this->createDummyMetadata('Foo'); + $bar = $this->createDummyMetadata('Bar'); + $baz = $this->createDummyMetadata('Baz'); + + $collection = new TransientMetadataCollection($foo); + $collection->include(new TransientMetadataCollection($bar, $baz)); + + self::assertCount(3, $collection); + self::assertSame([$foo, $bar, $baz], iterator_to_array($collection)); + } + + public function testMetadataInConstructor() : void + { + self::assertCount( + 2, + new TransientMetadataCollection( + $this->createDummyMetadata('A'), + $this->createDummyMetadata('B') + ) + ); + } + + public function testDuplicateMetadata() : void + { + $this->expectException(MetadataAlreadyExists::class); + $this->expectExceptionMessage('Metadata for annotation "Foo" already exists.'); + + new TransientMetadataCollection( + $this->createDummyMetadata('Foo'), + $this->createDummyMetadata('Bar'), + $this->createDummyMetadata('Foo') + ); + } + + public function testGettingNonexistentMetadata() : void + { + $collection = new TransientMetadataCollection(); + + $this->expectException(MetadataDoesNotExist::class); + $this->expectExceptionMessage('Metadata for annotation "Foo" does not exist.'); + + $collection->get('Foo'); + } + + private function createDummyMetadata(string $name) : AnnotationMetadata + { + return new AnnotationMetadata($name, AnnotationTarget::all(), false); + } +}