diff --git a/UPGRADE-2.7.md b/UPGRADE-2.7.md new file mode 100644 index 000000000..db75980f7 --- /dev/null +++ b/UPGRADE-2.7.md @@ -0,0 +1,6 @@ +# UPGRADE FROM 2.6 to 2.7 + +## Backward compatibility breaks + +* `Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver` no longer extends + `Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver`. diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php index edf8d5c61..31dc77ba4 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php @@ -5,16 +5,372 @@ namespace Doctrine\ODM\MongoDB\Mapping\Driver; use Doctrine\Common\Annotations\Reader; +use Doctrine\ODM\MongoDB\Events; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Mapping\Annotations\AbstractIndex; +use Doctrine\ODM\MongoDB\Mapping\Annotations\ShardKey; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\Mapping\MappingException; +use Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; +use MongoDB\Driver\Exception\UnexpectedValueException; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; + +use function array_merge; +use function array_replace; +use function assert; +use function class_exists; +use function constant; +use function count; +use function is_array; +use function MongoDB\BSON\fromJSON; +use function MongoDB\BSON\toPHP; +use function trigger_deprecation; /** - * The AnnotationDriver reads the mapping metadata from docblock annotations. + * The AtttributeDriver reads the mapping metadata from attributes. */ -class AttributeDriver extends AnnotationDriver +class AttributeDriver implements MappingDriver { + use ColocatedMappingDriver; + + /** + * @internal this property will be private in 3.0 + * + * @var Reader|AttributeReader + */ + protected $reader; + /** @param string|string[]|null $paths */ public function __construct($paths = null, ?Reader $reader = null) { - parent::__construct($reader ?? new AttributeReader(), $paths); + if ($reader !== null) { + trigger_deprecation( + 'doctrine/mongodb-odm', + '2.7', + 'Passing a $reader parameters to %s is deprecated', + __METHOD__, + ); + } + + $this->reader = $reader ?? new AttributeReader(); + + $this->addPaths((array) $paths); + } + + public function isTransient($className) + { + $classAttributes = $this->getClassAttributes(new ReflectionClass($className)); + + foreach ($classAttributes as $attribute) { + if ($attribute instanceof ODM\AbstractDocument) { + return false; + } + } + + return true; + } + + public function loadMetadataForClass($className, \Doctrine\Persistence\Mapping\ClassMetadata $metadata): void + { + assert($metadata instanceof ClassMetadata); + $reflClass = $metadata->getReflectionClass(); + + $classAttributes = $this->getClassAttributes($reflClass); + + $documentAttribute = null; + foreach ($classAttributes as $attribute) { + $classAttributes[$attribute::class] = $attribute; + + if ($attribute instanceof ODM\AbstractDocument) { + if ($documentAttribute !== null) { + throw MappingException::classCanOnlyBeMappedByOneAbstractDocument($className, $documentAttribute, $attribute); + } + + $documentAttribute = $attribute; + } + + // non-document class attributes + if ($attribute instanceof ODM\AbstractIndex) { + $this->addIndex($metadata, $attribute); + } + + if ($attribute instanceof ODM\Indexes) { + trigger_deprecation( + 'doctrine/mongodb-odm', + '2.2', + 'The "@Indexes" attribute used in class "%s" is deprecated. Specify all "@Index" and "@UniqueIndex" attributes on the class.', + $className, + ); + $value = $attribute->value; + foreach (is_array($value) ? $value : [$value] as $index) { + $this->addIndex($metadata, $index); + } + } elseif ($attribute instanceof ODM\InheritanceType) { + $metadata->setInheritanceType(constant(ClassMetadata::class . '::INHERITANCE_TYPE_' . $attribute->value)); + } elseif ($attribute instanceof ODM\DiscriminatorField) { + $metadata->setDiscriminatorField($attribute->value); + } elseif ($attribute instanceof ODM\DiscriminatorMap) { + $value = $attribute->value; + assert(is_array($value)); + $metadata->setDiscriminatorMap($value); + } elseif ($attribute instanceof ODM\DiscriminatorValue) { + $metadata->setDiscriminatorValue($attribute->value); + } elseif ($attribute instanceof ODM\ChangeTrackingPolicy) { + $metadata->setChangeTrackingPolicy(constant(ClassMetadata::class . '::CHANGETRACKING_' . $attribute->value)); + } elseif ($attribute instanceof ODM\DefaultDiscriminatorValue) { + $metadata->setDefaultDiscriminatorValue($attribute->value); + } elseif ($attribute instanceof ODM\ReadPreference) { + $metadata->setReadPreference($attribute->value, $attribute->tags ?? []); + } elseif ($attribute instanceof ODM\Validation) { + if (isset($attribute->validator)) { + try { + $validatorBson = fromJSON($attribute->validator); + } catch (UnexpectedValueException $e) { + throw MappingException::schemaValidationError($e->getCode(), $e->getMessage(), $className, 'validator'); + } + + $validator = toPHP($validatorBson, []); + $metadata->setValidator($validator); + } + + if (isset($attribute->action)) { + $metadata->setValidationAction($attribute->action); + } + + if (isset($attribute->level)) { + $metadata->setValidationLevel($attribute->level); + } + } + } + + if ($documentAttribute === null) { + throw MappingException::classIsNotAValidDocument($className); + } + + if ($documentAttribute instanceof ODM\MappedSuperclass) { + $metadata->isMappedSuperclass = true; + } elseif ($documentAttribute instanceof ODM\EmbeddedDocument) { + $metadata->isEmbeddedDocument = true; + } elseif ($documentAttribute instanceof ODM\QueryResultDocument) { + $metadata->isQueryResultDocument = true; + } elseif ($documentAttribute instanceof ODM\View) { + if (! $documentAttribute->rootClass) { + throw MappingException::viewWithoutRootClass($className); + } + + if (! class_exists($documentAttribute->rootClass)) { + throw MappingException::viewRootClassNotFound($className, $documentAttribute->rootClass); + } + + $metadata->markViewOf($documentAttribute->rootClass); + } elseif ($documentAttribute instanceof ODM\File) { + $metadata->isFile = true; + + if ($documentAttribute->chunkSizeBytes !== null) { + $metadata->setChunkSizeBytes($documentAttribute->chunkSizeBytes); + } + } + + if (isset($documentAttribute->db)) { + $metadata->setDatabase($documentAttribute->db); + } + + if (isset($documentAttribute->collection)) { + $metadata->setCollection($documentAttribute->collection); + } + + if (isset($documentAttribute->view)) { + $metadata->setCollection($documentAttribute->view); + } + + // Store bucketName as collection name for GridFS files + if (isset($documentAttribute->bucketName)) { + $metadata->setBucketName($documentAttribute->bucketName); + } + + if (isset($documentAttribute->repositoryClass)) { + $metadata->setCustomRepositoryClass($documentAttribute->repositoryClass); + } + + if (isset($documentAttribute->writeConcern)) { + $metadata->setWriteConcern($documentAttribute->writeConcern); + } + + if (isset($documentAttribute->indexes) && count($documentAttribute->indexes)) { + trigger_deprecation( + 'doctrine/mongodb-odm', + '2.2', + 'The "indexes" parameter in the "%s" attribute for class "%s" is deprecated. Specify all "@Index" and "@UniqueIndex" attributes on the class.', + $className, + $documentAttribute::class, + ); + + foreach ($documentAttribute->indexes as $index) { + $this->addIndex($metadata, $index); + } + } + + if (! empty($documentAttribute->readOnly)) { + $metadata->markReadOnly(); + } + + foreach ($reflClass->getProperties() as $property) { + if ( + ($metadata->isMappedSuperclass && ! $property->isPrivate()) + || + ($metadata->isInheritedField($property->name) && $property->getDeclaringClass()->name !== $metadata->name) + ) { + continue; + } + + $indexes = []; + $mapping = ['fieldName' => $property->getName()]; + $fieldAttribute = null; + + foreach ($this->getPropertyAttributes($property) as $propertyAttribute) { + if ($propertyAttribute instanceof ODM\AbstractField) { + $fieldAttribute = $propertyAttribute; + } + + if ($propertyAttribute instanceof ODM\AbstractIndex) { + $indexes[] = $propertyAttribute; + } + + if ($propertyAttribute instanceof ODM\Indexes) { + $value = $propertyAttribute->value; + foreach (is_array($value) ? $value : [$value] as $index) { + $indexes[] = $index; + } + } elseif ($propertyAttribute instanceof ODM\AlsoLoad) { + $mapping['alsoLoadFields'] = (array) $propertyAttribute->value; + } elseif ($propertyAttribute instanceof ODM\Version) { + $mapping['version'] = true; + } elseif ($propertyAttribute instanceof ODM\Lock) { + $mapping['lock'] = true; + } + } + + if ($fieldAttribute) { + $mapping = array_replace($mapping, (array) $fieldAttribute); + $metadata->mapField($mapping); + } + + if (! $indexes) { + continue; + } + + foreach ($indexes as $index) { + $name = $mapping['name'] ?? $mapping['fieldName']; + $keys = [$name => $index->order ?: 'asc']; + $this->addIndex($metadata, $index, $keys); + } + } + + // Set shard key after all fields to ensure we mapped all its keys + if (isset($classAttributes[ShardKey::class])) { + assert($classAttributes[ShardKey::class] instanceof ShardKey); + $this->setShardKey($metadata, $classAttributes[ShardKey::class]); + } + + foreach ($reflClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + /* Filter for the declaring class only. Callbacks from parent + * classes will already be registered. + */ + if ($method->getDeclaringClass()->name !== $reflClass->name) { + continue; + } + + foreach ($this->getMethodAttributes($method) as $methodAttribute) { + if ($methodAttribute instanceof ODM\AlsoLoad) { + $metadata->registerAlsoLoadMethod($method->getName(), $methodAttribute->value); + } + + if (! isset($classAttributes[ODM\HasLifecycleCallbacks::class])) { + continue; + } + + if ($methodAttribute instanceof ODM\PrePersist) { + $metadata->addLifecycleCallback($method->getName(), Events::prePersist); + } elseif ($methodAttribute instanceof ODM\PostPersist) { + $metadata->addLifecycleCallback($method->getName(), Events::postPersist); + } elseif ($methodAttribute instanceof ODM\PreUpdate) { + $metadata->addLifecycleCallback($method->getName(), Events::preUpdate); + } elseif ($methodAttribute instanceof ODM\PostUpdate) { + $metadata->addLifecycleCallback($method->getName(), Events::postUpdate); + } elseif ($methodAttribute instanceof ODM\PreRemove) { + $metadata->addLifecycleCallback($method->getName(), Events::preRemove); + } elseif ($methodAttribute instanceof ODM\PostRemove) { + $metadata->addLifecycleCallback($method->getName(), Events::postRemove); + } elseif ($methodAttribute instanceof ODM\PreLoad) { + $metadata->addLifecycleCallback($method->getName(), Events::preLoad); + } elseif ($methodAttribute instanceof ODM\PostLoad) { + $metadata->addLifecycleCallback($method->getName(), Events::postLoad); + } elseif ($methodAttribute instanceof ODM\PreFlush) { + $metadata->addLifecycleCallback($method->getName(), Events::preFlush); + } + } + } + } + + /** + * @param ClassMetadata $class + * @param array $keys + */ + private function addIndex(ClassMetadata $class, AbstractIndex $index, array $keys = []): void + { + $keys = array_merge($keys, $index->keys); + $options = []; + $allowed = ['name', 'background', 'unique', 'sparse', 'expireAfterSeconds']; + foreach ($allowed as $name) { + if (! isset($index->$name)) { + continue; + } + + $options[$name] = $index->$name; + } + + if (! empty($index->partialFilterExpression)) { + $options['partialFilterExpression'] = $index->partialFilterExpression; + } + + $options = array_merge($options, $index->options); + $class->addIndex($keys, $options); + } + + /** + * @param ClassMetadata $class + * + * @throws MappingException + */ + private function setShardKey(ClassMetadata $class, ODM\ShardKey $shardKey): void + { + $options = []; + $allowed = ['unique', 'numInitialChunks']; + foreach ($allowed as $name) { + if (! isset($shardKey->$name)) { + continue; + } + + $options[$name] = $shardKey->$name; + } + + $class->setShardKey($shardKey->keys, $options); + } + + /** @return Reader|AttributeReader */ + public function getReader() + { + trigger_deprecation( + 'doctrine/mongodb-odm', + '2.4', + '%s is deprecated with no replacement', + __METHOD__, + ); + + return $this->reader; } /** @@ -24,8 +380,38 @@ public function __construct($paths = null, ?Reader $reader = null) * * @return AttributeDriver */ - public static function create($paths = [], ?Reader $reader = null): AnnotationDriver + public static function create($paths = [], ?Reader $reader = null) { return new self($paths, $reader); } + + /** @return object[] */ + private function getClassAttributes(ReflectionClass $class): array + { + if ($this->reader instanceof AttributeReader) { + return $this->reader->getClassAttributes($class); + } + + return $this->reader->getClassAnnotations($class); + } + + /** @return object[] */ + private function getMethodAttributes(ReflectionMethod $method): array + { + if ($this->reader instanceof AttributeReader) { + return $this->reader->getMethodAttributes($method); + } + + return $this->reader->getMethodAnnotations($method); + } + + /** @return object[] */ + private function getPropertyAttributes(ReflectionProperty $property): array + { + if ($this->reader instanceof AttributeReader) { + return $this->reader->getPropertyAttributes($property); + } + + return $this->reader->getPropertyAnnotations($property); + } } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeReader.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeReader.php index 863c42487..3dcb5fe9e 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeReader.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeReader.php @@ -4,7 +4,6 @@ namespace Doctrine\ODM\MongoDB\Mapping\Driver; -use Doctrine\Common\Annotations\Reader; use Doctrine\ODM\MongoDB\Mapping\Annotations\Annotation; use ReflectionAttribute; use ReflectionClass; @@ -15,77 +14,26 @@ use function is_subclass_of; /** @internal */ -final class AttributeReader implements Reader +final class AttributeReader { - public function getClassAnnotations(ReflectionClass $class): array + /** @return array */ + public function getClassAttributes(ReflectionClass $class): array { return $this->convertToAttributeInstances($class->getAttributes()); } - /** - * @param class-string $annotationName - * - * @return T|null - * - * @template T - */ - public function getClassAnnotation(ReflectionClass $class, $annotationName) - { - foreach ($this->getClassAnnotations($class) as $annotation) { - if ($annotation instanceof $annotationName) { - return $annotation; - } - } - - return null; - } - - public function getMethodAnnotations(ReflectionMethod $method): array + /** @return array */ + public function getMethodAttributes(ReflectionMethod $method): array { return $this->convertToAttributeInstances($method->getAttributes()); } - /** - * @param class-string $annotationName - * - * @return T|null - * - * @template T - */ - public function getMethodAnnotation(ReflectionMethod $method, $annotationName) - { - foreach ($this->getMethodAnnotations($method) as $annotation) { - if ($annotation instanceof $annotationName) { - return $annotation; - } - } - - return null; - } - - public function getPropertyAnnotations(ReflectionProperty $property): array + /** @return array */ + public function getPropertyAttributes(ReflectionProperty $property): array { return $this->convertToAttributeInstances($property->getAttributes()); } - /** - * @param class-string $annotationName - * - * @return T|null - * - * @template T - */ - public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) - { - foreach ($this->getPropertyAnnotations($property) as $annotation) { - if ($annotation instanceof $annotationName) { - return $annotation; - } - } - - return null; - } - /** * @param ReflectionAttribute[] $attributes * diff --git a/psalm-baseline.xml b/psalm-baseline.xml index c62587de7..c17c9fa70 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + IteratorAggregate @@ -82,6 +82,13 @@ $mapping + $options + + + + + $mapping + $options diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractAnnotationDriverTestCase.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractAnnotationDriverTestCase.php index 753470311..aefb7ab16 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractAnnotationDriverTestCase.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractAnnotationDriverTestCase.php @@ -8,12 +8,16 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver; +use Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver; use Doctrine\ODM\MongoDB\Mapping\MappingException; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Documents\CmsUser; use Generator; use PHPUnit\Framework\Attributes\DataProvider; use stdClass; +use function assert; + abstract class AbstractAnnotationDriverTestCase extends AbstractMappingDriverTestCase { public function testFieldInheritance(): void @@ -159,11 +163,10 @@ public function testClassCanBeMappedByOneAbstractDocument(object $wrong, string $this->expectException(MappingException::class); $this->expectExceptionMessageMatches($messageRegExp); - $cm = new ClassMetadata($wrong::class); - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); + $cm = new ClassMetadata($wrong::class); + $driver = static::loadDriver(); - $annotationDriver->loadMetadataForClass($wrong::class, $cm); + $driver->loadMetadataForClass($wrong::class, $cm); } public static function provideClassCanBeMappedByOneAbstractDocument(): ?Generator @@ -173,7 +176,9 @@ public static function provideClassCanBeMappedByOneAbstractDocument(): ?Generato * @ODM\Document() * @ODM\EmbeddedDocument */ - new class () { + new #[ODM\Document] + #[ODM\EmbeddedDocument] + class () { }, '/as EmbeddedDocument because it was already mapped as Document\.$/', ]; @@ -183,7 +188,9 @@ public static function provideClassCanBeMappedByOneAbstractDocument(): ?Generato * @ODM\Document() * @ODM\File */ - new class () { + new #[ODM\Document] + #[ODM\File] + class () { }, '/as File because it was already mapped as Document\.$/', ]; @@ -193,7 +200,9 @@ public static function provideClassCanBeMappedByOneAbstractDocument(): ?Generato * @ODM\Document() * @ODM\QueryResultDocument */ - new class () { + new #[ODM\Document] + #[ODM\QueryResultDocument] + class () { }, '/as QueryResultDocument because it was already mapped as Document\.$/', ]; @@ -203,7 +212,9 @@ public static function provideClassCanBeMappedByOneAbstractDocument(): ?Generato * @ODM\Document() * @ODM\View */ - new class () { + new #[ODM\Document] + #[ODM\View] + class () { }, '/as View because it was already mapped as Document\.$/', ]; @@ -213,7 +224,9 @@ public static function provideClassCanBeMappedByOneAbstractDocument(): ?Generato * @ODM\Document() * @ODM\MappedSuperclass */ - new class () { + new #[ODM\Document] + #[ODM\MappedSuperclass] + class () { }, '/as MappedSuperclass because it was already mapped as Document\.$/', ]; @@ -223,7 +236,9 @@ public static function provideClassCanBeMappedByOneAbstractDocument(): ?Generato * @ODM\MappedSuperclass() * @ODM\Document */ - new class () { + new #[ODM\MappedSuperclass] + #[ODM\Document] + class () { }, '/as Document because it was already mapped as MappedSuperclass\.$/', ]; @@ -231,17 +246,17 @@ public static function provideClassCanBeMappedByOneAbstractDocument(): ?Generato public function testWrongValueForValidationValidatorShouldThrowException(): void { - $annotationDriver = $this->loadDriver(); - $classMetadata = new ClassMetadata(WrongValueForValidationValidator::class); + $driver = static::loadDriver(); + $classMetadata = new ClassMetadata(WrongValueForValidationValidator::class); $this->expectException(MappingException::class); $this->expectExceptionMessage('The following schema validation error occurred while parsing the "validator" property of the "Doctrine\ODM\MongoDB\Tests\Mapping\WrongValueForValidationValidator" class: "Got parse error at "w", position 0: "SPECIAL_EXPECTED"" (code 0).'); - $annotationDriver->loadMetadataForClass($classMetadata->name, $classMetadata); + $driver->loadMetadataForClass($classMetadata->name, $classMetadata); } - protected function loadDriverForCMSDocuments(): AnnotationDriver + protected function loadDriverForCMSDocuments(): MappingDriver { - $annotationDriver = $this->loadDriver(); - self::assertInstanceOf(AnnotationDriver::class, $annotationDriver); + $annotationDriver = static::loadDriver(); + assert($annotationDriver instanceof AnnotationDriver || $annotationDriver instanceof AttributeDriver); $annotationDriver->addPaths([__DIR__ . '/../../../../../Documents']); return $annotationDriver; diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/XmlMappingDriverTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/XmlMappingDriverTest.php index ebafc3304..c114a3022 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/XmlMappingDriverTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/XmlMappingDriverTest.php @@ -24,7 +24,7 @@ protected static function loadDriver(): MappingDriver public function testSetShardKeyOptionsByAttributes(): void { $class = new ClassMetadata(stdClass::class); - $driver = $this->loadDriver(); + $driver = static::loadDriver(); $element = new SimpleXMLElement(''); /** @uses XmlDriver::setShardKey */ @@ -41,7 +41,7 @@ public function testSetShardKeyOptionsByAttributes(): void public function testInvalidMappingFileTriggersException(): void { $className = InvalidMappingDocument::class; - $mappingDriver = $this->loadDriver(); + $mappingDriver = static::loadDriver(); $class = new ClassMetadata($className);