diff --git a/docs/en/cookbook/soft-delete-extension.rst b/docs/en/cookbook/soft-delete-extension.rst index 34272255f9..171b780c95 100644 --- a/docs/en/cookbook/soft-delete-extension.rst +++ b/docs/en/cookbook/soft-delete-extension.rst @@ -78,7 +78,10 @@ An implementation might look like this in a ``User`` document: { // ... - /** @ODM\Field(type="date") @ODM\Index */ + /** + * @ODM\Field(type="date") + * @ODM\Index + */ private $deletedAt; public function getDeletedAt(): ?\DateTime diff --git a/lib/Doctrine/ODM/MongoDB/Configuration.php b/lib/Doctrine/ODM/MongoDB/Configuration.php index 18eacc5f79..6aef5f2189 100644 --- a/lib/Doctrine/ODM/MongoDB/Configuration.php +++ b/lib/Doctrine/ODM/MongoDB/Configuration.php @@ -9,6 +9,7 @@ use Doctrine\Common\Cache\Psr6\CacheAdapter; use Doctrine\Common\Cache\Psr6\DoctrineProvider; use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactoryInterface; use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver; use Doctrine\ODM\MongoDB\PersistentCollection\DefaultPersistentCollectionFactory; use Doctrine\ODM\MongoDB\PersistentCollection\DefaultPersistentCollectionGenerator; @@ -86,7 +87,7 @@ class Configuration * @psalm-var array{ * autoGenerateHydratorClasses?: self::AUTOGENERATE_*, * autoGeneratePersistentCollectionClasses?: self::AUTOGENERATE_*, - * classMetadataFactoryName?: class-string, + * classMetadataFactoryName?: class-string, * defaultCommitOptions?: CommitOptions, * defaultDocumentRepositoryClassName?: class-string>, * defaultGridFSRepositoryClassName?: class-string>, @@ -409,13 +410,23 @@ public function getDefaultDB(): ?string return $this->attributes['defaultDB'] ?? null; } - /** @psalm-param class-string $cmfName */ + /** + * @psalm-param class-string $cmfName + * + * @throws MongoDBException If is not a ClassMetadataFactoryInterface. + */ public function setClassMetadataFactoryName(string $cmfName): void { + $reflectionClass = new ReflectionClass($cmfName); + + if (! $reflectionClass->implementsInterface(ClassMetadataFactoryInterface::class)) { + throw MongoDBException::invalidClassMetadataFactory($cmfName); + } + $this->attributes['classMetadataFactoryName'] = $cmfName; } - /** @psalm-return class-string */ + /** @psalm-return class-string */ public function getClassMetadataFactoryName(): string { if (! isset($this->attributes['classMetadataFactoryName'])) { @@ -474,7 +485,7 @@ public function getFilterParameters(string $name): array /** * @psalm-param class-string> $className * - * @throws MongoDBException If not is a ObjectRepository. + * @throws MongoDBException If is not an ObjectRepository. */ public function setDefaultDocumentRepositoryClassName(string $className): void { diff --git a/lib/Doctrine/ODM/MongoDB/DocumentManager.php b/lib/Doctrine/ODM/MongoDB/DocumentManager.php index ac6b7ee24d..7881b708dc 100644 --- a/lib/Doctrine/ODM/MongoDB/DocumentManager.php +++ b/lib/Doctrine/ODM/MongoDB/DocumentManager.php @@ -7,7 +7,7 @@ use Doctrine\Common\EventManager; use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; -use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactoryInterface; use Doctrine\ODM\MongoDB\Mapping\MappingException; use Doctrine\ODM\MongoDB\Proxy\Factory\ProxyFactory; use Doctrine\ODM\MongoDB\Proxy\Factory\StaticProxyFactory; @@ -19,6 +19,7 @@ use Doctrine\ODM\MongoDB\Repository\GridFSRepository; use Doctrine\ODM\MongoDB\Repository\RepositoryFactory; use Doctrine\ODM\MongoDB\Repository\ViewRepository; +use Doctrine\Persistence\Mapping\ProxyClassNameResolver; use Doctrine\Persistence\ObjectManager; use InvalidArgumentException; use Jean85\PrettyVersions; @@ -68,7 +69,7 @@ class DocumentManager implements ObjectManager /** * The metadata factory, used to retrieve the ODM metadata of document classes. */ - private ClassMetadataFactory $metadataFactory; + private ClassMetadataFactoryInterface $metadataFactory; /** * The UnitOfWork used to coordinate object-level transactions. @@ -131,7 +132,8 @@ class DocumentManager implements ObjectManager */ private ?FilterCollection $filterCollection = null; - private ClassNameResolver $classNameResolver; + /** @var ProxyClassNameResolver&ClassNameResolver */ + private ProxyClassNameResolver $classNameResolver; private static ?string $version = null; @@ -154,10 +156,13 @@ protected function __construct(?Client $client = null, ?Configuration $config = ], ); + $this->classNameResolver = new CachingClassNameResolver(new ProxyManagerClassNameResolver($this->config)); + $metadataFactoryClassName = $this->config->getClassMetadataFactoryName(); $this->metadataFactory = new $metadataFactoryClassName(); $this->metadataFactory->setDocumentManager($this); $this->metadataFactory->setConfiguration($this->config); + $this->metadataFactory->setProxyClassNameResolver($this->classNameResolver); $cacheDriver = $this->config->getMetadataCache(); if ($cacheDriver) { @@ -179,9 +184,6 @@ protected function __construct(?Client $client = null, ?Configuration $config = $this->schemaManager = new SchemaManager($this, $this->metadataFactory); $this->proxyFactory = new StaticProxyFactory($this); $this->repositoryFactory = $this->config->getRepositoryFactory(); - $this->classNameResolver = new CachingClassNameResolver(new ProxyManagerClassNameResolver($this->config)); - - $this->metadataFactory->setProxyClassNameResolver($this->classNameResolver); } /** @@ -220,7 +222,7 @@ public function getClient(): Client /** * Gets the metadata factory used to gather the metadata of classes. * - * @return ClassMetadataFactory + * @return ClassMetadataFactoryInterface */ public function getMetadataFactory() { diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php index e986c87d8b..4c11dda38d 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php @@ -37,12 +37,8 @@ * @internal * * @template-extends AbstractClassMetadataFactory - * - * @method list getAllMetadata() - * @method ClassMetadata[] getLoadedMetadata() - * @method ClassMetadata getMetadataFor($className) */ -final class ClassMetadataFactory extends AbstractClassMetadataFactory +final class ClassMetadataFactory extends AbstractClassMetadataFactory implements ClassMetadataFactoryInterface { /** @var string */ protected $cacheSalt = '$MONGODBODMCLASSMETADATA'; diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactoryInterface.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactoryInterface.php new file mode 100644 index 0000000000..3c543e3cc0 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactoryInterface.php @@ -0,0 +1,40 @@ + + * @method list getAllMetadata() + * @method ClassMetadata[] getLoadedMetadata() + * @method ClassMetadata getMetadataFor($className) + */ +interface ClassMetadataFactoryInterface extends ClassMetadataFactory +{ + /** + * Sets the cache for created metadata. + */ + public function setCache(CacheItemPoolInterface $cache): void; + + /** + * Sets the configuration for the factory. + */ + public function setConfiguration(Configuration $config): void; + + /** + * Sets the document manager owning the factory. + */ + public function setDocumentManager(DocumentManager $dm): void; + + /** + * Sets a resolver for real class names of a proxy. + */ + public function setProxyClassNameResolver(ProxyClassNameResolver $resolver): void; +} diff --git a/lib/Doctrine/ODM/MongoDB/MongoDBException.php b/lib/Doctrine/ODM/MongoDB/MongoDBException.php index 5d6582ac73..620bcc86d8 100644 --- a/lib/Doctrine/ODM/MongoDB/MongoDBException.php +++ b/lib/Doctrine/ODM/MongoDB/MongoDBException.php @@ -4,6 +4,7 @@ namespace Doctrine\ODM\MongoDB; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactoryInterface; use Doctrine\ODM\MongoDB\Repository\GridFSRepository; use Doctrine\Persistence\ObjectRepository; use Exception; @@ -60,6 +61,11 @@ public static function invalidGridFSRepository(string $className): self return new self(sprintf("Invalid repository class '%s'. It must be a %s.", $className, GridFSRepository::class)); } + public static function invalidClassMetadataFactory(string $className): self + { + return new self(sprintf("Invalid class metadata factory class '%s'. It must be a %s.", $className, ClassMetadataFactoryInterface::class)); + } + /** * @param string|string[] $expected * @param mixed $got diff --git a/lib/Doctrine/ODM/MongoDB/SchemaManager.php b/lib/Doctrine/ODM/MongoDB/SchemaManager.php index 39d43d9728..bfdf96f3bb 100644 --- a/lib/Doctrine/ODM/MongoDB/SchemaManager.php +++ b/lib/Doctrine/ODM/MongoDB/SchemaManager.php @@ -5,7 +5,7 @@ namespace Doctrine\ODM\MongoDB; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; -use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactoryInterface; use Doctrine\ODM\MongoDB\Repository\ViewRepository; use InvalidArgumentException; use MongoDB\Driver\Exception\RuntimeException; @@ -54,7 +54,7 @@ final class SchemaManager 'name', ]; - public function __construct(protected DocumentManager $dm, protected ClassMetadataFactory $metadataFactory) + public function __construct(protected DocumentManager $dm, protected ClassMetadataFactoryInterface $metadataFactory) { } diff --git a/lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/AbstractCommand.php b/lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/AbstractCommand.php index 85bb2dd478..ff6b6f53a3 100644 --- a/lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/AbstractCommand.php +++ b/lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/AbstractCommand.php @@ -5,7 +5,7 @@ namespace Doctrine\ODM\MongoDB\Tools\Console\Command\Schema; use Doctrine\ODM\MongoDB\DocumentManager; -use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactoryInterface; use Doctrine\ODM\MongoDB\SchemaManager; use MongoDB\Driver\WriteConcern; use Symfony\Component\Console\Command\Command; @@ -64,7 +64,7 @@ protected function getDocumentManager() return $this->getHelper('documentManager')->getDocumentManager(); } - /** @return ClassMetadataFactory */ + /** @return ClassMetadataFactoryInterface */ protected function getMetadataFactory() { return $this->getDocumentManager()->getMetadataFactory(); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f5db06931e..75c176202b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -506,7 +506,7 @@ parameters: path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php - - message: "#^Return type \\(Doctrine\\\\ODM\\\\MongoDB\\\\Mapping\\\\ClassMetadataFactory\\) of method Doctrine\\\\ODM\\\\MongoDB\\\\DocumentManager\\:\\:getMetadataFactory\\(\\) should be compatible with return type \\(Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadataFactory\\\\>\\) of method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getMetadataFactory\\(\\)$#" + message: "#^Return type \\(Doctrine\\\\ODM\\\\MongoDB\\\\Mapping\\\\ClassMetadataFactoryInterface\\) of method Doctrine\\\\ODM\\\\MongoDB\\\\DocumentManager\\:\\:getMetadataFactory\\(\\) should be compatible with return type \\(Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadataFactory\\\\>\\) of method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getMetadataFactory\\(\\)$#" count: 1 path: lib/Doctrine/ODM/MongoDB/DocumentManager.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 627ca4bedf..d5c86ce478 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -7,13 +7,14 @@ + implementsInterface(ClassMetadataFactoryInterface::class)]]> implementsInterface(GridFSRepository::class)]]> implementsInterface(ObjectRepository::class)]]> - ClassMetadataFactory + ClassMetadataFactoryInterface diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EnumTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EnumTest.php index 8c54445c97..e6714b1579 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EnumTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EnumTest.php @@ -10,11 +10,13 @@ use Documents81\Card; use Documents81\Suit; use Error; +use Jean85\PrettyVersions; use MongoDB\BSON\ObjectId; use ValueError; use function preg_quote; use function sprintf; +use function version_compare; /** @requires PHP >= 8.1 */ class EnumTest extends BaseTestCase @@ -35,6 +37,25 @@ public function testPersistNew(): void self::assertNull($saved->nullableSuit); } + public function testArrayOfEnums(): void + { + $persistenceVersion = PrettyVersions::getVersion('doctrine/persistence')->getPrettyVersion(); + if (version_compare('3.2.0', $persistenceVersion, '>')) { + self::markTestSkipped('Support for array of enums was introduced in doctrine/persistence 3.2.0'); + } + + $doc = new Card(); + $doc->suits = ['foo' => Suit::Clubs, 'bar' => Suit::Diamonds]; + + $this->dm->persist($doc); + $this->dm->flush(); + $this->dm->clear(); + + $saved = $this->dm->find(Card::class, $doc->id); + self::assertInstanceOf(Card::class, $saved); + self::assertSame(['foo' => Suit::Clubs, 'bar' => Suit::Diamonds], $saved->suits); + } + public function testLoadingInvalidBackingValueThrowsError(): void { $document = [ diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php index b3f3e4dd78..3eb6e2815d 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php @@ -7,7 +7,7 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Events; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; -use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactoryInterface; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use Doctrine\ODM\MongoDB\Tools\ResolveTargetDocumentListener; @@ -15,7 +15,7 @@ class ResolveTargetDocumentListenerTest extends BaseTestCase { protected ResolveTargetDocumentListener $listener; - private ClassMetadataFactory $factory; + private ClassMetadataFactoryInterface $factory; public function setUp(): void { diff --git a/tests/Documents81/Card.php b/tests/Documents81/Card.php index 0694b880ed..900d2f67b0 100644 --- a/tests/Documents81/Card.php +++ b/tests/Documents81/Card.php @@ -26,5 +26,13 @@ class Card #[ODM\Field(type: 'string', enumType: Suit::class, nullable: true)] public ?Suit $nullableSuit; + /** + * @ODM\Field(enumType=Suit::class) + * + * @var Suit[] + */ + #[ODM\Field(enumType: Suit::class)] + public array $suits; + public ?SuitNonBacked $suitNonBacked; }