diff --git a/UPGRADE-2.5.md b/UPGRADE-2.5.md index 86de15af2d..42d1bd630d 100644 --- a/UPGRADE-2.5.md +++ b/UPGRADE-2.5.md @@ -1,5 +1,10 @@ # UPGRADE FROM 2.4 to 2.5 +## Backward compatibility breaks + +* `Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver` no longer extends from + `Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver`. + ## PHP requirements * MongoDB ODM 2.5 requires PHP 7.4 or newer. If you're not running PHP 7.4 yet, diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php index edf8d5c612..de1770e413 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php @@ -5,16 +5,368 @@ 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 MongoDB\Driver\Exception\UnexpectedValueException; +use ReflectionClass; +use ReflectionMethod; + +use function array_merge; +use function array_replace; +use function assert; +use function class_exists; +use function constant; +use function count; +use function get_class; +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 extends CompatibilityAnnotationDriver { + use ColocatedMappingDriver; + + /** + * The annotation reader. + * + * @internal this property will be private in 3.0 + * + * @var Reader + */ + protected $reader; + /** @param string|string[]|null $paths */ public function __construct($paths = null, ?Reader $reader = null) { - parent::__construct($reader ?? new AttributeReader(), $paths); + $this->reader = $reader ?? new AttributeReader(); + + $this->addPaths((array) $paths); + } + + public function isTransient($className) + { + $classAnnotations = $this->reader->getClassAnnotations(new ReflectionClass($className)); + + foreach ($classAnnotations as $annot) { + if ($annot instanceof ODM\AbstractDocument) { + return false; + } + } + + return true; + } + + public function loadMetadataForClass($className, \Doctrine\Persistence\Mapping\ClassMetadata $metadata): void + { + assert($metadata instanceof ClassMetadata); + $reflClass = $metadata->getReflectionClass(); + + $classAnnotations = $this->reader->getClassAnnotations($reflClass); + + $documentAnnot = null; + foreach ($classAnnotations as $annot) { + $classAnnotations[get_class($annot)] = $annot; + + if ($annot instanceof ODM\AbstractDocument) { + if ($documentAnnot !== null) { + throw MappingException::classCanOnlyBeMappedByOneAbstractDocument($className, $documentAnnot, $annot); + } + + $documentAnnot = $annot; + } + + // non-document class annotations + if ($annot instanceof ODM\AbstractIndex) { + $this->addIndex($metadata, $annot); + } + + if ($annot instanceof ODM\Indexes) { + trigger_deprecation( + 'doctrine/mongodb-odm', + '2.2', + 'The "@Indexes" annotation used in class "%s" is deprecated. Specify all "@Index" and "@UniqueIndex" annotations on the class.', + $className + ); + $value = $annot->value; + foreach (is_array($value) ? $value : [$value] as $index) { + $this->addIndex($metadata, $index); + } + } elseif ($annot instanceof ODM\InheritanceType) { + $metadata->setInheritanceType(constant(ClassMetadata::class . '::INHERITANCE_TYPE_' . $annot->value)); + } elseif ($annot instanceof ODM\DiscriminatorField) { + $metadata->setDiscriminatorField($annot->value); + } elseif ($annot instanceof ODM\DiscriminatorMap) { + $value = $annot->value; + assert(is_array($value)); + $metadata->setDiscriminatorMap($value); + } elseif ($annot instanceof ODM\DiscriminatorValue) { + $metadata->setDiscriminatorValue($annot->value); + } elseif ($annot instanceof ODM\ChangeTrackingPolicy) { + $metadata->setChangeTrackingPolicy(constant(ClassMetadata::class . '::CHANGETRACKING_' . $annot->value)); + } elseif ($annot instanceof ODM\DefaultDiscriminatorValue) { + $metadata->setDefaultDiscriminatorValue($annot->value); + } elseif ($annot instanceof ODM\ReadPreference) { + $metadata->setReadPreference($annot->value, $annot->tags ?? []); + } elseif ($annot instanceof ODM\Validation) { + if (isset($annot->validator)) { + try { + $validatorBson = fromJSON($annot->validator); + } catch (UnexpectedValueException $e) { + throw MappingException::schemaValidationError($e->getCode(), $e->getMessage(), $className, 'validator'); + } + + $validator = toPHP($validatorBson, []); + $metadata->setValidator($validator); + } + + if (isset($annot->action)) { + $metadata->setValidationAction($annot->action); + } + + if (isset($annot->level)) { + $metadata->setValidationLevel($annot->level); + } + } + } + + if ($documentAnnot === null) { + throw MappingException::classIsNotAValidDocument($className); + } + + if ($documentAnnot instanceof ODM\MappedSuperclass) { + $metadata->isMappedSuperclass = true; + } elseif ($documentAnnot instanceof ODM\EmbeddedDocument) { + $metadata->isEmbeddedDocument = true; + } elseif ($documentAnnot instanceof ODM\QueryResultDocument) { + $metadata->isQueryResultDocument = true; + } elseif ($documentAnnot instanceof ODM\View) { + if (! $documentAnnot->rootClass) { + throw MappingException::viewWithoutRootClass($className); + } + + if (! class_exists($documentAnnot->rootClass)) { + throw MappingException::viewRootClassNotFound($className, $documentAnnot->rootClass); + } + + $metadata->markViewOf($documentAnnot->rootClass); + } elseif ($documentAnnot instanceof ODM\File) { + $metadata->isFile = true; + + if ($documentAnnot->chunkSizeBytes !== null) { + $metadata->setChunkSizeBytes($documentAnnot->chunkSizeBytes); + } + } + + if (isset($documentAnnot->db)) { + $metadata->setDatabase($documentAnnot->db); + } + + if (isset($documentAnnot->collection)) { + $metadata->setCollection($documentAnnot->collection); + } + + if (isset($documentAnnot->view)) { + $metadata->setCollection($documentAnnot->view); + } + + // Store bucketName as collection name for GridFS files + if (isset($documentAnnot->bucketName)) { + $metadata->setBucketName($documentAnnot->bucketName); + } + + if (isset($documentAnnot->repositoryClass)) { + $metadata->setCustomRepositoryClass($documentAnnot->repositoryClass); + } + + if (isset($documentAnnot->writeConcern)) { + $metadata->setWriteConcern($documentAnnot->writeConcern); + } + + if (isset($documentAnnot->indexes) && count($documentAnnot->indexes)) { + trigger_deprecation( + 'doctrine/mongodb-odm', + '2.2', + 'The "indexes" parameter in the "%s" annotation for class "%s" is deprecated. Specify all "@Index" and "@UniqueIndex" annotations on the class.', + $className, + get_class($documentAnnot) + ); + + foreach ($documentAnnot->indexes as $index) { + $this->addIndex($metadata, $index); + } + } + + if (! empty($documentAnnot->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()]; + $fieldAnnot = null; + + foreach ($this->reader->getPropertyAnnotations($property) as $annot) { + if ($annot instanceof ODM\AbstractField) { + $fieldAnnot = $annot; + } + + if ($annot instanceof ODM\AbstractIndex) { + $indexes[] = $annot; + } + + if ($annot instanceof ODM\Indexes) { + $value = $annot->value; + foreach (is_array($value) ? $value : [$value] as $index) { + $indexes[] = $index; + } + } elseif ($annot instanceof ODM\AlsoLoad) { + $mapping['alsoLoadFields'] = (array) $annot->value; + } elseif ($annot instanceof ODM\Version) { + $mapping['version'] = true; + } elseif ($annot instanceof ODM\Lock) { + $mapping['lock'] = true; + } + } + + if ($fieldAnnot) { + $mapping = array_replace($mapping, (array) $fieldAnnot); + $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($classAnnotations[ShardKey::class])) { + assert($classAnnotations[ShardKey::class] instanceof ShardKey); + $this->setShardKey($metadata, $classAnnotations[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->reader->getMethodAnnotations($method) as $annot) { + if ($annot instanceof ODM\AlsoLoad) { + $metadata->registerAlsoLoadMethod($method->getName(), $annot->value); + } + + if (! isset($classAnnotations[ODM\HasLifecycleCallbacks::class])) { + continue; + } + + if ($annot instanceof ODM\PrePersist) { + $metadata->addLifecycleCallback($method->getName(), Events::prePersist); + } elseif ($annot instanceof ODM\PostPersist) { + $metadata->addLifecycleCallback($method->getName(), Events::postPersist); + } elseif ($annot instanceof ODM\PreUpdate) { + $metadata->addLifecycleCallback($method->getName(), Events::preUpdate); + } elseif ($annot instanceof ODM\PostUpdate) { + $metadata->addLifecycleCallback($method->getName(), Events::postUpdate); + } elseif ($annot instanceof ODM\PreRemove) { + $metadata->addLifecycleCallback($method->getName(), Events::preRemove); + } elseif ($annot instanceof ODM\PostRemove) { + $metadata->addLifecycleCallback($method->getName(), Events::postRemove); + } elseif ($annot instanceof ODM\PreLoad) { + $metadata->addLifecycleCallback($method->getName(), Events::preLoad); + } elseif ($annot instanceof ODM\PostLoad) { + $metadata->addLifecycleCallback($method->getName(), Events::postLoad); + } elseif ($annot 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); + } + + /** + * Retrieve the current annotation reader + * + * @return Reader + */ + public function getReader() + { + trigger_deprecation( + 'doctrine/mongodb-odm', + '2.4', + '%s is deprecated with no replacement', + __METHOD__ + ); + + return $this->reader; } /** @@ -24,7 +376,7 @@ 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); } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractAnnotationDriverTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractAnnotationDriverTest.php index 1bdf671be3..49299bee45 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractAnnotationDriverTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractAnnotationDriverTest.php @@ -8,11 +8,14 @@ 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 stdClass; +use function assert; use function get_class; abstract class AbstractAnnotationDriverTest extends AbstractMappingDriverTest @@ -160,11 +163,10 @@ public function testClassCanBeMappedByOneAbstractDocument(object $wrong, string $this->expectException(MappingException::class); $this->expectExceptionMessageMatches($messageRegExp); - $cm = new ClassMetadata(get_class($wrong)); - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); + $cm = new ClassMetadata(get_class($wrong)); + $driver = $this->loadDriver(); - $annotationDriver->loadMetadataForClass(get_class($wrong), $cm); + $driver->loadMetadataForClass(get_class($wrong), $cm); } public function provideClassCanBeMappedByOneAbstractDocument(): ?Generator @@ -174,7 +176,9 @@ public function provideClassCanBeMappedByOneAbstractDocument(): ?Generator * @ODM\Document() * @ODM\EmbeddedDocument */ - new class () { + new #[ODM\Document] + #[ODM\EmbeddedDocument] + class () { }, '/as EmbeddedDocument because it was already mapped as Document\.$/', ]; @@ -184,7 +188,9 @@ public function provideClassCanBeMappedByOneAbstractDocument(): ?Generator * @ODM\Document() * @ODM\File */ - new class () { + new #[ODM\Document] + #[ODM\File] + class () { }, '/as File because it was already mapped as Document\.$/', ]; @@ -194,7 +200,9 @@ public function provideClassCanBeMappedByOneAbstractDocument(): ?Generator * @ODM\Document() * @ODM\QueryResultDocument */ - new class () { + new #[ODM\Document] + #[ODM\QueryResultDocument] + class () { }, '/as QueryResultDocument because it was already mapped as Document\.$/', ]; @@ -204,7 +212,9 @@ public function provideClassCanBeMappedByOneAbstractDocument(): ?Generator * @ODM\Document() * @ODM\View */ - new class () { + new #[ODM\Document] + #[ODM\View] + class () { }, '/as View because it was already mapped as Document\.$/', ]; @@ -214,7 +224,9 @@ public function provideClassCanBeMappedByOneAbstractDocument(): ?Generator * @ODM\Document() * @ODM\MappedSuperclass */ - new class () { + new #[ODM\Document] + #[ODM\MappedSuperclass] + class () { }, '/as MappedSuperclass because it was already mapped as Document\.$/', ]; @@ -224,7 +236,9 @@ public function provideClassCanBeMappedByOneAbstractDocument(): ?Generator * @ODM\MappedSuperclass() * @ODM\Document */ - new class () { + new #[ODM\MappedSuperclass] + #[ODM\Document] + class () { }, '/as Document because it was already mapped as MappedSuperclass\.$/', ]; @@ -232,17 +246,17 @@ public function provideClassCanBeMappedByOneAbstractDocument(): ?Generator public function testWrongValueForValidationValidatorShouldThrowException(): void { - $annotationDriver = $this->loadDriver(); - $classMetadata = new ClassMetadata(WrongValueForValidationValidator::class); + $driver = $this->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); + assert($annotationDriver instanceof AnnotationDriver || $annotationDriver instanceof AttributeDriver); $annotationDriver->addPaths([__DIR__ . '/../../../../../Documents']); return $annotationDriver;