diff --git a/composer.json b/composer.json index fdcf7c9a2..231e99273 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ "phpunit/phpunit": "^10.4", "squizlabs/php_codesniffer": "^3.5", "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher-contracts": "^2.0 || ^3.0", "vimeo/psalm": "~5.24.0" }, "conflict": { diff --git a/lib/Doctrine/ODM/MongoDB/DocumentManager.php b/lib/Doctrine/ODM/MongoDB/DocumentManager.php index 706b23ed8..3ad53185a 100644 --- a/lib/Doctrine/ODM/MongoDB/DocumentManager.php +++ b/lib/Doctrine/ODM/MongoDB/DocumentManager.php @@ -19,10 +19,12 @@ use Doctrine\ODM\MongoDB\Repository\GridFSRepository; use Doctrine\ODM\MongoDB\Repository\RepositoryFactory; use Doctrine\ODM\MongoDB\Repository\ViewRepository; +use Doctrine\ODM\MongoDB\Utility\EventDispatcher; use Doctrine\Persistence\Mapping\ProxyClassNameResolver; use Doctrine\Persistence\ObjectManager; use InvalidArgumentException; use Jean85\PrettyVersions; +use LogicException; use MongoDB\Client; use MongoDB\Collection; use MongoDB\Database; @@ -30,6 +32,7 @@ use MongoDB\GridFS\Bucket; use ProxyManager\Proxy\GhostObjectInterface; use RuntimeException; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Throwable; use function array_search; @@ -79,7 +82,12 @@ class DocumentManager implements ObjectManager /** * The event manager that is the central point of the event system. */ - private EventManager $eventManager; + private ?EventManager $eventManager; + + /** + * The event dispatcher that is the central point of the event system. + */ + private EventDispatcherInterface $eventDispatcher; /** * The Hydrator factory instance. @@ -141,11 +149,23 @@ class DocumentManager implements ObjectManager * Creates a new Document that operates on the given Mongo connection * and uses the given Configuration. */ - protected function __construct(?Client $client = null, ?Configuration $config = null, ?EventManager $eventManager = null) + protected function __construct(?Client $client = null, ?Configuration $config = null, EventManager|EventDispatcherInterface|null $eventDispatcher = null) { - $this->config = $config ?: new Configuration(); - $this->eventManager = $eventManager ?: new EventManager(); - $this->client = $client ?: new Client( + $this->config = $config ?: new Configuration(); + + if ($eventDispatcher instanceof EventDispatcherInterface) { + // This is a new feature, we can accept that the EventManager + // is not available when and EventDispatcher is injected. + $this->eventManager = null; + $this->eventDispatcher = $eventDispatcher; + } else { + // Backward compatibility with Doctrine EventManager + // @todo deprecate and create a new Symfony EventDispatcher instance + $this->eventManager = $eventDispatcher ?? new EventManager(); + $this->eventDispatcher = new EventDispatcher($this->eventManager); + } + + $this->client = $client ?: new Client( 'mongodb://127.0.0.1', [], [ @@ -173,13 +193,13 @@ protected function __construct(?Client $client = null, ?Configuration $config = $hydratorNs = $this->config->getHydratorNamespace(); $this->hydratorFactory = new HydratorFactory( $this, - $this->eventManager, + $this->eventDispatcher, $hydratorDir, $hydratorNs, $this->config->getAutoGenerateHydratorClasses(), ); - $this->unitOfWork = new UnitOfWork($this, $this->eventManager, $this->hydratorFactory); + $this->unitOfWork = new UnitOfWork($this, $this->eventDispatcher, $this->hydratorFactory); $this->schemaManager = new SchemaManager($this, $this->metadataFactory); $this->proxyFactory = new StaticProxyFactory($this); $this->repositoryFactory = $this->config->getRepositoryFactory(); @@ -197,16 +217,32 @@ public function getProxyFactory(): ProxyFactory * Creates a new Document that operates on the given Mongo connection * and uses the given Configuration. */ - public static function create(?Client $client = null, ?Configuration $config = null, ?EventManager $eventManager = null): DocumentManager + public static function create(?Client $client = null, ?Configuration $config = null, EventManager|EventDispatcherInterface|null $eventDispatcher = null): DocumentManager { - return new static($client, $config, $eventManager); + return new static($client, $config, $eventDispatcher); + } + + /** + * @todo should we return a {@see Symfony\Component\EventDispatcher\EventDispatcherInterface} instead? + * So that it's explicitly possible to add/remove listeners. Or we just rely on + * the object that is injected and let the user validate the subtype. + */ + public function getEventDispatcher(): EventDispatcherInterface + { + return $this->eventDispatcher; } /** * Gets the EventManager used by the DocumentManager. + * + * @deprecated Use getEventDispatcher() instead. */ public function getEventManager(): EventManager { + if (! $this->eventManager) { + throw new LogicException('Use getEventDispatcher() instead of getEventManager()'); + } + return $this->eventManager; } diff --git a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php index 52c1f3943..9097b1aae 100644 --- a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php @@ -13,7 +13,9 @@ use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Types\Type; use Doctrine\ODM\MongoDB\UnitOfWork; +use Doctrine\ODM\MongoDB\Utility\EventDispatcher; use ProxyManager\Proxy\GhostObjectInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function array_key_exists; use function chmod; @@ -47,9 +49,9 @@ final class HydratorFactory private DocumentManager $dm; /** - * The EventManager associated with this Hydrator + * The Event Dispatcher associated with this Hydrator */ - private EventManager $evm; + private EventDispatcherInterface $evm; /** * Which algorithm to use to automatically (re)generate hydrator classes. @@ -74,7 +76,7 @@ final class HydratorFactory private array $hydrators = []; /** @throws HydratorException */ - public function __construct(DocumentManager $dm, EventManager $evm, ?string $hydratorDir, ?string $hydratorNs, int $autoGenerate) + public function __construct(DocumentManager $dm, EventManager|EventDispatcherInterface $evm, ?string $hydratorDir, ?string $hydratorNs, int $autoGenerate) { if (! $hydratorDir) { throw HydratorException::hydratorDirectoryRequired(); @@ -85,7 +87,7 @@ public function __construct(DocumentManager $dm, EventManager $evm, ?string $hyd } $this->dm = $dm; - $this->evm = $evm; + $this->evm = $evm instanceof EventManager ? new EventDispatcher($evm) : $evm; $this->hydratorDir = $hydratorDir; $this->hydratorNamespace = $hydratorNs; $this->autoGenerate = $autoGenerate; @@ -433,7 +435,12 @@ public function hydrate(object $document, array $data, array $hints = []): array $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args); } - $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data)); + $eventArgs = new PreLoadEventArgs($document, $this->dm, $data); + if ($this->evm instanceof EventDispatcherInterface) { + $this->evm->dispatch($eventArgs, Events::preLoad); + } else { + $this->evm->dispatchEvent(Events::preLoad, $eventArgs); + } // alsoLoadMethods may transform the document before hydration if (! empty($metadata->alsoLoadMethods)) { @@ -470,7 +477,7 @@ public function hydrate(object $document, array $data, array $hints = []): array $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, [new LifecycleEventArgs($document, $this->dm)]); } - $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm)); + $this->evm->dispatch(new LifecycleEventArgs($document, $this->dm), Events::postLoad); return $data; } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php index 4c11dda38..77ebb726c 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php @@ -4,7 +4,6 @@ namespace Doctrine\ODM\MongoDB\Mapping; -use Doctrine\Common\EventManager; use Doctrine\ODM\MongoDB\Configuration; use Doctrine\ODM\MongoDB\ConfigurationException; use Doctrine\ODM\MongoDB\DocumentManager; @@ -21,6 +20,7 @@ use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Doctrine\Persistence\Mapping\ReflectionService; use ReflectionException; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function assert; use function get_class_methods; @@ -52,8 +52,8 @@ final class ClassMetadataFactory extends AbstractClassMetadataFactory implements /** @var MappingDriver The used metadata driver. */ private MappingDriver $driver; - /** @var EventManager The event manager instance */ - private EventManager $evm; + /** @var EventDispatcherInterface The event dispatcher instance */ + private EventDispatcherInterface $evm; public function setDocumentManager(DocumentManager $dm): void { @@ -77,20 +77,16 @@ protected function initialize(): void } $this->driver = $driver; - $this->evm = $this->dm->getEventManager(); + $this->evm = $this->dm->getEventDispatcher(); $this->initialized = true; } /** @param string $className */ protected function onNotFoundMetadata($className) { - if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) { - return null; - } - $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $this->dm); - $this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs); + $this->evm->dispatch($eventArgs, Events::onClassMetadataNotFound); return $eventArgs->getFoundMetadata(); } @@ -195,10 +191,12 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS $class->setParentClasses($nonSuperclassParents); - $this->evm->dispatchEvent( - Events::loadClassMetadata, - new LoadClassMetadataEventArgs($class, $this->dm), - ); + $eventArgs = new LoadClassMetadataEventArgs($class, $this->dm); + if ($this->evm instanceof EventDispatcherInterface) { + $this->evm->dispatch($eventArgs, Events::loadClassMetadata); + } else { + $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs); + } // phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed if ($class->isChangeTrackingNotify()) { diff --git a/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php b/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php index 737795fe4..06afad130 100644 --- a/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php @@ -32,7 +32,7 @@ final class StaticProxyFactory implements ProxyFactory public function __construct(DocumentManager $documentManager) { $this->uow = $documentManager->getUnitOfWork(); - $this->lifecycleEventManager = new LifecycleEventManager($documentManager, $this->uow, $documentManager->getEventManager()); + $this->lifecycleEventManager = new LifecycleEventManager($documentManager, $this->uow, $documentManager->getEventDispatcher()); $this->proxyFactory = $documentManager->getConfiguration()->buildGhostObjectFactory(); } diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index eb9591f99..ab9738ca3 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -29,6 +29,7 @@ use MongoDB\Driver\WriteConcern; use ProxyManager\Proxy\GhostObjectInterface; use ReflectionProperty; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Throwable; use UnexpectedValueException; @@ -234,9 +235,9 @@ final class UnitOfWork implements PropertyChangedListener private DocumentManager $dm; /** - * The EventManager used for dispatching events. + * The Event Dispatcher used for dispatching events. */ - private EventManager $evm; + private EventDispatcherInterface $evm; /** * Additional documents that are scheduled for removal. @@ -292,10 +293,10 @@ final class UnitOfWork implements PropertyChangedListener /** * Initializes a new UnitOfWork instance, bound to the given DocumentManager. */ - public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory) + public function __construct(DocumentManager $dm, EventManager|EventDispatcherInterface $evm, HydratorFactory $hydratorFactory) { $this->dm = $dm; - $this->evm = $evm; + $this->evm = $evm instanceof EventManager ? new Utility\EventDispatcher($evm) : $evm; $this->hydratorFactory = $hydratorFactory; $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm); $this->reflectionService = new RuntimeReflectionService(); @@ -425,7 +426,7 @@ public function commit(array $options = []): void } // Raise preFlush - $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm)); + $this->evm->dispatch(new Event\PreFlushEventArgs($this->dm), Events::preFlush); // Compute changes done since last commit. $this->computeChangeSets(); @@ -454,7 +455,7 @@ public function commit(array $options = []): void } } - $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm)); + $this->evm->dispatch(new Event\OnFlushEventArgs($this->dm), Events::onFlush); if ($this->useTransaction($options)) { $session = $this->dm->getClient()->startSession(); @@ -473,7 +474,7 @@ function (Session $session) use ($options): void { } // Raise postFlush - $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm)); + $this->evm->dispatch(new Event\PostFlushEventArgs($this->dm), Events::postFlush); // Clear up foreach ($this->visitedCollections as $collections) { @@ -2430,7 +2431,7 @@ public function clear(?string $documentName = null): void $event = new Event\OnClearEventArgs($this->dm, $documentName); } - $this->evm->dispatchEvent(Events::onClear, $event); + $this->evm->dispatch($event, Events::onClear); } /** diff --git a/lib/Doctrine/ODM/MongoDB/Utility/EventDispatcher.php b/lib/Doctrine/ODM/MongoDB/Utility/EventDispatcher.php new file mode 100644 index 000000000..d497a187c --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Utility/EventDispatcher.php @@ -0,0 +1,44 @@ +eventManager->dispatchEvent($eventName, $event); + + return $event; + } +} diff --git a/lib/Doctrine/ODM/MongoDB/Utility/LifecycleEventManager.php b/lib/Doctrine/ODM/MongoDB/Utility/LifecycleEventManager.php index a44ff8f25..1f95f6403 100644 --- a/lib/Doctrine/ODM/MongoDB/Utility/LifecycleEventManager.php +++ b/lib/Doctrine/ODM/MongoDB/Utility/LifecycleEventManager.php @@ -17,6 +17,7 @@ use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; use Doctrine\ODM\MongoDB\UnitOfWork; use MongoDB\Driver\Session; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function spl_object_hash; @@ -30,8 +31,11 @@ final class LifecycleEventManager /** @var array> */ private array $transactionalEvents = []; - public function __construct(private DocumentManager $dm, private UnitOfWork $uow, private EventManager $evm) + private EventDispatcherInterface $evm; + + public function __construct(private DocumentManager $dm, private UnitOfWork $uow, EventManager|EventDispatcherInterface $evm) { + $this->evm = $evm instanceof EventManager ? new EventDispatcher($evm) : $evm; } public function clearTransactionalState(): void @@ -55,7 +59,7 @@ public function enableTransactionalMode(Session $session): void public function documentNotFound(object $proxy, $id): bool { $eventArgs = new DocumentNotFoundEventArgs($proxy, $this->dm, $id); - $this->evm->dispatchEvent(Events::documentNotFound, $eventArgs); + $this->evm->dispatch($eventArgs, Events::documentNotFound); return $eventArgs->isExceptionDisabled(); } @@ -67,8 +71,7 @@ public function documentNotFound(object $proxy, $id): bool */ public function postCollectionLoad(PersistentCollectionInterface $coll): void { - $eventArgs = new PostCollectionLoadEventArgs($coll, $this->dm); - $this->evm->dispatchEvent(Events::postCollectionLoad, $eventArgs); + $this->evm->dispatch(new PostCollectionLoadEventArgs($coll, $this->dm), Events::postCollectionLoad); } /** @@ -300,7 +303,7 @@ private function dispatchEvent(ClassMetadata $class, string $eventName, ?EventAr return; } - $this->evm->dispatchEvent($eventName, $eventArgs); + $this->evm->dispatch($eventArgs, $eventName); } private function shouldDispatchEvent(object $document, string $eventName, ?Session $session): bool diff --git a/tests/Doctrine/ODM/MongoDB/Tests/DocumentManagerTest.php b/tests/Doctrine/ODM/MongoDB/Tests/DocumentManagerTest.php index 832037e53..b6c634976 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/DocumentManagerTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/DocumentManagerTest.php @@ -8,6 +8,7 @@ use Doctrine\Common\EventManager; use Doctrine\ODM\MongoDB\Aggregation\Builder as AggregationBuilder; use Doctrine\ODM\MongoDB\Configuration; +use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory; @@ -31,11 +32,13 @@ use Documents\Tournament\ParticipantSolo; use Documents\User; use InvalidArgumentException; +use LogicException; use MongoDB\BSON\ObjectId; use MongoDB\Client; use PHPUnit\Framework\Attributes\DataProvider; use RuntimeException; use stdClass; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class DocumentManagerTest extends BaseTestCase { @@ -261,6 +264,31 @@ public function testGetClassNameForAssociationReturnsTargetDocumentWithNullData( $mapping = ClassMetadataTestUtil::getFieldMapping(['targetDocument' => User::class]); self::assertEquals(User::class, $this->dm->getClassNameForAssociation($mapping, null)); } + + public function testCreateWithEventManager(): void + { + $config = static::getConfiguration(); + $client = new Client(self::getUri()); + + $eventManager = new EventManager(); + $dm = DocumentManager::create($client, $config, $eventManager); + self::assertSame($eventManager, $dm->getEventManager()); + self::assertInstanceOf(EventDispatcherInterface::class, $dm->getEventDispatcher()); + } + + public function testCreateWithEventDispatcher(): void + { + $config = static::getConfiguration(); + $client = new Client(self::getUri()); + + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $dm = DocumentManager::create($client, $config, $eventDispatcher); + self::assertSame($eventDispatcher, $dm->getEventDispatcher()); + + self::expectException(LogicException::class); + self::expectExceptionMessage('Use getEventDispatcher() instead of getEventManager()'); + $dm->getEventManager(); + } } #[ODM\Document]