diff --git a/CHANGELOG.md b/CHANGELOG.md index c8d0bb3..04881c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Complete support for Symfony 7 +- Support for Doctrine ORM 3 +- Remove limitations on usage on proxies! :tada: + +### Changed + +- New way of injecting the dispatcher to entities. This is great and also supports proxies! (which was previously a limitation) +- The easy crew got us! The configuration is now deadly simple. Look at the UPGRADE guide to learn how to move from v2 to v3. + ## [2.3.3] - 2023-05-16 ### Added diff --git a/README.md b/README.md index a829ae3..747771b 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,21 @@ As your model needs a dispatcher you need to call the `setDispatcher()` method a > It doesn't use the constructor to add the dispatcher because in PHP you can create objects without the constructor. For instance, that's what Doctrine does. +```php +use Biig\Component\Domain\Model\Instantiator\Instantiator; +use Doctrine\ORM\EntityManager; + +class SomeController +{ + public function index(Instantiator $instantiator, EntityManager $entityManager) + { + $model = $instantiator->instantiate(YourModel::class); + $entityManager->persist($model); + $entityManager->flush(); + } +} +``` + Integration to Symfony ---------------------- @@ -98,10 +113,13 @@ Learn more about [Symfony Integration](/docs/domain_event_dispatcher.md#symfony- Versions -------- -| Version | Status | Documentation | Symfony Version | PHP Version | -|---------|------------|---------------| --------------- | ------------| -| 1.x | Maintained | [v1][v1-doc] | '>= 3.3 && <5' | '>= 7.1' | -| 2.x | Latest | [v2][v2-doc] | '>= 4.3' | '>= 7.1' | +| Version | Status | Documentation | Symfony Version | PHP Version | Misc | +|---------|--------------|---------------|-----------------|-------------|---------------------------------| +| 1.x | Unmaintained | [v1][v1-doc] | >= 3.3 && <5 | >= 7.1 | | +| 2.x | Maintained | [v2][v2-doc] | >= 4.3 | >= 7.4 | | +| 3.x | Latest | [v3][v3-doc] | >= 5.4 | >= 8.1 | See [UPGRADE](v3-upgrade) guide | -[v1-doc]: https://github.com/biig-io/DomainComponent/tree/v1 -[v2-doc]: https://github.com/biig-io/DomainComponent +[v1-doc]: https://github.com/swagindustries/doctrine-domain-events/tree/v1.5.2/docs +[v2-doc]: https://github.com/swagindustries/doctrine-domain-events/tree/v2.3.3/docs +[v3-docs]: https://github.com/swagindustries/doctrine-domain-events/tree/master/docs +[v3-upgrade]: https://github.com/swagindustries/doctrine-domain-events/tree/master/UPGRADE.md diff --git a/Tests/Event/DelayedListenerTest.php b/Tests/Event/DelayedListenerTest.php index 2c8a709..de38fe2 100644 --- a/Tests/Event/DelayedListenerTest.php +++ b/Tests/Event/DelayedListenerTest.php @@ -7,7 +7,6 @@ use Biig\Component\Domain\Event\DomainEventDispatcher; use Biig\Component\Domain\Exception\InvalidDomainEvent; use Biig\Component\Domain\Model\DomainModel; -use Biig\Component\Domain\PostPersistListener\DoctrinePostPersistListener; use Biig\Component\Domain\Rule\PostPersistDomainRuleInterface; use Biig\Component\Domain\Tests\fixtures\Entity\FakeModel; use Biig\Component\Domain\Tests\SetupDatabaseTrait; @@ -103,7 +102,7 @@ public function testItDoesNotExecuteManyTimesSameEvent() { // Test setup $dispatcher = new DomainEventDispatcher(); - $entityManager = $this->setupDatabase($dispatcher, 'testItInsertInBddAfterFlushing'); + $entityManager = $this->setupDatabase($dispatcher, 'testItDoesNotExecuteManyTimesSameEvent'); $model = new FakeModel(); $model->setFoo(0); diff --git a/Tests/Model/Instantiator/DoctrineConfig/ClassMetadataFactoryTest.php b/Tests/Model/Instantiator/DoctrineConfig/ClassMetadataFactoryTest.php deleted file mode 100644 index 601c691..0000000 --- a/Tests/Model/Instantiator/DoctrineConfig/ClassMetadataFactoryTest.php +++ /dev/null @@ -1,54 +0,0 @@ -assertInstanceOf(\Doctrine\ORM\Mapping\ClassMetadataFactory::class, $factory); - } - - public function testItReturnAnInstanceOfClassMetadata() - { - $entityManager = $this->setupDatabase(new DomainEventDispatcher(), 'testItReturnAnInstanceOfClassMetadata'); - - $metadata = $entityManager->getMetadataFactory()->getMetadataFor(FakeModel::class); - - $this->assertInstanceOf(ClassMetadata::class, $metadata); - - $this->dropDatabase(); - } - - public function testItAllowToRetrieveDomainModel() - { - $dispatcher = $this->prophesize(DomainEventDispatcherInterface::class); - $dispatcher->dispatch(Argument::cetera())->shouldBeCalled(); - - $entityManager = $this->setupDatabase($dispatcher->reveal(), 'testItAllowToRetrieveDomainModel'); - - $res = $entityManager->getRepository(FakeModel::class)->findAll(); - - /** @var FakeModel $item */ - $item = reset($res); - $item->doAction(); - $this->dropDatabase(); - } -} diff --git a/Tests/Model/Instantiator/DoctrineConfig/ClassMetadataTest.php b/Tests/Model/Instantiator/DoctrineConfig/ClassMetadataTest.php deleted file mode 100644 index 236c124..0000000 --- a/Tests/Model/Instantiator/DoctrineConfig/ClassMetadataTest.php +++ /dev/null @@ -1,56 +0,0 @@ -metadata = new ClassMetadata(FakeModel::class, new Instantiator(new DomainEventDispatcher())); - } - - public function testItIsInstanceOfDoctrineClassMetadata() - { - $this->assertInstanceOf(\Doctrine\ORM\Mapping\ClassMetadata::class, $this->metadata); - } - - public function testItInstantiateEntities() - { - $model = $this->metadata->newInstance(); - - $this->assertInstanceOf(FakeModel::class, $model); - } - - public function testItsWakable() - { - $metadata = unserialize(serialize($this->metadata)); - - $this->assertInstanceOf(ClassMetadata::class, $metadata); - - if (interface_exists(\Doctrine\Persistence\Mapping\ReflectionService::class)) { - $refSer = $this->prophesize(\Doctrine\Persistence\Mapping\ReflectionService::class); - } else { - $refSer = $this->prophesize(\Doctrine\Common\Persistence\Mapping\ReflectionService::class); - } - $metadata->wakeupReflectionWithInstantiator($refSer->reveal(), new Instantiator(new DomainEventDispatcher())); - - $model = $metadata->newInstance(); - - $this->assertInstanceOf(FakeModel::class, $model); - $this->assertTrue($model->hasDispatcher()); - } -} diff --git a/Tests/Model/Instantiator/DoctrineConfig/InstantiatorTest.php b/Tests/Model/Instantiator/DoctrineConfig/InstantiatorTest.php deleted file mode 100644 index ba5671c..0000000 --- a/Tests/Model/Instantiator/DoctrineConfig/InstantiatorTest.php +++ /dev/null @@ -1,28 +0,0 @@ -assertInstanceOf(InstantiatorInterface::class, $instantiator); - } - - public function testItUseTheGivenInstantiator() - { - $instantiator = new Instantiator(new DomainEventDispatcher()); - - $model = $instantiator->instantiate(new FakeModel); - $this->assertTrue($model->hasDispatcher()); - } -} - - diff --git a/Tests/PostFlushListener/EntitiesHasDispatcherCheckerTest.php b/Tests/PostFlushListener/EntitiesHasDispatcherCheckerTest.php index 866617c..38b23f2 100644 --- a/Tests/PostFlushListener/EntitiesHasDispatcherCheckerTest.php +++ b/Tests/PostFlushListener/EntitiesHasDispatcherCheckerTest.php @@ -42,7 +42,7 @@ public function testAnEntityThatDoesntHaveDispatcherWhileFlushedThrowAnError() { self::bootKernel(['debug' => true]); // You should not create your entites this way in your own code ! - // Use the Biig\Component\Domain\Model\Instantiator\Instantiator service to instanciate your entities. + // Use the Biig\Component\Domain\Model\Instantiator\Instantiator service to instantiate your entities. $model = new FakeModel(); $this->expectException(FlushedEntityDoesntContainsDispatcherException::class); diff --git a/Tests/PostPersistListener/DoctrinePostPersistListenerTest.php b/Tests/PostPersistListener/DoctrinePostPersistListenerTest.php index cabcf85..da5b7e8 100644 --- a/Tests/PostPersistListener/DoctrinePostPersistListenerTest.php +++ b/Tests/PostPersistListener/DoctrinePostPersistListenerTest.php @@ -31,7 +31,7 @@ public function testItCallPersistForEachFlushedModel() $entityManager = $this->prophesize(EntityManager::class); $entityManager->getUnitOfWork()->willReturn($unitOfWork->reveal()); $onFlushEvent = $this->prophesize(OnFlushEventArgs::class); - $onFlushEvent->getEntityManager()->willReturn($entityManager->reveal()); + $onFlushEvent->getObjectManager()->willReturn($entityManager->reveal()); $postPersistListener = new DoctrinePostPersistListener($dispatcher->reveal()); $postPersistListener->onFlush($onFlushEvent->reveal()); diff --git a/Tests/SetupDatabaseTrait.php b/Tests/SetupDatabaseTrait.php index bb638bb..6f421f1 100644 --- a/Tests/SetupDatabaseTrait.php +++ b/Tests/SetupDatabaseTrait.php @@ -19,17 +19,14 @@ private function setupDatabase(DomainEventDispatcherInterface $dispatcher, strin copy(__DIR__ . '/fixtures/dbtest/initial_fake_model.db', $this->dbPath); $config = ORMSetup::createAttributeMetadataConfiguration(array(__DIR__ . '/../fixtures/Entity'), true); - $config->setClassMetadataFactoryName(ClassMetadataFactory::class); $conn = [ 'driver' => 'pdo_sqlite', 'path' => $this->dbPath, ]; $entityManager = EntityManager::create($conn, $config); - $entityManager->getEventManager()->addEventSubscriber(new DoctrinePostPersistListener($dispatcher)); - $entityManager->getEventManager()->addEventSubscriber(new PostLoadDispatcherInjectionListener($dispatcher)); - - $entityManager->getMetadataFactory()->setDispatcher($dispatcher); + $entityManager->getEventManager()->addEventListener(['postFlush', 'onFlush'], new DoctrinePostPersistListener($dispatcher)); + $entityManager->getEventManager()->addEventListener(['postLoad'], new PostLoadDispatcherInjectionListener($dispatcher)); return $entityManager; } diff --git a/Tests/Symfony/DependencyInjection/CompilerPass/InsertDispatcherInClassMetadataFactoryCompilerPassTest.php b/Tests/Symfony/DependencyInjection/CompilerPass/InsertDispatcherInClassMetadataFactoryCompilerPassTest.php deleted file mode 100644 index e81044d..0000000 --- a/Tests/Symfony/DependencyInjection/CompilerPass/InsertDispatcherInClassMetadataFactoryCompilerPassTest.php +++ /dev/null @@ -1,83 +0,0 @@ -assertInstanceOf(CompilerPassInterface::class, $pass); - } - - public function testItDecorateEntityManagersConfigurators() - { - $container = $this->prophesize(ContainerBuilder::class); - $container->getParameter('biig_domain.entity_managers')->willReturn(['hello', 'world']); - $container->getParameter('biig_domain_doctrine_domain_event_instantiator')->willReturn(true); - - $defHello = new Definition(); - $defWorld = new Definition(); - - $container->register('biig_domain.hello_configurator', EntityManagerConfigurator::class)->shouldBeCalled()->willReturn($defHello); - $container->register('biig_domain.world_configurator', EntityManagerConfigurator::class)->shouldBeCalled()->willReturn($defWorld); - - $compilerPass = new InsertDispatcherInClassMetadataFactoryCompilerPass(); - $compilerPass->process($container->reveal()); - - $this->assertFalse($defHello->isPublic()); - $this->assertFalse($defWorld->isPublic()); - - $this->assertInstanceOf(Reference::class, $defHello->getArgument(0)); - $this->assertInstanceOf(Reference::class, $defWorld->getArgument(0)); - - $this->assertEquals((string) $defHello->getArgument(0), 'biig_domain.hello_configurator.inner'); - $this->assertEquals((string) $defWorld->getArgument(0), 'biig_domain.world_configurator.inner'); - $this->assertEquals((string) $defHello->getArgument(1), 'biig_domain.dispatcher'); - $this->assertEquals((string) $defWorld->getArgument(1), 'biig_domain.dispatcher'); - } - - public function testItUseDoctrineDefaultIfNoEntityManagerProvided() - { - $container = $this->prophesize(ContainerBuilder::class); - $container->getParameter('biig_domain.entity_managers')->willReturn([]); - $container->getParameter('doctrine.default_entity_manager')->willReturn('default'); - $container->getParameter('biig_domain_doctrine_domain_event_instantiator')->willReturn(true); - - $configurator = new Definition(); - - $container->register('biig_domain.default_configurator', EntityManagerConfigurator::class)->shouldBeCalled()->willReturn($configurator); - - $compilerPass = new InsertDispatcherInClassMetadataFactoryCompilerPass(); - $compilerPass->process($container->reveal()); - - $this->assertFalse($configurator->isPublic()); - $this->assertInstanceOf(Reference::class, $configurator->getArgument(0)); - $this->assertEquals((string) $configurator->getArgument(0), 'biig_domain.default_configurator.inner'); - $this->assertEquals((string) $configurator->getArgument(1), 'biig_domain.dispatcher'); - } - - public function testItDoesNotAddConfigurationForEntityManagers() - { - $container = $this->prophesize(ContainerBuilder::class); - $container->getParameter('biig_domain.entity_managers')->willReturn(['hello', 'world']); - $container->getParameter('biig_domain_doctrine_domain_event_instantiator')->willReturn(false); - - $container->register(Argument::cetera())->shouldNotBeCalled(); - - $compilerPass = new InsertDispatcherInClassMetadataFactoryCompilerPass(); - $compilerPass->process($container->reveal()); - } -} diff --git a/Tests/Symfony/DependencyInjection/CompilerPass/RegisterListenersCompilerPassTest.php b/Tests/Symfony/DependencyInjection/CompilerPass/RegisterListenersCompilerPassTest.php new file mode 100644 index 0000000..029394b --- /dev/null +++ b/Tests/Symfony/DependencyInjection/CompilerPass/RegisterListenersCompilerPassTest.php @@ -0,0 +1,18 @@ +assertInstanceOf(PostLoadDispatcherInjectionListener::class, $this->getContainer()->get('biig_domain.postload_listener')); + $this->assertInstanceOf(DoctrinePostPersistListener::class, $this->getContainer()->get('biig_domain.post_persist_listener.doctrine_default')); + } +} diff --git a/Tests/Symfony/DependencyInjection/CompilerPass/VerifyDoctrineConfigurationCompilerPassTest.php b/Tests/Symfony/DependencyInjection/CompilerPass/VerifyDoctrineConfigurationCompilerPassTest.php deleted file mode 100644 index eca364d..0000000 --- a/Tests/Symfony/DependencyInjection/CompilerPass/VerifyDoctrineConfigurationCompilerPassTest.php +++ /dev/null @@ -1,66 +0,0 @@ -assertInstanceOf(CompilerPassInterface::class, $pass); - } - - public function testItThrowsAnErrorWhenTheConfigurationIsNotModifiedAsExpected() - { - $this->expectException(InvalidConfigurationException::class); - $container = $this->prophesize(ContainerBuilder::class); - $container->getParameter('biig_domain_doctrine_domain_event_instantiator')->willReturn(true); - $config = new Definition(Configuration::class); - $configChild = new ChildDefinition('doctrine.orm.configuration'); - - // The method call is to our ClassMetadataFactory if the previous parameter is defined, - // if something alter the value, we need to throw an error. - $configChild->addMethodCall('setClassMetadataFactoryName', [null]); - - $container->getDefinitions()->willReturn([ - 'doctrine.orm.configuration' => $config, - 'doctrine.orm.configuration.whatever' => $configChild, - ]); - - $compilerPass = new VerifyDoctrineConfigurationCompilerPass(); - $compilerPass->process($container->reveal()); - } - - public function testItDoesNothingWhenFeatureNotActivated() - { - $container = $this->prophesize(ContainerBuilder::class); - $container->getParameter('biig_domain_doctrine_domain_event_instantiator')->willReturn(false)->shouldBeCalled(); - $container->getDefinitions([])->shouldNotBeCalled(); - - $compilerPass = new VerifyDoctrineConfigurationCompilerPass(); - $compilerPass->process($container->reveal()); - } - - public function testItWorksWithFeatureActivatedButNoDoctrineConfiguration() - { - $container = new ContainerBuilder(); - $container->setParameter('biig_domain_doctrine_domain_event_instantiator', true); - $container->setDefinition('dispatcher', new Definition(DomainEventDispatcher::class)); - - $compilerPass = new VerifyDoctrineConfigurationCompilerPass(); - $this->assertNull($compilerPass->process($container)); - } -} diff --git a/Tests/Symfony/DependencyInjection/DomainExtensionTest.php b/Tests/Symfony/DependencyInjection/DomainExtensionTest.php index d3151ca..cc45295 100644 --- a/Tests/Symfony/DependencyInjection/DomainExtensionTest.php +++ b/Tests/Symfony/DependencyInjection/DomainExtensionTest.php @@ -9,38 +9,21 @@ class DomainExtensionTest extends TestCase { - public function testItAddsDoctrinePostPersistListenerToContainer() + public function testItAddsAParameterForEntityManagersSupported() { $extension = new DomainExtension(); $config = [[ - 'persist_listeners' => [ - 'doctrine' => ['default', 'custom_doctrine'], - ], + // notice that null would be resolved to empty array by the config component + 'entity_managers' => ['default', 'foo'], ]]; $container = new ContainerBuilder(new ParameterBag([ - 'kernel.debug' => false + 'kernel.debug' => false, ])); $extension->load($config, $container); - $array = [ - "biig_domain.post_persist_listener.doctrine_default" => [ - [ - "connection" => "default" - ] - ], - "biig_domain.post_persist_listener.doctrine_custom_doctrine" => [ - [ - "connection" => "custom_doctrine" - ] - ] - ]; - - $this->assertTrue($container->hasDefinition('biig_domain.post_persist_listener.doctrine_default')); - $this->assertTrue($container->hasDefinition('biig_domain.post_persist_listener.doctrine_custom_doctrine')); - - $this->assertEquals($container->findTaggedServiceIds('doctrine.event_subscriber'), $array); + $this->assertEquals(['default', 'foo'], $container->getParameter('biig_domain.entity_managers')); } public function testItDoesntRegisterDoctrinePostPersistListenerToContainer() diff --git a/Tests/Symfony/DependencyInjection/EntityManagerConfiguratorTest.php b/Tests/Symfony/DependencyInjection/EntityManagerConfiguratorTest.php deleted file mode 100644 index 6b6946c..0000000 --- a/Tests/Symfony/DependencyInjection/EntityManagerConfiguratorTest.php +++ /dev/null @@ -1,33 +0,0 @@ -prophesize(EntityManager::class); - $originalConfigurator = $this->prophesize(ManagerConfigurator::class)->reveal(); - $factory = new ClassMetadataFactory(); - - $entityManager->getMetadataFactory()->willReturn($factory); - - $configurator = new EntityManagerConfigurator($originalConfigurator, new DomainEventDispatcher()); - $configurator->configure($entityManager->reveal()); - - $ref = new \ReflectionObject($factory); - $property = $ref->getProperty('dispatcher'); - $property->setAccessible(true); - $this->assertNotNull($property->getValue($factory)); - } -} diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..3707132 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,29 @@ +From v2 to v3 +============= + +Configuration +------ + +### Before + +```yaml +biig_domain: + # Default to true + override_doctrine_instantiator: true + # Act on default if none specified + entity_managers: [] + # Were disabled if not specified + persist_listeners: + doctrine: ['default'] +``` + +### After + +ℹ️ No configuration is required, if the bundle is installed, all its features will be enabled! + +```yaml +biig_domain: + # You can specify on which entity_manager the bundle is active + # Defaults to empty array [] + entity_managers: ['default'] +``` diff --git a/composer.json b/composer.json index 9925dc6..8c5ee1a 100644 --- a/composer.json +++ b/composer.json @@ -4,9 +4,9 @@ "license": "MIT", "require": { "php": ">=8.1", - "symfony/event-dispatcher": "^5.0|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", "doctrine/doctrine-bundle": "^1.8|^2.0", - "doctrine/orm": "^2.6.3" + "doctrine/orm": "^2.18.3|^3.0" }, "authors": [ { @@ -16,7 +16,7 @@ ], "require-dev": { "phpunit/phpunit": "^9.5 || ^10", - "doctrine/orm": "^2.6.3", + "doctrine/orm": "^2.18.3", "friendsofphp/php-cs-fixer": "^3.49.0", "symfony/framework-bundle": "^5.0 || ^6.0", "phpspec/prophecy-phpunit": "^2.0.1", diff --git a/docs/domain_event_dispatcher.md b/docs/domain_event_dispatcher.md index c930bd4..a87a524 100644 --- a/docs/domain_event_dispatcher.md +++ b/docs/domain_event_dispatcher.md @@ -8,7 +8,7 @@ domain events. Make a rule ----------- -To make a new rule (its a listener) you should implement the `DomainRuleInterface`. +To make a new rule (it's a listener) you should implement the `DomainRuleInterface`. ### Standalone usage @@ -20,11 +20,13 @@ use Biig\Component\Domain\Rule\DomainRuleInterface; use Biig\Component\Domain\Event\DomainEvent; $dispatcher->addRule(new class implements DomainRuleInterface { - public function execute(DomainEvent $event) { + public function execute(DomainEvent $event) + { // add some specific behavior } - public function on() { + public function on() + { return 'on.event'; } }); @@ -32,7 +34,8 @@ $dispatcher->addRule(new class implements DomainRuleInterface { #### Add a post persist delayed rule -A post persist rule will occure only if the specified event is emit, but only after the data is persisted in storage. Basically flushed in the case of Doctrine. +A post persist rule will occur only if the specified event is emitted, but only after the data is persisted in storage. +Basically flushed in the case of Doctrine. ```php addRule(new class implements PostPersistDomainRuleInterface { - public function execute(DomainEvent $event) { + public function execute(DomainEvent $event) + { // add some specific behavior } - public function after() { - return 'on.event'; // You have to specify the model + public function after() + { + return 'on.event'; } }); ``` @@ -65,17 +70,17 @@ biig_domain: > Using PostPersistRule means that the flush of doctrine will (re)dispatch some events. If this is a great way to add features during your workflow... > This also means that using a Doctrine **flush** in a domain rule (even a different one) is something tricky. > -> However this package will not fail or end in infinite loop, it's 100% supported, but events order may be surprising. +> However, this package will not fail or end in infinite loop, it's 100% supported, but events order may be surprising. > > ⚠️⚠️⚠️⚠️⚠️⚠️⚠️ ### Symfony Integration -If you use the Symfony Bundle with auto-configuration of your services. +If you use the Symfony Bundle with autoconfiguration of your services. **You don't have anything to do.** -If you don't auto-discover your services and don't enable auto-configuration, then you will need to add the tag: +If you don't auto-discover your services and don't enable autoconfiguration, then you will need to add the tag: ```yaml My\Domain\Rule: tags: @@ -92,25 +97,15 @@ My\Domain\Rule: - { name: biig_domain.rule, event: 'your.event.name', method: 'execute', priority: 0 } ``` -_Notice: the priority field is optional._ +_Notice: the priority field is optional as well as method._ #### Configuration reference ```yaml biig_domain: - # It modifies the DoctrineBundle configuration to register a new - # ClassMetadataInfo class so the instantiator now set the domain event - # dispatcher to your models automatically - override_doctrine_instantiator: true - - # By default it will override the doctrine instantiator only for - # the "default" entity manager of your application. You can specify - # many entity managers if you want. - entity_managers: [] - - # Post persist events are not activated by default, you need to enable the post persist listeners - persist_listeners: - # As doctrine supports many connections, you need to enable your connections one by one - doctrine: ['default'] + # By default, the bundle will be active on all connections registered, + # but you can specify explicitly which connections it should be enabled on. + # Defaults to empty array [] + entity_managers: ['default'] ``` diff --git a/docs/injection_in_doctrine_entities.md b/docs/injection_in_doctrine_entities.md index db8ea6a..d863b3c 100644 --- a/docs/injection_in_doctrine_entities.md +++ b/docs/injection_in_doctrine_entities.md @@ -3,39 +3,31 @@ Injection of DomainEventDispatcher in Doctrine entities _This feature allows you to merge your doctrine entities with DDD model._ -_To achieve this goal it provides you a set of classes to extends doctrine behavior on entities instantiation._ +_To achieve this goal it provides you a set of classes to extends doctrine behavior on entity instantiation._ How it works ------------ Doctrine uses an `Instantiator` class to instantiate entities. (this is some kind of factory) -As this `Instantiator` is hardly instantiated by Doctrine, we need to extends the ORM core. Which mean -this feature **may** be in **conflict** with some other packages that may extends doctrine behavior (I don't know any). +As this `Instantiator` is hardly instantiated by Doctrine, we need to extend the ORM core. Which mean +this feature **may** be in **conflict** with some other packages that may extend doctrine behavior (I don't know any). ### Usage without integration ```php setClassMetadataFactoryName(new ClassMetadataFactory($dispatcher)); $entityManager = new \Doctrine\ORM\EntityManager($connection, $configuration); +$entityManager->getEventManager()->addEventSubscriber(new PostLoadDispatcherInjectionListener($dispatcher)); ``` ### Symfony integration -And then you can enable or disable this feature. Here is the default configuration: - -```yaml -biig_domain: - override_doctrine_instantiator: true -``` - ⚠ You need to know it ---------------------- diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f44639b..120d2b7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,5 @@ - - - - ./ - - - ./docs - ./Tests - ./vendor - - + @@ -19,4 +9,14 @@ ./Tests + + + ./ + + + ./docs + ./Tests + ./vendor + + diff --git a/phpunit.xml.dist.bak b/phpunit.xml.dist.bak deleted file mode 100644 index e7c2df6..0000000 --- a/phpunit.xml.dist.bak +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - ./Tests - - - - - - ./ - - ./docs - ./Tests - ./vendor - - - - diff --git a/src/Integration/Symfony/DependencyInjection/CompilerPass/InsertDispatcherInClassMetadataFactoryCompilerPass.php b/src/Integration/Symfony/DependencyInjection/CompilerPass/InsertDispatcherInClassMetadataFactoryCompilerPass.php deleted file mode 100644 index 9790a3a..0000000 --- a/src/Integration/Symfony/DependencyInjection/CompilerPass/InsertDispatcherInClassMetadataFactoryCompilerPass.php +++ /dev/null @@ -1,38 +0,0 @@ -getParameter('biig_domain.entity_managers'); - if (empty($entityManagers)) { - $entityManagers = [$container->getParameter('doctrine.default_entity_manager')]; - } - - if ($container->getParameter('biig_domain_doctrine_domain_event_instantiator')) { - foreach ($entityManagers as $entityManager) { - $this->addDecoratorToConfigurator($container, $entityManager); - } - } - } - - private function addDecoratorToConfigurator(ContainerBuilder $container, string $entityManager) - { - $serviceName = sprintf('biig_domain.%s_configurator', $entityManager); - $originalConfiguratorName = sprintf('doctrine.orm.%s_manager_configurator', $entityManager); - - $container->register($serviceName, EntityManagerConfigurator::class) - ->setDecoratedService($originalConfiguratorName) - ->addArgument(new Reference($serviceName . '.inner')) - ->addArgument(new Reference('biig_domain.dispatcher')) - ->setPublic(false) - ; - } -} diff --git a/src/Integration/Symfony/DependencyInjection/CompilerPass/RegisterListenersCompilerPass.php b/src/Integration/Symfony/DependencyInjection/CompilerPass/RegisterListenersCompilerPass.php new file mode 100644 index 0000000..18dd09c --- /dev/null +++ b/src/Integration/Symfony/DependencyInjection/CompilerPass/RegisterListenersCompilerPass.php @@ -0,0 +1,45 @@ + $connections */ + $entityManagers = $container->getParameter('doctrine.entity_managers'); + $supportedConnections = $container->getParameter('biig_domain.entity_managers'); + + // Doctrine bundle is probably not installed! + if (empty($entityManagers)) { + return; + } + + foreach ($entityManagers as $entityManagerName => $entityManagerServiceId) { + if (!empty($supportedConnections) && !in_array($entityManagerName, $supportedConnections)) { + continue; + } + + $container + ->setDefinition( + sprintf('biig_domain.post_persist_listener.doctrine_%s', $entityManagerName), + new Definition(DoctrinePostPersistListener::class) + ) + ->setArgument(0, new Reference('biig_domain.dispatcher')) + ->addTag('doctrine.event_listener', ['event' => 'postFlush', 'entity_manager' => $entityManagerName]) + ->addTag('doctrine.event_listener', ['event' => 'onFlush', 'entity_manager' => $entityManagerName]) + ; + + $container + ->getDefinition('biig_domain.postload_listener') + ->addTag('doctrine.event_listener', ['event' => 'postLoad', 'entity_manager' => $entityManagerName]) + ; + } + } +} diff --git a/src/Integration/Symfony/DependencyInjection/CompilerPass/VerifyDoctrineConfigurationCompilerPass.php b/src/Integration/Symfony/DependencyInjection/CompilerPass/VerifyDoctrineConfigurationCompilerPass.php deleted file mode 100644 index 6972527..0000000 --- a/src/Integration/Symfony/DependencyInjection/CompilerPass/VerifyDoctrineConfigurationCompilerPass.php +++ /dev/null @@ -1,40 +0,0 @@ -getParameter('biig_domain_doctrine_domain_event_instantiator')) { - $definitions = $container->getDefinitions(); - - foreach ($definitions as $name => $definition) { - if ($definition instanceof ChildDefinition && 'doctrine.orm.configuration' === $definition->getParent()) { - $calls = $definition->getMethodCalls(); - $this->verifyCalls($calls); - } - } - } - } - - /** - * @throws InvalidConfigurationException - */ - private function verifyCalls(array $calls) - { - foreach ($calls as $call) { - if ('setClassMetadataFactoryName' === $call[0]) { - if (ClassMetadataFactory::class !== $call[1][0]) { - throw new InvalidConfigurationException('The option "override_doctrine_instantiator", so this bundles tried to change the ClassMetadataFactory of doctrine by changing the DoctrineBundle configuration. The final configuration of the doctrine bundle doesn\'t looks like the one expected: Something probably altered the configuration. You may disable this feature by changing the default configuration or find what came override this. (It may be your manual configuration)'); - } - } - } - } -} diff --git a/src/Integration/Symfony/DependencyInjection/Configuration.php b/src/Integration/Symfony/DependencyInjection/Configuration.php index 329b97b..80bbc69 100644 --- a/src/Integration/Symfony/DependencyInjection/Configuration.php +++ b/src/Integration/Symfony/DependencyInjection/Configuration.php @@ -14,20 +14,9 @@ public function getConfigTreeBuilder(): TreeBuilder $treeBuilder ->getRootNode() ->children() - ->booleanNode('override_doctrine_instantiator') - ->defaultTrue() - ->end() ->arrayNode('entity_managers') ->scalarPrototype()->end() ->end() - ->arrayNode('persist_listeners') - ->addDefaultsIfNotSet() - ->children() - ->arrayNode('doctrine') - ->scalarPrototype()->end() - ->end() - ->end() - ->end() ->end() ; diff --git a/src/Integration/Symfony/DependencyInjection/DomainExtension.php b/src/Integration/Symfony/DependencyInjection/DomainExtension.php index 6b78202..cd9e884 100644 --- a/src/Integration/Symfony/DependencyInjection/DomainExtension.php +++ b/src/Integration/Symfony/DependencyInjection/DomainExtension.php @@ -2,17 +2,13 @@ namespace Biig\Component\Domain\Integration\Symfony\DependencyInjection; -use Biig\Component\Domain\Model\Instantiator\DoctrineConfig\ClassMetadataFactory; -use Biig\Component\Domain\PostPersistListener\DoctrinePostPersistListener; use Biig\Component\Domain\Rule\RuleInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; -use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; -use Symfony\Component\DependencyInjection\Reference; -class DomainExtension extends Extension implements PrependExtensionInterface +class DomainExtension extends Extension { public const DOMAIN_RULE_TAG = 'biig_domain.rule'; @@ -31,78 +27,13 @@ public function load(array $configs, ContainerBuilder $container): void $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $container->setParameter('biig_domain_doctrine_domain_event_instantiator', $config['override_doctrine_instantiator']); - $container->registerForAutoconfiguration(RuleInterface::class)->addTag(self::DOMAIN_RULE_TAG); $container->setParameter('biig_domain.entity_managers', $config['entity_managers']); - - if (!empty($config['persist_listeners']['doctrine'])) { - $this->registerDoctrinePostPersistListener($config['persist_listeners']['doctrine'], $container); - } - } - - /** - * This may fail if a bundle (registered after this one) or a compiler pass modify the parameter. - * The `VerifyDoctrineConfigurationCompilerPass` verify configuration integrity. - */ - public function prepend(ContainerBuilder $container): void - { - // get all bundles - $bundles = $container->getParameter('kernel.bundles'); - - if (isset($bundles['DoctrineBundle'])) { - // Pre-process the configuration - $configs = $container->getExtensionConfig($this->getAlias()); - $config = $this->processConfiguration(new Configuration(), $configs); - - // This is true by default - if ($config['override_doctrine_instantiator']) { - $doctrineConfig = $container->getExtensionConfig('doctrine'); - $doctrineClassMetadataFactoryConfig = $this->buildClassMetadataFactoryConfig($doctrineConfig); - - $container->prependExtensionConfig('doctrine', $doctrineClassMetadataFactoryConfig); - } - } } public function getAlias(): string { return 'biig_domain'; } - - private function registerDoctrinePostPersistListener(array $config, ContainerBuilder $container) - { - foreach ($config as $connection) { - $container - ->autowire( - sprintf('biig_domain.post_persist_listener.doctrine_%s', $connection), - DoctrinePostPersistListener::class - ) - ->setArgument(0, new Reference('biig_domain.dispatcher')) - ->addTag('doctrine.event_subscriber', ['connection' => $connection]) - ; - } - } - - private function buildClassMetadataFactoryConfig(array $doctrineConfig) - { - $doctrineClassMetadataFactoryConfig = [ - 'orm' => [ - 'entity_managers' => [ - 'default' => [ - 'class_metadata_factory_name' => ClassMetadataFactory::class, - ], - ], - ], - ]; - - if (isset($doctrineConfig[0]['orm']['entity_managers'])) { - foreach ($doctrineConfig[0]['orm']['entity_managers'] as $entityManagerName => $entityManagerConf) { - $doctrineClassMetadataFactoryConfig['orm']['entity_managers'][$entityManagerName]['class_metadata_factory_name'] = ClassMetadataFactory::class; - } - } - - return $doctrineClassMetadataFactoryConfig; - } } diff --git a/src/Integration/Symfony/DependencyInjection/EntityManagerConfigurator.php b/src/Integration/Symfony/DependencyInjection/EntityManagerConfigurator.php deleted file mode 100644 index dd83bc7..0000000 --- a/src/Integration/Symfony/DependencyInjection/EntityManagerConfigurator.php +++ /dev/null @@ -1,40 +0,0 @@ -originalConfigurator = $configurator; - $this->dispatcher = $dispatcher; - } - - public function configure(EntityManager $entityManager) - { - $this->originalConfigurator->configure($entityManager); - $metadataFactory = $entityManager->getMetadataFactory(); - - if ($metadataFactory instanceof ClassMetadataFactory) { - $metadataFactory->setDispatcher($this->dispatcher); - } - } -} diff --git a/src/Integration/Symfony/DomainBundle.php b/src/Integration/Symfony/DomainBundle.php index 70eb211..d92e664 100644 --- a/src/Integration/Symfony/DomainBundle.php +++ b/src/Integration/Symfony/DomainBundle.php @@ -3,9 +3,8 @@ namespace Biig\Component\Domain\Integration\Symfony; use Biig\Component\Domain\Integration\Symfony\DependencyInjection\CompilerPass\EnableDomainDenormalizerCompilerPass; -use Biig\Component\Domain\Integration\Symfony\DependencyInjection\CompilerPass\InsertDispatcherInClassMetadataFactoryCompilerPass; use Biig\Component\Domain\Integration\Symfony\DependencyInjection\CompilerPass\RegisterDomainRulesCompilerPass; -use Biig\Component\Domain\Integration\Symfony\DependencyInjection\CompilerPass\VerifyDoctrineConfigurationCompilerPass; +use Biig\Component\Domain\Integration\Symfony\DependencyInjection\CompilerPass\RegisterListenersCompilerPass; use Biig\Component\Domain\Integration\Symfony\DependencyInjection\DomainExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; @@ -22,8 +21,9 @@ public function build(ContainerBuilder $container) { parent::build($container); - $container->addCompilerPass(new VerifyDoctrineConfigurationCompilerPass()); - $container->addCompilerPass(new InsertDispatcherInClassMetadataFactoryCompilerPass()); + // Must be before RegisterEventListenersAndSubscribersPass + $container->addCompilerPass(new RegisterListenersCompilerPass(), priority: 30); + $container->addCompilerPass(new RegisterDomainRulesCompilerPass()); $container->addCompilerPass(new EnableDomainDenormalizerCompilerPass()); } diff --git a/src/Integration/Symfony/Resources/config/services.yaml b/src/Integration/Symfony/Resources/config/services.yaml index f95c0cf..66566af 100644 --- a/src/Integration/Symfony/Resources/config/services.yaml +++ b/src/Integration/Symfony/Resources/config/services.yaml @@ -13,3 +13,8 @@ services: alias: Biig\Component\Domain\Model\Instantiator\Instantiator public: true + # Tags added dynamically by the RegisterListenersCompilerPass + biig_domain.postload_listener: + class: Biig\Component\Domain\Model\Instantiator\DoctrineConfig\PostLoadDispatcherInjectionListener + arguments: + - '@biig_domain.dispatcher' diff --git a/src/Model/Instantiator/DoctrineConfig/ClassMetadata.php b/src/Model/Instantiator/DoctrineConfig/ClassMetadata.php deleted file mode 100644 index 7ac8bd0..0000000 --- a/src/Model/Instantiator/DoctrineConfig/ClassMetadata.php +++ /dev/null @@ -1,43 +0,0 @@ -instantiator = $instantiator; - } - - public function newInstance(): object - { - return $this->instantiator->instantiate(parent::newInstance($this->name)); - } - - /** - * @param ReflectionService $reflService - * @param DomainModelInstantiatorInterface $instantiator - */ - public function wakeupReflectionWithInstantiator($reflService, $instantiator) - { - $this->instantiator = $instantiator; - parent::wakeupReflection($reflService); - } -} diff --git a/src/Model/Instantiator/DoctrineConfig/ClassMetadataFactory.php b/src/Model/Instantiator/DoctrineConfig/ClassMetadataFactory.php deleted file mode 100644 index f646446..0000000 --- a/src/Model/Instantiator/DoctrineConfig/ClassMetadataFactory.php +++ /dev/null @@ -1,93 +0,0 @@ -dispatcher), $this->entityManager->getConfiguration()->getNamingStrategy()); - } - - public function setDispatcher(DomainEventDispatcherInterface $dispatcher) - { - $this->dispatcher = $dispatcher; - } - - protected function wakeupReflection(ClassMetadataInterface $class, ReflectionService $reflService): void - { - if ($class instanceof ClassMetadata) { - $class->wakeupReflectionWithInstantiator($reflService, new Instantiator($this->dispatcher)); - - return; - } - - $class->wakeupReflection($reflService); - } - - public function setEntityManager(EntityManagerInterface $em): void - { - $this->entityManager = $em; - parent::setEntityManager($em); - } - } -} else { - // Compatibility layer for Doctrine ORM <= 2.6 - final class ClassMetadataFactory extends BaseClassMetadataFactory - { - /** - * @var DomainEventDispatcherInterface - */ - private $dispatcher; - - /** - * @var EntityManagerInterface - */ - private $entityManager; - - public function newClassMetadataInstance($className): ClassMetadata - { - return new ClassMetadata($className, new Instantiator($this->dispatcher), $this->entityManager->getConfiguration()->getNamingStrategy()); - } - - public function setDispatcher(DomainEventDispatcherInterface $dispatcher) - { - $this->dispatcher = $dispatcher; - } - - protected function wakeupReflection(OldClassMetadataInterface $class, ReflectionService $reflService): void - { - if ($class instanceof ClassMetadata) { - $class->wakeupReflectionWithInstantiator($reflService, new Instantiator($this->dispatcher)); - - return; - } - - $class->wakeupReflection($reflService); - } - - public function setEntityManager(EntityManagerInterface $em): void - { - $this->entityManager = $em; - parent::setEntityManager($em); - } - } -} diff --git a/src/Model/Instantiator/DoctrineConfig/Instantiator.php b/src/Model/Instantiator/DoctrineConfig/Instantiator.php deleted file mode 100644 index 6cd82ef..0000000 --- a/src/Model/Instantiator/DoctrineConfig/Instantiator.php +++ /dev/null @@ -1,25 +0,0 @@ -injectDispatcher($object); - - return $object; - } -} diff --git a/src/Model/Instantiator/DoctrineConfig/PostLoadDispatcherInjectionListener.php b/src/Model/Instantiator/DoctrineConfig/PostLoadDispatcherInjectionListener.php index 620a6f2..71204fa 100644 --- a/src/Model/Instantiator/DoctrineConfig/PostLoadDispatcherInjectionListener.php +++ b/src/Model/Instantiator/DoctrineConfig/PostLoadDispatcherInjectionListener.php @@ -4,25 +4,15 @@ use Biig\Component\Domain\Event\DomainEventDispatcherInterface; use Biig\Component\Domain\Model\ModelInterface; -use Doctrine\Common\EventSubscriber; -use Doctrine\ORM\Event\LifecycleEventArgs; +use Doctrine\ORM\Event\PostLoadEventArgs; -class PostLoadDispatcherInjectionListener implements EventSubscriber +class PostLoadDispatcherInjectionListener { public function __construct(private DomainEventDispatcherInterface $dispatcher) { } - public function getSubscribedEvents() - { - return ['postLoad']; - } - - /** - * BC Layer: typing LifecycleEventArgs for previous Doctrine versions. - * New versions use \Doctrine\ORM\Event\PostLoadEventArgs. - */ - public function postLoad(LifecycleEventArgs $args) + public function postLoad(PostLoadEventArgs $args) { $entity = $args->getObject(); diff --git a/src/Model/Instantiator/DomainModelInstantiatorInterface.php b/src/Model/Instantiator/DomainModelInstantiatorInterface.php index d09b704..2be6d2e 100644 --- a/src/Model/Instantiator/DomainModelInstantiatorInterface.php +++ b/src/Model/Instantiator/DomainModelInstantiatorInterface.php @@ -9,11 +9,9 @@ interface DomainModelInstantiatorInterface * We do not inherit from the InstantiatorInterface to allow the usage of this component without doctrine. * This is also the reason to not have arguments in this method. * - * @param string $className - * - * @return object + * @param class-string $className */ - public function instantiate($className); + public function instantiate(string $className): object; /** * @param array ...$args diff --git a/src/Model/Instantiator/Instantiator.php b/src/Model/Instantiator/Instantiator.php index c705ddd..17dffe0 100644 --- a/src/Model/Instantiator/Instantiator.php +++ b/src/Model/Instantiator/Instantiator.php @@ -23,7 +23,10 @@ public function __construct(DomainEventDispatcherInterface $dispatcher) $this->dispatcher = $dispatcher; } - public function instantiate($className) + /** + * @param class-string $className + */ + public function instantiate(string $className): object { $object = new $className(); $this->injectDispatcher($object); @@ -47,7 +50,7 @@ public function instantiateViaStaticFactory(string $className, string $factoryMe return $object; } - protected function injectDispatcher($object) + protected function injectDispatcher(object $object): void { if ($object instanceof ModelInterface) { $object->setDispatcher($this->dispatcher); diff --git a/src/PostPersistListener/DoctrinePostPersistListener.php b/src/PostPersistListener/DoctrinePostPersistListener.php index a1d5ddc..574516f 100644 --- a/src/PostPersistListener/DoctrinePostPersistListener.php +++ b/src/PostPersistListener/DoctrinePostPersistListener.php @@ -5,7 +5,6 @@ use Biig\Component\Domain\Event\DomainEventDispatcherInterface; use Biig\Component\Domain\Model\DomainModel; use Biig\Component\Domain\Model\ModelInterface; -use Doctrine\Common\EventSubscriber; use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Event\PostFlushEventArgs; @@ -14,7 +13,7 @@ * * Note: this listener is non thread safe because of Doctrine limitation. */ -class DoctrinePostPersistListener extends AbstractBridgeListener implements EventSubscriber +class DoctrinePostPersistListener extends AbstractBridgeListener { /** * @var DomainModel[] @@ -27,17 +26,12 @@ public function __construct(DomainEventDispatcherInterface $dispatcher) $this->modelsStageForFlush = []; } - public function getSubscribedEvents() - { - return ['onFlush', 'postFlush']; - } - /** * Cache entities that are going to be flush. */ public function onFlush(OnFlushEventArgs $eventArgs) { - $entityManager = $eventArgs->getEntityManager(); + $entityManager = $eventArgs->getObjectManager(); $unitOfWork = $entityManager->getUnitOfWork(); foreach ($unitOfWork->getScheduledEntityInsertions() as $entity) { @@ -60,7 +54,7 @@ public function onFlush(OnFlushEventArgs $eventArgs) } /** - * Entities flushed are not accessible at this point so we take the cache. + * Entities flushed are not accessible at this point, so we take the cache. */ public function postFlush(PostFlushEventArgs $eventArgs) {