From be74687b3995ec8cdf86164ee925122e105713ce Mon Sep 17 00:00:00 2001 From: Peter Gribanov Date: Thu, 15 Jul 2021 17:48:53 +0300 Subject: [PATCH] use Events from Symfony EventDispatcher component --- .github/workflows/test.yaml | 46 +-- .php_cs.dist | 53 ++- README.md | 370 +----------------- composer.json | 15 +- .../Compiler/EventListenerPass.php | 62 --- src/DependencyInjection/Configuration.php | 90 ----- .../GpsLabDomainEventExtension.php | 82 +--- .../Aggregator/AbstractAggregateEvents.php | 16 + .../AbstractAggregateEventsRaiseInSelf.php | 16 + src/Event/Aggregator/AggregateEvents.php | 21 + .../AggregateEventsRaiseInSelfTrait.php | 67 ++++ src/Event/Aggregator/AggregateEventsTrait.php | 37 ++ src/Event/Listener/DomainEventPublisher.php | 100 ----- src/Event/Publisher.php | 63 +++ .../EventPuller.php => Event/Puller.php} | 33 +- .../Subscriber/DoctrineEventSubscriber.php | 52 +++ src/GpsLabDomainEventBundle.php | 19 +- src/Resources/config/bus.yml | 10 - src/Resources/config/locator.yml | 15 - src/Resources/config/publisher.yml | 10 - src/Resources/config/queue.yml | 8 - src/Resources/config/services.xml | 18 + .../Compiler/EventListenerPassTest.php | 235 ----------- .../DependencyInjection/ConfigurationTest.php | 60 --- .../GpsLabDomainEventExtensionTest.php | 115 +----- .../Listener/DomainEventPublisherTest.php | 277 ------------- tests/Event/PublisherTest.php | 197 ++++++++++ .../PullerTest.php} | 67 ++-- .../DoctrineEventSubscriberTest.php | 95 +++++ tests/Fixtures/SimpleObject.php | 40 +- tests/Fixtures/SimpleObjectProxy.php | 6 +- tests/GpsLabDomainEventBundleTest.php | 31 +- tests/bootstrap.php | 1 + 33 files changed, 732 insertions(+), 1595 deletions(-) delete mode 100644 src/DependencyInjection/Compiler/EventListenerPass.php delete mode 100644 src/DependencyInjection/Configuration.php create mode 100644 src/Event/Aggregator/AbstractAggregateEvents.php create mode 100644 src/Event/Aggregator/AbstractAggregateEventsRaiseInSelf.php create mode 100644 src/Event/Aggregator/AggregateEvents.php create mode 100644 src/Event/Aggregator/AggregateEventsRaiseInSelfTrait.php create mode 100644 src/Event/Aggregator/AggregateEventsTrait.php delete mode 100644 src/Event/Listener/DomainEventPublisher.php create mode 100644 src/Event/Publisher.php rename src/{Service/EventPuller.php => Event/Puller.php} (54%) create mode 100644 src/Event/Subscriber/DoctrineEventSubscriber.php delete mode 100644 src/Resources/config/bus.yml delete mode 100644 src/Resources/config/locator.yml delete mode 100644 src/Resources/config/publisher.yml delete mode 100644 src/Resources/config/queue.yml create mode 100644 src/Resources/config/services.xml delete mode 100644 tests/DependencyInjection/Compiler/EventListenerPassTest.php delete mode 100644 tests/DependencyInjection/ConfigurationTest.php delete mode 100644 tests/Event/Listener/DomainEventPublisherTest.php create mode 100644 tests/Event/PublisherTest.php rename tests/{Service/EventPullerTest.php => Event/PullerTest.php} (74%) create mode 100644 tests/Event/Subscriber/DoctrineEventSubscriberTest.php diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 47304e7..f92d43c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,58 +12,21 @@ jobs: fail-fast: false matrix: include: - - php: '5.5' - symfony: '2.8.*' - doctrine: '2.4.*' - phpunit: '4.8.*' - - php: '5.6' - symfony: '2.8.*' - doctrine: '2.4.*' - phpunit: '4.8.*' - - php: '7.0' - symfony: '2.8.*' - doctrine: '2.4.*' - phpunit: '4.8.*' - - php: '7.1' - symfony: '2.8.*' - doctrine: '2.4.*' - phpunit: '6.5.*' - - php: '7.2' - symfony: '2.8.*' - doctrine: '2.4.*' - phpunit: '6.5.*' - - php: '7.3' - symfony: '3.4.*' - doctrine: '2.6.*' - phpunit: '6.5.*' - - php: '7.4' - symfony: '3.4.*' - doctrine: '2.6.*' - phpunit: '6.5.*' - - php: '5.5' - symfony: '2.8.*' - doctrine: '2.5.*' - phpunit: '4.8.*' - php: '7.4' symfony: '4.4.*' doctrine: '2.6.*' - phpunit: '6.5.*' - php: '7.4' symfony: '5.*' doctrine: '2.7.*' - phpunit: '6.5.*' - php: '7.4' symfony: '4.4.*' doctrine: '2.7.*' - phpunit: '6.5.*' - php: '7.4' symfony: '4.4.*' doctrine: '2.8.*' - phpunit: '6.5.*' -# # require PHPUnit >= 8.5.12 -# - php: '8.0' -# symfony: '5.*' -# doctrine: '2.*' + - php: '8.0' + symfony: '5.*' + doctrine: '2.*' steps: - name: Checkout @@ -82,9 +45,6 @@ jobs: - name: Install Doctrine run: composer require doctrine/orm:"${{ matrix.doctrine }}" --no-update - - name: Install PHPUnit - run: composer require phpunit/phpunit:"${{ matrix.phpunit }}" --no-update - - name: "Install Composer dependencies (highest)" uses: "ramsey/composer-install@v1" with: diff --git a/.php_cs.dist b/.php_cs.dist index 438e574..a250992 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -1,4 +1,5 @@ true, + 'header_comment' => [ + 'comment_type' => 'PHPDoc', + 'header' => $header, + ], + 'single_line_throw' => false, + 'blank_line_after_opening_tag' => false, + 'yoda_style' => false, + 'ordered_imports' => [ + 'sort_algorithm' => 'alpha', + ], +]; + +$finder = PhpCsFixer\Finder::create() + ->in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ->notPath('bootstrap.php') +; + return PhpCsFixer\Config::create() - ->setRules([ - '@Symfony' => true, - 'array_syntax' => ['syntax' => 'short'], - 'header_comment' => [ - 'comment_type' => 'PHPDoc', - 'header' => $header, - ], - 'class_definition' => [ - 'multi_line_extends_each_single_line' => true, - ], - 'no_superfluous_phpdoc_tags' => false, - 'single_line_throw' => false, - 'blank_line_after_opening_tag' => false, - 'yoda_style' => false, - 'phpdoc_no_empty_return' => false, - 'ordered_imports' => [ - 'sort_algorithm' => 'alpha', - ], - 'list_syntax' => [ - 'syntax' => 'short', - ], - ]) - ->setFinder( - PhpCsFixer\Finder::create() - ->in(__DIR__.'/src') - ->in(__DIR__.'/tests') - ->notPath('bootstrap.php') - ) - ; \ No newline at end of file + ->setRules($rules) + ->setFinder($finder) +; diff --git a/README.md b/README.md index 0587b88..c9b4262 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ Domain event bundle Bundle to create the domain layer of your [Domain-driven design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design) application. -This [Symfony](https://symfony.com/) bundle is a wrapper for [gpslab/domain-event](https://github.com/gpslab/domain-event), look it for more details. - Installation ------------ @@ -22,73 +20,36 @@ Pretty simple with [Composer](http://packagist.org), run: composer req gpslab/domain-event-bundle ``` -Configuration -------------- - -Example configuration - -```yml -gpslab_domain_event: - # Event bus service - # Support 'listener_located', 'queue' or a custom service - # As a default used 'listener_located' - bus: 'listener_located' - - # Event queue service - # Support 'pull_memory', 'subscribe_executing' or a custom service - # As a default used 'pull_memory' - queue: 'pull_memory' - - # Event listener locator - # Support 'symfony', 'container', 'direct_binding' or custom service - # As a default used 'symfony' - locator: 'symfony' - - # Publish domain events post a Doctrine flush event - # As a default used 'false' - publish_on_flush: true -``` - Usage ----- Create a domain event ```php -use GpsLab\Domain\Event\Event +use Symfony\Contracts\EventDispatcher\Event; -class PurchaseOrderCreatedEvent implements Event +final class PurchaseOrderCreatedEvent extends Event { - private $customer_id; - private $create_at; + public CustomerId $customer_id; + public \DateTimeImmutable $create_at; public function __construct(CustomerId $customer_id, \DateTimeImmutable $create_at) { $this->customer_id = $customer_id; $this->create_at = $create_at; } - - public function customerId(): CustomerId - { - return $this->customer_id; - } - - public function createAt(): \DateTimeImmutable - { - return $this->create_at; - } } ``` Raise your event ```php -use GpsLab\Domain\Event\Aggregator\AbstractAggregateEvents; +use GpsLab\Bundle\DomainEvent\Event\Aggregator\AbstractAggregateEvents; -final class PurchaseOrder extends AbstractAggregateEvents +class PurchaseOrder extends AbstractAggregateEvents { - private $customer_id; - private $create_at; + private CustomerId $customer_id; + private \DateTimeImmutable $create_at; public function __construct(CustomerId $customer_id) { @@ -100,136 +61,24 @@ final class PurchaseOrder extends AbstractAggregateEvents } ``` -Create listener +Create event subscriber ```php -use GpsLab\Domain\Event\Event; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class SendEmailOnPurchaseOrderCreated +class SendEmailOnPurchaseOrderCreated implements EventSubscriberInterface { - private $mailer; + private \Swift_Mailer $mailer; public function __construct(\Swift_Mailer $mailer) { $this->mailer = $mailer; } - public function onPurchaseOrderCreated(PurchaseOrderCreatedEvent $event): void - { - $message = $this->mailer - ->createMessage() - ->setTo('recipient@example.com') - ->setBody(sprintf( - 'Purchase order created at %s for customer #%s', - $event->createAt()->format('Y-m-d'), - $event->customerId() - )); - - $this->mailer->send($message); - } -} -``` - -Register event listener - -```yml -services: - SendEmailOnPurchaseOrderCreated: - arguments: [ '@mailer' ] - tags: - - { name: domain_event.listener, event: PurchaseOrderCreatedEvent, method: onPurchaseOrderCreated } -``` - -Publish events in listener - -```php -use GpsLab\Domain\Event\Bus\EventBus; - -// get event bus from DI container -$bus = $this->get(EventBus::class); - -// do what you need to do on your Domain -$purchase_order = new PurchaseOrder(new CustomerId(1)); - -// this will clear the list of event in your AggregateEvents so an Event is trigger only once -$events = $purchase_order->pullEvents(); - -// You can have more than one event at a time. -foreach($events as $event) { - $bus->publish($event); -} - -// You can use one method -//$bus->pullAndPublish($purchase_order); -``` - -Listener method name --------------------- - -You do not need to specify the name of the event handler method. By default, the -[__invoke](http://php.net/manual/en/language.oop5.magic.php#object.invoke) method is used. - - -```php -use GpsLab\Domain\Event\Event; - -class SendEmailOnPurchaseOrderCreated -{ - private $mailer; - - public function __construct(\Swift_Mailer $mailer) - { - $this->mailer = $mailer; - } - - public function __invoke(PurchaseOrderCreatedEvent $event): void - { - $message = $this->mailer - ->createMessage() - ->setTo('recipient@example.com') - ->setBody(sprintf( - 'Purchase order created at %s for customer #%s', - $event->createAt()->format('Y-m-d'), - $event->customerId() - )); - - $this->mailer->send($message); - } -} -``` - -Register event listener - -```yml -services: - SendEmailOnPurchaseOrderCreated: - arguments: [ '@mailer' ] - tags: - - { name: domain_event.listener, event: PurchaseOrderCreatedEvent } -``` - -Event subscribers ------------------ - -Create subscriber - -```php -use GpsLab\Domain\Event\Event; -use GpsLab\Domain\Event\Listener\Subscriber; - -class SendEmailOnPurchaseOrderCreated implements Subscriber -{ - private $mailer; - - public function __construct(\Swift_Mailer $mailer) - { - $this->mailer = $mailer; - } - - public static function subscribedEvents(): array + public static function getSubscribedEvents(): array { return [ - PurchaseOrderCreatedEvent::class => ['onPurchaseOrderCreated'], + PurchaseOrderCreatedEvent::class => 'onPurchaseOrderCreated', ]; } @@ -240,8 +89,8 @@ class SendEmailOnPurchaseOrderCreated implements Subscriber ->setTo('recipient@example.com') ->setBody(sprintf( 'Purchase order created at %s for customer #%s', - $event->createAt()->format('Y-m-d'), - $event->customerId() + $event->create_at->format('Y-m-d'), + $event->customer_id, )); $this->mailer->send($message); @@ -249,193 +98,16 @@ class SendEmailOnPurchaseOrderCreated implements Subscriber } ``` -Register event subscriber - -```yml -services: - SendEmailOnPurchaseOrderCreated: - arguments: [ '@mailer' ] - tags: - - { name: domain_event.subscriber } -``` - -Use pull Predis queue ---------------------- - -Install [Predis](https://github.com/nrk/predis) with [Composer](http://packagist.org), run: - -```sh -composer require predis/predis -``` - -Register services: - -```yml -services: - # Predis - Predis\Client: - arguments: [ '127.0.0.1' ] - - # Events serializer for queue - GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer: - arguments: [ '@serializer', 'json' ] - - # Predis event queue - GpsLab\Domain\Event\Queue\Pull\PredisPullEventQueue: - arguments: - - '@Predis\Client' - - '@GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer' - - '@logger' - - 'event_queue_name' -``` - -Change config for use custom queue: - -```yml -gpslab_domain_event: - queue: 'GpsLab\Domain\Event\Queue\Pull\PredisPullEventQueue' -``` - -And now you can use custom queue: - -```php -use GpsLab\Domain\Event\Queue\EventQueue; - -$container->get(EventQueue::class)->publish($domain_event); -``` - -In latter pull events from queue: - -```php -use GpsLab\Domain\Event\Queue\EventQueue; - -$queue = $container->get(EventQueue::class); -$bus = $container->get(EventQueue::class); - -while ($event = $queue->pull()) { - $bus->publish($event); -} -``` - -Use Predis subscribe queue --------------------------- - -Install [Predis PubSub](https://github.com/Superbalist/php-pubsub-redis) adapter with [Composer](http://packagist.org), run: - -```sh -composer require superbalist/php-pubsub-redis -``` - -Register services: - -```yml -services: - # Predis - Predis\Client: - arguments: [ '127.0.0.1' ] - - # Predis PubSub adapter - Superbalist\PubSub\Redis\RedisPubSubAdapter: - arguments: [ '@Predis\Client' ] - - # Events serializer for queue - GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer: - arguments: [ '@serializer', 'json' ] - - # Predis event queue - GpsLab\Domain\Event\Queue\Subscribe\PredisSubscribeEventQueue: - arguments: - - '@Superbalist\PubSub\Redis\RedisPubSubAdapter' - - '@GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer' - - '@logger' - - 'event_queue_name' -``` - -Change config for use custom queue: - -```yml -gpslab_domain_event: - queue: 'GpsLab\Domain\Event\Queue\Subscribe\PredisSubscribeEventQueue' -``` - -And now you can use custom queue: +Publish events ```php -use GpsLab\Domain\Event\Queue\EventQueue; - -$container->get(EventQueue::class)->publish($domain_event); -``` - -Subscribe on the queue: - -```php -use GpsLab\Domain\Event\Queue\EventQueue; - -$container->get(EventQueue::class)->subscribe(function (Event $event) { - // do somthing -}); -``` - -> **Note** -> -> You can use subscribe handlers as a services and [tag](http://symfony.com/doc/current/service_container/tags.html) it -for optimize register. - -Many queues ------------ - -You can use many queues for separation the flows. For example, you want to handle events of different Bounded Contexts -separately from each other. - -```yml -services: - acme.domain.purchase_order.event.queue: - class: GpsLab\Domain\Event\Queue\Pull\PredisPullEventQueue - arguments: - - '@Superbalist\PubSub\Redis\RedisPubSubAdapter' - - '@GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer' - - '@logger' - - 'purchase_order_event_queue' - - acme.domain.article_comment.event.queue: - class: GpsLab\Domain\Event\Queue\Pull\PredisPullEventQueue - arguments: - - '@Superbalist\PubSub\Redis\RedisPubSubAdapter' - - '@GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer' - - '@logger' - - 'article_comment_event_queue' -``` - -And now you can use a different queues. - -In **Purchase order** Bounded Contexts. - -```php -$event = new PurchaseOrderCreatedEvent( - new CustomerId(1), - new \DateTimeImmutable() -); - -$container->get('acme.domain.purchase_order.event.queue')->publish($event); -``` - -In **Article comment** Bounded Contexts. - -```php -$event = new ArticleCommentedEvent( - new ArticleId(1), - new AuthorId(1), - $comment - new \DateTimeImmutable() -); +// do what you need to do on your Domain +$purchase_order = new PurchaseOrder(new CustomerId(1)); -$container->get('acme.domain.article_comment.event.queue')->publish($event); +$em->persist($purchase_order); +$em->flush(); ``` -> **Note** -> -> Similarly, you can split the subscribe queues. - License ------- diff --git a/composer.json b/composer.json index 8a97580..23bbea2 100644 --- a/composer.json +++ b/composer.json @@ -15,19 +15,20 @@ } }, "require": { - "php": ">=5.5.0", - "gpslab/domain-event": "~2.0", - "symfony/http-kernel": "~2.3|~3.0|~4.0|~5.0", - "symfony/dependency-injection": "~2.3|~3.0|~4.0|~5.0", - "symfony/expression-language": "~2.3|~3.0|~4.0|~5.0", + "php": ">=7.4.0", + "symfony/http-kernel": "~4.0|~5.0", + "symfony/dependency-injection": "~4.0|~5.0", + "symfony/config": "~4.0|~5.0", + "symfony/expression-language": "~4.0|~5.0", + "symfony/event-dispatcher": "~4.0|~5.0", "doctrine/orm": "~2.4" }, "require-dev": { - "phpunit/phpunit": "^4.8.36" + "phpunit/phpunit": "~9.5" }, "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } } } diff --git a/src/DependencyInjection/Compiler/EventListenerPass.php b/src/DependencyInjection/Compiler/EventListenerPass.php deleted file mode 100644 index 73f95c0..0000000 --- a/src/DependencyInjection/Compiler/EventListenerPass.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @license http://opensource.org/licenses/MIT - */ - -namespace GpsLab\Bundle\DomainEvent\DependencyInjection\Compiler; - -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; - -class EventListenerPass implements CompilerPassInterface -{ - /** - * @param ContainerBuilder $container - */ - public function process(ContainerBuilder $container) - { - if (!$container->has('domain_event.locator')) { - return; - } - - $current_locator = $container->findDefinition('domain_event.locator'); - $symfony_locator = $container->findDefinition('domain_event.locator.symfony'); - $container_locator = $container->findDefinition('domain_event.locator.container'); - - if ($current_locator === $symfony_locator || $current_locator === $container_locator) { - $this->registerListeners($container, $current_locator); - $this->registerSubscribers($container, $current_locator); - } - } - - /** - * @param ContainerBuilder $container - * @param Definition $current_locator - */ - private function registerListeners(ContainerBuilder $container, Definition $current_locator) - { - foreach ($container->findTaggedServiceIds('domain_event.listener') as $id => $attributes) { - foreach ($attributes as $attribute) { - $method = !empty($attribute['method']) ? $attribute['method'] : '__invoke'; - $current_locator->addMethodCall('registerService', [$attribute['event'], $id, $method]); - } - } - } - - /** - * @param ContainerBuilder $container - * @param Definition $current_locator - */ - private function registerSubscribers(ContainerBuilder $container, Definition $current_locator) - { - foreach ($container->findTaggedServiceIds('domain_event.subscriber') as $id => $attributes) { - $subscriber = $container->findDefinition($id); - $current_locator->addMethodCall('registerSubscriberService', [$id, $subscriber->getClass()]); - } - } -} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php deleted file mode 100644 index 0aef668..0000000 --- a/src/DependencyInjection/Configuration.php +++ /dev/null @@ -1,90 +0,0 @@ - - * @license http://opensource.org/licenses/MIT - */ - -namespace GpsLab\Bundle\DomainEvent\DependencyInjection; - -use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; -use Symfony\Component\Config\Definition\ConfigurationInterface; - -class Configuration implements ConfigurationInterface -{ - /** - * Config tree builder. - * - * Example config: - * - * gpslab_domain_event: - * bus: 'listener_located' - * queue: 'pull_memory' - * locator: 'symfony' - * - * @return TreeBuilder - */ - public function getConfigTreeBuilder() - { - $tree_builder = $this->createTreeBuilder('gpslab_domain_event'); - $root = $this->getRootNode($tree_builder, 'gpslab_domain_event'); - - $bus = $root->children()->scalarNode('bus'); - $bus->cannotBeEmpty()->defaultValue('listener_located'); - - $queue = $root->children()->scalarNode('queue'); - $queue->cannotBeEmpty()->defaultValue('pull_memory'); - - $locator = $root->children()->scalarNode('locator'); - $locator->cannotBeEmpty()->defaultValue('symfony'); - - $publish_on_flush = $root->children()->booleanNode('publish_on_flush'); - $publish_on_flush->defaultValue(false); - - return $tree_builder; - } - - /** - * @param string $name - * - * @return TreeBuilder - */ - private function createTreeBuilder($name) - { - // Symfony 4.2 + - if (method_exists(TreeBuilder::class, '__construct')) { - return new TreeBuilder($name); - } - - // Symfony 4.1 and below - return new TreeBuilder(); - } - - /** - * @param TreeBuilder $tree_builder - * @param string $name - * - * @return ArrayNodeDefinition - */ - private function getRootNode(TreeBuilder $tree_builder, $name) - { - if (method_exists($tree_builder, 'getRootNode')) { - // Symfony 4.2 + - $root = $tree_builder->getRootNode(); - } else { - // Symfony 4.1 and below - $root = $tree_builder->root($name); - } - - // @codeCoverageIgnoreStart - if (!($root instanceof ArrayNodeDefinition)) { // should be always false - throw new \RuntimeException(sprintf('The root node should be instance of %s, got %s instead.', ArrayNodeDefinition::class, get_class($root))); - } - // @codeCoverageIgnoreEnd - - return $root; - } -} diff --git a/src/DependencyInjection/GpsLabDomainEventExtension.php b/src/DependencyInjection/GpsLabDomainEventExtension.php index 771760f..4e1f466 100644 --- a/src/DependencyInjection/GpsLabDomainEventExtension.php +++ b/src/DependencyInjection/GpsLabDomainEventExtension.php @@ -1,4 +1,5 @@ load('queue.yml'); - $loader->load('bus.yml'); - $loader->load('locator.yml'); - $loader->load('publisher.yml'); - - $config = $this->processConfiguration(new Configuration(), $configs); - - $container->setAlias('domain_event.bus', $this->busRealName($config['bus'])); - $container->setAlias('domain_event.queue', $this->queueRealName($config['queue'])); - $container->setAlias('domain_event.locator', $this->locatorRealName($config['locator'])); - $container->setAlias(EventBus::class, $this->busRealName($config['bus'])); - $container->setAlias(EventQueue::class, $this->queueRealName($config['queue'])); - - $container->getDefinition('domain_event.publisher')->replaceArgument(2, $config['publish_on_flush']); - - // subscribers tagged automatically - if (method_exists($container, 'registerForAutoconfiguration')) { - $container - ->registerForAutoconfiguration(Subscriber::class) - ->addTag('domain_event.subscriber') - ->setAutowired(true) - ; - } - } - - /** - * @param string $name - * - * @return string - */ - private function busRealName($name) - { - if (in_array($name, ['listener_located', 'queue'])) { - return 'domain_event.bus.'.$name; - } - - return $name; - } - - /** - * @param string $name - * - * @return string - */ - private function queueRealName($name) - { - if (in_array($name, ['pull_memory', 'subscribe_executing'])) { - return 'domain_event.queue.'.$name; - } - - return $name; - } - - /** - * @param string $name - * - * @return string - */ - private function locatorRealName($name) - { - if (in_array($name, ['direct_binding', 'container', 'symfony'])) { - return 'domain_event.locator.'.$name; - } - - return $name; + $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.xml'); } - /** - * @return string - */ - public function getAlias() + public function getAlias(): string { return 'gpslab_domain_event'; } diff --git a/src/Event/Aggregator/AbstractAggregateEvents.php b/src/Event/Aggregator/AbstractAggregateEvents.php new file mode 100644 index 0000000..abb84d8 --- /dev/null +++ b/src/Event/Aggregator/AbstractAggregateEvents.php @@ -0,0 +1,16 @@ + + * @license http://opensource.org/licenses/MIT + */ + +namespace GpsLab\Bundle\DomainEvent\Event\Aggregator; + +abstract class AbstractAggregateEvents implements AggregateEvents +{ + use AggregateEventsTrait; +} diff --git a/src/Event/Aggregator/AbstractAggregateEventsRaiseInSelf.php b/src/Event/Aggregator/AbstractAggregateEventsRaiseInSelf.php new file mode 100644 index 0000000..db1927e --- /dev/null +++ b/src/Event/Aggregator/AbstractAggregateEventsRaiseInSelf.php @@ -0,0 +1,16 @@ + + * @license http://opensource.org/licenses/MIT + */ + +namespace GpsLab\Bundle\DomainEvent\Event\Aggregator; + +abstract class AbstractAggregateEventsRaiseInSelf implements AggregateEvents +{ + use AggregateEventsRaiseInSelfTrait; +} diff --git a/src/Event/Aggregator/AggregateEvents.php b/src/Event/Aggregator/AggregateEvents.php new file mode 100644 index 0000000..c4672b9 --- /dev/null +++ b/src/Event/Aggregator/AggregateEvents.php @@ -0,0 +1,21 @@ + + * @license http://opensource.org/licenses/MIT + */ + +namespace GpsLab\Bundle\DomainEvent\Event\Aggregator; + +use Symfony\Contracts\EventDispatcher\Event; + +interface AggregateEvents +{ + /** + * @return Event[] + */ + public function pullEvents(): array; +} diff --git a/src/Event/Aggregator/AggregateEventsRaiseInSelfTrait.php b/src/Event/Aggregator/AggregateEventsRaiseInSelfTrait.php new file mode 100644 index 0000000..98adeb3 --- /dev/null +++ b/src/Event/Aggregator/AggregateEventsRaiseInSelfTrait.php @@ -0,0 +1,67 @@ + + * @license http://opensource.org/licenses/MIT + */ + +namespace GpsLab\Bundle\DomainEvent\Event\Aggregator; + +use Symfony\Contracts\EventDispatcher\Event; + +trait AggregateEventsRaiseInSelfTrait +{ + /** + * @var Event[] + */ + private array $events = []; + + private function raiseInSelf(Event $event): void + { + $method = $this->eventHandlerName($event); + + // if method is not exists is not a critical error + if (method_exists($this, $method)) { + $this->{$method}($event); + } + } + + protected function raise(Event $event): void + { + $this->events[] = $event; + $this->raiseInSelf($event); + } + + /** + * @return Event[] + */ + public function pullEvents(): array + { + $events = $this->events; + $this->events = []; + + return $events; + } + + /** + * Get handler method name from event. + * + * Override this method if you want to change algorithm to generate the handler method name. + */ + protected function eventHandlerName(Event $event): string + { + $class = get_class($event); + + if ('Event' === substr($class, -5)) { + $class = substr($class, 0, -5); + } + + $class = str_replace('_', '\\', $class); // convert names for classes not in namespace + $parts = explode('\\', $class); + + return 'on'.end($parts); + } +} diff --git a/src/Event/Aggregator/AggregateEventsTrait.php b/src/Event/Aggregator/AggregateEventsTrait.php new file mode 100644 index 0000000..26063c6 --- /dev/null +++ b/src/Event/Aggregator/AggregateEventsTrait.php @@ -0,0 +1,37 @@ + + * @license http://opensource.org/licenses/MIT + */ + +namespace GpsLab\Bundle\DomainEvent\Event\Aggregator; + +use Symfony\Contracts\EventDispatcher\Event; + +trait AggregateEventsTrait +{ + /** + * @var Event[] + */ + private array $events = []; + + protected function raise(Event $event): void + { + $this->events[] = $event; + } + + /** + * @return Event[] + */ + public function pullEvents(): array + { + $events = $this->events; + $this->events = []; + + return $events; + } +} diff --git a/src/Event/Listener/DomainEventPublisher.php b/src/Event/Listener/DomainEventPublisher.php deleted file mode 100644 index 423e04d..0000000 --- a/src/Event/Listener/DomainEventPublisher.php +++ /dev/null @@ -1,100 +0,0 @@ - - * @license http://opensource.org/licenses/MIT - */ - -namespace GpsLab\Bundle\DomainEvent\Event\Listener; - -use Doctrine\Common\EventSubscriber; -use Doctrine\ORM\Event\OnFlushEventArgs; -use Doctrine\ORM\Event\PostFlushEventArgs; -use Doctrine\ORM\Events; -use GpsLab\Bundle\DomainEvent\Service\EventPuller; -use GpsLab\Domain\Event\Bus\EventBus; -use GpsLab\Domain\Event\Event; - -class DomainEventPublisher implements EventSubscriber -{ - /** - * @var EventPuller - */ - private $puller; - - /** - * @var EventBus - */ - private $bus; - - /** - * @var bool - */ - private $enable; - - /** - * @var Event[] - */ - private $events = []; - - /** - * @param EventPuller $puller - * @param EventBus $bus - * @param bool $enable - */ - public function __construct(EventPuller $puller, EventBus $bus, $enable) - { - $this->bus = $bus; - $this->puller = $puller; - $this->enable = $enable; - } - - /** - * @return array - */ - public function getSubscribedEvents() - { - if (!$this->enable) { - return []; - } - - return [ - Events::onFlush, - Events::postFlush, - ]; - } - - /** - * @param OnFlushEventArgs $args - */ - public function onFlush(OnFlushEventArgs $args) - { - // aggregate events from deleted entities - $this->events = $this->puller->pull($args->getEntityManager()->getUnitOfWork()); - } - - /** - * @param PostFlushEventArgs $args - */ - public function postFlush(PostFlushEventArgs $args) - { - // aggregate PreRemove/PostRemove events - $events = array_merge($this->events, $this->puller->pull($args->getEntityManager()->getUnitOfWork())); - - // clear aggregate events before publish it - // it necessary for fix recursive publish of events - $this->events = []; - - // flush only if has domain events - // it necessary for fix recursive handle flush - if (!empty($events)) { - foreach ($events as $event) { - $this->bus->publish($event); - } - - $args->getEntityManager()->flush(); - } - } -} diff --git a/src/Event/Publisher.php b/src/Event/Publisher.php new file mode 100644 index 0000000..c320b0f --- /dev/null +++ b/src/Event/Publisher.php @@ -0,0 +1,63 @@ + + * @license http://opensource.org/licenses/MIT + */ + +namespace GpsLab\Bundle\DomainEvent\Event; + +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Contracts\EventDispatcher\Event; + +class Publisher +{ + private Puller $puller; + private EventDispatcher $dispatcher; + + /** + * @var Event[][] + */ + private array $events = []; + + public function __construct(Puller $puller, EventDispatcher $dispatcher) + { + $this->dispatcher = $dispatcher; + $this->puller = $puller; + } + + public function aggregateEvents(EntityManagerInterface $em): void + { + $emid = spl_object_id($em); + + $this->events[$emid] = array_merge($this->events[$emid] ?? [], $this->puller->pull($em->getUnitOfWork())); + } + + public function dispatchEvents(EntityManagerInterface $em): void + { + $emid = spl_object_id($em); + + if (!isset($this->events[$emid])) { + return; // no events for dispatch + } + + // clear aggregate events before publish it + // it necessary for fix recursive publish of events + $events = $this->events[$emid]; + unset($this->events[$emid]); + + // flush only if has domain events + // it necessary for fix recursive handle flush + if ($events !== []) { + foreach ($events as $event) { + $this->dispatcher->dispatch($event); + } + + $em->flush(); + } + } +} diff --git a/src/Service/EventPuller.php b/src/Event/Puller.php similarity index 54% rename from src/Service/EventPuller.php rename to src/Event/Puller.php index 657b855..0e72cfc 100644 --- a/src/Service/EventPuller.php +++ b/src/Event/Puller.php @@ -1,4 +1,5 @@ pullFromEntities($uow->getScheduledEntityDeletions())); - $events = array_merge($events, $this->pullFromEntities($uow->getScheduledEntityInsertions())); - $events = array_merge($events, $this->pullFromEntities($uow->getScheduledEntityUpdates())); + $events[] = $this->pullFromEntities($uow->getScheduledEntityDeletions()); + $events[] = $this->pullFromEntities($uow->getScheduledEntityInsertions()); + $events[] = $this->pullFromEntities($uow->getScheduledEntityUpdates()); // other entities foreach ($uow->getIdentityMap() as $entities) { - $events = array_merge($events, $this->pullFromEntities($entities)); + $events[] = $this->pullFromEntities($entities); } - return $events; + return array_merge([], ...$events); } /** - * @param array $entities + * @param object[] $entities * * @return Event[] */ - private function pullFromEntities(array $entities) + private function pullFromEntities(array $entities): array { $events = []; @@ -58,10 +57,10 @@ private function pullFromEntities(array $entities) } if ($entity instanceof AggregateEvents) { - $events = array_merge($events, $entity->pullEvents()); + $events[] = $entity->pullEvents(); } } - return $events; + return array_merge([], ...$events); } } diff --git a/src/Event/Subscriber/DoctrineEventSubscriber.php b/src/Event/Subscriber/DoctrineEventSubscriber.php new file mode 100644 index 0000000..a3fa85d --- /dev/null +++ b/src/Event/Subscriber/DoctrineEventSubscriber.php @@ -0,0 +1,52 @@ + + * @license http://opensource.org/licenses/MIT + */ + +namespace GpsLab\Bundle\DomainEvent\Event\Subscriber; + +use Doctrine\Common\EventSubscriber; +use Doctrine\ORM\Event\OnFlushEventArgs; +use Doctrine\ORM\Event\PostFlushEventArgs; +use Doctrine\ORM\Events; +use GpsLab\Bundle\DomainEvent\Event\Publisher; + +class DoctrineEventSubscriber implements EventSubscriber +{ + private Publisher $publisher; + + public function __construct(Publisher $publisher) + { + $this->publisher = $publisher; + } + + /** + * @return string[] + */ + public function getSubscribedEvents(): array + { + return [ + Events::onFlush, + Events::postFlush, + ]; + } + + public function onFlush(OnFlushEventArgs $args): void + { + // aggregate events from deleted entities + $this->publisher->aggregateEvents($args->getEntityManager()); + } + + public function postFlush(PostFlushEventArgs $args): void + { + // aggregate PreRemove/PostRemove events + $this->publisher->aggregateEvents($args->getEntityManager()); + + $this->publisher->dispatchEvents($args->getEntityManager()); + } +} diff --git a/src/GpsLabDomainEventBundle.php b/src/GpsLabDomainEventBundle.php index 58bc983..6d386ac 100644 --- a/src/GpsLabDomainEventBundle.php +++ b/src/GpsLabDomainEventBundle.php @@ -1,4 +1,5 @@ addCompilerPass(new EventListenerPass()); - } - - /** - * @return GpsLabDomainEventExtension - */ - public function getContainerExtension() - { - if (!($this->extension instanceof GpsLabDomainEventExtension)) { + if (!$this->extension instanceof GpsLabDomainEventExtension) { $this->extension = new GpsLabDomainEventExtension(); } diff --git a/src/Resources/config/bus.yml b/src/Resources/config/bus.yml deleted file mode 100644 index 75cf381..0000000 --- a/src/Resources/config/bus.yml +++ /dev/null @@ -1,10 +0,0 @@ -services: - domain_event.bus.listener_located: - class: GpsLab\Domain\Event\Bus\ListenerLocatedEventBus - arguments: [ '@domain_event.locator' ] - public: false - - domain_event.bus.queue: - class: GpsLab\Domain\Event\Bus\QueueEventBus - arguments: [ '@domain_event.queue' ] - public: false diff --git a/src/Resources/config/locator.yml b/src/Resources/config/locator.yml deleted file mode 100644 index cebec6d..0000000 --- a/src/Resources/config/locator.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - domain_event.locator.symfony: - class: GpsLab\Domain\Event\Listener\Locator\SymfonyContainerEventListenerLocator - calls: - - [ setContainer, [ '@service_container' ] ] - public: false - - domain_event.locator.container: - class: GpsLab\Domain\Event\Listener\Locator\ContainerEventListenerLocator - arguments: [ '@service_container' ] - public: false - - domain_event.locator.direct_binding: - class: GpsLab\Domain\Event\Listener\Locator\DirectBindingEventListenerLocator - public: false diff --git a/src/Resources/config/publisher.yml b/src/Resources/config/publisher.yml deleted file mode 100644 index d2c524e..0000000 --- a/src/Resources/config/publisher.yml +++ /dev/null @@ -1,10 +0,0 @@ -services: - domain_event.publisher: - class: GpsLab\Bundle\DomainEvent\Event\Listener\DomainEventPublisher - arguments: [ '@domain_event.puller', '@domain_event.bus', ~ ] - tags: - - { name: doctrine.event_subscriber } - - domain_event.puller: - class: GpsLab\Bundle\DomainEvent\Service\EventPuller - public: false diff --git a/src/Resources/config/queue.yml b/src/Resources/config/queue.yml deleted file mode 100644 index cedb3b7..0000000 --- a/src/Resources/config/queue.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - domain_event.queue.pull_memory: - class: GpsLab\Domain\Event\Queue\Pull\MemoryPullEventQueue - public: false - - domain_event.queue.subscribe_executing: - class: GpsLab\Domain\Event\Queue\Subscribe\ExecutingSubscribeEventQueue - public: false diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml new file mode 100644 index 0000000..5db2d1f --- /dev/null +++ b/src/Resources/config/services.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/tests/DependencyInjection/Compiler/EventListenerPassTest.php b/tests/DependencyInjection/Compiler/EventListenerPassTest.php deleted file mode 100644 index ca77a41..0000000 --- a/tests/DependencyInjection/Compiler/EventListenerPassTest.php +++ /dev/null @@ -1,235 +0,0 @@ - - * @license http://opensource.org/licenses/MIT - */ - -namespace GpsLab\Bundle\DomainEvent\Tests\DependencyInjection\Compiler; - -use GpsLab\Bundle\DomainEvent\DependencyInjection\Compiler\EventListenerPass; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; - -class EventListenerPassTest extends TestCase -{ - /** - * @var \PHPUnit_Framework_MockObject_MockObject|ContainerBuilder - */ - private $container; - - /** - * @var EventListenerPass - */ - private $pass; - - protected function setUp() - { - if (PHP_VERSION_ID >= 70000) { - $this->markTestSkipped(sprintf('Impossible to mock "%s" on PHP 7', ContainerBuilder::class)); - } - - $this->container = $this - ->getMockBuilder(ContainerBuilder::class) - ->disableOriginalConstructor() - ->getMock() - ; - $this->pass = new EventListenerPass(); - } - - public function testProcessNoLocator() - { - $this->container - ->expects($this->once()) - ->method('has') - ->with('domain_event.locator') - ->will($this->returnValue(false)) - ; - $this->container - ->expects($this->never()) - ->method('findDefinition') - ; - $this->container - ->expects($this->never()) - ->method('findTaggedServiceIds') - ; - - $this->pass->process($this->container); - } - - public function testProcessCustomLocator() - { - // create fake definitions to distinguish them - $symfony_locator = new Definition(null, ['symfony']); - $container_locator = new Definition(null, ['container']); - $current_locator = new Definition(null, ['custom']); - - $this->container - ->expects($this->at(0)) - ->method('has') - ->with('domain_event.locator') - ->will($this->returnValue(true)) - ; - $this->container - ->expects($this->at(1)) - ->method('findDefinition') - ->with('domain_event.locator') - ->will($this->returnValue($current_locator)) - ; - $this->container - ->expects($this->at(2)) - ->method('findDefinition') - ->with('domain_event.locator.symfony') - ->will($this->returnValue($symfony_locator)) - ; - $this->container - ->expects($this->at(3)) - ->method('findDefinition') - ->with('domain_event.locator.container') - ->will($this->returnValue($container_locator)) - ; - $this->container - ->expects($this->never()) - ->method('findTaggedServiceIds') - ; - - $this->pass->process($this->container); - } - - /** - * @return array - */ - public function locators() - { - if (PHP_VERSION_ID >= 70000) { - $this->markTestSkipped(sprintf('Impossible to mock "%s" on PHP 7', Definition::class)); - } - - $locator = $this->getMock(Definition::class); - - return [ - [ - $locator, - new Definition(), - $locator, - ], - [ - new Definition(), - $locator, - $locator, - ], - ]; - } - - /** - * @dataProvider locators - * - * @param \PHPUnit_Framework_MockObject_MockObject|Definition $symfony_locator - * @param \PHPUnit_Framework_MockObject_MockObject|Definition $container_locator - * @param \PHPUnit_Framework_MockObject_MockObject|Definition $current_locator - */ - public function testProcess( - Definition $symfony_locator, - Definition $container_locator, - Definition $current_locator - ) { - $listeners = [ - 'foo' => [ - ['event' => 'PurchaseOrderCompletedEvent', 'method' => 'onPurchaseOrderCompleted'], - ['event' => 'PurchaseOrderCreated', 'method' => 'onPurchaseOrderCreated'], - ], - 'bar' => [ - ['event' => 'PurchaseOrderCompletedEvent'], - ], - 'baz' => [ - ['event' => 'PurchaseOrderCreated', 'method' => 'handle'], - ], - ]; - $subscribers = [ - 'foo' => [], - 'bar' => [], - 'baz' => [], - ]; - - $locator_index = 0; - $container_index = 0; - - $this->container - ->expects($this->at($container_index++)) - ->method('has') - ->with('domain_event.locator') - ->will($this->returnValue(true)) - ; - $this->container - ->expects($this->at($container_index++)) - ->method('findDefinition') - ->with('domain_event.locator') - ->will($this->returnValue($current_locator)) - ; - $this->container - ->expects($this->at($container_index++)) - ->method('findDefinition') - ->with('domain_event.locator.symfony') - ->will($this->returnValue($symfony_locator)) - ; - $this->container - ->expects($this->at($container_index++)) - ->method('findDefinition') - ->with('domain_event.locator.container') - ->will($this->returnValue($container_locator)) - ; - $this->container - ->expects($this->at($container_index++)) - ->method('findTaggedServiceIds') - ->with('domain_event.listener') - ->will($this->returnValue($listeners)) - ; - $this->container - ->expects($this->at($container_index++)) - ->method('findTaggedServiceIds') - ->with('domain_event.subscriber') - ->will($this->returnValue($subscribers)) - ; - - foreach ($listeners as $id => $attributes) { - foreach ($attributes as $attribute) { - $method = !empty($attribute['method']) ? $attribute['method'] : '__invoke'; - - $current_locator - ->expects($this->at($locator_index++)) - ->method('addMethodCall') - ->with('registerService', [$attribute['event'], $id, $method]) - ; - } - } - - foreach ($subscribers as $id => $attributes) { - $class_name = $id; - - $subscriber = $this->getMock(Definition::class); - $subscriber - ->expects($this->once()) - ->method('getClass') - ->will($this->returnValue($class_name)) - ; - - $this->container - ->expects($this->at($container_index++)) - ->method('findDefinition') - ->with($id) - ->will($this->returnValue($subscriber)) - ; - - $current_locator - ->expects($this->at($locator_index++)) - ->method('addMethodCall') - ->with('registerSubscriberService', [$id, $class_name]) - ; - } - - $this->pass->process($this->container); - } -} diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php deleted file mode 100644 index 524a7f8..0000000 --- a/tests/DependencyInjection/ConfigurationTest.php +++ /dev/null @@ -1,60 +0,0 @@ - - * @license http://opensource.org/licenses/MIT - */ - -namespace GpsLab\Bundle\DomainEvent\Tests\DependencyInjection; - -use GpsLab\Bundle\DomainEvent\DependencyInjection\Configuration; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Config\Definition\ArrayNode; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; -use Symfony\Component\Config\Definition\ScalarNode; - -class ConfigurationTest extends TestCase -{ - /** - * @var Configuration - */ - private $configuration; - - protected function setUp() - { - $this->configuration = new Configuration(); - } - - public function testConfigTree() - { - $tree_builder = $this->configuration->getConfigTreeBuilder(); - - $this->assertInstanceOf(TreeBuilder::class, $tree_builder); - - /* @var $tree ArrayNode */ - $tree = $tree_builder->buildTree(); - - $this->assertInstanceOf(ArrayNode::class, $tree); - $this->assertEquals('gpslab_domain_event', $tree->getName()); - - /* @var $children ScalarNode[] */ - $children = $tree->getChildren(); - - $this->assertInternalType('array', $children); - $this->assertEquals(['bus', 'queue', 'locator', 'publish_on_flush'], array_keys($children)); - - $this->assertInstanceOf(ScalarNode::class, $children['bus']); - $this->assertEquals('listener_located', $children['bus']->getDefaultValue()); - $this->assertFalse($children['bus']->isRequired()); - - $this->assertInstanceOf(ScalarNode::class, $children['queue']); - $this->assertEquals('pull_memory', $children['queue']->getDefaultValue()); - $this->assertFalse($children['queue']->isRequired()); - - $this->assertInstanceOf(ScalarNode::class, $children['locator']); - $this->assertEquals('symfony', $children['locator']->getDefaultValue()); - $this->assertFalse($children['locator']->isRequired()); - } -} diff --git a/tests/DependencyInjection/GpsLabDomainEventExtensionTest.php b/tests/DependencyInjection/GpsLabDomainEventExtensionTest.php index f13cfc8..5b9cf84 100644 --- a/tests/DependencyInjection/GpsLabDomainEventExtensionTest.php +++ b/tests/DependencyInjection/GpsLabDomainEventExtensionTest.php @@ -1,4 +1,5 @@ extension = new GpsLabDomainEventExtension(); $this->container = new ContainerBuilder(); } - /** - * @return array - */ - public function config() + public function testLoad(): void { - return [ - [ - [], - 'domain_event.bus.listener_located', - 'domain_event.queue.pull_memory', - 'domain_event.locator.symfony', - false, - ], - [ - [ - 'gpslab_domain_event' => [ - 'bus' => 'queue', - 'queue' => 'subscribe_executing', - 'locator' => 'container', - 'publish_on_flush' => false, - ], - ], - 'domain_event.bus.queue', - 'domain_event.queue.subscribe_executing', - 'domain_event.locator.container', - false, - ], - [ - [ - 'gpslab_domain_event' => [ - 'bus' => 'queue', - 'queue' => 'subscribe_executing', - 'locator' => 'direct_binding', - 'publish_on_flush' => true, - ], - ], - 'domain_event.bus.queue', - 'domain_event.queue.subscribe_executing', - 'domain_event.locator.direct_binding', - true, - ], - [ - [ - 'gpslab_domain_event' => [ - 'bus' => 'acme.domain.event.bus', - 'queue' => 'acme.domain.event.queue', - 'locator' => 'acme.domain.event.locator', - 'publish_on_flush' => true, - ], - ], - 'acme.domain.event.bus', - 'acme.domain.event.queue', - 'acme.domain.event.locator', - true, - ], - ]; - } - - /** - * @dataProvider config - * - * @param array $config - * @param string $bus - * @param string $queue - * @param string $locator - * @param bool $publish_on_flush - */ - public function testLoad(array $config, $bus, $queue, $locator, $publish_on_flush) - { - $this->extension->load($config, $this->container); - - $this->assertEquals($bus, $this->container->getAlias('domain_event.bus')); - $this->assertEquals($queue, $this->container->getAlias('domain_event.queue')); - $this->assertEquals($locator, $this->container->getAlias('domain_event.locator')); - $this->assertEquals($bus, $this->container->getAlias(EventBus::class)); - $this->assertEquals($queue, $this->container->getAlias(EventQueue::class)); - - $publisher = $this->container->getDefinition('domain_event.publisher'); - $this->assertEquals($publish_on_flush, $publisher->getArgument(2)); + $this->extension->load([], $this->container); - if (method_exists($this->container, 'registerForAutoconfiguration')) { - $has_subscriber = false; - foreach ($this->container->getAutoconfiguredInstanceof() as $key => $definition) { - if ($key === Subscriber::class) { - $has_subscriber = true; - $this->assertTrue($definition->hasTag('domain_event.subscriber')); - $this->assertTrue($definition->isAutowired()); - } - } - $this->assertTrue($has_subscriber); - } + $this->assertTrue($this->container->hasDefinition(Puller::class)); + $this->assertTrue($this->container->hasDefinition(Publisher::class)); + $this->assertTrue($this->container->hasDefinition(DoctrineEventSubscriber::class)); } - public function testAlias() + public function testAlias(): void { $this->assertEquals('gpslab_domain_event', $this->extension->getAlias()); } diff --git a/tests/Event/Listener/DomainEventPublisherTest.php b/tests/Event/Listener/DomainEventPublisherTest.php deleted file mode 100644 index 3b32612..0000000 --- a/tests/Event/Listener/DomainEventPublisherTest.php +++ /dev/null @@ -1,277 +0,0 @@ - - * @license http://opensource.org/licenses/MIT - */ - -namespace GpsLab\Bundle\DomainEvent\Tests\Event\Listener; - -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Event\OnFlushEventArgs; -use Doctrine\ORM\Event\PostFlushEventArgs; -use Doctrine\ORM\Events; -use Doctrine\ORM\UnitOfWork; -use GpsLab\Bundle\DomainEvent\Event\Listener\DomainEventPublisher; -use GpsLab\Bundle\DomainEvent\Service\EventPuller; -use GpsLab\Domain\Event\Bus\EventBus; -use GpsLab\Domain\Event\Event; -use PHPUnit\Framework\TestCase; - -class DomainEventPublisherTest extends TestCase -{ - /** - * @var \PHPUnit_Framework_MockObject_MockObject|EventBus - */ - private $bus; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|EventPuller - */ - private $puller; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|EntityManagerInterface - */ - private $em; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|UnitOfWork - */ - private $uow; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|OnFlushEventArgs - */ - private $on_flush; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|PostFlushEventArgs - */ - private $post_flush; - - /** - * @var DomainEventPublisher - */ - private $publisher; - - protected function setUp() - { - $this->bus = $this->getMockBuilder(EventBus::class)->getMock(); - $this->puller = $this->getMockBuilder(EventPuller::class)->getMock(); - $this->em = $this->getMockBuilder(EntityManagerInterface::class)->getMock(); - - $this->on_flush = $this - ->getMockBuilder(OnFlushEventArgs::class) - ->disableOriginalConstructor() - ->getMock() - ; - $this->on_flush - ->expects($this->any()) - ->method('getEntityManager') - ->will($this->returnValue($this->em)) - ; - - $this->post_flush = $this - ->getMockBuilder(PostFlushEventArgs::class) - ->disableOriginalConstructor() - ->getMock() - ; - $this->post_flush - ->expects($this->any()) - ->method('getEntityManager') - ->will($this->returnValue($this->em)) - ; - - $this->uow = $this - ->getMockBuilder(UnitOfWork::class) - ->disableOriginalConstructor() - ->getMock() - ; - - $this->publisher = new DomainEventPublisher($this->puller, $this->bus, true); - } - - public function testDisabled() - { - $publisher = new DomainEventPublisher($this->puller, $this->bus, false); - $this->assertEquals([], $publisher->getSubscribedEvents()); - } - - public function testEnabled() - { - $publisher = new DomainEventPublisher($this->puller, $this->bus, true); - $this->assertEquals([Events::onFlush, Events::postFlush], $publisher->getSubscribedEvents()); - } - - public function testPreFlush() - { - $this->em - ->expects($this->once()) - ->method('getUnitOfWork') - ->will($this->returnValue($this->uow)) - ; - - $this->puller - ->expects($this->once()) - ->method('pull') - ->with($this->uow) - ; - - $this->publisher->onFlush($this->on_flush); - } - - /** - * @return array - */ - public function events() - { - $events1 = [ - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - ]; - $events2 = [ - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - ]; - - return [ - [[], [], []], - [$events1, []], - [[], $events2], - [$events1, $events2], - ]; - } - - /** - * @dataProvider events - * - * @param array $remove_events - * @param array $exist_events - */ - public function testPublishEvents(array $remove_events, array $exist_events) - { - $this->puller - ->expects($this->at(0)) - ->method('pull') - ->with($this->uow) - ->will($this->returnValue($remove_events)) - ; - $this->puller - ->expects($this->at(1)) - ->method('pull') - ->with($this->uow) - ->will($this->returnValue($exist_events)) - ; - - $expected_events = array_merge($remove_events, $exist_events); - - if ($expected_events) { - foreach ($expected_events as $i => $expected_event) { - $this->bus - ->expects($this->at($i)) - ->method('publish') - ->with($expected_event) - ; - } - $this->em - ->expects($this->once()) - ->method('flush') - ; - } else { - $this->bus - ->expects($this->never()) - ->method('publish') - ; - $this->em - ->expects($this->never()) - ->method('flush') - ; - } - $this->em - ->expects($this->atLeastOnce()) - ->method('getUnitOfWork') - ->will($this->returnValue($this->uow)) - ; - - $this->publisher->onFlush($this->on_flush); - $this->publisher->postFlush($this->post_flush); - } - - public function testRecursivePublish() - { - $remove_events1 = [ - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - ]; - $remove_events2 = [ - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - ]; - $exist_events1 = [ - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - ]; - $exist_events2 = [ - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - $this->getMockBuilder(Event::class)->getMock(), - ]; - - $this->em - ->expects($this->atLeastOnce()) - ->method('getUnitOfWork') - ->will($this->returnValue($this->uow)) - ; - $this->em - ->expects($this->exactly(2)) - ->method('flush') - ; - - $this->puller - ->expects($this->at(0)) - ->method('pull') - ->with($this->uow) - ->will($this->returnValue($remove_events1)) - ; - $this->puller - ->expects($this->at(1)) - ->method('pull') - ->with($this->uow) - ->will($this->returnValue($exist_events1)) - ; - $this->puller - ->expects($this->at(2)) - ->method('pull') - ->with($this->uow) - ->will($this->returnValue($remove_events2)) - ; - $this->puller - ->expects($this->at(3)) - ->method('pull') - ->with($this->uow) - ->will($this->returnValue($exist_events2)) - ; - - $expected_events = array_merge($remove_events1, $exist_events1, $remove_events2, $exist_events2); - foreach ($expected_events as $i => $expected_event) { - $this->bus - ->expects($this->at($i)) - ->method('publish') - ->with($expected_event) - ; - } - - $this->publisher->onFlush($this->on_flush); - $this->publisher->postFlush($this->post_flush); - // recursive call - $this->publisher->onFlush($this->on_flush); - $this->publisher->postFlush($this->post_flush); - } -} diff --git a/tests/Event/PublisherTest.php b/tests/Event/PublisherTest.php new file mode 100644 index 0000000..288272f --- /dev/null +++ b/tests/Event/PublisherTest.php @@ -0,0 +1,197 @@ + + * @license http://opensource.org/licenses/MIT + */ + +namespace GpsLab\Bundle\DomainEvent\Tests\Event; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\UnitOfWork; +use GpsLab\Bundle\DomainEvent\Event\Publisher; +use GpsLab\Bundle\DomainEvent\Event\Puller; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\EventDispatcher\Event; + +class PublisherTest extends TestCase +{ + /** + * @var MockObject&EventDispatcherInterface + */ + private EventDispatcherInterface $dispatcher; + + /** + * @var MockObject&Puller + */ + private Puller $puller; + + /** + * @var MockObject&EntityManagerInterface + */ + private EntityManagerInterface $em; + + /** + * @var MockObject&UnitOfWork + */ + private UnitOfWork $uow; + + private Publisher $publisher; + + protected function setUp(): void + { + $this->dispatcher = $this->getMockBuilder(EventDispatcher::class)->getMock(); + $this->puller = $this->getMockBuilder(Puller::class)->getMock(); + $this->em = $this->getMockBuilder(EntityManagerInterface::class)->getMock(); + $this->uow = $this + ->getMockBuilder(UnitOfWork::class) + ->disableOriginalConstructor() + ->getMock() + ; + + $this->publisher = new Publisher($this->puller, $this->dispatcher); + } + + public function testNothingDispatch(): void + { + $this->dispatcher + ->expects($this->never()) + ->method('dispatch') + ; + $this->em + ->expects($this->never()) + ->method('flush') + ; + + $this->publisher->dispatchEvents($this->em); + } + + public function testDispatchEvents(): void + { + $this->em + ->expects($this->once()) + ->method('getUnitOfWork') + ->willReturn($this->uow) + ; + + $events = [ + new Event(), + new Event(), + new Event(), + ]; + $arguments = []; + + foreach ($events as $event) { + $arguments[] = [$event]; + } + + $this->puller + ->expects($this->once()) + ->method('pull') + ->with($this->uow) + ->willReturn($events) + ; + + $this->dispatcher + ->expects($this->atLeastOnce()) + ->method('dispatch') + ->withConsecutive(...$arguments) + ; + + $this->em + ->expects($this->once()) + ->method('flush') + ; + + $this->publisher->aggregateEvents($this->em); + $this->publisher->dispatchEvents($this->em); + } + + /** + * @return Event[][][] + */ + public function provideEvents(): array + { + return [ + [ + [ + new Event(), + new Event(), + new Event(), + ], + [], + ], + [ + [], + [ + new Event(), + new Event(), + new Event(), + ], + ], + [ + [ + new Event(), + new Event(), + new Event(), + ], + [ + new Event(), + new Event(), + new Event(), + ], + ], + ]; + } + + /** + * @dataProvider provideEvents + * + * @param Event[] $first_loop + * @param Event[] $second_loop + */ + public function testDispatchEventsRecursive(array $first_loop, array $second_loop): void + { + $this->em + ->expects($this->atLeastOnce()) + ->method('getUnitOfWork') + ->willReturn($this->uow) + ; + + $arguments = []; + + foreach (array_merge($first_loop, $second_loop) as $event) { + $arguments[] = [$event]; + } + + $this->puller + ->expects($this->atLeastOnce()) + ->method('pull') + ->with($this->uow) + ->willReturnOnConsecutiveCalls($first_loop, $second_loop) + ; + + $this->dispatcher + ->expects($this->atLeastOnce()) + ->method('dispatch') + ->withConsecutive(...$arguments) + ; + + $this->em + ->expects($first_loop !== [] && $second_loop !== [] ? $this->atLeastOnce() : $this->once()) + ->method('flush') + ; + + $this->publisher->aggregateEvents($this->em); + $this->publisher->dispatchEvents($this->em); + // recursive call + $this->publisher->aggregateEvents($this->em); + $this->publisher->dispatchEvents($this->em); + } +} diff --git a/tests/Service/EventPullerTest.php b/tests/Event/PullerTest.php similarity index 74% rename from tests/Service/EventPullerTest.php rename to tests/Event/PullerTest.php index 51dfdea..9ba1ee5 100644 --- a/tests/Service/EventPullerTest.php +++ b/tests/Event/PullerTest.php @@ -1,4 +1,5 @@ uow = $this ->getMockBuilder(UnitOfWork::class) @@ -37,13 +36,13 @@ protected function setUp() ->getMock() ; - $this->puller = new EventPuller(); + $this->puller = new Puller(); } /** - * @return array + * @return Event[][][] */ - public function events() + public function provideEvents(): array { $events1 = [ $this->getMockBuilder(Event::class)->getMock(), @@ -88,31 +87,34 @@ public function events() } /** - * @dataProvider events + * @dataProvider provideEvents * - * @param \PHPUnit_Framework_MockObject_MockObject[] $deletions_events - * @param \PHPUnit_Framework_MockObject_MockObject[] $insertions_events - * @param \PHPUnit_Framework_MockObject_MockObject[] $updates_events - * @param \PHPUnit_Framework_MockObject_MockObject[] $map_events + * @param Event[] $deletions_events + * @param Event[] $insertions_events + * @param Event[] $updates_events + * @param Event[] $map_events */ public function testPull( array $deletions_events, array $insertions_events, array $updates_events, array $map_events - ) { + ): void { if ($map_events) { - $slice = round(count($map_events) / 2); + $slice = (int) round(count($map_events) / 2); + $aggregator1 = $this->getMockBuilder(AggregateEvents::class)->getMock(); $aggregator1 ->expects($this->once()) ->method('pullEvents') - ->will($this->returnValue(array_slice($map_events, 0, $slice))); + ->willReturn(array_slice($map_events, 0, $slice)) + ; $aggregator2 = $this->getMockBuilder(AggregateEvents::class)->getMock(); $aggregator2 ->expects($this->once()) ->method('pullEvents') - ->will($this->returnValue(array_slice($map_events, $slice))); + ->willReturn(array_slice($map_events, $slice)) + ; $map = [ [ @@ -135,22 +137,22 @@ public function testPull( $this->uow ->expects($this->once()) ->method('getScheduledEntityDeletions') - ->will($this->returnValue($this->getEntitiesFroEvents($deletions_events))) + ->willReturn($this->getEntitiesFroEvents($deletions_events)) ; $this->uow ->expects($this->once()) ->method('getScheduledEntityInsertions') - ->will($this->returnValue($this->getEntitiesFroEvents($insertions_events))) + ->willReturn($this->getEntitiesFroEvents($insertions_events)) ; $this->uow ->expects($this->once()) ->method('getScheduledEntityUpdates') - ->will($this->returnValue($this->getEntitiesFroEvents($updates_events))) + ->willReturn($this->getEntitiesFroEvents($updates_events)) ; $this->uow ->expects($this->once()) ->method('getIdentityMap') - ->will($this->returnValue($map)) + ->willReturn($map) ; $expected_events = array_merge( @@ -168,24 +170,25 @@ public function testPull( * * @return object[] */ - private function getEntitiesFroEvents(array $events) + private function getEntitiesFroEvents(array $events): array { if (!$events) { return []; } - $slice = round(count($events) / 2); + $slice = (int) round(count($events) / 2); + $aggregator1 = $this->getMockBuilder(AggregateEvents::class)->getMock(); $aggregator1 ->expects($this->once()) ->method('pullEvents') - ->will($this->returnValue(array_slice($events, 0, $slice))) + ->willReturn(array_slice($events, 0, $slice)) ; $aggregator2 = $this->getMockBuilder(AggregateEvents::class)->getMock(); $aggregator2 ->expects($this->once()) ->method('pullEvents') - ->will($this->returnValue(array_slice($events, $slice))) + ->willReturn(array_slice($events, $slice)) ; return [ diff --git a/tests/Event/Subscriber/DoctrineEventSubscriberTest.php b/tests/Event/Subscriber/DoctrineEventSubscriberTest.php new file mode 100644 index 0000000..4ea8207 --- /dev/null +++ b/tests/Event/Subscriber/DoctrineEventSubscriberTest.php @@ -0,0 +1,95 @@ + + * @license http://opensource.org/licenses/MIT + */ + +namespace GpsLab\Bundle\DomainEvent\Tests\Event\Subscriber; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\OnFlushEventArgs; +use Doctrine\ORM\Event\PostFlushEventArgs; +use GpsLab\Bundle\DomainEvent\Event\Publisher; +use GpsLab\Bundle\DomainEvent\Event\Subscriber\DoctrineEventSubscriber; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class DoctrineEventSubscriberTest extends TestCase +{ + /** + * @var MockObject&Publisher + */ + private Publisher $publisher; + + /** + * @var MockObject&EntityManagerInterface + */ + private EntityManagerInterface $em; + + private DoctrineEventSubscriber $subscriber; + + protected function setUp(): void + { + $this->em = $this->getMockBuilder(EntityManagerInterface::class)->getMock(); + $this->publisher = $this + ->getMockBuilder(Publisher::class) + ->disableOriginalConstructor() + ->getMock() + ; + + $this->subscriber = new DoctrineEventSubscriber($this->publisher); + } + + public function testPreFlush(): void + { + $event = $this + ->getMockBuilder(OnFlushEventArgs::class) + ->disableOriginalConstructor() + ->getMock() + ; + $event + ->expects($this->once()) + ->method('getEntityManager') + ->willReturn($this->em) + ; + + $this->publisher + ->expects($this->once()) + ->method('aggregateEvents') + ->with($this->em) + ; + + $this->subscriber->onFlush($event); + } + + public function testPostFlush(): void + { + $event = $this + ->getMockBuilder(PostFlushEventArgs::class) + ->disableOriginalConstructor() + ->getMock() + ; + $event + ->expects($this->atLeastOnce()) + ->method('getEntityManager') + ->willReturn($this->em) + ; + + $this->publisher + ->expects($this->once()) + ->method('aggregateEvents') + ->with($this->em) + ; + $this->publisher + ->expects($this->once()) + ->method('dispatchEvents') + ->with($this->em) + ; + + $this->subscriber->postFlush($event); + } +} diff --git a/tests/Fixtures/SimpleObject.php b/tests/Fixtures/SimpleObject.php index ab6e539..e3d8701 100644 --- a/tests/Fixtures/SimpleObject.php +++ b/tests/Fixtures/SimpleObject.php @@ -1,55 +1,37 @@ - * @copyright Copyright (c) 2011, Peter Gribanov - * @license http://opensource.org/licenses/MIT + * @author Peter Gribanov + * @license http://opensource.org/licenses/MIT */ namespace GpsLab\Bundle\DomainEvent\Tests\Fixtures; class SimpleObject { - /** - * @var string - */ - private $foo; - - /** - * @var string - */ - protected $camelCase = 'boo'; - - /** - * @return string - */ - public function getFoo() + private string $foo; + + protected string $camelCase = 'boo'; + + public function getFoo(): string { return $this->foo; } - /** - * @param string $foo - */ - public function setFoo($foo) + public function setFoo(string $foo): void { $this->foo = $foo; } - /** - * @return string - */ - public function getCamelCase() + public function getCamelCase(): string { return $this->camelCase; } - /** - * @param string $camelCase - */ - public function setCamelCase($camelCase) + public function setCamelCase(string $camelCase): void { $this->camelCase = $camelCase; } diff --git a/tests/Fixtures/SimpleObjectProxy.php b/tests/Fixtures/SimpleObjectProxy.php index a921bfe..eedf0f5 100644 --- a/tests/Fixtures/SimpleObjectProxy.php +++ b/tests/Fixtures/SimpleObjectProxy.php @@ -1,11 +1,11 @@ - * @copyright Copyright (c) 2011, Peter Gribanov - * @license http://opensource.org/licenses/MIT + * @author Peter Gribanov + * @license http://opensource.org/licenses/MIT */ namespace GpsLab\Bundle\DomainEvent\Tests\Fixtures; diff --git a/tests/GpsLabDomainEventBundleTest.php b/tests/GpsLabDomainEventBundleTest.php index 8510371..07db886 100644 --- a/tests/GpsLabDomainEventBundleTest.php +++ b/tests/GpsLabDomainEventBundleTest.php @@ -1,4 +1,5 @@ bundle = new GpsLabDomainEventBundle(); - $this->container = new ContainerBuilder(); } - public function testCorrectBundle() + public function testCorrectBundle(): void { $this->assertInstanceOf(Bundle::class, $this->bundle); } - public function testBuild() - { - $this->bundle->build($this->container); - - $has_event_listener_pass = false; - foreach ($this->container->getCompiler()->getPassConfig()->getBeforeOptimizationPasses() as $pass) { - $has_event_listener_pass = $pass instanceof EventListenerPass ?: $has_event_listener_pass; - } - $this->assertTrue($has_event_listener_pass); - } - - public function testContainerExtension() + public function testContainerExtension(): void { $this->assertInstanceOf(GpsLabDomainEventExtension::class, $this->bundle->getContainerExtension()); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 5dea307..112bd97 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,5 @@