├── .coveralls.yml ├── .editorconfig ├── .github └── workflows │ ├── qa.yaml │ └── test.yaml ├── .gitignore ├── .php_cs.dist ├── .scrutinizer.yml ├── LICENSE ├── README.md ├── UPGRADE-2.0.md ├── composer.json ├── phpunit.xml.dist ├── src ├── DependencyInjection │ ├── Compiler │ │ └── EventListenerPass.php │ ├── Configuration.php │ └── GpsLabDomainEventExtension.php ├── Event │ └── Listener │ │ └── DomainEventPublisher.php ├── GpsLabDomainEventBundle.php ├── Resources │ └── config │ │ ├── bus.yml │ │ ├── locator.yml │ │ ├── publisher.yml │ │ └── queue.yml └── Service │ └── EventPuller.php └── tests ├── DependencyInjection ├── Compiler │ └── EventListenerPassTest.php ├── ConfigurationTest.php └── GpsLabDomainEventExtensionTest.php ├── Event └── Listener │ └── DomainEventPublisherTest.php ├── Fixtures ├── SimpleObject.php └── SimpleObjectProxy.php ├── GpsLabDomainEventBundleTest.php ├── Service └── EventPullerTest.php └── bootstrap.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: build/coverage-clover.xml 3 | json_path: build/coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; top-most EditorConfig file 2 | root = true 3 | 4 | ; Unix-style newlines 5 | [*] 6 | end_of_line = LF 7 | 8 | [*.php] 9 | indent_style = space 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.github/workflows/qa.yaml: -------------------------------------------------------------------------------- 1 | name: Quality assurance 2 | 3 | on: [push, pull_request ] 4 | 5 | jobs: 6 | php-cs-fixer: 7 | name: PHP CS Fixer 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Install PHP with extensions 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: 7.4 19 | coverage: none 20 | tools: composer:v2 21 | 22 | - name: "Install Composer dependencies (highest)" 23 | uses: "ramsey/composer-install@v1" 24 | with: 25 | dependency-versions: "highest" 26 | composer-options: "--prefer-dist --prefer-stable" 27 | 28 | - name: PHP Code Style Fixer (php-cs-fixer) 29 | uses: OskarStark/php-cs-fixer-ga@2.16.7 -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request ] 4 | 5 | jobs: 6 | test: 7 | name: PHP ${{ matrix.php }} + Symfony ${{ matrix.symfony }} + Doctrine ORM ${{ matrix.doctrine }} 8 | runs-on: ubuntu-latest 9 | continue-on-error: false 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - php: '5.5' 16 | symfony: '2.8.*' 17 | doctrine: '2.4.*' 18 | phpunit: '4.8.*' 19 | - php: '5.6' 20 | symfony: '2.8.*' 21 | doctrine: '2.4.*' 22 | phpunit: '4.8.*' 23 | - php: '7.0' 24 | symfony: '2.8.*' 25 | doctrine: '2.4.*' 26 | phpunit: '4.8.*' 27 | - php: '7.1' 28 | symfony: '2.8.*' 29 | doctrine: '2.4.*' 30 | phpunit: '6.5.*' 31 | - php: '7.2' 32 | symfony: '2.8.*' 33 | doctrine: '2.4.*' 34 | phpunit: '6.5.*' 35 | - php: '7.3' 36 | symfony: '3.4.*' 37 | doctrine: '2.6.*' 38 | phpunit: '6.5.*' 39 | - php: '7.4' 40 | symfony: '3.4.*' 41 | doctrine: '2.6.*' 42 | phpunit: '6.5.*' 43 | - php: '5.5' 44 | symfony: '2.8.*' 45 | doctrine: '2.5.*' 46 | phpunit: '4.8.*' 47 | - php: '7.4' 48 | symfony: '4.4.*' 49 | doctrine: '2.6.*' 50 | phpunit: '6.5.*' 51 | - php: '7.4' 52 | symfony: '5.*' 53 | doctrine: '2.7.*' 54 | phpunit: '6.5.*' 55 | - php: '7.4' 56 | symfony: '4.4.*' 57 | doctrine: '2.7.*' 58 | phpunit: '6.5.*' 59 | - php: '7.4' 60 | symfony: '4.4.*' 61 | doctrine: '2.8.*' 62 | phpunit: '6.5.*' 63 | # # require PHPUnit >= 8.5.12 64 | # - php: '8.0' 65 | # symfony: '5.*' 66 | # doctrine: '2.*' 67 | 68 | steps: 69 | - name: Checkout 70 | uses: actions/checkout@v2 71 | 72 | - name: Install PHP 73 | uses: shivammathur/setup-php@v2 74 | with: 75 | php-version: ${{ matrix.php }} 76 | coverage: xdebug 77 | tools: composer:v2 78 | 79 | - name: Install Symfony 80 | run: composer require symfony/symfony:"${{ matrix.symfony }}" --no-update 81 | 82 | - name: Install Doctrine 83 | run: composer require doctrine/orm:"${{ matrix.doctrine }}" --no-update 84 | 85 | - name: Install PHPUnit 86 | run: composer require phpunit/phpunit:"${{ matrix.phpunit }}" --no-update 87 | 88 | - name: "Install Composer dependencies (highest)" 89 | uses: "ramsey/composer-install@v1" 90 | with: 91 | dependency-versions: "highest" 92 | composer-options: "--prefer-dist --prefer-stable" 93 | 94 | - name: Run Tests 95 | run: vendor/bin/phpunit --coverage-clover build/coverage-clover.xml 96 | 97 | - name: Send coverage results to Scrutinizer CI 98 | run: | 99 | wget https://scrutinizer-ci.com/ocular.phar 100 | php ocular.phar code-coverage:upload --format=php-clover build/coverage-clover.xml 101 | 102 | - name: Send coverage results to Coveralls 103 | env: 104 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 105 | COVERALLS_PARALLEL: true 106 | COVERALLS_FLAG_NAME: 'PHP ${{ matrix.php }} + Symfony ${{ matrix.symfony }} + Doctrine ORM ${{ matrix.doctrine }}' 107 | run: | 108 | composer global require php-coveralls/php-coveralls 109 | php-coveralls --coverage_clover=build/coverage-clover.xml -v -c .coveralls.yml 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /build/ 3 | phpunit.xml 4 | composer.lock 5 | .php_cs.cache 6 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | 7 | @license http://opensource.org/licenses/MIT 8 | EOF; 9 | 10 | return PhpCsFixer\Config::create() 11 | ->setRules([ 12 | '@Symfony' => true, 13 | 'array_syntax' => ['syntax' => 'short'], 14 | 'header_comment' => [ 15 | 'comment_type' => 'PHPDoc', 16 | 'header' => $header, 17 | ], 18 | 'class_definition' => [ 19 | 'multi_line_extends_each_single_line' => true, 20 | ], 21 | 'no_superfluous_phpdoc_tags' => false, 22 | 'single_line_throw' => false, 23 | 'blank_line_after_opening_tag' => false, 24 | 'yoda_style' => false, 25 | 'phpdoc_no_empty_return' => false, 26 | 'ordered_imports' => [ 27 | 'sort_algorithm' => 'alpha', 28 | ], 29 | 'list_syntax' => [ 30 | 'syntax' => 'short', 31 | ], 32 | ]) 33 | ->setFinder( 34 | PhpCsFixer\Finder::create() 35 | ->in(__DIR__.'/src') 36 | ->in(__DIR__.'/tests') 37 | ->notPath('bootstrap.php') 38 | ) 39 | ; -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - php 3 | 4 | tools: 5 | external_code_coverage: true 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 GPS Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Stable Version](https://img.shields.io/packagist/v/gpslab/domain-event-bundle.svg?maxAge=3600&label=stable)](https://packagist.org/packages/gpslab/domain-event-bundle) 2 | [![PHP Version Support](https://img.shields.io/travis/php-v/gpslab/domain-event-bundle.svg?maxAge=3600)](https://packagist.org/packages/gpslab/domain-event-bundle) 3 | [![Total Downloads](https://img.shields.io/packagist/dt/gpslab/domain-event-bundle.svg?maxAge=3600)](https://packagist.org/packages/gpslab/domain-event-bundle) 4 | [![Build Status](https://img.shields.io/github/checks-status/gpslab/domain-event-bundle/master.svg?label=build&maxAge=3600)](https://travis-ci.org/gpslab/domain-event-bundle) 5 | [![Coverage Status](https://img.shields.io/coveralls/gpslab/domain-event-bundle.svg?maxAge=3600)](https://coveralls.io/github/gpslab/domain-event-bundle?branch=master) 6 | [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/gpslab/domain-event-bundle.svg?maxAge=3600)](https://scrutinizer-ci.com/g/gpslab/domain-event-bundle/?branch=master) 7 | [![License](https://img.shields.io/packagist/l/gpslab/domain-event-bundle.svg?maxAge=3600)](https://github.com/gpslab/domain-event-bundle) 8 | 9 | Domain event bundle 10 | =================== 11 | 12 | Bundle to create the domain layer of your [Domain-driven design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design) application. 13 | 14 | This [Symfony](https://symfony.com/) bundle is a wrapper for [gpslab/domain-event](https://github.com/gpslab/domain-event), look it for more details. 15 | 16 | Installation 17 | ------------ 18 | 19 | Pretty simple with [Composer](http://packagist.org), run: 20 | 21 | ```sh 22 | composer req gpslab/domain-event-bundle 23 | ``` 24 | 25 | Configuration 26 | ------------- 27 | 28 | Example configuration 29 | 30 | ```yml 31 | gpslab_domain_event: 32 | # Event bus service 33 | # Support 'listener_located', 'queue' or a custom service 34 | # As a default used 'listener_located' 35 | bus: 'listener_located' 36 | 37 | # Event queue service 38 | # Support 'pull_memory', 'subscribe_executing' or a custom service 39 | # As a default used 'pull_memory' 40 | queue: 'pull_memory' 41 | 42 | # Event listener locator 43 | # Support 'symfony', 'container', 'direct_binding' or custom service 44 | # As a default used 'symfony' 45 | locator: 'symfony' 46 | 47 | # Publish domain events post a Doctrine flush event 48 | # As a default used 'false' 49 | publish_on_flush: true 50 | ``` 51 | 52 | Usage 53 | ----- 54 | 55 | Create a domain event 56 | 57 | ```php 58 | use GpsLab\Domain\Event\Event 59 | 60 | class PurchaseOrderCreatedEvent implements Event 61 | { 62 | private $customer_id; 63 | private $create_at; 64 | 65 | public function __construct(CustomerId $customer_id, \DateTimeImmutable $create_at) 66 | { 67 | $this->customer_id = $customer_id; 68 | $this->create_at = $create_at; 69 | } 70 | 71 | public function customerId(): CustomerId 72 | { 73 | return $this->customer_id; 74 | } 75 | 76 | public function createAt(): \DateTimeImmutable 77 | { 78 | return $this->create_at; 79 | } 80 | } 81 | ``` 82 | 83 | Raise your event 84 | 85 | ```php 86 | use GpsLab\Domain\Event\Aggregator\AbstractAggregateEvents; 87 | 88 | final class PurchaseOrder extends AbstractAggregateEvents 89 | { 90 | private $customer_id; 91 | private $create_at; 92 | 93 | public function __construct(CustomerId $customer_id) 94 | { 95 | $this->customer_id = $customer_id; 96 | $this->create_at = new \DateTimeImmutable(); 97 | 98 | $this->raise(new PurchaseOrderCreatedEvent($customer_id, $this->create_at)); 99 | } 100 | } 101 | ``` 102 | 103 | Create listener 104 | 105 | ```php 106 | use GpsLab\Domain\Event\Event; 107 | 108 | class SendEmailOnPurchaseOrderCreated 109 | { 110 | private $mailer; 111 | 112 | public function __construct(\Swift_Mailer $mailer) 113 | { 114 | $this->mailer = $mailer; 115 | } 116 | 117 | public function onPurchaseOrderCreated(PurchaseOrderCreatedEvent $event): void 118 | { 119 | $message = $this->mailer 120 | ->createMessage() 121 | ->setTo('recipient@example.com') 122 | ->setBody(sprintf( 123 | 'Purchase order created at %s for customer #%s', 124 | $event->createAt()->format('Y-m-d'), 125 | $event->customerId() 126 | )); 127 | 128 | $this->mailer->send($message); 129 | } 130 | } 131 | ``` 132 | 133 | Register event listener 134 | 135 | ```yml 136 | services: 137 | SendEmailOnPurchaseOrderCreated: 138 | arguments: [ '@mailer' ] 139 | tags: 140 | - { name: domain_event.listener, event: PurchaseOrderCreatedEvent, method: onPurchaseOrderCreated } 141 | ``` 142 | 143 | Publish events in listener 144 | 145 | ```php 146 | use GpsLab\Domain\Event\Bus\EventBus; 147 | 148 | // get event bus from DI container 149 | $bus = $this->get(EventBus::class); 150 | 151 | // do what you need to do on your Domain 152 | $purchase_order = new PurchaseOrder(new CustomerId(1)); 153 | 154 | // this will clear the list of event in your AggregateEvents so an Event is trigger only once 155 | $events = $purchase_order->pullEvents(); 156 | 157 | // You can have more than one event at a time. 158 | foreach($events as $event) { 159 | $bus->publish($event); 160 | } 161 | 162 | // You can use one method 163 | //$bus->pullAndPublish($purchase_order); 164 | ``` 165 | 166 | Listener method name 167 | -------------------- 168 | 169 | You do not need to specify the name of the event handler method. By default, the 170 | [__invoke](http://php.net/manual/en/language.oop5.magic.php#object.invoke) method is used. 171 | 172 | 173 | ```php 174 | use GpsLab\Domain\Event\Event; 175 | 176 | class SendEmailOnPurchaseOrderCreated 177 | { 178 | private $mailer; 179 | 180 | public function __construct(\Swift_Mailer $mailer) 181 | { 182 | $this->mailer = $mailer; 183 | } 184 | 185 | public function __invoke(PurchaseOrderCreatedEvent $event): void 186 | { 187 | $message = $this->mailer 188 | ->createMessage() 189 | ->setTo('recipient@example.com') 190 | ->setBody(sprintf( 191 | 'Purchase order created at %s for customer #%s', 192 | $event->createAt()->format('Y-m-d'), 193 | $event->customerId() 194 | )); 195 | 196 | $this->mailer->send($message); 197 | } 198 | } 199 | ``` 200 | 201 | Register event listener 202 | 203 | ```yml 204 | services: 205 | SendEmailOnPurchaseOrderCreated: 206 | arguments: [ '@mailer' ] 207 | tags: 208 | - { name: domain_event.listener, event: PurchaseOrderCreatedEvent } 209 | ``` 210 | 211 | Event subscribers 212 | ----------------- 213 | 214 | Create subscriber 215 | 216 | ```php 217 | use GpsLab\Domain\Event\Event; 218 | use GpsLab\Domain\Event\Listener\Subscriber; 219 | 220 | class SendEmailOnPurchaseOrderCreated implements Subscriber 221 | { 222 | private $mailer; 223 | 224 | public function __construct(\Swift_Mailer $mailer) 225 | { 226 | $this->mailer = $mailer; 227 | } 228 | 229 | public static function subscribedEvents(): array 230 | { 231 | return [ 232 | PurchaseOrderCreatedEvent::class => ['onPurchaseOrderCreated'], 233 | ]; 234 | } 235 | 236 | public function onPurchaseOrderCreated(PurchaseOrderCreatedEvent $event): void 237 | { 238 | $message = $this->mailer 239 | ->createMessage() 240 | ->setTo('recipient@example.com') 241 | ->setBody(sprintf( 242 | 'Purchase order created at %s for customer #%s', 243 | $event->createAt()->format('Y-m-d'), 244 | $event->customerId() 245 | )); 246 | 247 | $this->mailer->send($message); 248 | } 249 | } 250 | ``` 251 | 252 | Register event subscriber 253 | 254 | ```yml 255 | services: 256 | SendEmailOnPurchaseOrderCreated: 257 | arguments: [ '@mailer' ] 258 | tags: 259 | - { name: domain_event.subscriber } 260 | ``` 261 | 262 | Use pull Predis queue 263 | --------------------- 264 | 265 | Install [Predis](https://github.com/nrk/predis) with [Composer](http://packagist.org), run: 266 | 267 | ```sh 268 | composer require predis/predis 269 | ``` 270 | 271 | Register services: 272 | 273 | ```yml 274 | services: 275 | # Predis 276 | Predis\Client: 277 | arguments: [ '127.0.0.1' ] 278 | 279 | # Events serializer for queue 280 | GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer: 281 | arguments: [ '@serializer', 'json' ] 282 | 283 | # Predis event queue 284 | GpsLab\Domain\Event\Queue\Pull\PredisPullEventQueue: 285 | arguments: 286 | - '@Predis\Client' 287 | - '@GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer' 288 | - '@logger' 289 | - 'event_queue_name' 290 | ``` 291 | 292 | Change config for use custom queue: 293 | 294 | ```yml 295 | gpslab_domain_event: 296 | queue: 'GpsLab\Domain\Event\Queue\Pull\PredisPullEventQueue' 297 | ``` 298 | 299 | And now you can use custom queue: 300 | 301 | ```php 302 | use GpsLab\Domain\Event\Queue\EventQueue; 303 | 304 | $container->get(EventQueue::class)->publish($domain_event); 305 | ``` 306 | 307 | In latter pull events from queue: 308 | 309 | ```php 310 | use GpsLab\Domain\Event\Queue\EventQueue; 311 | 312 | $queue = $container->get(EventQueue::class); 313 | $bus = $container->get(EventQueue::class); 314 | 315 | while ($event = $queue->pull()) { 316 | $bus->publish($event); 317 | } 318 | ``` 319 | 320 | Use Predis subscribe queue 321 | -------------------------- 322 | 323 | Install [Predis PubSub](https://github.com/Superbalist/php-pubsub-redis) adapter with [Composer](http://packagist.org), run: 324 | 325 | ```sh 326 | composer require superbalist/php-pubsub-redis 327 | ``` 328 | 329 | Register services: 330 | 331 | ```yml 332 | services: 333 | # Predis 334 | Predis\Client: 335 | arguments: [ '127.0.0.1' ] 336 | 337 | # Predis PubSub adapter 338 | Superbalist\PubSub\Redis\RedisPubSubAdapter: 339 | arguments: [ '@Predis\Client' ] 340 | 341 | # Events serializer for queue 342 | GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer: 343 | arguments: [ '@serializer', 'json' ] 344 | 345 | # Predis event queue 346 | GpsLab\Domain\Event\Queue\Subscribe\PredisSubscribeEventQueue: 347 | arguments: 348 | - '@Superbalist\PubSub\Redis\RedisPubSubAdapter' 349 | - '@GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer' 350 | - '@logger' 351 | - 'event_queue_name' 352 | ``` 353 | 354 | Change config for use custom queue: 355 | 356 | ```yml 357 | gpslab_domain_event: 358 | queue: 'GpsLab\Domain\Event\Queue\Subscribe\PredisSubscribeEventQueue' 359 | ``` 360 | 361 | And now you can use custom queue: 362 | 363 | ```php 364 | use GpsLab\Domain\Event\Queue\EventQueue; 365 | 366 | $container->get(EventQueue::class)->publish($domain_event); 367 | ``` 368 | 369 | Subscribe on the queue: 370 | 371 | ```php 372 | use GpsLab\Domain\Event\Queue\EventQueue; 373 | 374 | $container->get(EventQueue::class)->subscribe(function (Event $event) { 375 | // do somthing 376 | }); 377 | ``` 378 | 379 | > **Note** 380 | > 381 | > You can use subscribe handlers as a services and [tag](http://symfony.com/doc/current/service_container/tags.html) it 382 | for optimize register. 383 | 384 | Many queues 385 | ----------- 386 | 387 | You can use many queues for separation the flows. For example, you want to handle events of different Bounded Contexts 388 | separately from each other. 389 | 390 | ```yml 391 | services: 392 | acme.domain.purchase_order.event.queue: 393 | class: GpsLab\Domain\Event\Queue\Pull\PredisPullEventQueue 394 | arguments: 395 | - '@Superbalist\PubSub\Redis\RedisPubSubAdapter' 396 | - '@GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer' 397 | - '@logger' 398 | - 'purchase_order_event_queue' 399 | 400 | acme.domain.article_comment.event.queue: 401 | class: GpsLab\Domain\Event\Queue\Pull\PredisPullEventQueue 402 | arguments: 403 | - '@Superbalist\PubSub\Redis\RedisPubSubAdapter' 404 | - '@GpsLab\Domain\Event\Queue\Serializer\SymfonySerializer' 405 | - '@logger' 406 | - 'article_comment_event_queue' 407 | ``` 408 | 409 | And now you can use a different queues. 410 | 411 | In **Purchase order** Bounded Contexts. 412 | 413 | ```php 414 | $event = new PurchaseOrderCreatedEvent( 415 | new CustomerId(1), 416 | new \DateTimeImmutable() 417 | ); 418 | 419 | $container->get('acme.domain.purchase_order.event.queue')->publish($event); 420 | ``` 421 | 422 | In **Article comment** Bounded Contexts. 423 | 424 | ```php 425 | $event = new ArticleCommentedEvent( 426 | new ArticleId(1), 427 | new AuthorId(1), 428 | $comment 429 | new \DateTimeImmutable() 430 | ); 431 | 432 | $container->get('acme.domain.article_comment.event.queue')->publish($event); 433 | ``` 434 | 435 | > **Note** 436 | > 437 | > Similarly, you can split the subscribe queues. 438 | 439 | License 440 | ------- 441 | 442 | This bundle is under the [MIT license](http://opensource.org/licenses/MIT). See the complete license in the file: LICENSE 443 | -------------------------------------------------------------------------------- /UPGRADE-2.0.md: -------------------------------------------------------------------------------- 1 | UPGRADE FROM 1.x to 2.0 2 | ======================= 3 | 4 | Configuration 5 | ------------- 6 | 7 | Before: 8 | 9 | ```yml 10 | gpslab_domain_event: 11 | # Event bus service 12 | # Support 'listener_located', 'queue' or a custom service 13 | # As a default used 'listener_located' 14 | bus: 'listener_located' 15 | 16 | # Event queue service 17 | # Support 'memory', 'memory_unique' or a custom service 18 | # As a default used 'memory_unique' 19 | queue: 'memory_unique' 20 | 21 | # Event listener locator 22 | # Support 'voter', 'named_event' or custom service 23 | # As a default used 'named_event' 24 | locator: 'named_event' 25 | 26 | # Evant name resolver used in 'named_event' event listener locator 27 | # Support 'event_class', 'event_class_last_part', 'named_event' or a custom service 28 | # As a default used 'event_class' 29 | name_resolver: 'event_class' 30 | ``` 31 | 32 | After: 33 | 34 | ```yml 35 | gpslab_domain_event: 36 | # Event bus service 37 | # Support 'listener_located', 'queue' or a custom service 38 | # As a default used 'listener_located' 39 | bus: 'listener_located' 40 | 41 | # Event queue service 42 | # Support 'pull_memory', 'subscribe_executing' or a custom service 43 | # As a default used 'pull_memory' 44 | queue: 'pull_memory' 45 | 46 | # Event listener locator 47 | # Support 'symfony', 'container', 'direct_binding' or custom service 48 | # As a default used 'symfony' 49 | locator: 'symfony' 50 | 51 | # Publish domain events post a Doctrine flush event 52 | # As a default used 'false' 53 | publish_on_flush: true 54 | ``` 55 | 56 | Publish domain events 57 | --------------------- 58 | 59 | * Publish domain events post a Doctrine flush event. 60 | 61 | Removed locators 62 | ---------------- 63 | 64 | * Removed `domain_event.locator.voter` service. 65 | * Removed `domain_event.locator.named_event` service. 66 | * Ignore tag `domain_event.locator.voter` for `VoterLocator`. 67 | * Ignore tag `domain_event.locator.named_event` for `NamedEventLocator`. 68 | 69 | Created locators 70 | ---------------- 71 | 72 | * Created `domain_event.locator.symfony` service of 73 | `GpsLab\Domain\Event\Listener\Locator\SymfonyContainerEventListenerLocator` locator. 74 | * Created `domain_event.locator.container` service of 75 | `GpsLab\Domain\Event\Listener\Locator\ContainerEventListenerLocator` locator. 76 | * Created `domain_event.locator.direct_binding` service of 77 | `GpsLab\Domain\Event\Listener\Locator\DirectBindingEventListenerLocator` locator. 78 | 79 | Removed name resolvers 80 | ---------------------- 81 | 82 | * Removed `domain_event.name_resolver.event_class_last_part` name resolver service. 83 | * Removed `domain_event.name_resolver.event_class` name resolver service. 84 | * Removed `domain_event.name_resolver.named_event` name resolver service. 85 | 86 | Removed queue 87 | ------------- 88 | 89 | * Removed `domain_event.queue.memory` queue service. 90 | * Removed `domain_event.queue.memory_unique` queue service. 91 | 92 | Created queue 93 | ------------- 94 | 95 | * Created `domain_event.queue.pull_memory` service of `GpsLab\Domain\Event\Queue\Pull\MemoryPullEventQueue` queue. 96 | * Created `domain_event.queue.subscribe_executing` service of 97 | `GpsLab\Domain\Event\Queue\Subscribe\ExecutingSubscribeEventQueue` queue. 98 | 99 | Changed event bus 100 | ----------------- 101 | 102 | * Changed class for `domain_event.bus.listener_located`. 103 | 104 | Before used `GpsLab\Domain\Event\Bus\Bus` class. 105 | 106 | After used `GpsLab\Domain\Event\Bus\ListenerLocatedEventBus` class. 107 | 108 | * Service `domain_event.bus.queue` not use a `domain_event.bus.listener_located` service. 109 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gpslab/domain-event-bundle", 3 | "license": "MIT", 4 | "type": "symfony-bundle", 5 | "homepage": "https://github.com/gpslab/domain-event-bundle", 6 | "description": "Bundle to create the domain layer of your DDD application", 7 | "autoload": { 8 | "psr-4": { 9 | "GpsLab\\Bundle\\DomainEvent\\": "src/" 10 | } 11 | }, 12 | "autoload-dev": { 13 | "psr-4": { 14 | "GpsLab\\Bundle\\DomainEvent\\Tests\\": "tests/" 15 | } 16 | }, 17 | "require": { 18 | "php": ">=5.5.0", 19 | "gpslab/domain-event": "~2.0", 20 | "symfony/http-kernel": "~2.3|~3.0|~4.0|~5.0", 21 | "symfony/dependency-injection": "~2.3|~3.0|~4.0|~5.0", 22 | "symfony/expression-language": "~2.3|~3.0|~4.0|~5.0", 23 | "doctrine/orm": "~2.4" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^4.8.36" 27 | }, 28 | "extra": { 29 | "branch-alias": { 30 | "dev-master": "2.0-dev" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ./src 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/EventListenerPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\DependencyInjection\Compiler; 11 | 12 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 13 | use Symfony\Component\DependencyInjection\ContainerBuilder; 14 | use Symfony\Component\DependencyInjection\Definition; 15 | 16 | class EventListenerPass implements CompilerPassInterface 17 | { 18 | /** 19 | * @param ContainerBuilder $container 20 | */ 21 | public function process(ContainerBuilder $container) 22 | { 23 | if (!$container->has('domain_event.locator')) { 24 | return; 25 | } 26 | 27 | $current_locator = $container->findDefinition('domain_event.locator'); 28 | $symfony_locator = $container->findDefinition('domain_event.locator.symfony'); 29 | $container_locator = $container->findDefinition('domain_event.locator.container'); 30 | 31 | if ($current_locator === $symfony_locator || $current_locator === $container_locator) { 32 | $this->registerListeners($container, $current_locator); 33 | $this->registerSubscribers($container, $current_locator); 34 | } 35 | } 36 | 37 | /** 38 | * @param ContainerBuilder $container 39 | * @param Definition $current_locator 40 | */ 41 | private function registerListeners(ContainerBuilder $container, Definition $current_locator) 42 | { 43 | foreach ($container->findTaggedServiceIds('domain_event.listener') as $id => $attributes) { 44 | foreach ($attributes as $attribute) { 45 | $method = !empty($attribute['method']) ? $attribute['method'] : '__invoke'; 46 | $current_locator->addMethodCall('registerService', [$attribute['event'], $id, $method]); 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * @param ContainerBuilder $container 53 | * @param Definition $current_locator 54 | */ 55 | private function registerSubscribers(ContainerBuilder $container, Definition $current_locator) 56 | { 57 | foreach ($container->findTaggedServiceIds('domain_event.subscriber') as $id => $attributes) { 58 | $subscriber = $container->findDefinition($id); 59 | $current_locator->addMethodCall('registerSubscriberService', [$id, $subscriber->getClass()]); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\DependencyInjection; 11 | 12 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 13 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 14 | use Symfony\Component\Config\Definition\ConfigurationInterface; 15 | 16 | class Configuration implements ConfigurationInterface 17 | { 18 | /** 19 | * Config tree builder. 20 | * 21 | * Example config: 22 | * 23 | * gpslab_domain_event: 24 | * bus: 'listener_located' 25 | * queue: 'pull_memory' 26 | * locator: 'symfony' 27 | * 28 | * @return TreeBuilder 29 | */ 30 | public function getConfigTreeBuilder() 31 | { 32 | $tree_builder = $this->createTreeBuilder('gpslab_domain_event'); 33 | $root = $this->getRootNode($tree_builder, 'gpslab_domain_event'); 34 | 35 | $bus = $root->children()->scalarNode('bus'); 36 | $bus->cannotBeEmpty()->defaultValue('listener_located'); 37 | 38 | $queue = $root->children()->scalarNode('queue'); 39 | $queue->cannotBeEmpty()->defaultValue('pull_memory'); 40 | 41 | $locator = $root->children()->scalarNode('locator'); 42 | $locator->cannotBeEmpty()->defaultValue('symfony'); 43 | 44 | $publish_on_flush = $root->children()->booleanNode('publish_on_flush'); 45 | $publish_on_flush->defaultValue(false); 46 | 47 | return $tree_builder; 48 | } 49 | 50 | /** 51 | * @param string $name 52 | * 53 | * @return TreeBuilder 54 | */ 55 | private function createTreeBuilder($name) 56 | { 57 | // Symfony 4.2 + 58 | if (method_exists(TreeBuilder::class, '__construct')) { 59 | return new TreeBuilder($name); 60 | } 61 | 62 | // Symfony 4.1 and below 63 | return new TreeBuilder(); 64 | } 65 | 66 | /** 67 | * @param TreeBuilder $tree_builder 68 | * @param string $name 69 | * 70 | * @return ArrayNodeDefinition 71 | */ 72 | private function getRootNode(TreeBuilder $tree_builder, $name) 73 | { 74 | if (method_exists($tree_builder, 'getRootNode')) { 75 | // Symfony 4.2 + 76 | $root = $tree_builder->getRootNode(); 77 | } else { 78 | // Symfony 4.1 and below 79 | $root = $tree_builder->root($name); 80 | } 81 | 82 | // @codeCoverageIgnoreStart 83 | if (!($root instanceof ArrayNodeDefinition)) { // should be always false 84 | throw new \RuntimeException(sprintf('The root node should be instance of %s, got %s instead.', ArrayNodeDefinition::class, get_class($root))); 85 | } 86 | // @codeCoverageIgnoreEnd 87 | 88 | return $root; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/DependencyInjection/GpsLabDomainEventExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\DependencyInjection; 11 | 12 | use GpsLab\Domain\Event\Bus\EventBus; 13 | use GpsLab\Domain\Event\Listener\Subscriber; 14 | use GpsLab\Domain\Event\Queue\EventQueue; 15 | use Symfony\Component\Config\FileLocator; 16 | use Symfony\Component\DependencyInjection\ContainerBuilder; 17 | use Symfony\Component\DependencyInjection\Loader; 18 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 19 | 20 | class GpsLabDomainEventExtension extends Extension 21 | { 22 | /** 23 | * @param array $configs 24 | * @param ContainerBuilder $container 25 | */ 26 | public function load(array $configs, ContainerBuilder $container) 27 | { 28 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 29 | $loader->load('queue.yml'); 30 | $loader->load('bus.yml'); 31 | $loader->load('locator.yml'); 32 | $loader->load('publisher.yml'); 33 | 34 | $config = $this->processConfiguration(new Configuration(), $configs); 35 | 36 | $container->setAlias('domain_event.bus', $this->busRealName($config['bus'])); 37 | $container->setAlias('domain_event.queue', $this->queueRealName($config['queue'])); 38 | $container->setAlias('domain_event.locator', $this->locatorRealName($config['locator'])); 39 | $container->setAlias(EventBus::class, $this->busRealName($config['bus'])); 40 | $container->setAlias(EventQueue::class, $this->queueRealName($config['queue'])); 41 | 42 | $container->getDefinition('domain_event.publisher')->replaceArgument(2, $config['publish_on_flush']); 43 | 44 | // subscribers tagged automatically 45 | if (method_exists($container, 'registerForAutoconfiguration')) { 46 | $container 47 | ->registerForAutoconfiguration(Subscriber::class) 48 | ->addTag('domain_event.subscriber') 49 | ->setAutowired(true) 50 | ; 51 | } 52 | } 53 | 54 | /** 55 | * @param string $name 56 | * 57 | * @return string 58 | */ 59 | private function busRealName($name) 60 | { 61 | if (in_array($name, ['listener_located', 'queue'])) { 62 | return 'domain_event.bus.'.$name; 63 | } 64 | 65 | return $name; 66 | } 67 | 68 | /** 69 | * @param string $name 70 | * 71 | * @return string 72 | */ 73 | private function queueRealName($name) 74 | { 75 | if (in_array($name, ['pull_memory', 'subscribe_executing'])) { 76 | return 'domain_event.queue.'.$name; 77 | } 78 | 79 | return $name; 80 | } 81 | 82 | /** 83 | * @param string $name 84 | * 85 | * @return string 86 | */ 87 | private function locatorRealName($name) 88 | { 89 | if (in_array($name, ['direct_binding', 'container', 'symfony'])) { 90 | return 'domain_event.locator.'.$name; 91 | } 92 | 93 | return $name; 94 | } 95 | 96 | /** 97 | * @return string 98 | */ 99 | public function getAlias() 100 | { 101 | return 'gpslab_domain_event'; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Event/Listener/DomainEventPublisher.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\Event\Listener; 11 | 12 | use Doctrine\Common\EventSubscriber; 13 | use Doctrine\ORM\Event\OnFlushEventArgs; 14 | use Doctrine\ORM\Event\PostFlushEventArgs; 15 | use Doctrine\ORM\Events; 16 | use GpsLab\Bundle\DomainEvent\Service\EventPuller; 17 | use GpsLab\Domain\Event\Bus\EventBus; 18 | use GpsLab\Domain\Event\Event; 19 | 20 | class DomainEventPublisher implements EventSubscriber 21 | { 22 | /** 23 | * @var EventPuller 24 | */ 25 | private $puller; 26 | 27 | /** 28 | * @var EventBus 29 | */ 30 | private $bus; 31 | 32 | /** 33 | * @var bool 34 | */ 35 | private $enable; 36 | 37 | /** 38 | * @var Event[] 39 | */ 40 | private $events = []; 41 | 42 | /** 43 | * @param EventPuller $puller 44 | * @param EventBus $bus 45 | * @param bool $enable 46 | */ 47 | public function __construct(EventPuller $puller, EventBus $bus, $enable) 48 | { 49 | $this->bus = $bus; 50 | $this->puller = $puller; 51 | $this->enable = $enable; 52 | } 53 | 54 | /** 55 | * @return array 56 | */ 57 | public function getSubscribedEvents() 58 | { 59 | if (!$this->enable) { 60 | return []; 61 | } 62 | 63 | return [ 64 | Events::onFlush, 65 | Events::postFlush, 66 | ]; 67 | } 68 | 69 | /** 70 | * @param OnFlushEventArgs $args 71 | */ 72 | public function onFlush(OnFlushEventArgs $args) 73 | { 74 | // aggregate events from deleted entities 75 | $this->events = $this->puller->pull($args->getEntityManager()->getUnitOfWork()); 76 | } 77 | 78 | /** 79 | * @param PostFlushEventArgs $args 80 | */ 81 | public function postFlush(PostFlushEventArgs $args) 82 | { 83 | // aggregate PreRemove/PostRemove events 84 | $events = array_merge($this->events, $this->puller->pull($args->getEntityManager()->getUnitOfWork())); 85 | 86 | // clear aggregate events before publish it 87 | // it necessary for fix recursive publish of events 88 | $this->events = []; 89 | 90 | // flush only if has domain events 91 | // it necessary for fix recursive handle flush 92 | if (!empty($events)) { 93 | foreach ($events as $event) { 94 | $this->bus->publish($event); 95 | } 96 | 97 | $args->getEntityManager()->flush(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/GpsLabDomainEventBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent; 11 | 12 | use GpsLab\Bundle\DomainEvent\DependencyInjection\Compiler\EventListenerPass; 13 | use GpsLab\Bundle\DomainEvent\DependencyInjection\GpsLabDomainEventExtension; 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\HttpKernel\Bundle\Bundle; 16 | 17 | class GpsLabDomainEventBundle extends Bundle 18 | { 19 | /** 20 | * @param ContainerBuilder $container 21 | */ 22 | public function build(ContainerBuilder $container) 23 | { 24 | $container->addCompilerPass(new EventListenerPass()); 25 | } 26 | 27 | /** 28 | * @return GpsLabDomainEventExtension 29 | */ 30 | public function getContainerExtension() 31 | { 32 | if (!($this->extension instanceof GpsLabDomainEventExtension)) { 33 | $this->extension = new GpsLabDomainEventExtension(); 34 | } 35 | 36 | return $this->extension; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Resources/config/bus.yml: -------------------------------------------------------------------------------- 1 | services: 2 | domain_event.bus.listener_located: 3 | class: GpsLab\Domain\Event\Bus\ListenerLocatedEventBus 4 | arguments: [ '@domain_event.locator' ] 5 | public: false 6 | 7 | domain_event.bus.queue: 8 | class: GpsLab\Domain\Event\Bus\QueueEventBus 9 | arguments: [ '@domain_event.queue' ] 10 | public: false 11 | -------------------------------------------------------------------------------- /src/Resources/config/locator.yml: -------------------------------------------------------------------------------- 1 | services: 2 | domain_event.locator.symfony: 3 | class: GpsLab\Domain\Event\Listener\Locator\SymfonyContainerEventListenerLocator 4 | calls: 5 | - [ setContainer, [ '@service_container' ] ] 6 | public: false 7 | 8 | domain_event.locator.container: 9 | class: GpsLab\Domain\Event\Listener\Locator\ContainerEventListenerLocator 10 | arguments: [ '@service_container' ] 11 | public: false 12 | 13 | domain_event.locator.direct_binding: 14 | class: GpsLab\Domain\Event\Listener\Locator\DirectBindingEventListenerLocator 15 | public: false 16 | -------------------------------------------------------------------------------- /src/Resources/config/publisher.yml: -------------------------------------------------------------------------------- 1 | services: 2 | domain_event.publisher: 3 | class: GpsLab\Bundle\DomainEvent\Event\Listener\DomainEventPublisher 4 | arguments: [ '@domain_event.puller', '@domain_event.bus', ~ ] 5 | tags: 6 | - { name: doctrine.event_subscriber } 7 | 8 | domain_event.puller: 9 | class: GpsLab\Bundle\DomainEvent\Service\EventPuller 10 | public: false 11 | -------------------------------------------------------------------------------- /src/Resources/config/queue.yml: -------------------------------------------------------------------------------- 1 | services: 2 | domain_event.queue.pull_memory: 3 | class: GpsLab\Domain\Event\Queue\Pull\MemoryPullEventQueue 4 | public: false 5 | 6 | domain_event.queue.subscribe_executing: 7 | class: GpsLab\Domain\Event\Queue\Subscribe\ExecutingSubscribeEventQueue 8 | public: false 9 | -------------------------------------------------------------------------------- /src/Service/EventPuller.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\Service; 11 | 12 | use Doctrine\Common\Persistence\Proxy as CommonProxy; 13 | use Doctrine\Persistence\Proxy; 14 | use Doctrine\ORM\UnitOfWork; 15 | use GpsLab\Domain\Event\Aggregator\AggregateEvents; 16 | use GpsLab\Domain\Event\Event; 17 | 18 | class EventPuller 19 | { 20 | /** 21 | * @param UnitOfWork $uow 22 | * 23 | * @return Event[] 24 | */ 25 | public function pull(UnitOfWork $uow) 26 | { 27 | $events = []; 28 | 29 | $events = array_merge($events, $this->pullFromEntities($uow->getScheduledEntityDeletions())); 30 | $events = array_merge($events, $this->pullFromEntities($uow->getScheduledEntityInsertions())); 31 | $events = array_merge($events, $this->pullFromEntities($uow->getScheduledEntityUpdates())); 32 | 33 | // other entities 34 | foreach ($uow->getIdentityMap() as $entities) { 35 | $events = array_merge($events, $this->pullFromEntities($entities)); 36 | } 37 | 38 | return $events; 39 | } 40 | 41 | /** 42 | * @param array $entities 43 | * 44 | * @return Event[] 45 | */ 46 | private function pullFromEntities(array $entities) 47 | { 48 | $events = []; 49 | 50 | foreach ($entities as $entity) { 51 | // ignore Doctrine not initialized proxy classes 52 | // proxy class can't have a domain events 53 | if ( 54 | ($entity instanceof Proxy && !$entity->__isInitialized()) || 55 | ($entity instanceof CommonProxy && !$entity->__isInitialized()) 56 | ) { 57 | continue; 58 | } 59 | 60 | if ($entity instanceof AggregateEvents) { 61 | $events = array_merge($events, $entity->pullEvents()); 62 | } 63 | } 64 | 65 | return $events; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/DependencyInjection/Compiler/EventListenerPassTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\Tests\DependencyInjection\Compiler; 11 | 12 | use GpsLab\Bundle\DomainEvent\DependencyInjection\Compiler\EventListenerPass; 13 | use PHPUnit\Framework\TestCase; 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Definition; 16 | 17 | class EventListenerPassTest extends TestCase 18 | { 19 | /** 20 | * @var \PHPUnit_Framework_MockObject_MockObject|ContainerBuilder 21 | */ 22 | private $container; 23 | 24 | /** 25 | * @var EventListenerPass 26 | */ 27 | private $pass; 28 | 29 | protected function setUp() 30 | { 31 | if (PHP_VERSION_ID >= 70000) { 32 | $this->markTestSkipped(sprintf('Impossible to mock "%s" on PHP 7', ContainerBuilder::class)); 33 | } 34 | 35 | $this->container = $this 36 | ->getMockBuilder(ContainerBuilder::class) 37 | ->disableOriginalConstructor() 38 | ->getMock() 39 | ; 40 | $this->pass = new EventListenerPass(); 41 | } 42 | 43 | public function testProcessNoLocator() 44 | { 45 | $this->container 46 | ->expects($this->once()) 47 | ->method('has') 48 | ->with('domain_event.locator') 49 | ->will($this->returnValue(false)) 50 | ; 51 | $this->container 52 | ->expects($this->never()) 53 | ->method('findDefinition') 54 | ; 55 | $this->container 56 | ->expects($this->never()) 57 | ->method('findTaggedServiceIds') 58 | ; 59 | 60 | $this->pass->process($this->container); 61 | } 62 | 63 | public function testProcessCustomLocator() 64 | { 65 | // create fake definitions to distinguish them 66 | $symfony_locator = new Definition(null, ['symfony']); 67 | $container_locator = new Definition(null, ['container']); 68 | $current_locator = new Definition(null, ['custom']); 69 | 70 | $this->container 71 | ->expects($this->at(0)) 72 | ->method('has') 73 | ->with('domain_event.locator') 74 | ->will($this->returnValue(true)) 75 | ; 76 | $this->container 77 | ->expects($this->at(1)) 78 | ->method('findDefinition') 79 | ->with('domain_event.locator') 80 | ->will($this->returnValue($current_locator)) 81 | ; 82 | $this->container 83 | ->expects($this->at(2)) 84 | ->method('findDefinition') 85 | ->with('domain_event.locator.symfony') 86 | ->will($this->returnValue($symfony_locator)) 87 | ; 88 | $this->container 89 | ->expects($this->at(3)) 90 | ->method('findDefinition') 91 | ->with('domain_event.locator.container') 92 | ->will($this->returnValue($container_locator)) 93 | ; 94 | $this->container 95 | ->expects($this->never()) 96 | ->method('findTaggedServiceIds') 97 | ; 98 | 99 | $this->pass->process($this->container); 100 | } 101 | 102 | /** 103 | * @return array 104 | */ 105 | public function locators() 106 | { 107 | if (PHP_VERSION_ID >= 70000) { 108 | $this->markTestSkipped(sprintf('Impossible to mock "%s" on PHP 7', Definition::class)); 109 | } 110 | 111 | $locator = $this->getMock(Definition::class); 112 | 113 | return [ 114 | [ 115 | $locator, 116 | new Definition(), 117 | $locator, 118 | ], 119 | [ 120 | new Definition(), 121 | $locator, 122 | $locator, 123 | ], 124 | ]; 125 | } 126 | 127 | /** 128 | * @dataProvider locators 129 | * 130 | * @param \PHPUnit_Framework_MockObject_MockObject|Definition $symfony_locator 131 | * @param \PHPUnit_Framework_MockObject_MockObject|Definition $container_locator 132 | * @param \PHPUnit_Framework_MockObject_MockObject|Definition $current_locator 133 | */ 134 | public function testProcess( 135 | Definition $symfony_locator, 136 | Definition $container_locator, 137 | Definition $current_locator 138 | ) { 139 | $listeners = [ 140 | 'foo' => [ 141 | ['event' => 'PurchaseOrderCompletedEvent', 'method' => 'onPurchaseOrderCompleted'], 142 | ['event' => 'PurchaseOrderCreated', 'method' => 'onPurchaseOrderCreated'], 143 | ], 144 | 'bar' => [ 145 | ['event' => 'PurchaseOrderCompletedEvent'], 146 | ], 147 | 'baz' => [ 148 | ['event' => 'PurchaseOrderCreated', 'method' => 'handle'], 149 | ], 150 | ]; 151 | $subscribers = [ 152 | 'foo' => [], 153 | 'bar' => [], 154 | 'baz' => [], 155 | ]; 156 | 157 | $locator_index = 0; 158 | $container_index = 0; 159 | 160 | $this->container 161 | ->expects($this->at($container_index++)) 162 | ->method('has') 163 | ->with('domain_event.locator') 164 | ->will($this->returnValue(true)) 165 | ; 166 | $this->container 167 | ->expects($this->at($container_index++)) 168 | ->method('findDefinition') 169 | ->with('domain_event.locator') 170 | ->will($this->returnValue($current_locator)) 171 | ; 172 | $this->container 173 | ->expects($this->at($container_index++)) 174 | ->method('findDefinition') 175 | ->with('domain_event.locator.symfony') 176 | ->will($this->returnValue($symfony_locator)) 177 | ; 178 | $this->container 179 | ->expects($this->at($container_index++)) 180 | ->method('findDefinition') 181 | ->with('domain_event.locator.container') 182 | ->will($this->returnValue($container_locator)) 183 | ; 184 | $this->container 185 | ->expects($this->at($container_index++)) 186 | ->method('findTaggedServiceIds') 187 | ->with('domain_event.listener') 188 | ->will($this->returnValue($listeners)) 189 | ; 190 | $this->container 191 | ->expects($this->at($container_index++)) 192 | ->method('findTaggedServiceIds') 193 | ->with('domain_event.subscriber') 194 | ->will($this->returnValue($subscribers)) 195 | ; 196 | 197 | foreach ($listeners as $id => $attributes) { 198 | foreach ($attributes as $attribute) { 199 | $method = !empty($attribute['method']) ? $attribute['method'] : '__invoke'; 200 | 201 | $current_locator 202 | ->expects($this->at($locator_index++)) 203 | ->method('addMethodCall') 204 | ->with('registerService', [$attribute['event'], $id, $method]) 205 | ; 206 | } 207 | } 208 | 209 | foreach ($subscribers as $id => $attributes) { 210 | $class_name = $id; 211 | 212 | $subscriber = $this->getMock(Definition::class); 213 | $subscriber 214 | ->expects($this->once()) 215 | ->method('getClass') 216 | ->will($this->returnValue($class_name)) 217 | ; 218 | 219 | $this->container 220 | ->expects($this->at($container_index++)) 221 | ->method('findDefinition') 222 | ->with($id) 223 | ->will($this->returnValue($subscriber)) 224 | ; 225 | 226 | $current_locator 227 | ->expects($this->at($locator_index++)) 228 | ->method('addMethodCall') 229 | ->with('registerSubscriberService', [$id, $class_name]) 230 | ; 231 | } 232 | 233 | $this->pass->process($this->container); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /tests/DependencyInjection/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\Tests\DependencyInjection; 11 | 12 | use GpsLab\Bundle\DomainEvent\DependencyInjection\Configuration; 13 | use PHPUnit\Framework\TestCase; 14 | use Symfony\Component\Config\Definition\ArrayNode; 15 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 16 | use Symfony\Component\Config\Definition\ScalarNode; 17 | 18 | class ConfigurationTest extends TestCase 19 | { 20 | /** 21 | * @var Configuration 22 | */ 23 | private $configuration; 24 | 25 | protected function setUp() 26 | { 27 | $this->configuration = new Configuration(); 28 | } 29 | 30 | public function testConfigTree() 31 | { 32 | $tree_builder = $this->configuration->getConfigTreeBuilder(); 33 | 34 | $this->assertInstanceOf(TreeBuilder::class, $tree_builder); 35 | 36 | /* @var $tree ArrayNode */ 37 | $tree = $tree_builder->buildTree(); 38 | 39 | $this->assertInstanceOf(ArrayNode::class, $tree); 40 | $this->assertEquals('gpslab_domain_event', $tree->getName()); 41 | 42 | /* @var $children ScalarNode[] */ 43 | $children = $tree->getChildren(); 44 | 45 | $this->assertInternalType('array', $children); 46 | $this->assertEquals(['bus', 'queue', 'locator', 'publish_on_flush'], array_keys($children)); 47 | 48 | $this->assertInstanceOf(ScalarNode::class, $children['bus']); 49 | $this->assertEquals('listener_located', $children['bus']->getDefaultValue()); 50 | $this->assertFalse($children['bus']->isRequired()); 51 | 52 | $this->assertInstanceOf(ScalarNode::class, $children['queue']); 53 | $this->assertEquals('pull_memory', $children['queue']->getDefaultValue()); 54 | $this->assertFalse($children['queue']->isRequired()); 55 | 56 | $this->assertInstanceOf(ScalarNode::class, $children['locator']); 57 | $this->assertEquals('symfony', $children['locator']->getDefaultValue()); 58 | $this->assertFalse($children['locator']->isRequired()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/DependencyInjection/GpsLabDomainEventExtensionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\Tests\DependencyInjection; 11 | 12 | use GpsLab\Bundle\DomainEvent\DependencyInjection\GpsLabDomainEventExtension; 13 | use GpsLab\Domain\Event\Bus\EventBus; 14 | use GpsLab\Domain\Event\Listener\Subscriber; 15 | use GpsLab\Domain\Event\Queue\EventQueue; 16 | use PHPUnit\Framework\TestCase; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | 19 | class GpsLabDomainEventExtensionTest extends TestCase 20 | { 21 | /** 22 | * @var GpsLabDomainEventExtension 23 | */ 24 | private $extension; 25 | 26 | /** 27 | * @var ContainerBuilder 28 | */ 29 | private $container; 30 | 31 | protected function setUp() 32 | { 33 | $this->extension = new GpsLabDomainEventExtension(); 34 | $this->container = new ContainerBuilder(); 35 | } 36 | 37 | /** 38 | * @return array 39 | */ 40 | public function config() 41 | { 42 | return [ 43 | [ 44 | [], 45 | 'domain_event.bus.listener_located', 46 | 'domain_event.queue.pull_memory', 47 | 'domain_event.locator.symfony', 48 | false, 49 | ], 50 | [ 51 | [ 52 | 'gpslab_domain_event' => [ 53 | 'bus' => 'queue', 54 | 'queue' => 'subscribe_executing', 55 | 'locator' => 'container', 56 | 'publish_on_flush' => false, 57 | ], 58 | ], 59 | 'domain_event.bus.queue', 60 | 'domain_event.queue.subscribe_executing', 61 | 'domain_event.locator.container', 62 | false, 63 | ], 64 | [ 65 | [ 66 | 'gpslab_domain_event' => [ 67 | 'bus' => 'queue', 68 | 'queue' => 'subscribe_executing', 69 | 'locator' => 'direct_binding', 70 | 'publish_on_flush' => true, 71 | ], 72 | ], 73 | 'domain_event.bus.queue', 74 | 'domain_event.queue.subscribe_executing', 75 | 'domain_event.locator.direct_binding', 76 | true, 77 | ], 78 | [ 79 | [ 80 | 'gpslab_domain_event' => [ 81 | 'bus' => 'acme.domain.event.bus', 82 | 'queue' => 'acme.domain.event.queue', 83 | 'locator' => 'acme.domain.event.locator', 84 | 'publish_on_flush' => true, 85 | ], 86 | ], 87 | 'acme.domain.event.bus', 88 | 'acme.domain.event.queue', 89 | 'acme.domain.event.locator', 90 | true, 91 | ], 92 | ]; 93 | } 94 | 95 | /** 96 | * @dataProvider config 97 | * 98 | * @param array $config 99 | * @param string $bus 100 | * @param string $queue 101 | * @param string $locator 102 | * @param bool $publish_on_flush 103 | */ 104 | public function testLoad(array $config, $bus, $queue, $locator, $publish_on_flush) 105 | { 106 | $this->extension->load($config, $this->container); 107 | 108 | $this->assertEquals($bus, $this->container->getAlias('domain_event.bus')); 109 | $this->assertEquals($queue, $this->container->getAlias('domain_event.queue')); 110 | $this->assertEquals($locator, $this->container->getAlias('domain_event.locator')); 111 | $this->assertEquals($bus, $this->container->getAlias(EventBus::class)); 112 | $this->assertEquals($queue, $this->container->getAlias(EventQueue::class)); 113 | 114 | $publisher = $this->container->getDefinition('domain_event.publisher'); 115 | $this->assertEquals($publish_on_flush, $publisher->getArgument(2)); 116 | 117 | if (method_exists($this->container, 'registerForAutoconfiguration')) { 118 | $has_subscriber = false; 119 | foreach ($this->container->getAutoconfiguredInstanceof() as $key => $definition) { 120 | if ($key === Subscriber::class) { 121 | $has_subscriber = true; 122 | $this->assertTrue($definition->hasTag('domain_event.subscriber')); 123 | $this->assertTrue($definition->isAutowired()); 124 | } 125 | } 126 | $this->assertTrue($has_subscriber); 127 | } 128 | } 129 | 130 | public function testAlias() 131 | { 132 | $this->assertEquals('gpslab_domain_event', $this->extension->getAlias()); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Event/Listener/DomainEventPublisherTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\Tests\Event\Listener; 11 | 12 | use Doctrine\ORM\EntityManagerInterface; 13 | use Doctrine\ORM\Event\OnFlushEventArgs; 14 | use Doctrine\ORM\Event\PostFlushEventArgs; 15 | use Doctrine\ORM\Events; 16 | use Doctrine\ORM\UnitOfWork; 17 | use GpsLab\Bundle\DomainEvent\Event\Listener\DomainEventPublisher; 18 | use GpsLab\Bundle\DomainEvent\Service\EventPuller; 19 | use GpsLab\Domain\Event\Bus\EventBus; 20 | use GpsLab\Domain\Event\Event; 21 | use PHPUnit\Framework\TestCase; 22 | 23 | class DomainEventPublisherTest extends TestCase 24 | { 25 | /** 26 | * @var \PHPUnit_Framework_MockObject_MockObject|EventBus 27 | */ 28 | private $bus; 29 | 30 | /** 31 | * @var \PHPUnit_Framework_MockObject_MockObject|EventPuller 32 | */ 33 | private $puller; 34 | 35 | /** 36 | * @var \PHPUnit_Framework_MockObject_MockObject|EntityManagerInterface 37 | */ 38 | private $em; 39 | 40 | /** 41 | * @var \PHPUnit_Framework_MockObject_MockObject|UnitOfWork 42 | */ 43 | private $uow; 44 | 45 | /** 46 | * @var \PHPUnit_Framework_MockObject_MockObject|OnFlushEventArgs 47 | */ 48 | private $on_flush; 49 | 50 | /** 51 | * @var \PHPUnit_Framework_MockObject_MockObject|PostFlushEventArgs 52 | */ 53 | private $post_flush; 54 | 55 | /** 56 | * @var DomainEventPublisher 57 | */ 58 | private $publisher; 59 | 60 | protected function setUp() 61 | { 62 | $this->bus = $this->getMockBuilder(EventBus::class)->getMock(); 63 | $this->puller = $this->getMockBuilder(EventPuller::class)->getMock(); 64 | $this->em = $this->getMockBuilder(EntityManagerInterface::class)->getMock(); 65 | 66 | $this->on_flush = $this 67 | ->getMockBuilder(OnFlushEventArgs::class) 68 | ->disableOriginalConstructor() 69 | ->getMock() 70 | ; 71 | $this->on_flush 72 | ->expects($this->any()) 73 | ->method('getEntityManager') 74 | ->will($this->returnValue($this->em)) 75 | ; 76 | 77 | $this->post_flush = $this 78 | ->getMockBuilder(PostFlushEventArgs::class) 79 | ->disableOriginalConstructor() 80 | ->getMock() 81 | ; 82 | $this->post_flush 83 | ->expects($this->any()) 84 | ->method('getEntityManager') 85 | ->will($this->returnValue($this->em)) 86 | ; 87 | 88 | $this->uow = $this 89 | ->getMockBuilder(UnitOfWork::class) 90 | ->disableOriginalConstructor() 91 | ->getMock() 92 | ; 93 | 94 | $this->publisher = new DomainEventPublisher($this->puller, $this->bus, true); 95 | } 96 | 97 | public function testDisabled() 98 | { 99 | $publisher = new DomainEventPublisher($this->puller, $this->bus, false); 100 | $this->assertEquals([], $publisher->getSubscribedEvents()); 101 | } 102 | 103 | public function testEnabled() 104 | { 105 | $publisher = new DomainEventPublisher($this->puller, $this->bus, true); 106 | $this->assertEquals([Events::onFlush, Events::postFlush], $publisher->getSubscribedEvents()); 107 | } 108 | 109 | public function testPreFlush() 110 | { 111 | $this->em 112 | ->expects($this->once()) 113 | ->method('getUnitOfWork') 114 | ->will($this->returnValue($this->uow)) 115 | ; 116 | 117 | $this->puller 118 | ->expects($this->once()) 119 | ->method('pull') 120 | ->with($this->uow) 121 | ; 122 | 123 | $this->publisher->onFlush($this->on_flush); 124 | } 125 | 126 | /** 127 | * @return array 128 | */ 129 | public function events() 130 | { 131 | $events1 = [ 132 | $this->getMockBuilder(Event::class)->getMock(), 133 | $this->getMockBuilder(Event::class)->getMock(), 134 | $this->getMockBuilder(Event::class)->getMock(), 135 | ]; 136 | $events2 = [ 137 | $this->getMockBuilder(Event::class)->getMock(), 138 | $this->getMockBuilder(Event::class)->getMock(), 139 | $this->getMockBuilder(Event::class)->getMock(), 140 | $this->getMockBuilder(Event::class)->getMock(), 141 | ]; 142 | 143 | return [ 144 | [[], [], []], 145 | [$events1, []], 146 | [[], $events2], 147 | [$events1, $events2], 148 | ]; 149 | } 150 | 151 | /** 152 | * @dataProvider events 153 | * 154 | * @param array $remove_events 155 | * @param array $exist_events 156 | */ 157 | public function testPublishEvents(array $remove_events, array $exist_events) 158 | { 159 | $this->puller 160 | ->expects($this->at(0)) 161 | ->method('pull') 162 | ->with($this->uow) 163 | ->will($this->returnValue($remove_events)) 164 | ; 165 | $this->puller 166 | ->expects($this->at(1)) 167 | ->method('pull') 168 | ->with($this->uow) 169 | ->will($this->returnValue($exist_events)) 170 | ; 171 | 172 | $expected_events = array_merge($remove_events, $exist_events); 173 | 174 | if ($expected_events) { 175 | foreach ($expected_events as $i => $expected_event) { 176 | $this->bus 177 | ->expects($this->at($i)) 178 | ->method('publish') 179 | ->with($expected_event) 180 | ; 181 | } 182 | $this->em 183 | ->expects($this->once()) 184 | ->method('flush') 185 | ; 186 | } else { 187 | $this->bus 188 | ->expects($this->never()) 189 | ->method('publish') 190 | ; 191 | $this->em 192 | ->expects($this->never()) 193 | ->method('flush') 194 | ; 195 | } 196 | $this->em 197 | ->expects($this->atLeastOnce()) 198 | ->method('getUnitOfWork') 199 | ->will($this->returnValue($this->uow)) 200 | ; 201 | 202 | $this->publisher->onFlush($this->on_flush); 203 | $this->publisher->postFlush($this->post_flush); 204 | } 205 | 206 | public function testRecursivePublish() 207 | { 208 | $remove_events1 = [ 209 | $this->getMockBuilder(Event::class)->getMock(), 210 | $this->getMockBuilder(Event::class)->getMock(), 211 | ]; 212 | $remove_events2 = [ 213 | $this->getMockBuilder(Event::class)->getMock(), 214 | $this->getMockBuilder(Event::class)->getMock(), 215 | $this->getMockBuilder(Event::class)->getMock(), 216 | ]; 217 | $exist_events1 = [ 218 | $this->getMockBuilder(Event::class)->getMock(), 219 | $this->getMockBuilder(Event::class)->getMock(), 220 | ]; 221 | $exist_events2 = [ 222 | $this->getMockBuilder(Event::class)->getMock(), 223 | $this->getMockBuilder(Event::class)->getMock(), 224 | $this->getMockBuilder(Event::class)->getMock(), 225 | ]; 226 | 227 | $this->em 228 | ->expects($this->atLeastOnce()) 229 | ->method('getUnitOfWork') 230 | ->will($this->returnValue($this->uow)) 231 | ; 232 | $this->em 233 | ->expects($this->exactly(2)) 234 | ->method('flush') 235 | ; 236 | 237 | $this->puller 238 | ->expects($this->at(0)) 239 | ->method('pull') 240 | ->with($this->uow) 241 | ->will($this->returnValue($remove_events1)) 242 | ; 243 | $this->puller 244 | ->expects($this->at(1)) 245 | ->method('pull') 246 | ->with($this->uow) 247 | ->will($this->returnValue($exist_events1)) 248 | ; 249 | $this->puller 250 | ->expects($this->at(2)) 251 | ->method('pull') 252 | ->with($this->uow) 253 | ->will($this->returnValue($remove_events2)) 254 | ; 255 | $this->puller 256 | ->expects($this->at(3)) 257 | ->method('pull') 258 | ->with($this->uow) 259 | ->will($this->returnValue($exist_events2)) 260 | ; 261 | 262 | $expected_events = array_merge($remove_events1, $exist_events1, $remove_events2, $exist_events2); 263 | foreach ($expected_events as $i => $expected_event) { 264 | $this->bus 265 | ->expects($this->at($i)) 266 | ->method('publish') 267 | ->with($expected_event) 268 | ; 269 | } 270 | 271 | $this->publisher->onFlush($this->on_flush); 272 | $this->publisher->postFlush($this->post_flush); 273 | // recursive call 274 | $this->publisher->onFlush($this->on_flush); 275 | $this->publisher->postFlush($this->post_flush); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /tests/Fixtures/SimpleObject.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\DomainEvent\Tests\Fixtures; 12 | 13 | class SimpleObject 14 | { 15 | /** 16 | * @var string 17 | */ 18 | private $foo; 19 | 20 | /** 21 | * @var string 22 | */ 23 | protected $camelCase = 'boo'; 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function getFoo() 29 | { 30 | return $this->foo; 31 | } 32 | 33 | /** 34 | * @param string $foo 35 | */ 36 | public function setFoo($foo) 37 | { 38 | $this->foo = $foo; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getCamelCase() 45 | { 46 | return $this->camelCase; 47 | } 48 | 49 | /** 50 | * @param string $camelCase 51 | */ 52 | public function setCamelCase($camelCase) 53 | { 54 | $this->camelCase = $camelCase; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Fixtures/SimpleObjectProxy.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\DomainEvent\Tests\Fixtures; 12 | 13 | use Doctrine\Common\Persistence\Proxy as CommonProxy; 14 | use Doctrine\Persistence\Proxy; 15 | 16 | if (interface_exists(CommonProxy::class)) { 17 | class SimpleObjectProxy extends SimpleObject implements CommonProxy 18 | { 19 | /** 20 | * @var bool 21 | */ 22 | public $__isInitialized__ = false; 23 | 24 | public function __load() 25 | { 26 | if (!$this->__isInitialized__) { 27 | $this->camelCase = 'proxy-boo'; 28 | $this->__isInitialized__ = true; 29 | } 30 | } 31 | 32 | /** 33 | * @return bool 34 | */ 35 | public function __isInitialized() 36 | { 37 | return $this->__isInitialized__; 38 | } 39 | } 40 | } else { 41 | class SimpleObjectProxy extends SimpleObject implements Proxy 42 | { 43 | /** 44 | * @var bool 45 | */ 46 | public $__isInitialized__ = false; 47 | 48 | public function __load() 49 | { 50 | if (!$this->__isInitialized__) { 51 | $this->camelCase = 'proxy-boo'; 52 | $this->__isInitialized__ = true; 53 | } 54 | } 55 | 56 | /** 57 | * @return bool 58 | */ 59 | public function __isInitialized() 60 | { 61 | return $this->__isInitialized__; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/GpsLabDomainEventBundleTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\Tests; 11 | 12 | use GpsLab\Bundle\DomainEvent\DependencyInjection\Compiler\EventListenerPass; 13 | use GpsLab\Bundle\DomainEvent\DependencyInjection\GpsLabDomainEventExtension; 14 | use GpsLab\Bundle\DomainEvent\GpsLabDomainEventBundle; 15 | use PHPUnit\Framework\TestCase; 16 | use Symfony\Component\DependencyInjection\ContainerBuilder; 17 | use Symfony\Component\HttpKernel\Bundle\Bundle; 18 | 19 | class GpsLabDomainEventBundleTest extends TestCase 20 | { 21 | /** 22 | * @var GpsLabDomainEventBundle 23 | */ 24 | private $bundle; 25 | 26 | /** 27 | * @var ContainerBuilder 28 | */ 29 | private $container; 30 | 31 | protected function setUp() 32 | { 33 | $this->bundle = new GpsLabDomainEventBundle(); 34 | $this->container = new ContainerBuilder(); 35 | } 36 | 37 | public function testCorrectBundle() 38 | { 39 | $this->assertInstanceOf(Bundle::class, $this->bundle); 40 | } 41 | 42 | public function testBuild() 43 | { 44 | $this->bundle->build($this->container); 45 | 46 | $has_event_listener_pass = false; 47 | foreach ($this->container->getCompiler()->getPassConfig()->getBeforeOptimizationPasses() as $pass) { 48 | $has_event_listener_pass = $pass instanceof EventListenerPass ?: $has_event_listener_pass; 49 | } 50 | $this->assertTrue($has_event_listener_pass); 51 | } 52 | 53 | public function testContainerExtension() 54 | { 55 | $this->assertInstanceOf(GpsLabDomainEventExtension::class, $this->bundle->getContainerExtension()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Service/EventPullerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace GpsLab\Bundle\DomainEvent\Tests\Service; 11 | 12 | use Doctrine\ORM\UnitOfWork; 13 | use GpsLab\Bundle\DomainEvent\Service\EventPuller; 14 | use GpsLab\Bundle\DomainEvent\Tests\Fixtures\SimpleObject; 15 | use GpsLab\Bundle\DomainEvent\Tests\Fixtures\SimpleObjectProxy; 16 | use GpsLab\Domain\Event\Aggregator\AggregateEvents; 17 | use GpsLab\Domain\Event\Event; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class EventPullerTest extends TestCase 21 | { 22 | /** 23 | * @var \PHPUnit_Framework_MockObject_MockObject|UnitOfWork 24 | */ 25 | private $uow; 26 | 27 | /** 28 | * @var EventPuller 29 | */ 30 | private $puller; 31 | 32 | protected function setUp() 33 | { 34 | $this->uow = $this 35 | ->getMockBuilder(UnitOfWork::class) 36 | ->disableOriginalConstructor() 37 | ->getMock() 38 | ; 39 | 40 | $this->puller = new EventPuller(); 41 | } 42 | 43 | /** 44 | * @return array 45 | */ 46 | public function events() 47 | { 48 | $events1 = [ 49 | $this->getMockBuilder(Event::class)->getMock(), 50 | $this->getMockBuilder(Event::class)->getMock(), 51 | $this->getMockBuilder(Event::class)->getMock(), 52 | $this->getMockBuilder(Event::class)->getMock(), 53 | $this->getMockBuilder(Event::class)->getMock(), 54 | ]; 55 | $events2 = [ 56 | $this->getMockBuilder(Event::class)->getMock(), 57 | $this->getMockBuilder(Event::class)->getMock(), 58 | $this->getMockBuilder(Event::class)->getMock(), 59 | $this->getMockBuilder(Event::class)->getMock(), 60 | ]; 61 | $events3 = [ 62 | $this->getMockBuilder(Event::class)->getMock(), 63 | $this->getMockBuilder(Event::class)->getMock(), 64 | $this->getMockBuilder(Event::class)->getMock(), 65 | ]; 66 | $events4 = [ 67 | $this->getMockBuilder(Event::class)->getMock(), 68 | $this->getMockBuilder(Event::class)->getMock(), 69 | ]; 70 | 71 | return [ 72 | [[], [], [], []], 73 | [$events1, [], [], []], 74 | [[], $events1, [], []], 75 | [[], [], $events1, []], 76 | [[], [], [], $events1], 77 | [$events1, $events2, [], []], 78 | [$events1, [], $events2, []], 79 | [$events1, [], [], $events2], 80 | [[], $events1, [], $events2], 81 | [[], [], $events1, $events2], 82 | [$events1, $events2, $events3, []], 83 | [$events1, $events2, [], $events3], 84 | [$events1, [], $events2, $events3], 85 | [[], $events1, $events2, $events3], 86 | [$events1, $events2, $events3, $events4], 87 | ]; 88 | } 89 | 90 | /** 91 | * @dataProvider events 92 | * 93 | * @param \PHPUnit_Framework_MockObject_MockObject[] $deletions_events 94 | * @param \PHPUnit_Framework_MockObject_MockObject[] $insertions_events 95 | * @param \PHPUnit_Framework_MockObject_MockObject[] $updates_events 96 | * @param \PHPUnit_Framework_MockObject_MockObject[] $map_events 97 | */ 98 | public function testPull( 99 | array $deletions_events, 100 | array $insertions_events, 101 | array $updates_events, 102 | array $map_events 103 | ) { 104 | if ($map_events) { 105 | $slice = round(count($map_events) / 2); 106 | $aggregator1 = $this->getMockBuilder(AggregateEvents::class)->getMock(); 107 | $aggregator1 108 | ->expects($this->once()) 109 | ->method('pullEvents') 110 | ->will($this->returnValue(array_slice($map_events, 0, $slice))); 111 | $aggregator2 = $this->getMockBuilder(AggregateEvents::class)->getMock(); 112 | $aggregator2 113 | ->expects($this->once()) 114 | ->method('pullEvents') 115 | ->will($this->returnValue(array_slice($map_events, $slice))); 116 | 117 | $map = [ 118 | [ 119 | $this->getMockBuilder(SimpleObjectProxy::class)->getMock(), 120 | $aggregator1, 121 | ], 122 | [ 123 | $aggregator2, 124 | new SimpleObject(), 125 | ], 126 | [ 127 | new SimpleObject(), 128 | $this->getMockBuilder(SimpleObjectProxy::class)->getMock(), 129 | ], 130 | ]; 131 | } else { 132 | $map = []; 133 | } 134 | 135 | $this->uow 136 | ->expects($this->once()) 137 | ->method('getScheduledEntityDeletions') 138 | ->will($this->returnValue($this->getEntitiesFroEvents($deletions_events))) 139 | ; 140 | $this->uow 141 | ->expects($this->once()) 142 | ->method('getScheduledEntityInsertions') 143 | ->will($this->returnValue($this->getEntitiesFroEvents($insertions_events))) 144 | ; 145 | $this->uow 146 | ->expects($this->once()) 147 | ->method('getScheduledEntityUpdates') 148 | ->will($this->returnValue($this->getEntitiesFroEvents($updates_events))) 149 | ; 150 | $this->uow 151 | ->expects($this->once()) 152 | ->method('getIdentityMap') 153 | ->will($this->returnValue($map)) 154 | ; 155 | 156 | $expected_events = array_merge( 157 | $deletions_events, 158 | $insertions_events, 159 | $updates_events, 160 | $map_events 161 | ); 162 | 163 | $this->assertEquals($expected_events, $this->puller->pull($this->uow)); 164 | } 165 | 166 | /** 167 | * @param Event[] $events 168 | * 169 | * @return object[] 170 | */ 171 | private function getEntitiesFroEvents(array $events) 172 | { 173 | if (!$events) { 174 | return []; 175 | } 176 | 177 | $slice = round(count($events) / 2); 178 | $aggregator1 = $this->getMockBuilder(AggregateEvents::class)->getMock(); 179 | $aggregator1 180 | ->expects($this->once()) 181 | ->method('pullEvents') 182 | ->will($this->returnValue(array_slice($events, 0, $slice))) 183 | ; 184 | $aggregator2 = $this->getMockBuilder(AggregateEvents::class)->getMock(); 185 | $aggregator2 186 | ->expects($this->once()) 187 | ->method('pullEvents') 188 | ->will($this->returnValue(array_slice($events, $slice))) 189 | ; 190 | 191 | return [ 192 | $this->getMockBuilder(SimpleObjectProxy::class)->getMock(), 193 | new SimpleObject(), 194 | $aggregator1, 195 | $aggregator2, 196 | ]; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |