├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── php.yml │ └── split.yml ├── .gitignore ├── .phive └── phars.xml ├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── monorepo-builder.php ├── packages ├── domain-event-contracts │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Attribute │ │ ├── AsImmediateDomainEventListener.php │ │ ├── AsPostFlushDomainEventListener.php │ │ ├── AsPreFlushDomainEventListener.php │ │ └── AsPublishedDomainEventListener.php │ │ ├── DomainEventEmitterInterface.php │ │ ├── DomainEventEmitterTrait.php │ │ ├── DomainEventImmediateDispatcher.php │ │ └── EquatableDomainEventInterface.php ├── domain-event-outbox │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── composer.json │ ├── config │ │ ├── debug.php │ │ └── services.php │ ├── examples │ │ ├── messenger-default.yaml │ │ └── messenger-outbox.yaml │ └── src │ │ ├── Command │ │ └── MessageRelayCommand.php │ │ ├── DependencyInjection │ │ ├── CompilerPass │ │ │ ├── OutboxEntityPass.php │ │ │ └── RemoveUnusedPass.php │ │ ├── Configuration.php │ │ └── RekalogikaDomainEventOutboxExtension.php │ │ ├── Doctrine │ │ ├── EntityManagerOutboxReader.php │ │ └── OutboxReaderFactory.php │ │ ├── Entity │ │ ├── ErrorEvent.php │ │ └── OutboxMessage.php │ │ ├── EventListener │ │ ├── DomainEventDispatchListener.php │ │ └── RenameTableListener.php │ │ ├── Exception │ │ ├── ExceptionInterface.php │ │ ├── LogicException.php │ │ ├── RuntimeException.php │ │ └── UnserializeFailureException.php │ │ ├── Message │ │ └── MessageRelayStartMessage.php │ │ ├── MessageHandler │ │ └── MessageRelayStartMessageHandler.php │ │ ├── MessagePreparer │ │ ├── ChainMessagePreparer.php │ │ └── UserIdentifierMessagePreparer.php │ │ ├── MessagePreparerInterface.php │ │ ├── MessageRelay │ │ ├── MessageRelay.php │ │ └── MessageRelayAll.php │ │ ├── MessageRelayInterface.php │ │ ├── OutboxReaderFactoryInterface.php │ │ ├── OutboxReaderInterface.php │ │ ├── RekalogikaDomainEventOutboxBundle.php │ │ ├── Schedule │ │ └── MessageRelayProvider.php │ │ └── Stamp │ │ ├── ObjectManagerNameStamp.php │ │ └── UserIdentifierStamp.php └── domain-event │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── composer.json │ ├── config │ ├── debug.php │ └── services.php │ └── src │ ├── Contracts │ └── DomainEventAwareEntityManagerInterface.php │ ├── DependencyInjection │ ├── CompilerPass │ │ ├── EntityManagerDecoratorPass.php │ │ └── ProfilerWorkaroundPass.php │ ├── Constants.php │ └── RekalogikaDomainEventExtension.php │ ├── Doctrine │ ├── AbstractManagerRegistryDecorator.php │ ├── DoctrineEventListener.php │ ├── DomainEventAwareEntityManager.php │ ├── DomainEventAwareManagerRegistryImplementation.php │ └── DomainEventReaper.php │ ├── DomainEventAwareEntityManagerInterface.php │ ├── DomainEventAwareManagerRegistry.php │ ├── DomainEventAwareObjectManager.php │ ├── DomainEventManagerInterface.php │ ├── Event │ ├── DomainEventImmediateDispatchEvent.php │ ├── DomainEventPostFlushDispatchEvent.php │ └── DomainEventPreFlushDispatchEvent.php │ ├── EventDispatcher │ ├── EventDispatchers.php │ └── ImmediateEventDispatchingDomainEventDispatcher.php │ ├── Exception │ ├── ExceptionInterface.php │ ├── FlushNotAllowedException.php │ ├── InvalidOperationException.php │ ├── LogicException.php │ ├── RuntimeException.php │ ├── SafeguardTriggeredException.php │ └── UndispatchedEventsException.php │ ├── ImmediateDomainEventDispatcherInstaller.php │ ├── Model │ ├── DomainEventStore.php │ └── TransactionAwareDomainEventStore.php │ └── RekalogikaDomainEventBundle.php ├── phpstan.neon.dist ├── phpunit.xml.dist ├── psalm.xml ├── rector.php └── tests ├── Framework ├── Entity │ ├── Book.php │ ├── Review.php │ └── User.php ├── Entity2 │ ├── Comment.php │ └── Post.php ├── Event │ ├── AbstractBookEvent.php │ ├── AbstractReviewEvent.php │ ├── BookChanged.php │ ├── BookChecked.php │ ├── BookCreated.php │ ├── BookDummyChanged.php │ ├── BookDummyMethodCalled.php │ ├── BookDummyMethodForFlushCalled.php │ ├── BookDummyMethodForInfiniteLoopCalled.php │ ├── BookDummyMethodForNestedRecordEventCalled.php │ ├── BookRemoved.php │ ├── BookReviewAdded.php │ ├── BookReviewRemoved.php │ ├── ReviewChanged.php │ ├── ReviewCreated.php │ └── ReviewRemoved.php ├── Event2 │ ├── AbstractCommentEvent.php │ ├── AbstractPostEvent.php │ ├── PostChanged.php │ ├── PostCreated.php │ └── PostRemoved.php ├── EventListener │ ├── BookDummyChangedListener.php │ ├── BookDummyMethodCalledListener.php │ ├── BookDummyMethodForFlushListener.php │ ├── BookDummyMethodForInfiniteLoopCalledListener.php │ ├── BookDummyMethodForNestedRecordEventListener.php │ ├── BookEventEventBusListener.php │ ├── BookEventImmediateListener.php │ ├── BookEventPostFlushListener.php │ ├── BookEventPreFlushListener.php │ └── PostEventEventBusListener.php ├── Kernel.php ├── Repository │ ├── BookRepository.php │ └── ReviewRepository.php ├── Resources │ └── config │ │ ├── packages │ │ ├── debug.yaml │ │ ├── doctrine.yaml │ │ ├── framework.yaml │ │ ├── lock.yaml │ │ ├── messenger.yaml │ │ ├── monolog.yaml │ │ ├── routing.yaml │ │ ├── security.yaml │ │ └── web_profiler.yaml │ │ ├── routes.yaml │ │ ├── routes │ │ └── web_profiler.yaml │ │ └── services_test.php ├── Security │ └── AccessTokenHandler.php └── Tests │ ├── BasicDomainEventTest.php │ ├── DecorationTest.php │ ├── DomainEventTestCase.php │ ├── EquatableEventTest.php │ ├── IntegrationTest.php │ ├── OutboxSetupTest.php │ ├── OutboxTest.php │ ├── PreFlushTest.php │ ├── RemoveTest.php │ ├── ResetTest.php │ ├── Transaction2Test.php │ └── TransactionTest.php ├── Integration ├── DomainEventTest.phpx ├── Event │ ├── AbstractEntityDomainEvent.php │ ├── EntityCreated.php │ ├── EntityNameChanged.php │ ├── EntityRemoved.php │ ├── EquatableEvent.php │ └── NonEquatableEvent.php ├── EventListener │ ├── DomainEventListener.php │ ├── EquatableEventListener.php │ └── FlushingDomainEventListener.php ├── Factory.php ├── Model │ └── Entity.php └── Service │ └── DomainEventEmitterCollectorStub.phpx └── bin └── console /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | labels: 3 | - "*" 4 | exclude: 5 | labels: 6 | - dependencies 7 | - minor 8 | - release 9 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "main", "test" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | strategy: 16 | matrix: 17 | operating-system: [ubuntu-latest] 18 | php: [ '8.1', '8.2', '8.3', '8.4' ] 19 | symfony: [ '6.*', '7.*' ] 20 | dep: [highest,lowest] 21 | exclude: 22 | - php: '8.1' 23 | symfony: '7.*' 24 | 25 | runs-on: ${{ matrix.operating-system }} 26 | 27 | name: Symfony ${{ matrix.symfony }}, ${{ matrix.dep }} deps, PHP ${{ matrix.php }}, ${{ matrix.operating-system }} 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Install PHP 33 | uses: shivammathur/setup-php@v2 34 | with: 35 | php-version: ${{ matrix.php }} 36 | extensions: intl 37 | tools: flex 38 | 39 | - name: Validate composer.json and composer.lock 40 | run: composer validate --strict 41 | 42 | - name: Cache Composer packages 43 | id: composer-cache 44 | uses: actions/cache@v4 45 | with: 46 | path: vendor 47 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 48 | restore-keys: | 49 | ${{ runner.os }}-php- 50 | 51 | - name: Install dependencies 52 | uses: ramsey/composer-install@v3 53 | with: 54 | dependency-versions: ${{ matrix.dep }} 55 | composer-options: --prefer-dist --no-progress --ignore-platform-reqs 56 | env: 57 | SYMFONY_REQUIRE: ${{ matrix.symfony }} 58 | 59 | - name: Run psalm 60 | run: vendor/bin/psalm 61 | if: matrix.dep == 'highest' 62 | 63 | - name: Run phpstan 64 | run: vendor/bin/phpstan analyse 65 | if: matrix.dep == 'highest' 66 | 67 | - name: Lint container 68 | run: tests/bin/console lint:container 69 | 70 | - name: Validate monorepo 71 | run: vendor/bin/monorepo-builder validate 72 | 73 | - name: Run phpunit 74 | run: | 75 | export SYMFONY_DEPRECATIONS_HELPER='max[direct]=0' 76 | vendor/bin/phpunit -------------------------------------------------------------------------------- /.github/workflows/split.yml: -------------------------------------------------------------------------------- 1 | name: 'Packages Split' 2 | 3 | on: 4 | workflow_dispatch: null 5 | push: 6 | branches: 7 | - main 8 | tags: 9 | - '*' 10 | 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} 13 | 14 | jobs: 15 | packages_split: 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | # define package to repository map 22 | package: 23 | - 24 | local_path: 'domain-event' 25 | split_repository: 'domain-event' 26 | - 27 | local_path: 'domain-event-contracts' 28 | split_repository: 'domain-event-contracts' 29 | - 30 | local_path: 'domain-event-outbox' 31 | split_repository: 'domain-event-outbox' 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | # no tag 37 | - 38 | if: "!startsWith(github.ref, 'refs/tags/')" 39 | uses: "danharrin/monorepo-split-github-action@v2.3.0" 40 | with: 41 | # ↓ split "packages/easy-coding-standard" directory 42 | package_directory: 'packages/${{ matrix.package.local_path }}' 43 | 44 | # ↓ into https://github.com/symplify/easy-coding-standard repository 45 | repository_organization: 'rekalogika' 46 | repository_name: '${{ matrix.package.split_repository }}' 47 | 48 | # [optional, with "github.com" as default] 49 | repository_host: github.com 50 | 51 | # ↓ the user signed under the split commit 52 | user_name: "Priyadi Iman Nurcahyo" 53 | user_email: "1102197+priyadi@users.noreply.github.com" 54 | 55 | # with tag 56 | - 57 | if: "startsWith(github.ref, 'refs/tags/')" 58 | uses: "danharrin/monorepo-split-github-action@v2.3.0" 59 | with: 60 | tag: ${GITHUB_REF#refs/tags/} 61 | 62 | # ↓ split "packages/easy-coding-standard" directory 63 | package_directory: 'packages/${{ matrix.package.local_path }}' 64 | 65 | # ↓ into https://github.com/symplify/easy-coding-standard repository 66 | repository_organization: 'rekalogika' 67 | repository_name: '${{ matrix.package.split_repository }}' 68 | 69 | # [optional, with "github.com" as default] 70 | repository_host: github.com 71 | 72 | # ↓ the user signed under the split commit 73 | user_name: "Priyadi Iman Nurcahyo" 74 | user_email: "1102197+priyadi@users.noreply.github.com" 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | .phpunit.cache 4 | .php-cs-fixer.cache 5 | tools 6 | var/ 7 | rector.log -------------------------------------------------------------------------------- /.phive/phars.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/packages/domain-event/config') 5 | ->in(__DIR__ . '/packages/domain-event/src') 6 | ->in(__DIR__ . '/packages/domain-event-contracts/src') 7 | ->in(__DIR__ . '/packages/domain-event-outbox/src') 8 | ->in(__DIR__ . '/tests'); 9 | 10 | $config = new PhpCsFixer\Config(); 11 | return $config->setRules([ 12 | '@PER-CS2.0' => true, 13 | '@PER-CS2.0:risky' => true, 14 | 'fully_qualified_strict_types' => true, 15 | 'global_namespace_import' => [ 16 | 'import_classes' => false, 17 | 'import_constants' => false, 18 | 'import_functions' => false, 19 | ], 20 | 'no_unneeded_import_alias' => true, 21 | 'no_unused_imports' => true, 22 | 'ordered_imports' => [ 23 | 'sort_algorithm' => 'alpha', 24 | 'imports_order' => ['class', 'function', 'const'] 25 | ], 26 | 'declare_strict_types' => true, 27 | 'native_function_invocation' => ['include' => ['@compiler_optimized']], 28 | 'header_comment' => [ 29 | 'header' => << 33 | 34 | For the full copyright and license information, please view the LICENSE file 35 | that was distributed with this source code. 36 | EOF, 37 | ] 38 | ]) 39 | ->setFinder($finder); 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023-present Priyadi Iman Nurcahyo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PHP=php 2 | 3 | .PHONY: test 4 | test: clean phpstan psalm monorepo-validate lint-container phpunit 5 | 6 | .PHONY: clean 7 | clean: 8 | rm -rf var 9 | 10 | .PHONY: monorepo 11 | monorepo: monorepo-validate monorepo-merge 12 | 13 | .PHONY: monorepo-validate 14 | monorepo-validate: 15 | vendor/bin/monorepo-builder validate 16 | 17 | .PHONY: monorepo-merge 18 | monorepo-merge: 19 | $(PHP) vendor/bin/monorepo-builder merge 20 | 21 | .PHONY: monorepo-release-% 22 | monorepo-release-%: 23 | git update-index --really-refresh > /dev/null; git diff-index --quiet HEAD || (echo "Working directory is not clean, aborting" && exit 1) 24 | [ $$(git branch --show-current) == main ] || (echo "Not on main branch, aborting" && exit 1) 25 | $(PHP) vendor/bin/monorepo-builder release $* 26 | git switch -c release/$* 27 | git add . 28 | git commit -m "release: $*" 29 | 30 | .PHONY: lint-container 31 | lint-container: 32 | $(PHP) tests/bin/console lint:container 33 | 34 | .PHONY: phpstan 35 | phpstan: 36 | $(PHP) vendor/bin/phpstan analyse 37 | 38 | .PHONY: psalm 39 | psalm: 40 | $(PHP) vendor/bin/psalm 41 | 42 | .PHONY: phpunit 43 | phpunit: clean 44 | $(eval c ?=) 45 | $(PHP) vendor/bin/phpunit $(c) 46 | 47 | .PHONY: php-cs-fixer 48 | php-cs-fixer: tools/php-cs-fixer 49 | $(PHP) $< fix --config=.php-cs-fixer.dist.php --verbose --allow-risky=yes 50 | 51 | .PHONY: tools/php-cs-fixer 52 | tools/php-cs-fixer: 53 | phive install php-cs-fixer 54 | 55 | .PHONY: dump 56 | dump: 57 | $(PHP) tests/bin/console server:dump 58 | 59 | .PHONY: rector 60 | rector: 61 | $(PHP) vendor/bin/rector process > rector.log 62 | make php-cs-fixer -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rekalogika/domain-event-src", 3 | "description": "Domain Event Framework for Symfony", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Priyadi Iman Nurcahyo", 8 | "email": "priyadi@rekalogika.com" 9 | } 10 | ], 11 | "type": "library", 12 | "require": { 13 | "php": "^8.1", 14 | "doctrine/doctrine-bundle": "^2.11.3 || ^2.12", 15 | "doctrine/orm": "^2.16 || ^3.0", 16 | "doctrine/persistence": "^3.2 || ^4.0", 17 | "psr/event-dispatcher": "^1.0", 18 | "symfony/config": "^6.4 || ^7.0", 19 | "symfony/dependency-injection": "^6.4 || ^7.0", 20 | "symfony/event-dispatcher": "^6.4 || ^7.0", 21 | "symfony/http-kernel": "^6.4 || ^7.0", 22 | "symfony/lock": "^6.4 || ^7.0", 23 | "symfony/messenger": "^6.4 || ^7.0", 24 | "symfony/service-contracts": "^3.0", 25 | "symfony/uid": "^6.4 || ^7.0", 26 | "symfony/var-exporter": "^6.4 || ^7.0" 27 | }, 28 | "require-dev": { 29 | "bnf/phpstan-psr-container": "^1.0", 30 | "ekino/phpstan-banned-code": "^1.0 || ^2.0", 31 | "mockery/mockery": "^1.6", 32 | "phpstan/phpstan": "^1.12", 33 | "phpstan/phpstan-deprecation-rules": "^1.1", 34 | "phpstan/phpstan-phpunit": "^1.3", 35 | "phpunit/phpunit": "^10.2", 36 | "psalm/plugin-phpunit": "^0.18.4 || ^0.19.0", 37 | "rector/rector": "^1.2", 38 | "symfony/debug-bundle": "^6.4 || ^7.0", 39 | "symfony/doctrine-bridge": "^6.4 || ^7.0", 40 | "symfony/framework-bundle": "^6.4 || ^7.0", 41 | "symfony/monolog-bundle": "^3.0", 42 | "symfony/runtime": "^6.4 || ^7.0", 43 | "symfony/scheduler": "^6.4 || ^7.0", 44 | "symfony/security-bundle": "^6.4 || ^7.0", 45 | "symfony/stopwatch": "^6.4 || ^7.0", 46 | "symfony/twig-bundle": "^6.4 || ^7.0", 47 | "symfony/web-profiler-bundle": "^6.4 || ^7.0", 48 | "symfony/yaml": "^6.4 || ^7.0", 49 | "symplify/monorepo-builder": "^11.2.20 || ^11.3", 50 | "twig/twig": "^3.0", 51 | "vimeo/psalm": "^5.26" 52 | }, 53 | "autoload": { 54 | "psr-4": { 55 | "Rekalogika\\Contracts\\DomainEvent\\": "packages/domain-event-contracts/src/", 56 | "Rekalogika\\DomainEvent\\": "packages/domain-event/src/", 57 | "Rekalogika\\DomainEvent\\Outbox\\": "packages/domain-event-outbox/src/" 58 | } 59 | }, 60 | "autoload-dev": { 61 | "psr-4": { 62 | "Rekalogika\\DomainEvent\\Tests\\": "tests/" 63 | } 64 | }, 65 | "config": { 66 | "sort-packages": true, 67 | "allow-plugins": { 68 | "symfony/runtime": true 69 | } 70 | }, 71 | "replace": { 72 | "rekalogika/domain-event": "2.5.2", 73 | "rekalogika/domain-event-contracts": "2.5.2", 74 | "rekalogika/domain-event-outbox": "2.5.2" 75 | }, 76 | "minimum-stability": "stable" 77 | } 78 | -------------------------------------------------------------------------------- /monorepo-builder.php: -------------------------------------------------------------------------------- 1 | packageDirectories([__DIR__ . '/packages']); 12 | $mbConfig->defaultBranch('main'); 13 | $mbConfig->disableDefaultWorkers(); 14 | 15 | $mbConfig->workers([ 16 | UpdateReplaceReleaseWorker::class, 17 | SetCurrentMutualDependenciesReleaseWorker::class, 18 | UpdateBranchAliasReleaseWorker::class, 19 | ]); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023-present Priyadi Iman Nurcahyo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/README.md: -------------------------------------------------------------------------------- 1 | # rekalogika/domain-event-contracts 2 | 3 | Contains interfaces, traits, attributes, and nominal classes to be used by 4 | domain objects utilizing the `rekalogika/domain-event` framework. 5 | 6 | ## Documentation 7 | 8 | [rekalogika.dev/domain-event](https://rekalogika.dev/domain-event) 9 | 10 | ## License 11 | 12 | MIT 13 | 14 | ## Contributing 15 | 16 | The `rekalogika/domain-event-contracts` repository is a read-only repo split 17 | from the main repo. Issues and pull requests should be submitted to the 18 | [rekalogika/domain-event-src](https://github.com/rekalogika/domain-event-src) 19 | monorepo. 20 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rekalogika/domain-event-contracts", 3 | "description": "Interfaces, Traits and Nominal Classes used by Domain Entities Implementing Domain Events", 4 | "homepage": "https://rekalogika.dev/domain-event", 5 | "keywords": [ 6 | "domain-event", 7 | "domain", 8 | "event", 9 | "symfony", 10 | "doctrine", 11 | "event-dispatcher", 12 | "event-listener", 13 | "domain-driven-design", 14 | "ddd", 15 | "cqrs" 16 | ], 17 | "type": "library", 18 | "license": "MIT", 19 | "authors": [ 20 | { 21 | "name": "Priyadi Iman Nurcahyo", 22 | "email": "priyadi@rekalogika.com" 23 | } 24 | ], 25 | "autoload": { 26 | "psr-4": { 27 | "Rekalogika\\Contracts\\DomainEvent\\": "src/" 28 | } 29 | }, 30 | "require": { 31 | "psr/event-dispatcher": "^1.0" 32 | }, 33 | "extra": { 34 | "branch-alias": { 35 | "dev-main": "2.6-dev" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/src/Attribute/AsImmediateDomainEventListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Contracts\DomainEvent\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] 17 | class AsImmediateDomainEventListener 18 | { 19 | public function __construct( 20 | public ?string $event = null, 21 | public ?string $method = null, 22 | public int $priority = 0, 23 | ) {} 24 | } 25 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/src/Attribute/AsPostFlushDomainEventListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Contracts\DomainEvent\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] 17 | class AsPostFlushDomainEventListener 18 | { 19 | public function __construct( 20 | public ?string $event = null, 21 | public ?string $method = null, 22 | public int $priority = 0, 23 | ) {} 24 | } 25 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/src/Attribute/AsPreFlushDomainEventListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Contracts\DomainEvent\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] 17 | class AsPreFlushDomainEventListener 18 | { 19 | public function __construct( 20 | public ?string $event = null, 21 | public ?string $method = null, 22 | public int $priority = 0, 23 | ) {} 24 | } 25 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/src/Attribute/AsPublishedDomainEventListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Contracts\DomainEvent\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] 17 | class AsPublishedDomainEventListener 18 | { 19 | public function __construct( 20 | public ?string $event = null, 21 | public ?string $method = null, 22 | public int $priority = 0, 23 | ) {} 24 | } 25 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/src/DomainEventEmitterInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Contracts\DomainEvent; 15 | 16 | /** 17 | * Interface implemented by classes that records domain events, typically your 18 | * domain entities. 19 | */ 20 | interface DomainEventEmitterInterface 21 | { 22 | /** 23 | * Returns all domain events recorded by the entity, and delete them. 24 | * 25 | * @return array 26 | */ 27 | public function popRecordedEvents(): array; 28 | 29 | /** 30 | * Called when the object is removed from the persistence layer. 31 | */ 32 | public function __remove(): void; 33 | } 34 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/src/DomainEventEmitterTrait.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Contracts\DomainEvent; 15 | 16 | /** 17 | * Helper trait to implement DomainEventEmitterInterface. 18 | */ 19 | trait DomainEventEmitterTrait 20 | { 21 | /** 22 | * @var array 23 | */ 24 | private array $recordedDomainEvents = []; 25 | 26 | /** 27 | * @return array 28 | */ 29 | final public function popRecordedEvents(): array 30 | { 31 | $recordedEvents = $this->recordedDomainEvents; 32 | $this->recordedDomainEvents = []; 33 | 34 | return $recordedEvents; 35 | } 36 | 37 | /** 38 | * Called by the object to record an event, and immediately dispatch it 39 | * using the DomainEventImmediateDispatcher. Returns the event object as 40 | * returned by the immediate dispatcher. It does not get anything back from 41 | * preflush or postflush events. 42 | * 43 | * @template T of object 44 | * @param T $event 45 | * @return T 46 | */ 47 | protected function recordEvent( 48 | object $event, 49 | ): object { 50 | if ($event instanceof EquatableDomainEventInterface) { 51 | $hash = $event->getSignature(); 52 | $this->recordedDomainEvents[$hash] = $event; 53 | } else { 54 | $this->recordedDomainEvents[] = $event; 55 | } 56 | 57 | // returns the event object as returned by the immediate dispatcher 58 | return DomainEventImmediateDispatcher::dispatch($event); 59 | } 60 | 61 | public function __remove(): void {} 62 | } 63 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/src/DomainEventImmediateDispatcher.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Contracts\DomainEvent; 15 | 16 | use Psr\EventDispatcher\EventDispatcherInterface; 17 | 18 | /** 19 | * Dispatches domain events immediately. 20 | */ 21 | final class DomainEventImmediateDispatcher 22 | { 23 | private static ?EventDispatcherInterface $eventDispatcher = null; 24 | 25 | /** 26 | * Disallow instantiation 27 | */ 28 | private function __construct() {} 29 | 30 | /** 31 | * Called at the beginning of the request to install the event dispatcher. 32 | */ 33 | public static function install(EventDispatcherInterface $eventDispatcher): void 34 | { 35 | self::$eventDispatcher = $eventDispatcher; 36 | } 37 | 38 | public static function uninstall(): void 39 | { 40 | self::$eventDispatcher = null; 41 | } 42 | 43 | /** 44 | * Dispatches an event using the installed event dispatcher 45 | * 46 | * @template T of object 47 | * @param T $event 48 | * @return T 49 | */ 50 | public static function dispatch(object $event): object 51 | { 52 | if (self::$eventDispatcher === null) { 53 | throw new \RuntimeException('ImmediateDomainEventDispatcher has not been initialized'); 54 | } 55 | 56 | /** @var T */ 57 | $result = self::$eventDispatcher->dispatch($event); 58 | 59 | return $result; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/domain-event-contracts/src/EquatableDomainEventInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Contracts\DomainEvent; 15 | 16 | /** 17 | * Provides a method that can be used to determine whether two events should be 18 | * considered equal. 19 | */ 20 | interface EquatableDomainEventInterface 21 | { 22 | public function getSignature(): string; 23 | } 24 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023-present Priyadi Iman Nurcahyo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/README.md: -------------------------------------------------------------------------------- 1 | # rekalogika/domain-event-outbox 2 | 3 | Implementation of the transactional outbox pattern on top of the 4 | `rekalogika/domain-event` package. 5 | 6 | Full documentation is available at 7 | [rekalogika.dev/domain-event](https://rekalogika.dev/domain-event). 8 | 9 | ## Synopsis 10 | 11 | ```php 12 | // 13 | // The event 14 | // 15 | 16 | final readonly class PostChanged 17 | { 18 | public function __construct(public string $postId) {} 19 | } 20 | 21 | // 22 | // The entity 23 | // 24 | 25 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterInterface; 26 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterTrait; 27 | 28 | class Post implements DomainEventEmitterInterface 29 | { 30 | use DomainEventEmitterTrait; 31 | 32 | // ... 33 | 34 | public function setTitle(string $title): void 35 | { 36 | $this->title = $title; 37 | // highlight-next-line 38 | $this->recordEvent(new PostChanged($this->id)); 39 | } 40 | 41 | // ... 42 | } 43 | 44 | // 45 | // The listener 46 | // 47 | 48 | use Psr\Log\LoggerInterface; 49 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPublishedDomainEventListener; 50 | 51 | class PostEventListener 52 | { 53 | public function __construct(private LoggerInterface $logger) {} 54 | 55 | // highlight-next-line 56 | #[AsPublishedDomainEventListener] 57 | public function onPostChanged(PostChanged $event) { 58 | $postId = $event->postId; 59 | 60 | $this->logger->info("Post $postId has been changed."); 61 | } 62 | } 63 | 64 | // 65 | // The caller 66 | // 67 | 68 | use Doctrine\ORM\EntityManagerInterface; 69 | 70 | /** @var Post $post */ 71 | /** @var EntityManagerInterface $entityManager */ 72 | 73 | $post->setTitle('New title'); 74 | $entityManager->flush(); 75 | 76 | // During the flush above, the event will be recorded in the outbox table in the 77 | // database. Then the message relay service is executed, and will publish the 78 | // events on the event bus. When the event bus announces the event, the listener 79 | // will be executed. 80 | ``` 81 | 82 | ## Documentation 83 | 84 | [rekalogika.dev/domain-event](https://rekalogika.dev/domain-event). 85 | 86 | ## License 87 | 88 | MIT 89 | 90 | ## Contributing 91 | 92 | The `rekalogika/domain-event-outbox` repository is a read-only repo split from 93 | the main repo. Issues and pull requests should be submitted to the 94 | [rekalogika/domain-event-src](https://github.com/rekalogika/domain-event-src) 95 | monorepo. 96 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rekalogika/domain-event-outbox", 3 | "description": "Implementation of the transactional outbox pattern on top of rekalogika/domain-event", 4 | "homepage": "https://rekalogika.dev/domain-event", 5 | "keywords": [ 6 | "domain-event", 7 | "domain", 8 | "event", 9 | "symfony", 10 | "doctrine", 11 | "event-dispatcher", 12 | "event-listener", 13 | "domain-driven-design", 14 | "ddd", 15 | "cqrs", 16 | "outbox", 17 | "microservice" 18 | ], 19 | "type": "symfony-bundle", 20 | "license": "MIT", 21 | "authors": [ 22 | { 23 | "name": "Priyadi Iman Nurcahyo", 24 | "email": "priyadi@rekalogika.com" 25 | } 26 | ], 27 | "autoload": { 28 | "psr-4": { 29 | "Rekalogika\\DomainEvent\\Outbox\\": "src/" 30 | } 31 | }, 32 | "require": { 33 | "doctrine/doctrine-bundle": "^2.11.3 || ^2.12", 34 | "doctrine/orm": "^2.16 || ^3.0", 35 | "doctrine/persistence": "^3.2 || ^4.0", 36 | "rekalogika/domain-event": "^2.5.2", 37 | "rekalogika/domain-event-contracts": "^2.5.2", 38 | "symfony/config": "^6.4 || ^7.0", 39 | "symfony/dependency-injection": "^6.4 || ^7.0", 40 | "symfony/http-kernel": "^6.4 || ^7.0", 41 | "symfony/lock": "^6.4 || ^7.0", 42 | "symfony/messenger": "^6.4 || ^7.0" 43 | }, 44 | "extra": { 45 | "branch-alias": { 46 | "dev-main": "2.6-dev" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/config/debug.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | use Doctrine\ORM\Mapping\Driver\AttributeDriver; 15 | use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; 16 | 17 | return static function (ContainerConfigurator $containerConfigurator): void { 18 | $services = $containerConfigurator->services(); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/examples/messenger-default.yaml: -------------------------------------------------------------------------------- 1 | # default messenger.yaml with explicit default bus 2 | 3 | framework: 4 | messenger: 5 | failure_transport: failed 6 | 7 | transports: 8 | # https://symfony.com/doc/current/messenger.html#transport-configuration 9 | async: 10 | dsn: '%env(MESSENGER_TRANSPORT_DSN)%' 11 | options: 12 | use_notify: true 13 | check_delayed_interval: 60000 14 | retry_strategy: 15 | max_retries: 3 16 | multiplier: 2 17 | failed: 'doctrine://default?queue_name=failed' 18 | # sync: 'sync://' 19 | 20 | default_bus: messenger.bus.default 21 | 22 | buses: 23 | messenger.bus.default: null 24 | 25 | routing: 26 | Symfony\Component\Mailer\Messenger\SendEmailMessage: async 27 | Symfony\Component\Notifier\Message\ChatMessage: async 28 | Symfony\Component\Notifier\Message\SmsMessage: async 29 | 30 | # Route your messages to the transports 31 | # 'App\Message\YourMessage': async 32 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/examples/messenger-outbox.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | buses: 4 | rekalogika.domain_event.bus: 5 | default_middleware: 6 | allow_no_handlers: true 7 | 8 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Command/MessageRelayCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Command; 15 | 16 | use Rekalogika\DomainEvent\Outbox\MessageRelay\MessageRelayAll; 17 | use Rekalogika\DomainEvent\Outbox\MessageRelayInterface; 18 | use Symfony\Component\Console\Command\Command; 19 | use Symfony\Component\Console\Input\InputArgument; 20 | use Symfony\Component\Console\Input\InputInterface; 21 | use Symfony\Component\Console\Input\InputOption; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | 24 | /** 25 | * Runs the message relay manually from the command line. 26 | */ 27 | final class MessageRelayCommand extends Command 28 | { 29 | public function __construct( 30 | private readonly string $defaultManagerName, 31 | private readonly MessageRelayInterface $messageRelay, 32 | private readonly MessageRelayAll $messageRelayAll, 33 | ) { 34 | parent::__construct(); 35 | } 36 | 37 | #[\Override] 38 | protected function configure(): void 39 | { 40 | $this->addArgument( 41 | name: 'managerName', 42 | mode: InputArgument::OPTIONAL, 43 | description: 'The name of the entity manager to relay messages from.', 44 | ); 45 | 46 | $this->addOption( 47 | name: 'all', 48 | shortcut: 'a', 49 | mode: InputOption::VALUE_NONE, 50 | description: 'Relay messages from all entity managers.', 51 | ); 52 | } 53 | 54 | #[\Override] 55 | protected function execute(InputInterface $input, OutputInterface $output): int 56 | { 57 | $managerName = $input->getArgument('managerName') ?? $this->defaultManagerName; 58 | $isAll = (bool) $input->getOption('all'); 59 | 60 | if ($isAll) { 61 | $this->messageRelayAll->relayAll(); 62 | 63 | return Command::SUCCESS; 64 | } 65 | 66 | if (!\is_string($managerName)) { 67 | throw new \InvalidArgumentException('The manager name must be a string.'); 68 | } 69 | 70 | do { 71 | $messagesRelayed = $this->messageRelay->relayMessages($managerName); 72 | } while ($messagesRelayed > 0); 73 | 74 | return Command::SUCCESS; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/DependencyInjection/CompilerPass/OutboxEntityPass.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\DependencyInjection\CompilerPass; 15 | 16 | use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass; 17 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | 20 | /** 21 | * @internal 22 | */ 23 | final class OutboxEntityPass implements CompilerPassInterface 24 | { 25 | #[\Override] 26 | public function process(ContainerBuilder $container): void 27 | { 28 | $entityManagers = $container->getParameter('doctrine.entity_managers'); 29 | \assert(\is_array($entityManagers)); 30 | 31 | /** 32 | * @var string $name 33 | */ 34 | foreach (array_keys($entityManagers) as $name) { 35 | $parameterKey = \sprintf('rekalogika.domain_event.doctrine.orm.%s_entity_manager', $name); 36 | $container->setParameter($parameterKey, $name); 37 | 38 | $path = realpath(__DIR__ . '/../../Entity'); 39 | if (false === $path) { 40 | throw new \RuntimeException('Entity path not found'); 41 | } 42 | 43 | $pass = DoctrineOrmMappingsPass::createAttributeMappingDriver( 44 | namespaces: ['Rekalogika\DomainEvent\Outbox\Entity'], 45 | directories: [$path], 46 | managerParameters: [$parameterKey], 47 | reportFieldsWhereDeclared: true, 48 | ); 49 | 50 | $pass->process($container); 51 | 52 | $container->getParameterBag()->remove($parameterKey); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/DependencyInjection/CompilerPass/RemoveUnusedPass.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\DependencyInjection\CompilerPass; 15 | 16 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 19 | 20 | /** 21 | * @internal 22 | */ 23 | final class RemoveUnusedPass implements CompilerPassInterface 24 | { 25 | #[\Override] 26 | public function process(ContainerBuilder $container): void 27 | { 28 | if (!interface_exists(TokenStorageInterface::class)) { 29 | $container->removeDefinition('rekalogika.domain_event.outbox.message_preparer.user_identifier'); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\DependencyInjection; 15 | 16 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 17 | use Symfony\Component\Config\Definition\ConfigurationInterface; 18 | 19 | class Configuration implements ConfigurationInterface 20 | { 21 | #[\Override] 22 | public function getConfigTreeBuilder(): TreeBuilder 23 | { 24 | $treeBuilder = new TreeBuilder('rekalogika_domain_event_outbox'); 25 | $rootNode = $treeBuilder->getRootNode(); 26 | 27 | $rootNode 28 | ->children() 29 | ->scalarNode('outbox_table') 30 | ->info('Table name used to store the outbox messages.') 31 | ->defaultValue('rekalogika_event_outbox') 32 | ->end() 33 | ->end(); 34 | 35 | return $treeBuilder; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/DependencyInjection/RekalogikaDomainEventOutboxExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\DependencyInjection; 15 | 16 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPublishedDomainEventListener; 17 | use Rekalogika\DomainEvent\Outbox\MessagePreparerInterface; 18 | use Symfony\Component\Config\FileLocator; 19 | use Symfony\Component\DependencyInjection\ChildDefinition; 20 | use Symfony\Component\DependencyInjection\ContainerBuilder; 21 | use Symfony\Component\DependencyInjection\Extension\Extension; 22 | use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; 23 | 24 | class RekalogikaDomainEventOutboxExtension extends Extension 25 | { 26 | /** 27 | * @param array $configs 28 | */ 29 | #[\Override] 30 | public function load(array $configs, ContainerBuilder $container): void 31 | { 32 | $debug = (bool) $container->getParameter('kernel.debug'); 33 | 34 | $loader = new PhpFileLoader( 35 | $container, 36 | new FileLocator(__DIR__ . '/../../config'), 37 | ); 38 | $loader->load('services.php'); 39 | 40 | if ($debug) { 41 | $loader->load('debug.php'); 42 | } 43 | 44 | $configuration = new Configuration(); 45 | $config = $this->processConfiguration($configuration, $configs); 46 | 47 | $outboxTable = $config['outbox_table'] ?? null; 48 | \assert(\is_string($outboxTable), 'The "outbox_table" option must be a string.'); 49 | $container->setParameter('rekalogika.domain_event.outbox.outbox_table', $outboxTable); 50 | 51 | $container 52 | ->registerForAutoconfiguration(MessagePreparerInterface::class) 53 | ->addTag('rekalogika.domain_event.outbox.message_preparer'); 54 | 55 | $container->registerAttributeForAutoconfiguration( 56 | AsPublishedDomainEventListener::class, 57 | static function ( 58 | ChildDefinition $definition, 59 | AsPublishedDomainEventListener $attribute, 60 | \Reflector $reflector, 61 | ): void { 62 | if ( 63 | !$reflector instanceof \ReflectionClass 64 | && !$reflector instanceof \ReflectionMethod 65 | ) { 66 | return; 67 | } 68 | 69 | $tagAttributes = get_object_vars($attribute); 70 | $tagAttributes['bus'] = 'rekalogika.domain_event.bus'; 71 | if ($reflector instanceof \ReflectionMethod) { 72 | if (isset($tagAttributes['method'])) { 73 | throw new \LogicException(\sprintf('AsPreFlushDomainEventListener attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name)); 74 | } 75 | 76 | $tagAttributes['method'] = $reflector->getName(); 77 | } 78 | 79 | $definition->addTag( 80 | 'messenger.message_handler', 81 | $tagAttributes, 82 | ); 83 | }, 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Doctrine/EntityManagerOutboxReader.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Doctrine; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | use Rekalogika\DomainEvent\Outbox\Entity\ErrorEvent; 18 | use Rekalogika\DomainEvent\Outbox\Entity\OutboxMessage; 19 | use Rekalogika\DomainEvent\Outbox\Exception\UnserializeFailureException; 20 | use Rekalogika\DomainEvent\Outbox\OutboxReaderInterface; 21 | use Symfony\Component\Messenger\Envelope; 22 | 23 | class EntityManagerOutboxReader implements OutboxReaderInterface 24 | { 25 | public function __construct( 26 | private readonly EntityManagerInterface $entityManager, 27 | ) {} 28 | 29 | #[\Override] 30 | public function getOutboxMessages(int $limit): iterable 31 | { 32 | $this->entityManager->beginTransaction(); 33 | 34 | $queryBuilder = $this->entityManager->createQueryBuilder(); 35 | 36 | $queryBuilder 37 | ->from(OutboxMessage::class, 'o') 38 | ->select('o') 39 | ->where('o.error = false') 40 | ->orderBy('o.id', 'ASC') 41 | ->setMaxResults($limit); 42 | 43 | $result = $queryBuilder->getQuery()->getResult(); 44 | \assert(\is_array($result)); 45 | 46 | foreach ($result as $row) { 47 | \assert($row instanceof OutboxMessage); 48 | 49 | $id = $row->getId(); 50 | 51 | try { 52 | $event = $row->getEvent(); 53 | yield $id => $event; 54 | } catch (UnserializeFailureException) { 55 | yield $id => new Envelope(new ErrorEvent()); 56 | } 57 | } 58 | } 59 | 60 | #[\Override] 61 | public function removeOutboxMessageById(int|string $id): void 62 | { 63 | /** @var OutboxMessage */ 64 | $object = $this->entityManager->getReference(OutboxMessage::class, $id); 65 | $this->entityManager->remove($object); 66 | } 67 | 68 | #[\Override] 69 | public function flagError(int|string $id): void 70 | { 71 | /** @var OutboxMessage */ 72 | $object = $this->entityManager->getReference(OutboxMessage::class, $id); 73 | $object->setError(true); 74 | } 75 | 76 | #[\Override] 77 | public function flush(): void 78 | { 79 | $this->entityManager->flush(); 80 | $this->entityManager->commit(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Doctrine/OutboxReaderFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Doctrine; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | use Doctrine\Persistence\ManagerRegistry; 18 | use Rekalogika\DomainEvent\Outbox\OutboxReaderFactoryInterface; 19 | use Rekalogika\DomainEvent\Outbox\OutboxReaderInterface; 20 | 21 | class OutboxReaderFactory implements OutboxReaderFactoryInterface 22 | { 23 | public function __construct(private readonly ManagerRegistry $managerRegistry) {} 24 | 25 | #[\Override] 26 | public function createOutboxReader(string $managerName): OutboxReaderInterface 27 | { 28 | $manager = $this->managerRegistry->getManager($managerName); 29 | 30 | if ($manager instanceof EntityManagerInterface) { 31 | return new EntityManagerOutboxReader($manager); 32 | } 33 | 34 | throw new \InvalidArgumentException(\sprintf('Object manager with name "%s" is an instance of "%s", but it is unsupported', $managerName, $manager::class)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Entity/ErrorEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Entity; 15 | 16 | /** 17 | * A sentinel event that represents an error in the outbox. 18 | */ 19 | class ErrorEvent {} 20 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Entity/OutboxMessage.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Entity; 15 | 16 | use Doctrine\DBAL\Types\Types; 17 | use Doctrine\ORM\Mapping\Column; 18 | use Doctrine\ORM\Mapping\Entity; 19 | use Doctrine\ORM\Mapping\GeneratedValue; 20 | use Doctrine\ORM\Mapping\Id; 21 | use Doctrine\ORM\Mapping\Index; 22 | use Doctrine\ORM\Mapping\Table; 23 | use Rekalogika\DomainEvent\Outbox\Exception\LogicException; 24 | use Rekalogika\DomainEvent\Outbox\Exception\UnserializeFailureException; 25 | use Symfony\Component\Messenger\Envelope; 26 | 27 | #[Entity()] 28 | #[Table(name: 'rekalogika_event_outbox')] 29 | #[Index(fields: ['error'])] 30 | class OutboxMessage 31 | { 32 | #[Id] 33 | #[Column(type: Types::BIGINT)] 34 | #[GeneratedValue] 35 | private ?int $id = null; 36 | 37 | #[Column(type: Types::TEXT)] 38 | private string $event; 39 | 40 | #[Column(type: Types::BOOLEAN, options: ["default" => false])] 41 | private bool $error = false; 42 | 43 | private ?Envelope $cachedResult = null; 44 | 45 | public function __construct(Envelope $event) 46 | { 47 | $this->event = base64_encode(serialize($event)); 48 | $this->cachedResult = $event; 49 | } 50 | 51 | public function getId(): int 52 | { 53 | if (null === $this->id) { 54 | throw new LogicException('ID is not set'); 55 | } 56 | 57 | return $this->id; 58 | } 59 | 60 | public function getEvent(): Envelope 61 | { 62 | if (null !== $this->cachedResult) { 63 | return $this->cachedResult; 64 | } 65 | 66 | $decoded = base64_decode($this->event); 67 | $result = unserialize($decoded); 68 | 69 | if (false === $result) { 70 | throw new UnserializeFailureException($this->event); 71 | } 72 | 73 | if (!$result instanceof Envelope) { 74 | throw new UnserializeFailureException($this->event); 75 | } 76 | 77 | return $this->cachedResult = $result; 78 | } 79 | 80 | public function isError(): bool 81 | { 82 | return $this->error; 83 | } 84 | 85 | public function setError(bool $error): self 86 | { 87 | $this->error = $error; 88 | 89 | return $this; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/EventListener/DomainEventDispatchListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\EventListener; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | use Rekalogika\Contracts\DomainEvent\EquatableDomainEventInterface; 18 | use Rekalogika\DomainEvent\DomainEventAwareManagerRegistry; 19 | use Rekalogika\DomainEvent\Event\DomainEventPreFlushDispatchEvent; 20 | use Rekalogika\DomainEvent\Outbox\Entity\OutboxMessage; 21 | use Rekalogika\DomainEvent\Outbox\Message\MessageRelayStartMessage; 22 | use Rekalogika\DomainEvent\Outbox\MessagePreparerInterface; 23 | use Symfony\Component\Messenger\Envelope; 24 | use Symfony\Component\Messenger\MessageBusInterface; 25 | use Symfony\Contracts\Service\ResetInterface; 26 | 27 | /** 28 | * Listen when a domain event is dispatched, and save it to the outbox table. 29 | */ 30 | class DomainEventDispatchListener implements ResetInterface 31 | { 32 | /** 33 | * @var array 34 | */ 35 | public array $managerNames = []; 36 | 37 | /** 38 | * @var array 39 | */ 40 | public array $dispatchedEquatableEvents = []; 41 | 42 | public function __construct( 43 | private readonly MessagePreparerInterface $messagePreparer, 44 | private readonly MessageBusInterface $messageBus, 45 | private readonly DomainEventAwareManagerRegistry $managerRegistry, 46 | ) {} 47 | 48 | #[\Override] 49 | public function reset(): void 50 | { 51 | $this->onTerminate(); 52 | } 53 | 54 | public function onPreFlushDispatch(DomainEventPreFlushDispatchEvent $event): void 55 | { 56 | $domainEvent = $event->getDomainEvent(); 57 | 58 | // check if the same event is already dispatched 59 | if ($domainEvent instanceof EquatableDomainEventInterface) { 60 | $signature = $domainEvent->getSignature(); 61 | 62 | if (isset($this->dispatchedEquatableEvents[$signature])) { 63 | return; 64 | } 65 | 66 | $this->dispatchedEquatableEvents[$signature] = true; 67 | } 68 | 69 | $objectManager = $event->getObjectManager(); 70 | 71 | if (!$objectManager instanceof EntityManagerInterface) { 72 | return; 73 | } 74 | 75 | $managerName = $this->managerRegistry->getManagerName($objectManager); 76 | 77 | $envelope = new Envelope($domainEvent); 78 | $envelope = $this->messagePreparer->prepareMessage($envelope); 79 | 80 | if (null === $envelope) { 81 | return; 82 | } 83 | 84 | $objectManager->persist(new OutboxMessage($envelope)); 85 | $this->managerNames[$managerName] = true; 86 | } 87 | 88 | public function onTerminate(): void 89 | { 90 | foreach (array_keys($this->managerNames) as $managerName) { 91 | $this->messageBus->dispatch(new MessageRelayStartMessage($managerName)); 92 | } 93 | 94 | $this->managerNames = []; 95 | $this->dispatchedEquatableEvents = []; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/EventListener/RenameTableListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\EventListener; 15 | 16 | use Doctrine\ORM\Event\LoadClassMetadataEventArgs; 17 | use Rekalogika\DomainEvent\Outbox\Entity\OutboxMessage; 18 | 19 | /** 20 | * Renames the outbox table according to the configuration. 21 | */ 22 | class RenameTableListener 23 | { 24 | public function __construct(private readonly string $outboxTable) {} 25 | 26 | public function loadClassMetadata(LoadClassMetadataEventArgs $event): void 27 | { 28 | $metadata = $event->getClassMetadata(); 29 | $reflectionClass = $metadata->getReflectionClass(); 30 | 31 | // @phpstan-ignore identical.alwaysFalse 32 | if ($reflectionClass === null) { 33 | return; 34 | } 35 | 36 | if ($reflectionClass->getName() === OutboxMessage::class) { 37 | $metadata->setPrimaryTable(['name' => $this->outboxTable]); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Exception; 15 | 16 | interface ExceptionInterface extends \Throwable {} 17 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Exception; 15 | 16 | class LogicException extends \LogicException implements ExceptionInterface {} 17 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Exception; 15 | 16 | class RuntimeException extends \RuntimeException implements ExceptionInterface {} 17 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Exception/UnserializeFailureException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Exception; 15 | 16 | class UnserializeFailureException extends RuntimeException 17 | { 18 | public function __construct(string $serializedText) 19 | { 20 | parent::__construct(\sprintf('Failed to unserialize serialized event object: "%s"', $serializedText)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Message/MessageRelayStartMessage.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Message; 15 | 16 | /** 17 | * Delivered to the message bus to instruct its handler to start the message 18 | * relay 19 | */ 20 | class MessageRelayStartMessage 21 | { 22 | public function __construct(private readonly string $managerName) {} 23 | 24 | public function getManagerName(): string 25 | { 26 | return $this->managerName; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/MessageHandler/MessageRelayStartMessageHandler.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\MessageHandler; 15 | 16 | use Rekalogika\DomainEvent\Outbox\Message\MessageRelayStartMessage; 17 | use Rekalogika\DomainEvent\Outbox\MessageRelayInterface; 18 | 19 | /** 20 | * Starts the message relay after receiving the message 21 | */ 22 | class MessageRelayStartMessageHandler 23 | { 24 | public function __construct(private readonly MessageRelayInterface $messageRelay) {} 25 | 26 | public function __invoke(MessageRelayStartMessage $message): void 27 | { 28 | do { 29 | $messagesRelayed = $this->messageRelay->relayMessages($message->getManagerName()); 30 | } while ($messagesRelayed > 0); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/MessagePreparer/ChainMessagePreparer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\MessagePreparer; 15 | 16 | use Rekalogika\DomainEvent\Outbox\MessagePreparerInterface; 17 | use Symfony\Component\Messenger\Envelope; 18 | 19 | /** 20 | * Prepares the message before it is saved to the outbox table. 21 | */ 22 | class ChainMessagePreparer implements MessagePreparerInterface 23 | { 24 | /** 25 | * @param iterable $messagePreparers 26 | */ 27 | public function __construct(private readonly iterable $messagePreparers) {} 28 | 29 | #[\Override] 30 | public function prepareMessage(Envelope $envelope): ?Envelope 31 | { 32 | foreach ($this->messagePreparers as $messagePreparer) { 33 | $envelope = $messagePreparer->prepareMessage($envelope); 34 | 35 | if (null === $envelope) { 36 | return null; 37 | } 38 | } 39 | 40 | return $envelope; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/MessagePreparer/UserIdentifierMessagePreparer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\MessagePreparer; 15 | 16 | use Rekalogika\DomainEvent\Outbox\MessagePreparerInterface; 17 | use Rekalogika\DomainEvent\Outbox\Stamp\UserIdentifierStamp; 18 | use Symfony\Component\Messenger\Envelope; 19 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 20 | 21 | /** 22 | * Prepares the message before it is saved to the outbox table. 23 | */ 24 | class UserIdentifierMessagePreparer implements MessagePreparerInterface 25 | { 26 | public function __construct(private readonly TokenStorageInterface $tokenStorage) {} 27 | 28 | #[\Override] 29 | public function prepareMessage(Envelope $envelope): ?Envelope 30 | { 31 | $user = $this->tokenStorage->getToken()?->getUser(); 32 | 33 | if (null !== $user) { 34 | $envelope = $envelope->with(new UserIdentifierStamp($user->getUserIdentifier())); 35 | } 36 | 37 | return $envelope; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/MessagePreparerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox; 15 | 16 | use Symfony\Component\Messenger\Envelope; 17 | 18 | /** 19 | * Prepares the message before it is saved to the outbox table. Returns null if 20 | * the message should not be delivered to the outbox. 21 | */ 22 | interface MessagePreparerInterface 23 | { 24 | public function prepareMessage(Envelope $envelope): ?Envelope; 25 | } 26 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/MessageRelay/MessageRelayAll.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\MessageRelay; 15 | 16 | use Doctrine\Persistence\ManagerRegistry; 17 | use Rekalogika\DomainEvent\Outbox\MessageRelayInterface; 18 | 19 | /** 20 | * Relay messages from all managers 21 | */ 22 | final class MessageRelayAll 23 | { 24 | public function __construct( 25 | private readonly ManagerRegistry $managerRegistry, 26 | private readonly MessageRelayInterface $messageRelay, 27 | ) {} 28 | 29 | public function relayAll(): void 30 | { 31 | $managerNames = $this->managerRegistry->getManagerNames(); 32 | 33 | foreach ($managerNames as $managerName => $serviceId) { 34 | do { 35 | $messagesRelayed = $this->messageRelay->relayMessages($managerName); 36 | } while ($messagesRelayed > 0); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/MessageRelayInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox; 15 | 16 | /** 17 | * Get the messages from the outbox and sends them to the message bus. 18 | */ 19 | interface MessageRelayInterface 20 | { 21 | /** 22 | * Relays messages from the outbox to the message bus. 23 | * 24 | * @param string $managerName The name of the entity manager to relay 25 | * messages from. 26 | * @return int The amount of messages cleared from the outbox, not 27 | * necessarily sent to the event bus. 28 | */ 29 | public function relayMessages(string $managerName): int; 30 | } 31 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/OutboxReaderFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox; 15 | 16 | /** 17 | * Creates outbox readers. 18 | */ 19 | interface OutboxReaderFactoryInterface 20 | { 21 | public function createOutboxReader(string $managerName): OutboxReaderInterface; 22 | } 23 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/OutboxReaderInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox; 15 | 16 | use Symfony\Component\Messenger\Envelope; 17 | 18 | /** 19 | * Manages the messages in the outbox queue. 20 | */ 21 | interface OutboxReaderInterface 22 | { 23 | /** 24 | * Gets messages from the outbox queue. Starting from the earlier first. 25 | * Should implicitly start a transaction, that will be committed using 26 | * `flush()`. 27 | * 28 | * @return iterable 29 | */ 30 | public function getOutboxMessages(int $limit): iterable; 31 | 32 | /** 33 | * Removes a message from the outbox queue by its id. 34 | */ 35 | public function removeOutboxMessageById(int|string $id): void; 36 | 37 | /** 38 | * Flags a message as errored. 39 | */ 40 | public function flagError(int|string $id): void; 41 | 42 | /** 43 | * Commits the transaction started by getOutboxMessages. 44 | */ 45 | public function flush(): void; 46 | } 47 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/RekalogikaDomainEventOutboxBundle.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox; 15 | 16 | use Rekalogika\DomainEvent\Outbox\DependencyInjection\CompilerPass\OutboxEntityPass; 17 | use Rekalogika\DomainEvent\Outbox\DependencyInjection\CompilerPass\RemoveUnusedPass; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\HttpKernel\Bundle\Bundle; 20 | 21 | class RekalogikaDomainEventOutboxBundle extends Bundle 22 | { 23 | #[\Override] 24 | public function build(ContainerBuilder $container): void 25 | { 26 | parent::build($container); 27 | 28 | $container->addCompilerPass(new OutboxEntityPass()); 29 | $container->addCompilerPass(new RemoveUnusedPass()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Schedule/MessageRelayProvider.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Schedule; 15 | 16 | use Rekalogika\DomainEvent\Outbox\Message\MessageRelayStartMessage; 17 | use Symfony\Component\Scheduler\RecurringMessage; 18 | use Symfony\Component\Scheduler\Schedule; 19 | use Symfony\Component\Scheduler\ScheduleProviderInterface; 20 | 21 | final class MessageRelayProvider implements ScheduleProviderInterface 22 | { 23 | /** 24 | * @param array $entityManagers 25 | */ 26 | public function __construct( 27 | private readonly array $entityManagers, 28 | ) {} 29 | 30 | #[\Override] 31 | public function getSchedule(): Schedule 32 | { 33 | $schedule = new Schedule(); 34 | 35 | foreach (array_keys($this->entityManagers) as $name) { 36 | $schedule->add(RecurringMessage::every('1 hour', new MessageRelayStartMessage($name))); 37 | } 38 | 39 | return $schedule; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Stamp/ObjectManagerNameStamp.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Stamp; 15 | 16 | use Symfony\Component\Messenger\Stamp\StampInterface; 17 | 18 | final class ObjectManagerNameStamp implements StampInterface 19 | { 20 | public function __construct(private readonly string $objectManagerName) {} 21 | 22 | public function getObjectManagerName(): string 23 | { 24 | return $this->objectManagerName; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/domain-event-outbox/src/Stamp/UserIdentifierStamp.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Outbox\Stamp; 15 | 16 | use Symfony\Component\Messenger\Stamp\StampInterface; 17 | 18 | final class UserIdentifierStamp implements StampInterface 19 | { 20 | public function __construct(private readonly string $userIdentifier) {} 21 | 22 | public function getUserIdentifier(): string 23 | { 24 | return $this->userIdentifier; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/domain-event/.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | var -------------------------------------------------------------------------------- /packages/domain-event/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023-present Priyadi Iman Nurcahyo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/domain-event/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rekalogika/domain-event", 3 | "description": "Domain Event Implementation for Symfony and Doctrine", 4 | "homepage": "https://rekalogika.dev/domain-event", 5 | "keywords": [ 6 | "domain-event", 7 | "domain", 8 | "event", 9 | "symfony", 10 | "doctrine", 11 | "event-dispatcher", 12 | "event-listener" 13 | ], 14 | "type": "symfony-bundle", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Priyadi Iman Nurcahyo", 19 | "email": "priyadi@rekalogika.com" 20 | } 21 | ], 22 | "autoload": { 23 | "psr-4": { 24 | "Rekalogika\\DomainEvent\\": "src/" 25 | } 26 | }, 27 | "require": { 28 | "doctrine/doctrine-bundle": "^2.11.3 || ^2.12", 29 | "doctrine/orm": "^2.16 || ^3.0", 30 | "doctrine/persistence": "^3.2 || ^4.0", 31 | "psr/event-dispatcher": "^1.0", 32 | "rekalogika/domain-event-contracts": "^2.5.2", 33 | "symfony/config": "^6.4 || ^7.0", 34 | "symfony/dependency-injection": "^6.4 || ^7.0", 35 | "symfony/event-dispatcher": "^6.4 || ^7.0", 36 | "symfony/http-kernel": "^6.4 || ^7.0", 37 | "symfony/service-contracts": "^3.0", 38 | "symfony/var-exporter": "^6.4 || ^7.0" 39 | }, 40 | "extra": { 41 | "branch-alias": { 42 | "dev-main": "2.6-dev" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/domain-event/config/debug.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | use Rekalogika\DomainEvent\DependencyInjection\Constants; 15 | use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; 16 | use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; 17 | 18 | use function Symfony\Component\DependencyInjection\Loader\Configurator\service; 19 | 20 | return static function (ContainerConfigurator $containerConfigurator): void { 21 | $services = $containerConfigurator->services(); 22 | 23 | $services 24 | ->set( 25 | 'debug.' . Constants::EVENT_DISPATCHER_IMMEDIATE, 26 | TraceableEventDispatcher::class, 27 | ) 28 | ->decorate(Constants::EVENT_DISPATCHER_IMMEDIATE) 29 | ->args([ 30 | service('.inner'), 31 | service('debug.stopwatch'), 32 | null, 33 | service('.virtual_request_stack')->nullOnInvalid(), 34 | ]) 35 | ->tag('monolog.logger', ['channel' => 'event']) 36 | ->tag('kernel.reset', ['method' => 'reset']); 37 | 38 | $services 39 | ->set( 40 | 'debug.' . Constants::EVENT_DISPATCHER_PRE_FLUSH, 41 | TraceableEventDispatcher::class, 42 | ) 43 | ->decorate(Constants::EVENT_DISPATCHER_PRE_FLUSH) 44 | ->args([ 45 | service('.inner'), 46 | service('debug.stopwatch'), 47 | null, 48 | service('.virtual_request_stack')->nullOnInvalid(), 49 | ]) 50 | ->tag('monolog.logger', ['channel' => 'event']) 51 | ->tag('kernel.reset', ['method' => 'reset']); 52 | 53 | $services 54 | ->set( 55 | 'debug.' . Constants::EVENT_DISPATCHER_POST_FLUSH, 56 | TraceableEventDispatcher::class, 57 | ) 58 | ->decorate(Constants::EVENT_DISPATCHER_POST_FLUSH) 59 | ->args([ 60 | service('.inner'), 61 | service('debug.stopwatch'), 62 | null, 63 | service('.virtual_request_stack')->nullOnInvalid(), 64 | ]) 65 | ->tag('monolog.logger', ['channel' => 'event']) 66 | ->tag('kernel.reset', ['method' => 'reset']); 67 | }; 68 | -------------------------------------------------------------------------------- /packages/domain-event/src/Contracts/DomainEventAwareEntityManagerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Contracts; 15 | 16 | use Rekalogika\DomainEvent\DomainEventAwareEntityManagerInterface as DomainEventDomainEventAwareEntityManagerInterface; 17 | 18 | /** 19 | * @deprecated Please use Rekalogika\DomainEvent\DomainEventAwareEntityManagerInterface instead 20 | */ 21 | interface DomainEventAwareEntityManagerInterface extends DomainEventDomainEventAwareEntityManagerInterface {} 22 | -------------------------------------------------------------------------------- /packages/domain-event/src/DependencyInjection/CompilerPass/EntityManagerDecoratorPass.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\DependencyInjection\CompilerPass; 15 | 16 | use Rekalogika\DomainEvent\DependencyInjection\Constants; 17 | use Rekalogika\DomainEvent\Doctrine\DomainEventAwareEntityManager; 18 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 19 | use Symfony\Component\DependencyInjection\ContainerBuilder; 20 | use Symfony\Component\DependencyInjection\Reference; 21 | 22 | /** 23 | * Decorates entity managers manually instead of relying on DI's decoration. 24 | * 25 | * `ManagerRegistry::resetManager()` relies on the fact that entity managers are 26 | * lazy. However, when we decorate entity managers, the original entity manager 27 | * won't lazy anymore. This causes `resetManager()` to fail. the workaround is to 28 | * manually decorate entity managers. 29 | * 30 | * @internal 31 | */ 32 | final class EntityManagerDecoratorPass implements CompilerPassInterface 33 | { 34 | #[\Override] 35 | public function process(ContainerBuilder $container): void 36 | { 37 | $entityManagers = $container->getParameter('doctrine.entity_managers'); 38 | \assert(\is_array($entityManagers)); 39 | 40 | $eventDispatchers = $container->getDefinition(Constants::EVENT_DISPATCHERS); 41 | 42 | /** 43 | * @var string $name 44 | * @var string $serviceId 45 | */ 46 | foreach ($entityManagers as $name => $serviceId) { 47 | $service = $container->getDefinition($serviceId); 48 | $realServiceId = $serviceId . '.real'; 49 | 50 | $container 51 | ->setDefinition($realServiceId, $service); 52 | 53 | $container 54 | ->register($serviceId, DomainEventAwareEntityManager::class) 55 | ->setArguments([ 56 | '$wrapped' => new Reference($realServiceId), 57 | '$eventDispatchers' => $eventDispatchers, 58 | ]) 59 | ->setPublic(true) 60 | ->addTag('kernel.reset', ['method' => 'reset']) 61 | ->addTag('rekalogika.domain_event.entity_manager', ['name' => $name]); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/domain-event/src/DependencyInjection/CompilerPass/ProfilerWorkaroundPass.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\DependencyInjection\CompilerPass; 15 | 16 | use Doctrine\Bundle\DoctrineBundle\Controller\ProfilerController; 17 | use Rekalogika\DomainEvent\DependencyInjection\Constants; 18 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 19 | use Symfony\Component\DependencyInjection\ContainerBuilder; 20 | use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; 21 | 22 | /** 23 | * Workaround for this error: 24 | * 25 | * [ERROR] Invalid definition for service 26 | * "Doctrine\Bundle\DoctrineBundle\Controller\ProfilerController": argument 2 of 27 | * "Doctrine\Bundle\DoctrineBundle\Controller\ProfilerController::__construct()" 28 | * accepts "Doctrine\Bundle\DoctrineBundle\Registry", 29 | * "Rekalogika\DomainEvent\Doctrine\DomainEventAwareManagerRegistryImplementation" 30 | * passed. 31 | * 32 | * fix in upstream: https://github.com/doctrine/DoctrineBundle/pull/1764 33 | * 34 | * @todo remove after we bump to doctrine/doctrine-bundle 2.12 35 | * 36 | * @internal 37 | */ 38 | final class ProfilerWorkaroundPass implements CompilerPassInterface 39 | { 40 | #[\Override] 41 | public function process(ContainerBuilder $container): void 42 | { 43 | try { 44 | $doctrine = $container->getDefinition(Constants::REAL_MANAGER_REGISTRY); 45 | 46 | $profilerController = $container->getDefinition(ProfilerController::class); 47 | $profilerController->setArgument(1, $doctrine); 48 | } catch (ServiceNotFoundException) { 49 | // ignore 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/domain-event/src/DependencyInjection/Constants.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\DependencyInjection; 15 | 16 | final class Constants 17 | { 18 | public const EVENT_DISPATCHERS = 'rekalogika.domain_event.dispatchers'; 19 | 20 | public const EVENT_DISPATCHER_IMMEDIATE = 'rekalogika.domain_event.dispatcher.immediate'; 21 | 22 | public const EVENT_DISPATCHER_PRE_FLUSH = 'rekalogika.domain_event.dispatcher.pre_flush'; 23 | 24 | public const EVENT_DISPATCHER_POST_FLUSH = 'rekalogika.domain_event.dispatcher.post_flush'; 25 | 26 | public const IMMEDIATE_EVENT_DISPATCHING_DISPATCHER = 'rekalogika.domain_event.immediate_event_dispatching_dispatcher'; 27 | 28 | public const DOCTRINE_EVENT_LISTENER = 'rekalogika.domain_event.doctrine.event_listener'; 29 | 30 | public const REAPER = 'rekalogika.domain_event.reaper'; 31 | 32 | public const IMMEDIATE_DISPATCHER_INSTALLER = 'rekalogika.domain_event.immediate_dispatcher_installer'; 33 | 34 | public const MANAGER_REGISTRY = 'rekalogika.domain_event.doctrine'; 35 | 36 | public const REAL_MANAGER_REGISTRY = 'rekalogika.domain_event.doctrine.real'; 37 | 38 | private function __construct() {} 39 | } 40 | -------------------------------------------------------------------------------- /packages/domain-event/src/Doctrine/AbstractManagerRegistryDecorator.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Doctrine; 15 | 16 | use Doctrine\Persistence\ManagerRegistry; 17 | use Doctrine\Persistence\ObjectManager; 18 | use Doctrine\Persistence\ObjectRepository; 19 | 20 | abstract class AbstractManagerRegistryDecorator implements ManagerRegistry 21 | { 22 | public function __construct(private readonly ManagerRegistry $wrapped) {} 23 | 24 | #[\Override] 25 | public function getDefaultManagerName(): string 26 | { 27 | return $this->wrapped->getDefaultManagerName(); 28 | } 29 | 30 | #[\Override] 31 | public function getManager(?string $name = null): ObjectManager 32 | { 33 | return $this->wrapped->getManager($name); 34 | } 35 | 36 | #[\Override] 37 | public function getManagers(): array 38 | { 39 | return $this->wrapped->getManagers(); 40 | } 41 | 42 | #[\Override] 43 | public function resetManager(?string $name = null): ObjectManager 44 | { 45 | return $this->wrapped->resetManager($name); 46 | } 47 | 48 | #[\Override] 49 | public function getManagerNames(): array 50 | { 51 | return $this->wrapped->getManagerNames(); 52 | } 53 | 54 | #[\Override] 55 | public function getRepository(string $persistentObject, ?string $persistentManagerName = null): ObjectRepository 56 | { 57 | return $this->wrapped->getRepository($persistentObject, $persistentManagerName); 58 | } 59 | 60 | #[\Override] 61 | public function getManagerForClass(string $class): ?ObjectManager 62 | { 63 | return $this->wrapped->getManagerForClass($class); 64 | } 65 | 66 | #[\Override] 67 | public function getDefaultConnectionName(): string 68 | { 69 | return $this->wrapped->getDefaultConnectionName(); 70 | } 71 | 72 | #[\Override] 73 | public function getConnection(?string $name = null): object 74 | { 75 | return $this->wrapped->getConnection($name); 76 | } 77 | 78 | #[\Override] 79 | public function getConnections(): array 80 | { 81 | return $this->wrapped->getConnections(); 82 | } 83 | 84 | #[\Override] 85 | public function getConnectionNames(): array 86 | { 87 | return $this->wrapped->getConnectionNames(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/domain-event/src/Doctrine/DoctrineEventListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Doctrine; 15 | 16 | use Doctrine\ORM\Event\PostPersistEventArgs; 17 | use Doctrine\ORM\Event\PostRemoveEventArgs; 18 | use Doctrine\ORM\Event\PostUpdateEventArgs; 19 | use Doctrine\ORM\Event\PreRemoveEventArgs; 20 | use Doctrine\Persistence\ObjectManager; 21 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterInterface; 22 | use Rekalogika\DomainEvent\DomainEventAwareManagerRegistry; 23 | 24 | /** 25 | * Listen to Doctrine events to collect domain events 26 | */ 27 | final class DoctrineEventListener 28 | { 29 | public function __construct( 30 | private readonly DomainEventAwareManagerRegistry $managerRegistry, 31 | ) {} 32 | 33 | public function postPersist(PostPersistEventArgs $args): void 34 | { 35 | $this->collectEvents($args->getObject(), $args->getObjectManager()); 36 | } 37 | 38 | public function preRemove(PreRemoveEventArgs $args): void 39 | { 40 | $this->processRemove($args->getObject()); 41 | $this->collectEvents($args->getObject(), $args->getObjectManager()); 42 | } 43 | 44 | public function postRemove(PostRemoveEventArgs $args): void 45 | { 46 | $this->collectEvents($args->getObject(), $args->getObjectManager()); 47 | } 48 | 49 | public function postUpdate(PostUpdateEventArgs $args): void 50 | { 51 | $this->collectEvents($args->getObject(), $args->getObjectManager()); 52 | } 53 | 54 | private function collectEvents(object $entity, ObjectManager $objectManager): void 55 | { 56 | if (!$entity instanceof DomainEventEmitterInterface) { 57 | return; 58 | } 59 | 60 | $decoratedObjectManager = $this->managerRegistry 61 | ->getDomainEventAwareManager($objectManager); 62 | 63 | $events = $entity->popRecordedEvents(); 64 | $decoratedObjectManager->recordDomainEvent($events); 65 | } 66 | 67 | private function processRemove(object $entity): void 68 | { 69 | if ($entity instanceof DomainEventEmitterInterface) { 70 | $entity->__remove(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/domain-event/src/Doctrine/DomainEventReaper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Doctrine; 15 | 16 | use Rekalogika\DomainEvent\DomainEventAwareEntityManagerInterface; 17 | 18 | /** 19 | * Clears domain events from DomainEventAwareEntityManager if an exception 20 | * bubbles up to the kernel. This will prevent DomainEventAwareEntityManager 21 | * from adding another, possibly confusing error due to the fact there are 22 | * undispatched events in its queue. 23 | */ 24 | final class DomainEventReaper 25 | { 26 | /** 27 | * @param iterable $entityManagers 28 | */ 29 | public function __construct( 30 | private readonly iterable $entityManagers, 31 | ) {} 32 | 33 | public function onKernelException(): void 34 | { 35 | foreach ($this->entityManagers as $entityManager) { 36 | $entityManager->clearDomainEvents(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/domain-event/src/DomainEventAwareEntityManagerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | 18 | interface DomainEventAwareEntityManagerInterface extends 19 | EntityManagerInterface, 20 | DomainEventAwareObjectManager {} 21 | -------------------------------------------------------------------------------- /packages/domain-event/src/DomainEventAwareManagerRegistry.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent; 15 | 16 | use Doctrine\Persistence\ManagerRegistry; 17 | use Doctrine\Persistence\ObjectManager; 18 | 19 | interface DomainEventAwareManagerRegistry extends ManagerRegistry 20 | { 21 | /** 22 | * Gets the real registry that is being decorated by this instance. 23 | */ 24 | public function getRealRegistry(): ManagerRegistry; 25 | 26 | /** 27 | * Gets the manager name of the given object manager. 28 | */ 29 | public function getManagerName(ObjectManager $manager): string; 30 | 31 | /** 32 | * The domain-event-aware version of `getManager()` 33 | */ 34 | public function getDomainEventAwareManager( 35 | ObjectManager $objectManager, 36 | ): DomainEventAwareObjectManager; 37 | 38 | /** 39 | * The domain-event-aware version of `getManagers()` 40 | * 41 | * @return array 42 | */ 43 | public function getDomainEventAwareManagers(): array; 44 | 45 | /** 46 | * The domain-event-aware version of `getManagerForClass()` 47 | * 48 | * @param class-string $class 49 | */ 50 | public function getDomainEventAwareManagerForClass( 51 | string $class, 52 | ): ?DomainEventAwareObjectManager; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /packages/domain-event/src/DomainEventAwareObjectManager.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent; 15 | 16 | use Doctrine\Persistence\ObjectManager; 17 | 18 | interface DomainEventAwareObjectManager extends 19 | ObjectManager, 20 | DomainEventManagerInterface {} 21 | -------------------------------------------------------------------------------- /packages/domain-event/src/DomainEventManagerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent; 15 | 16 | use Doctrine\Persistence\ObjectManager; 17 | 18 | interface DomainEventManagerInterface 19 | { 20 | /** 21 | * Gets the original object manager associated with this domain event 22 | * manager 23 | */ 24 | public function getObjectManager(): ObjectManager; 25 | 26 | /** 27 | * Enables or disables auto dispatch 28 | */ 29 | public function setAutoDispatchDomainEvents(bool $autoDispatch): void; 30 | 31 | /** 32 | * Returns true if auto dispatch is enabled 33 | */ 34 | public function isAutoDispatchDomainEvents(): bool; 35 | 36 | /** 37 | * Manually dispatch all pending events before flushing 38 | */ 39 | public function dispatchPreFlushDomainEvents(): int; 40 | 41 | /** 42 | * Manually dispatch all pending events after flushing 43 | */ 44 | public function dispatchPostFlushDomainEvents(): int; 45 | 46 | /** 47 | * Clears all pending events without dispatching 48 | */ 49 | public function clearDomainEvents(): void; 50 | 51 | /** 52 | * Returns and clears all pending domain events. This is taken from the 53 | * post-flush queue. 54 | * 55 | * @return iterable 56 | */ 57 | public function popDomainEvents(): iterable; 58 | 59 | /** 60 | * Manually adds a domain event to the queue, to be dispatched later. This 61 | * is added to both the pre-flush and post-flush queues. 62 | * 63 | * @param object|iterable $event 64 | */ 65 | public function recordDomainEvent(object|iterable $event): void; 66 | } 67 | -------------------------------------------------------------------------------- /packages/domain-event/src/Event/DomainEventImmediateDispatchEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Event; 15 | 16 | class DomainEventImmediateDispatchEvent 17 | { 18 | final public function __construct(private readonly object $domainEvent) {} 19 | 20 | public function getDomainEvent(): object 21 | { 22 | return $this->domainEvent; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/domain-event/src/Event/DomainEventPostFlushDispatchEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Event; 15 | 16 | use Rekalogika\DomainEvent\DomainEventAwareObjectManager; 17 | 18 | class DomainEventPostFlushDispatchEvent 19 | { 20 | final public function __construct( 21 | private readonly DomainEventAwareObjectManager $objectManager, 22 | private readonly object $domainEvent, 23 | ) {} 24 | 25 | public function getDomainEvent(): object 26 | { 27 | return $this->domainEvent; 28 | } 29 | 30 | public function getObjectManager(): DomainEventAwareObjectManager 31 | { 32 | return $this->objectManager; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/domain-event/src/Event/DomainEventPreFlushDispatchEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Event; 15 | 16 | use Rekalogika\DomainEvent\DomainEventAwareObjectManager; 17 | 18 | class DomainEventPreFlushDispatchEvent 19 | { 20 | final public function __construct( 21 | private readonly DomainEventAwareObjectManager $objectManager, 22 | private readonly object $domainEvent, 23 | ) {} 24 | 25 | public function getDomainEvent(): object 26 | { 27 | return $this->domainEvent; 28 | } 29 | 30 | public function getObjectManager(): DomainEventAwareObjectManager 31 | { 32 | return $this->objectManager; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/domain-event/src/EventDispatcher/EventDispatchers.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\EventDispatcher; 15 | 16 | use Psr\EventDispatcher\EventDispatcherInterface; 17 | 18 | /** 19 | * Contains all the different event dispatchers used in the domain event system. 20 | */ 21 | final class EventDispatchers 22 | { 23 | public function __construct( 24 | private readonly EventDispatcherInterface $defaultEventDispatcher, 25 | private readonly EventDispatcherInterface $immediateEventDispatcher, 26 | private readonly EventDispatcherInterface $preFlushEventDispatcher, 27 | private readonly EventDispatcherInterface $postFlushEventDispatcher, 28 | ) {} 29 | 30 | public function getDefaultEventDispatcher(): EventDispatcherInterface 31 | { 32 | return $this->defaultEventDispatcher; 33 | } 34 | 35 | public function getImmediateEventDispatcher(): EventDispatcherInterface 36 | { 37 | return $this->immediateEventDispatcher; 38 | } 39 | 40 | public function getPreFlushEventDispatcher(): EventDispatcherInterface 41 | { 42 | return $this->preFlushEventDispatcher; 43 | } 44 | 45 | public function getPostFlushEventDispatcher(): EventDispatcherInterface 46 | { 47 | return $this->postFlushEventDispatcher; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/domain-event/src/EventDispatcher/ImmediateEventDispatchingDomainEventDispatcher.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\EventDispatcher; 15 | 16 | use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface; 17 | use Rekalogika\DomainEvent\Event\DomainEventImmediateDispatchEvent; 18 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 19 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 20 | 21 | /** 22 | * Decorates the immediate event dispatcher, so that the event to be dispatched 23 | * is also dispatched to the default event dispatcher, wrapped by the 24 | * `DomainEventImmediateDispatchEvent` 25 | */ 26 | final class ImmediateEventDispatchingDomainEventDispatcher implements 27 | EventDispatcherInterface 28 | { 29 | public function __construct( 30 | private readonly EventDispatcherInterface $decorated, 31 | private readonly PsrEventDispatcherInterface $defaultEventDispatcher, 32 | ) {} 33 | 34 | // @phpstan-ignore-next-line 35 | #[\Override] 36 | public function addListener(string $eventName, callable|array $listener, int $priority = 0): void 37 | { 38 | /** @psalm-suppress MixedArgumentTypeCoercion @phpstan-ignore-next-line */ 39 | $this->decorated->addListener($eventName, $listener, $priority); 40 | } 41 | 42 | #[\Override] 43 | public function addSubscriber(EventSubscriberInterface $subscriber): void 44 | { 45 | $this->decorated->addSubscriber($subscriber); 46 | } 47 | 48 | // @phpstan-ignore-next-line 49 | #[\Override] 50 | public function removeListener(string $eventName, callable|array $listener): void 51 | { 52 | /** @psalm-suppress MixedArgumentTypeCoercion @phpstan-ignore-next-line */ 53 | $this->decorated->removeListener($eventName, $listener); 54 | } 55 | 56 | #[\Override] 57 | public function removeSubscriber(EventSubscriberInterface $subscriber): void 58 | { 59 | $this->decorated->removeSubscriber($subscriber); 60 | } 61 | 62 | #[\Override] 63 | public function getListeners(?string $eventName = null): array 64 | { 65 | return $this->decorated->getListeners($eventName); 66 | } 67 | 68 | // @phpstan-ignore-next-line 69 | #[\Override] 70 | public function getListenerPriority(string $eventName, callable|array $listener): ?int 71 | { 72 | /** @psalm-suppress MixedArgumentTypeCoercion @phpstan-ignore-next-line */ 73 | return $this->decorated->getListenerPriority($eventName, $listener); 74 | } 75 | 76 | #[\Override] 77 | public function hasListeners(?string $eventName = null): bool 78 | { 79 | return $this->decorated->hasListeners($eventName); 80 | } 81 | 82 | #[\Override] 83 | public function dispatch(object $event, ?string $eventName = null): object 84 | { 85 | $this->defaultEventDispatcher->dispatch(new DomainEventImmediateDispatchEvent($event)); 86 | 87 | return $this->decorated->dispatch($event, $eventName); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/domain-event/src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Exception; 15 | 16 | interface ExceptionInterface extends \Throwable {} 17 | -------------------------------------------------------------------------------- /packages/domain-event/src/Exception/FlushNotAllowedException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Exception; 15 | 16 | class FlushNotAllowedException extends LogicException 17 | { 18 | public function __construct() 19 | { 20 | parent::__construct('"flush()" is not allowed inside a pre-flush domain event listener.'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/domain-event/src/Exception/InvalidOperationException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Exception; 15 | 16 | class InvalidOperationException extends LogicException {} 17 | -------------------------------------------------------------------------------- /packages/domain-event/src/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Exception; 15 | 16 | class LogicException extends \LogicException implements ExceptionInterface {} 17 | -------------------------------------------------------------------------------- /packages/domain-event/src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Exception; 15 | 16 | class RuntimeException extends \RuntimeException implements ExceptionInterface {} 17 | -------------------------------------------------------------------------------- /packages/domain-event/src/Exception/SafeguardTriggeredException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Exception; 15 | 16 | class SafeguardTriggeredException extends RuntimeException {} 17 | -------------------------------------------------------------------------------- /packages/domain-event/src/Exception/UndispatchedEventsException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Exception; 15 | 16 | use Rekalogika\DomainEvent\Model\DomainEventStore; 17 | 18 | class UndispatchedEventsException extends LogicException 19 | { 20 | public function __construct(DomainEventStore $preFlushEvents, DomainEventStore $postFlushEvents) 21 | { 22 | $num = \count($preFlushEvents) + \count($postFlushEvents); 23 | 24 | parent::__construct(\sprintf('There are still %d undispatched domain events. If you disable autodispatch, you have to dispatch them manually or clear them.', $num)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/domain-event/src/ImmediateDomainEventDispatcherInstaller.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent; 15 | 16 | use Psr\EventDispatcher\EventDispatcherInterface; 17 | use Rekalogika\Contracts\DomainEvent\DomainEventImmediateDispatcher; 18 | 19 | /** 20 | * Installs and uninstalls the immediate domain event dispatcher. 21 | */ 22 | final class ImmediateDomainEventDispatcherInstaller 23 | { 24 | public function __construct( 25 | private readonly EventDispatcherInterface $eventDispatcher, 26 | ) {} 27 | 28 | public function install(): void 29 | { 30 | DomainEventImmediateDispatcher::install($this->eventDispatcher); 31 | } 32 | 33 | public function uninstall(): void 34 | { 35 | DomainEventImmediateDispatcher::uninstall(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/domain-event/src/Model/DomainEventStore.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Model; 15 | 16 | use Rekalogika\Contracts\DomainEvent\EquatableDomainEventInterface; 17 | 18 | /** 19 | * @implements \IteratorAggregate 20 | */ 21 | class DomainEventStore implements \IteratorAggregate, \Countable 22 | { 23 | /** 24 | * @var array 25 | */ 26 | private array $events = []; 27 | 28 | /** 29 | * Adds an event to the store. 30 | * 31 | * @param object|iterable $event 32 | */ 33 | public function add(object|iterable $event): void 34 | { 35 | if (is_iterable($event)) { 36 | foreach ($event as $anEvent) { 37 | $this->add($anEvent); 38 | } 39 | 40 | return; 41 | } 42 | 43 | if ($event instanceof EquatableDomainEventInterface) { 44 | $signature = $event->getSignature(); 45 | 46 | $this->events[$signature] = $event; 47 | } else { 48 | $this->events[] = $event; 49 | } 50 | } 51 | 52 | public function clear(): void 53 | { 54 | $this->events = []; 55 | } 56 | 57 | /** 58 | * Return the events and clear the store. 59 | * 60 | * @return iterable 61 | */ 62 | public function pop(): iterable 63 | { 64 | $events = $this->events; 65 | $this->clear(); 66 | 67 | yield from $events; 68 | } 69 | 70 | /** 71 | * @return \Traversable 72 | */ 73 | #[\Override] 74 | public function getIterator(): \Traversable 75 | { 76 | yield from $this->events; 77 | } 78 | 79 | #[\Override] 80 | public function count(): int 81 | { 82 | return \count($this->events); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/domain-event/src/Model/TransactionAwareDomainEventStore.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Model; 15 | 16 | class TransactionAwareDomainEventStore extends DomainEventStore 17 | { 18 | private ?self $transactionStore = null; 19 | 20 | #[\Override] 21 | public function add(object|iterable $event): void 22 | { 23 | if ($this->transactionStore !== null) { 24 | $this->transactionStore->add($event); 25 | } else { 26 | parent::add($event); 27 | } 28 | } 29 | 30 | #[\Override] 31 | public function clear(): void 32 | { 33 | $this->transactionStore = null; 34 | parent::clear(); 35 | } 36 | 37 | #[\Override] 38 | public function count(): int 39 | { 40 | $countFromTransaction = $this->transactionStore !== null ? parent::count() : 0; 41 | 42 | return parent::count() + $countFromTransaction; 43 | } 44 | 45 | public function beginTransaction(): void 46 | { 47 | if ($this->transactionStore !== null) { 48 | $this->transactionStore->beginTransaction(); 49 | } else { 50 | $this->transactionStore = new self(); 51 | } 52 | } 53 | 54 | /** 55 | * @return bool false if there is no transaction in progress 56 | */ 57 | public function commit(): bool 58 | { 59 | if ($this->transactionStore === null) { 60 | return false; 61 | } 62 | 63 | $result = $this->transactionStore->commit(); 64 | 65 | if ($result === false) { 66 | $transactionStore = $this->transactionStore; 67 | $this->transactionStore = null; 68 | $this->add($transactionStore->pop()); 69 | } 70 | 71 | return true; 72 | } 73 | 74 | /** 75 | * @return bool false if there is no transaction in progress 76 | */ 77 | public function rollback(): bool 78 | { 79 | if ($this->transactionStore === null) { 80 | return false; 81 | } 82 | 83 | $result = $this->transactionStore->rollback(); 84 | 85 | if ($result === false) { 86 | $this->transactionStore = null; 87 | } 88 | 89 | return true; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/domain-event/src/RekalogikaDomainEventBundle.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent; 15 | 16 | use Rekalogika\DomainEvent\DependencyInjection\CompilerPass\EntityManagerDecoratorPass; 17 | use Rekalogika\DomainEvent\DependencyInjection\CompilerPass\ProfilerWorkaroundPass; 18 | use Rekalogika\DomainEvent\DependencyInjection\Constants; 19 | use Symfony\Component\DependencyInjection\ContainerBuilder; 20 | use Symfony\Component\HttpKernel\Bundle\Bundle; 21 | 22 | class RekalogikaDomainEventBundle extends Bundle 23 | { 24 | #[\Override] 25 | public function build(ContainerBuilder $container): void 26 | { 27 | parent::build($container); 28 | 29 | $container->addCompilerPass(new EntityManagerDecoratorPass()); 30 | $container->addCompilerPass(new ProfilerWorkaroundPass()); 31 | } 32 | 33 | #[\Override] 34 | public function boot(): void 35 | { 36 | $installer = $this->container?->get(Constants::IMMEDIATE_DISPATCHER_INSTALLER); 37 | 38 | if ($installer instanceof ImmediateDomainEventDispatcherInstaller) { 39 | /** @var ImmediateDomainEventDispatcherInstaller $installer */ 40 | $installer->install(); 41 | } 42 | } 43 | 44 | #[\Override] 45 | public function shutdown(): void 46 | { 47 | $installer = $this->container?->get(Constants::IMMEDIATE_DISPATCHER_INSTALLER); 48 | 49 | if ($installer instanceof ImmediateDomainEventDispatcherInstaller) { 50 | /** @var ImmediateDomainEventDispatcherInstaller $installer */ 51 | $installer->uninstall(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - packages 5 | - tests 6 | checkBenevolentUnionTypes: true 7 | checkExplicitMixedMissingReturn: true 8 | checkFunctionNameCase: true 9 | checkInternalClassCaseSensitivity: true 10 | reportMaybesInPropertyPhpDocTypes: true 11 | treatPhpDocTypesAsCertain: false 12 | ignoreErrors: 13 | - 14 | message: '#Attribute class Override does not exist#' 15 | reportUnmatched: false 16 | - '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::children#' 17 | - 18 | message: '#Property .* is never assigned .+ so it can be removed from the property type.#' 19 | reportUnmatched: false 20 | - 21 | message: '#has PHPDoc tag @method for method find(One)?By\(\) parameter#' 22 | reportUnmatched: false 23 | includes: 24 | - vendor/phpstan/phpstan-phpunit/extension.neon 25 | - vendor/phpstan/phpstan-phpunit/rules.neon 26 | - vendor/phpstan/phpstan-deprecation-rules/rules.neon 27 | - vendor/bnf/phpstan-psr-container/extension.neon 28 | - vendor/ekino/phpstan-banned-code/extension.neon 29 | - phar://phpstan.phar/conf/bleedingEdge.neon 30 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | packages/domain-event/src 20 | packages/domain-event-contracts/src 21 | 22 | 23 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPhpVersion(PhpVersion::PHP_83) 17 | ->withPaths([ 18 | __DIR__ . '/packages', 19 | __DIR__ . '/tests', 20 | ]) 21 | ->withImportNames(importShortClasses: false) 22 | ->withPreparedSets( 23 | deadCode: true, 24 | codeQuality: true, 25 | codingStyle: true, 26 | typeDeclarations: true, 27 | privatization: true, 28 | instanceOf: true, 29 | strictBooleans: true, 30 | symfonyCodeQuality: true, 31 | doctrineCodeQuality: true, 32 | ) 33 | ->withPhpSets(php81: true) 34 | ->withRules([ 35 | AddOverrideAttributeToOverriddenMethodsRector::class, 36 | ]) 37 | ->withSkip([ 38 | // potential cognitive burden 39 | FlipTypeControlToUseExclusiveTypeRector::class, 40 | 41 | // results in too long variables 42 | CatchExceptionNameMatchingTypeRector::class, 43 | 44 | // makes code unreadable 45 | DisallowedShortTernaryRuleFixerRector::class, 46 | 47 | ImproveDoctrineCollectionDocTypeInEntityRector::class, 48 | 49 | MakeInheritedMethodVisibilitySameAsParentRector::class => [ 50 | __DIR__ . '/tests/*', 51 | ], 52 | 53 | RemoveUnusedPublicMethodParameterRector::class => [ 54 | __DIR__ . '/tests/Integration/EventListener/*', 55 | __DIR__ . '/tests/Framework/EventListener/*', 56 | ], 57 | ]); 58 | -------------------------------------------------------------------------------- /tests/Framework/Entity/Review.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Entity; 15 | 16 | use Doctrine\ORM\Mapping as ORM; 17 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterInterface; 18 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterTrait; 19 | use Rekalogika\DomainEvent\Tests\Framework\Event\ReviewChanged; 20 | use Rekalogika\DomainEvent\Tests\Framework\Event\ReviewCreated; 21 | use Rekalogika\DomainEvent\Tests\Framework\Event\ReviewRemoved; 22 | use Rekalogika\DomainEvent\Tests\Framework\Repository\ReviewRepository; 23 | use Symfony\Bridge\Doctrine\Types\UuidType; 24 | use Symfony\Component\Uid\Uuid; 25 | 26 | #[ORM\Entity(repositoryClass: ReviewRepository::class)] 27 | class Review implements DomainEventEmitterInterface 28 | { 29 | use DomainEventEmitterTrait; 30 | 31 | #[ORM\Id] 32 | #[ORM\Column(type: UuidType::NAME, unique: true, nullable: false)] 33 | private Uuid $id; 34 | 35 | /** 36 | * @var int<1,5> 37 | */ 38 | #[ORM\Column] 39 | private int $rating = 3; 40 | 41 | #[ORM\Column] 42 | private ?string $body = null; 43 | 44 | #[ORM\ManyToOne( 45 | targetEntity: Book::class, 46 | inversedBy: 'reviews', 47 | )] 48 | private ?Book $book = null; 49 | 50 | public function __construct() 51 | { 52 | $this->id = Uuid::v7(); 53 | $this->recordEvent(new ReviewCreated($this)); 54 | } 55 | 56 | #[\Override] 57 | public function __remove(): void 58 | { 59 | $this->recordEvent(new ReviewRemoved($this)); 60 | } 61 | 62 | public function getId(): Uuid 63 | { 64 | return $this->id; 65 | } 66 | 67 | public function getBook(): ?Book 68 | { 69 | return $this->book; 70 | } 71 | 72 | public function setBook(?Book $book): self 73 | { 74 | $originalBook = $this->book; 75 | $this->book = $book; 76 | 77 | if ($originalBook !== $book) { 78 | $this->recordEvent(new ReviewChanged($this)); 79 | } 80 | 81 | return $this; 82 | } 83 | 84 | public function getBody(): ?string 85 | { 86 | return $this->body; 87 | } 88 | 89 | public function setBody(?string $body): self 90 | { 91 | $originalBody = $this->body; 92 | $this->body = $body; 93 | 94 | if ($originalBody !== $body) { 95 | $this->recordEvent(new ReviewChanged($this)); 96 | } 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * @return int<1,5> 103 | */ 104 | public function getRating(): int 105 | { 106 | return $this->rating; 107 | } 108 | 109 | /** 110 | * @param int<1,5> $rating 111 | */ 112 | public function setRating(int $rating): self 113 | { 114 | $originalRating = $this->rating; 115 | $this->rating = $rating; 116 | 117 | if ($originalRating !== $rating) { 118 | $this->recordEvent(new ReviewChanged($this)); 119 | } 120 | 121 | return $this; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Framework/Entity/User.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Entity; 15 | 16 | use Symfony\Component\Security\Core\User\UserInterface; 17 | 18 | class User implements UserInterface 19 | { 20 | /** 21 | * @param non-empty-string $username 22 | * @param list $roles 23 | */ 24 | public function __construct( 25 | private readonly string $username = 'user', 26 | private readonly array $roles = ['ROLE_USER'], 27 | ) {} 28 | 29 | #[\Override] 30 | public function getRoles(): array 31 | { 32 | return $this->roles; 33 | } 34 | 35 | #[\Override] 36 | public function eraseCredentials(): void {} 37 | 38 | #[\Override] 39 | public function getUserIdentifier(): string 40 | { 41 | return $this->username; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Framework/Entity2/Comment.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Entity2; 15 | 16 | use Doctrine\ORM\Mapping as ORM; 17 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterInterface; 18 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterTrait; 19 | use Symfony\Bridge\Doctrine\Types\UuidType; 20 | use Symfony\Component\Uid\Uuid; 21 | 22 | #[ORM\Entity()] 23 | class Comment implements DomainEventEmitterInterface 24 | { 25 | use DomainEventEmitterTrait; 26 | 27 | #[ORM\Id] 28 | #[ORM\Column(type: UuidType::NAME, unique: true, nullable: false)] 29 | private Uuid $id; 30 | 31 | #[ORM\Column] 32 | private ?string $content = null; 33 | 34 | #[ORM\ManyToOne( 35 | targetEntity: Post::class, 36 | inversedBy: 'comment', 37 | )] 38 | private ?Post $post = null; 39 | 40 | public function __construct() 41 | { 42 | $this->id = Uuid::v7(); 43 | } 44 | 45 | #[\Override] 46 | public function __remove(): void {} 47 | 48 | public function getId(): Uuid 49 | { 50 | return $this->id; 51 | } 52 | 53 | public function getPost(): ?Post 54 | { 55 | return $this->post; 56 | } 57 | 58 | public function setPost(?Post $post): self 59 | { 60 | $this->post = $post; 61 | 62 | return $this; 63 | } 64 | 65 | public function getContent(): ?string 66 | { 67 | return $this->content; 68 | } 69 | 70 | public function setContent(?string $content): self 71 | { 72 | $this->content = $content; 73 | 74 | return $this; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Framework/Entity2/Post.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Entity2; 15 | 16 | use Doctrine\Common\Collections\ArrayCollection; 17 | use Doctrine\Common\Collections\Collection; 18 | use Doctrine\DBAL\Types\Types; 19 | use Doctrine\ORM\Mapping as ORM; 20 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterInterface; 21 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterTrait; 22 | use Rekalogika\DomainEvent\Tests\Framework\Event2\PostChanged; 23 | use Rekalogika\DomainEvent\Tests\Framework\Event2\PostCreated; 24 | use Rekalogika\DomainEvent\Tests\Framework\Event2\PostRemoved; 25 | use Symfony\Bridge\Doctrine\Types\UuidType; 26 | use Symfony\Component\Uid\Uuid; 27 | 28 | #[ORM\Entity()] 29 | class Post implements DomainEventEmitterInterface 30 | { 31 | use DomainEventEmitterTrait; 32 | 33 | #[ORM\Id] 34 | #[ORM\Column(type: UuidType::NAME, unique: true, nullable: false)] 35 | private Uuid $id; 36 | 37 | /** 38 | * @var Collection 39 | */ 40 | #[ORM\OneToMany( 41 | targetEntity: Comment::class, 42 | mappedBy: 'post', 43 | cascade: ['persist', 'remove'], 44 | orphanRemoval: true, 45 | fetch: 'EXTRA_LAZY', 46 | indexBy: 'id', 47 | )] 48 | private Collection $comments; 49 | 50 | public function __construct( 51 | #[ORM\Column] 52 | private ?string $title, 53 | #[ORM\Column(type: Types::TEXT)] 54 | private ?string $content, 55 | ) { 56 | $this->id = Uuid::v7(); 57 | $this->comments = new ArrayCollection(); 58 | $this->recordEvent(new PostCreated($this)); 59 | } 60 | 61 | #[\Override] 62 | public function __remove(): void 63 | { 64 | $this->recordEvent(new PostRemoved($this)); 65 | } 66 | 67 | public function getId(): Uuid 68 | { 69 | return $this->id; 70 | } 71 | 72 | public function getTitle(): ?string 73 | { 74 | return $this->title; 75 | } 76 | 77 | public function setTitle(?string $title): self 78 | { 79 | $oldTitle = $this->title; 80 | $this->title = $title; 81 | 82 | if ($oldTitle !== $title) { 83 | $this->recordEvent(new PostChanged($this)); 84 | } 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * @return Collection 91 | */ 92 | public function getComments(): Collection 93 | { 94 | return $this->comments; 95 | } 96 | 97 | public function addComment(Comment $comment): self 98 | { 99 | if (!$this->comments->contains($comment)) { 100 | $this->comments[] = $comment; 101 | $comment->setPost($this); 102 | } 103 | 104 | return $this; 105 | } 106 | 107 | public function removeComment(Comment $comment): self 108 | { 109 | // set the owning side to null (unless already changed) 110 | if ($this->comments->removeElement($comment) && $comment->getPost() === $this) { 111 | $comment->setPost(null); 112 | } 113 | 114 | return $this; 115 | } 116 | 117 | public function getContent(): ?string 118 | { 119 | return $this->content; 120 | } 121 | 122 | public function setContent(?string $content): self 123 | { 124 | $this->content = $content; 125 | 126 | return $this; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/Framework/Event/AbstractBookEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | use Rekalogika\Contracts\DomainEvent\EquatableDomainEventInterface; 17 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Book; 18 | use Symfony\Component\Uid\Uuid; 19 | 20 | abstract class AbstractBookEvent implements EquatableDomainEventInterface 21 | { 22 | private readonly Uuid $id; 23 | 24 | public function __construct(Book $book) 25 | { 26 | $this->id = $book->getId(); 27 | } 28 | 29 | final public function getId(): Uuid 30 | { 31 | return $this->id; 32 | } 33 | 34 | #[\Override] 35 | final public function getSignature(): string 36 | { 37 | return hash('xxh128', serialize($this)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Framework/Event/AbstractReviewEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | use Rekalogika\Contracts\DomainEvent\EquatableDomainEventInterface; 17 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Review; 18 | use Symfony\Component\Uid\Uuid; 19 | 20 | abstract class AbstractReviewEvent implements EquatableDomainEventInterface 21 | { 22 | private readonly Uuid $id; 23 | 24 | public function __construct(Review $review) 25 | { 26 | $this->id = $review->getId(); 27 | } 28 | 29 | final public function getId(): Uuid 30 | { 31 | return $this->id; 32 | } 33 | 34 | #[\Override] 35 | final public function getSignature(): string 36 | { 37 | return hash('xxh128', serialize($this)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookChanged.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class BookChanged extends AbstractBookEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookChecked.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class BookChecked extends AbstractBookEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookCreated.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class BookCreated extends AbstractBookEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookDummyChanged.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class BookDummyChanged 17 | { 18 | public function __construct( 19 | private readonly string $previous, 20 | private readonly string $now, 21 | ) {} 22 | 23 | public function getPrevious(): string 24 | { 25 | return $this->previous; 26 | } 27 | 28 | public function getNow(): string 29 | { 30 | return $this->now; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookDummyMethodCalled.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class BookDummyMethodCalled extends AbstractBookEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookDummyMethodForFlushCalled.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class BookDummyMethodForFlushCalled extends AbstractBookEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookDummyMethodForInfiniteLoopCalled.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class BookDummyMethodForInfiniteLoopCalled extends AbstractBookEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookDummyMethodForNestedRecordEventCalled.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class BookDummyMethodForNestedRecordEventCalled extends AbstractBookEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookRemoved.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class BookRemoved extends AbstractBookEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookReviewAdded.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Book; 17 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Review; 18 | use Symfony\Component\Uid\Uuid; 19 | 20 | final class BookReviewAdded extends AbstractBookEvent 21 | { 22 | private readonly Uuid $reviewId; 23 | 24 | public function __construct(Book $book, Review $review) 25 | { 26 | parent::__construct($book); 27 | $this->reviewId = $review->getId(); 28 | } 29 | 30 | public function getReviewId(): Uuid 31 | { 32 | return $this->reviewId; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Framework/Event/BookReviewRemoved.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Book; 17 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Review; 18 | use Symfony\Component\Uid\Uuid; 19 | 20 | final class BookReviewRemoved extends AbstractBookEvent 21 | { 22 | private readonly Uuid $reviewId; 23 | 24 | public function __construct(Book $book, Review $review) 25 | { 26 | parent::__construct($book); 27 | $this->reviewId = $review->getId(); 28 | } 29 | 30 | public function getReviewId(): Uuid 31 | { 32 | return $this->reviewId; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Framework/Event/ReviewChanged.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class ReviewChanged extends AbstractReviewEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event/ReviewCreated.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class ReviewCreated extends AbstractReviewEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event/ReviewRemoved.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event; 15 | 16 | final class ReviewRemoved extends AbstractReviewEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event2/AbstractCommentEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event2; 15 | 16 | use Rekalogika\Contracts\DomainEvent\EquatableDomainEventInterface; 17 | use Rekalogika\DomainEvent\Tests\Framework\Entity2\Comment; 18 | use Symfony\Component\Uid\Uuid; 19 | 20 | abstract class AbstractCommentEvent implements EquatableDomainEventInterface 21 | { 22 | private readonly Uuid $id; 23 | 24 | public function __construct(Comment $book) 25 | { 26 | $this->id = $book->getId(); 27 | } 28 | 29 | final public function getId(): Uuid 30 | { 31 | return $this->id; 32 | } 33 | 34 | #[\Override] 35 | final public function getSignature(): string 36 | { 37 | return hash('xxh128', serialize($this)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Framework/Event2/AbstractPostEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event2; 15 | 16 | use Rekalogika\Contracts\DomainEvent\EquatableDomainEventInterface; 17 | use Rekalogika\DomainEvent\Tests\Framework\Entity2\Post; 18 | use Symfony\Component\Uid\Uuid; 19 | 20 | abstract class AbstractPostEvent implements EquatableDomainEventInterface 21 | { 22 | private readonly Uuid $id; 23 | 24 | public function __construct(Post $book) 25 | { 26 | $this->id = $book->getId(); 27 | } 28 | 29 | final public function getId(): Uuid 30 | { 31 | return $this->id; 32 | } 33 | 34 | #[\Override] 35 | final public function getSignature(): string 36 | { 37 | return hash('xxh128', serialize($this)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Framework/Event2/PostChanged.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event2; 15 | 16 | final class PostChanged extends AbstractPostEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event2/PostCreated.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event2; 15 | 16 | final class PostCreated extends AbstractPostEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/Event2/PostRemoved.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Event2; 15 | 16 | final class PostRemoved extends AbstractPostEvent {} 17 | -------------------------------------------------------------------------------- /tests/Framework/EventListener/BookDummyChangedListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\EventListener; 15 | 16 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPostFlushDomainEventListener; 17 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPreFlushDomainEventListener; 18 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookDummyChanged; 19 | 20 | final class BookDummyChangedListener 21 | { 22 | /** 23 | * @var list 24 | */ 25 | public array $preFlush = []; 26 | 27 | /** 28 | * @var list 29 | */ 30 | public array $postFlush = []; 31 | 32 | #[AsPreFlushDomainEventListener()] 33 | public function onPreFlush(BookDummyChanged $event): void 34 | { 35 | $this->preFlush[] = $event; 36 | } 37 | 38 | #[AsPostFlushDomainEventListener()] 39 | public function onPostFlush(BookDummyChanged $event): void 40 | { 41 | $this->postFlush[] = $event; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /tests/Framework/EventListener/BookDummyMethodCalledListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\EventListener; 15 | 16 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPreFlushDomainEventListener; 17 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookDummyMethodCalled; 18 | 19 | final class BookDummyMethodCalledListener 20 | { 21 | private bool $dummyMethodCalled = false; 22 | 23 | #[AsPreFlushDomainEventListener()] 24 | public function onDummyMethodCalled(BookDummyMethodCalled $event): void 25 | { 26 | $this->dummyMethodCalled = true; 27 | } 28 | 29 | public function isDummyMethodCalled(): bool 30 | { 31 | return $this->dummyMethodCalled; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Framework/EventListener/BookDummyMethodForFlushListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\EventListener; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPreFlushDomainEventListener; 18 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookDummyMethodForFlushCalled; 19 | 20 | final class BookDummyMethodForFlushListener 21 | { 22 | public function __construct(private readonly EntityManagerInterface $entityManager) {} 23 | 24 | #[AsPreFlushDomainEventListener()] 25 | public function onDummyMethodCalled(BookDummyMethodForFlushCalled $event): void 26 | { 27 | // flush is not allowed in preFlush 28 | $this->entityManager->flush(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Framework/EventListener/BookDummyMethodForInfiniteLoopCalledListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\EventListener; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPreFlushDomainEventListener; 18 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Book; 19 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookDummyMethodForInfiniteLoopCalled; 20 | 21 | final class BookDummyMethodForInfiniteLoopCalledListener 22 | { 23 | public function __construct(private readonly EntityManagerInterface $entityManager) {} 24 | 25 | #[AsPreFlushDomainEventListener()] 26 | public function onDummyMethodCalled(BookDummyMethodForInfiniteLoopCalled $event): void 27 | { 28 | $bookId = $event->getId(); 29 | $book = $this->entityManager->find(Book::class, $bookId); 30 | \assert($book instanceof Book); 31 | 32 | $book->dummyMethodForInfiniteLoop(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Framework/EventListener/BookDummyMethodForNestedRecordEventListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\EventListener; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPreFlushDomainEventListener; 18 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Book; 19 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookDummyMethodForNestedRecordEventCalled; 20 | 21 | final class BookDummyMethodForNestedRecordEventListener 22 | { 23 | private bool $dummyMethodForNestedRecordEventCalled = false; 24 | 25 | public function __construct(private readonly EntityManagerInterface $entityManager) {} 26 | 27 | #[AsPreFlushDomainEventListener()] 28 | public function onDummyMethodCalled(BookDummyMethodForNestedRecordEventCalled $event): void 29 | { 30 | $bookId = $event->getId(); 31 | $book = $this->entityManager->find(Book::class, $bookId); 32 | \assert($book instanceof Book); 33 | $book->dummyMethod(); 34 | 35 | $this->dummyMethodForNestedRecordEventCalled = true; 36 | } 37 | 38 | public function isDummyMethodForNestedRecordEventCalled(): bool 39 | { 40 | return $this->dummyMethodForNestedRecordEventCalled; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Framework/EventListener/BookEventEventBusListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\EventListener; 15 | 16 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPublishedDomainEventListener; 17 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookChanged; 18 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookCreated; 19 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookRemoved; 20 | 21 | final class BookEventEventBusListener 22 | { 23 | private bool $onCreateCalled = false; 24 | 25 | private bool $onRemoveCalled = false; 26 | 27 | private int $onChangeCalled = 0; 28 | 29 | // skipped for testing purpose 30 | // #[AsDomainEventBusListener()] 31 | // public function onCreate(BookCreated $event): void 32 | // { 33 | // $this->onCreateCalled = true; 34 | // } 35 | 36 | #[AsPublishedDomainEventListener()] 37 | public function onChange(BookChanged $event): void 38 | { 39 | $this->onChangeCalled++; 40 | } 41 | 42 | #[AsPublishedDomainEventListener()] 43 | public function onRemove(BookRemoved $event): void 44 | { 45 | $this->onRemoveCalled = true; 46 | } 47 | 48 | public function onCreateCalled(): bool 49 | { 50 | return $this->onCreateCalled; 51 | } 52 | 53 | public function onRemoveCalled(): bool 54 | { 55 | return $this->onRemoveCalled; 56 | } 57 | 58 | public function onChangeCalled(): int 59 | { 60 | return $this->onChangeCalled; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Framework/EventListener/BookEventImmediateListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\EventListener; 15 | 16 | use Rekalogika\Contracts\DomainEvent\Attribute\AsImmediateDomainEventListener; 17 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookCreated; 18 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookRemoved; 19 | 20 | final class BookEventImmediateListener 21 | { 22 | private bool $onCreateCalled = false; 23 | 24 | private bool $onRemoveCalled = false; 25 | 26 | #[AsImmediateDomainEventListener()] 27 | public function onCreate(BookCreated $event): void 28 | { 29 | $this->onCreateCalled = true; 30 | } 31 | 32 | #[AsImmediateDomainEventListener()] 33 | public function onRemove(BookRemoved $event): void 34 | { 35 | $this->onRemoveCalled = true; 36 | } 37 | 38 | public function onCreateCalled(): bool 39 | { 40 | return $this->onCreateCalled; 41 | } 42 | 43 | public function onRemoveCalled(): bool 44 | { 45 | return $this->onRemoveCalled; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Framework/EventListener/BookEventPostFlushListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\EventListener; 15 | 16 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPostFlushDomainEventListener; 17 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookChanged; 18 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookCreated; 19 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookRemoved; 20 | 21 | final class BookEventPostFlushListener 22 | { 23 | private bool $onCreateCalled = false; 24 | 25 | private bool $onRemoveCalled = false; 26 | 27 | private int $onChangeCalled = 0; 28 | 29 | #[AsPostFlushDomainEventListener()] 30 | public function onCreate(BookCreated $event): void 31 | { 32 | $this->onCreateCalled = true; 33 | } 34 | 35 | #[AsPostFlushDomainEventListener()] 36 | public function onChange(BookChanged $event): void 37 | { 38 | $this->onChangeCalled++; 39 | } 40 | 41 | #[AsPostFlushDomainEventListener()] 42 | public function onRemove(BookRemoved $event): void 43 | { 44 | $this->onRemoveCalled = true; 45 | } 46 | 47 | public function onCreateCalled(): bool 48 | { 49 | return $this->onCreateCalled; 50 | } 51 | 52 | public function onRemoveCalled(): bool 53 | { 54 | return $this->onRemoveCalled; 55 | } 56 | 57 | public function onChangeCalled(): int 58 | { 59 | return $this->onChangeCalled; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Framework/EventListener/BookEventPreFlushListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\EventListener; 15 | 16 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPreFlushDomainEventListener; 17 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookChanged; 18 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookCreated; 19 | use Rekalogika\DomainEvent\Tests\Framework\Event\BookRemoved; 20 | 21 | final class BookEventPreFlushListener 22 | { 23 | private bool $onCreateCalled = false; 24 | 25 | private bool $onRemoveCalled = false; 26 | 27 | private int $onChangeCalled = 0; 28 | 29 | #[AsPreFlushDomainEventListener()] 30 | public function onCreate(BookCreated $event): void 31 | { 32 | $this->onCreateCalled = true; 33 | } 34 | 35 | #[AsPreFlushDomainEventListener()] 36 | public function onChange(BookChanged $event): void 37 | { 38 | $this->onChangeCalled++; 39 | } 40 | 41 | #[AsPreFlushDomainEventListener()] 42 | public function onRemove(BookRemoved $event): void 43 | { 44 | $this->onRemoveCalled = true; 45 | } 46 | 47 | public function onCreateCalled(): bool 48 | { 49 | return $this->onCreateCalled; 50 | } 51 | 52 | public function onRemoveCalled(): bool 53 | { 54 | return $this->onRemoveCalled; 55 | } 56 | 57 | public function onChangeCalled(): int 58 | { 59 | return $this->onChangeCalled; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Framework/EventListener/PostEventEventBusListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\EventListener; 15 | 16 | use Rekalogika\Contracts\DomainEvent\Attribute\AsPublishedDomainEventListener; 17 | use Rekalogika\DomainEvent\Tests\Framework\Event2\PostChanged; 18 | use Rekalogika\DomainEvent\Tests\Framework\Event2\PostCreated; 19 | use Rekalogika\DomainEvent\Tests\Framework\Event2\PostRemoved; 20 | 21 | final class PostEventEventBusListener 22 | { 23 | private bool $onCreateCalled = false; 24 | 25 | private bool $onRemoveCalled = false; 26 | 27 | private int $onChangeCalled = 0; 28 | 29 | // skipped for testing purpose 30 | // #[AsDomainEventBusListener()] 31 | // public function onCreate(PostCreated $event): void 32 | // { 33 | // $this->onCreateCalled = true; 34 | // } 35 | 36 | #[AsPublishedDomainEventListener()] 37 | public function onChange(PostChanged $event): void 38 | { 39 | $this->onChangeCalled++; 40 | } 41 | 42 | #[AsPublishedDomainEventListener()] 43 | public function onRemove(PostRemoved $event): void 44 | { 45 | $this->onRemoveCalled = true; 46 | } 47 | 48 | public function onCreateCalled(): bool 49 | { 50 | return $this->onCreateCalled; 51 | } 52 | 53 | public function onRemoveCalled(): bool 54 | { 55 | return $this->onRemoveCalled; 56 | } 57 | 58 | public function onChangeCalled(): int 59 | { 60 | return $this->onChangeCalled; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Framework/Kernel.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework; 15 | 16 | use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; 17 | use Rekalogika\DomainEvent\Outbox\RekalogikaDomainEventOutboxBundle; 18 | use Rekalogika\DomainEvent\RekalogikaDomainEventBundle; 19 | use Symfony\Bundle\DebugBundle\DebugBundle; 20 | use Symfony\Bundle\FrameworkBundle\FrameworkBundle; 21 | use Symfony\Bundle\MonologBundle\MonologBundle; 22 | use Symfony\Bundle\SecurityBundle\SecurityBundle; 23 | use Symfony\Bundle\TwigBundle\TwigBundle; 24 | use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; 25 | use Symfony\Component\Config\FileLocator; 26 | use Symfony\Component\Config\Loader\LoaderInterface; 27 | use Symfony\Component\DependencyInjection\ContainerBuilder; 28 | use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; 29 | use Symfony\Component\HttpKernel\Kernel as HttpKernelKernel; 30 | 31 | class Kernel extends HttpKernelKernel 32 | { 33 | /** 34 | * @param array $config 35 | */ 36 | public function __construct( 37 | string $environment = 'test', 38 | bool $debug = true, 39 | private readonly array $config = [], 40 | ) { 41 | parent::__construct($environment, $debug); 42 | } 43 | 44 | #[\Override] 45 | public function registerBundles(): iterable 46 | { 47 | return [ 48 | new FrameworkBundle(), 49 | new DoctrineBundle(), 50 | new WebProfilerBundle(), 51 | new TwigBundle(), 52 | new DebugBundle(), 53 | new SecurityBundle(), 54 | new MonologBundle(), 55 | new RekalogikaDomainEventBundle(), 56 | new RekalogikaDomainEventOutboxBundle(), 57 | ]; 58 | } 59 | 60 | #[\Override] 61 | public function build(ContainerBuilder $container): void 62 | { 63 | $loader = new PhpFileLoader( 64 | $container, 65 | new FileLocator(__DIR__ . '/Resources/config'), 66 | ); 67 | 68 | $loader->load('services_test.php'); 69 | } 70 | 71 | #[\Override] 72 | public function getProjectDir(): string 73 | { 74 | return \dirname(__DIR__, 2); 75 | } 76 | 77 | public function getConfigDir(): string 78 | { 79 | return __DIR__ . '/Resources/config/'; 80 | } 81 | 82 | #[\Override] 83 | public function registerContainerConfiguration(LoaderInterface $loader): void 84 | { 85 | $loader->load($this->getConfigDir() . '/packages/*' . '.yaml', 'glob'); 86 | 87 | $loader->load(function (ContainerBuilder $container): void { 88 | $container->loadFromExtension('rekalogika_domain_event', $this->config); 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/Framework/Repository/BookRepository.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Repository; 15 | 16 | use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; 17 | use Doctrine\ORM\EntityManagerInterface; 18 | use Doctrine\Persistence\ManagerRegistry; 19 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Book; 20 | 21 | /** 22 | * @extends ServiceEntityRepository 23 | * 24 | * @method Book|null find($id, $lockMode = null, $lockVersion = null) 25 | * @method Book|null findOneBy(array $criteria, array $orderBy = null) 26 | * @method Book[] findAll() 27 | * @method Book[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) 28 | */ 29 | class BookRepository extends ServiceEntityRepository 30 | { 31 | public function __construct(ManagerRegistry $registry) 32 | { 33 | parent::__construct($registry, Book::class); 34 | } 35 | 36 | #[\Override] 37 | public function getEntityManager(): EntityManagerInterface 38 | { 39 | return parent::getEntityManager(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Framework/Repository/ReviewRepository.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Repository; 15 | 16 | use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; 17 | use Doctrine\Persistence\ManagerRegistry; 18 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Review; 19 | 20 | /** 21 | * @extends ServiceEntityRepository 22 | * 23 | * @method Review|null find($id, $lockMode = null, $lockVersion = null) 24 | * @method Review|null findOneBy(array $criteria, array $orderBy = null) 25 | * @method Review[] findAll() 26 | * @method Review[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) 27 | */ 28 | class ReviewRepository extends ServiceEntityRepository 29 | { 30 | public function __construct(ManagerRegistry $registry) 31 | { 32 | parent::__construct($registry, Review::class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Framework/Resources/config/packages/debug.yaml: -------------------------------------------------------------------------------- 1 | debug: 2 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. 3 | # See the "server:dump" command to start a new server. 4 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" 5 | -------------------------------------------------------------------------------- /tests/Framework/Resources/config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | connections: 4 | default: 5 | driver: pdo_sqlite 6 | memory: true 7 | charset: UTF8 8 | use_savepoints: true 9 | other: 10 | driver: pdo_sqlite 11 | memory: true 12 | charset: UTF8 13 | use_savepoints: true 14 | 15 | orm: 16 | auto_generate_proxy_classes: true 17 | enable_lazy_ghost_objects: true 18 | default_entity_manager: default 19 | entity_managers: 20 | default: 21 | connection: default 22 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 23 | mappings: 24 | Book: 25 | is_bundle: false 26 | type: attribute 27 | dir: "%kernel.project_dir%/tests/Framework/Entity" 28 | prefix: 'Rekalogika\DomainEvent\Tests\Framework\Entity' 29 | other: 30 | connection: other 31 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 32 | mappings: 33 | Post: 34 | is_bundle: false 35 | type: attribute 36 | dir: "%kernel.project_dir%/tests/Framework/Entity2" 37 | prefix: 'Rekalogika\DomainEvent\Tests\Framework\Entity2' 38 | -------------------------------------------------------------------------------- /tests/Framework/Resources/config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | kernel.secret: test 3 | 4 | framework: 5 | test: true 6 | http_method_override: false 7 | handle_all_throwables: true 8 | php_errors: 9 | log: true 10 | uid: 11 | default_uuid_version: 7 12 | time_based_uuid_version: 7 13 | scheduler: 14 | enabled: true 15 | -------------------------------------------------------------------------------- /tests/Framework/Resources/config/packages/lock.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | lock: flock 3 | -------------------------------------------------------------------------------- /tests/Framework/Resources/config/packages/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | transports: 4 | async: "in-memory://" 5 | 6 | default_bus: messenger.bus.default 7 | 8 | buses: 9 | messenger.bus.default: null 10 | rekalogika.domain_event.bus: 11 | default_middleware: 12 | allow_no_handlers: true 13 | 14 | routing: 15 | 'Rekalogika\DomainEvent\Tests\*': async -------------------------------------------------------------------------------- /tests/Framework/Resources/config/packages/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | channels: 3 | - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists 4 | handlers: 5 | main: 6 | type: fingers_crossed 7 | action_level: error 8 | handler: nested 9 | excluded_http_codes: [404, 405] 10 | channels: ["!event"] 11 | formatter: monolog.formatter.json 12 | nested: 13 | type: stream 14 | path: "%kernel.logs_dir%/%kernel.environment%.log" 15 | level: debug 16 | 17 | -------------------------------------------------------------------------------- /tests/Framework/Resources/config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | resource: "%kernel.project_dir%/tests/Framework/Resources/config/routes.yaml" -------------------------------------------------------------------------------- /tests/Framework/Resources/config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords 3 | password_hashers: 4 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' 5 | # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider 6 | providers: 7 | users_in_memory: { memory: null } 8 | firewalls: 9 | dev: 10 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 11 | security: false 12 | main: 13 | pattern: ^/ 14 | stateless: true 15 | access_token: 16 | token_handler: Rekalogika\DomainEvent\Tests\Framework\Security\AccessTokenHandler 17 | 18 | # lazy: true 19 | # provider: users_in_memory 20 | 21 | # activate different ways to authenticate 22 | # https://symfony.com/doc/current/security.html#the-firewall 23 | 24 | # https://symfony.com/doc/current/security/impersonating_user.html 25 | # switch_user: true 26 | 27 | # Easy way to control access for large sections of your site 28 | # Note: Only the *first* access control that matches will be used 29 | access_control: 30 | - { path: ^/, roles: PUBLIC_ACCESS } 31 | # - { path: ^/profile, roles: ROLE_USER } 32 | 33 | when@test: 34 | security: 35 | password_hashers: 36 | # By default, password hashers are resource intensive and take time. This is 37 | # important to generate secure password hashes. In tests however, secure hashes 38 | # are not important, waste resources and increase test times. The following 39 | # reduces the work factor to the lowest possible values. 40 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 41 | algorithm: auto 42 | cost: 4 # Lowest possible value for bcrypt 43 | time_cost: 3 # Lowest possible value for argon 44 | memory_cost: 10 # Lowest possible value for argon 45 | -------------------------------------------------------------------------------- /tests/Framework/Resources/config/packages/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: 7 | only_exceptions: false 8 | collect_serializer_data: true 9 | -------------------------------------------------------------------------------- /tests/Framework/Resources/config/routes.yaml: -------------------------------------------------------------------------------- 1 | _app: 2 | resource: 'routes/*.yaml' 3 | type: yaml -------------------------------------------------------------------------------- /tests/Framework/Resources/config/routes/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler_wdt: 2 | resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" 3 | prefix: /_wdt 4 | 5 | web_profiler_profiler: 6 | resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" 7 | prefix: /_profiler 8 | -------------------------------------------------------------------------------- /tests/Framework/Resources/config/services_test.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | use Rekalogika\DomainEvent\DependencyInjection\Constants; 15 | use Rekalogika\DomainEvent\DomainEventAwareEntityManagerInterface; 16 | use Rekalogika\DomainEvent\DomainEventAwareManagerRegistry; 17 | use Rekalogika\DomainEvent\Outbox\OutboxReaderFactoryInterface; 18 | use Symfony\Bundle\SecurityBundle\Security; 19 | use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; 20 | 21 | return static function (ContainerConfigurator $containerConfigurator): void { 22 | $services = $containerConfigurator->services(); 23 | 24 | $services->defaults() 25 | ->autowire() 26 | ->autoconfigure() 27 | ->public(); 28 | 29 | $serviceIds = [ 30 | Constants::EVENT_DISPATCHER_IMMEDIATE, 31 | Constants::EVENT_DISPATCHER_PRE_FLUSH, 32 | Constants::EVENT_DISPATCHER_POST_FLUSH, 33 | Constants::EVENT_DISPATCHERS, 34 | Constants::DOCTRINE_EVENT_LISTENER, 35 | DomainEventAwareManagerRegistry::class, 36 | DomainEventAwareEntityManagerInterface::class, 37 | Constants::IMMEDIATE_DISPATCHER_INSTALLER, 38 | Constants::REAPER, 39 | OutboxReaderFactoryInterface::class, 40 | Security::class, 41 | ]; 42 | 43 | foreach ($serviceIds as $serviceId) { 44 | $services 45 | ->alias('test.' . $serviceId, $serviceId) 46 | ->public(); 47 | } 48 | 49 | $services 50 | ->load('Rekalogika\DomainEvent\Tests\Framework\EventListener\\', '../../EventListener/') 51 | ->load('Rekalogika\DomainEvent\Tests\Framework\Repository\\', '../../Repository/') 52 | ->load('Rekalogika\DomainEvent\Tests\Framework\Security\\', '../../Security/'); 53 | }; 54 | -------------------------------------------------------------------------------- /tests/Framework/Security/AccessTokenHandler.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Security; 15 | 16 | use Rekalogika\DomainEvent\Tests\Framework\Entity\User; 17 | use Symfony\Component\Security\Core\Exception\BadCredentialsException; 18 | use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; 19 | use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; 20 | 21 | class AccessTokenHandler implements AccessTokenHandlerInterface 22 | { 23 | #[\Override] 24 | public function getUserBadgeFrom(string $accessToken): UserBadge 25 | { 26 | return match ($accessToken) { 27 | 'user' => new UserBadge( 28 | 'user', 29 | fn(string $userIdentifier): User => new User(), 30 | ), 31 | default => throw new BadCredentialsException('Invalid credentials.'), 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Framework/Tests/BasicDomainEventTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Tests; 15 | 16 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Book; 17 | use Rekalogika\DomainEvent\Tests\Framework\EventListener\BookEventImmediateListener; 18 | use Rekalogika\DomainEvent\Tests\Framework\EventListener\BookEventPostFlushListener; 19 | use Rekalogika\DomainEvent\Tests\Framework\EventListener\BookEventPreFlushListener; 20 | 21 | final class BasicDomainEventTest extends DomainEventTestCase 22 | { 23 | public function testImmediateListener(): void 24 | { 25 | $listener = static::getContainer()->get(BookEventImmediateListener::class); 26 | $this->assertInstanceOf(BookEventImmediateListener::class, $listener); 27 | 28 | $this->assertFalse($listener->onCreateCalled()); 29 | new Book('title', 'description'); 30 | $this->assertTrue($listener->onCreateCalled()); 31 | } 32 | 33 | public function testPreFlushListener(): void 34 | { 35 | $listener = static::getContainer()->get(BookEventPreFlushListener::class); 36 | $this->assertInstanceOf(BookEventPreFlushListener::class, $listener); 37 | 38 | $this->assertFalse($listener->onCreateCalled()); 39 | 40 | $book = new Book('title', 'description'); 41 | static::getEntityManager()->persist($book); 42 | static::getEntityManager()->flush(); 43 | 44 | $this->assertTrue($listener->onCreateCalled()); 45 | } 46 | 47 | public function testPostFlushListener(): void 48 | { 49 | $listener = static::getContainer()->get(BookEventPostFlushListener::class); 50 | $this->assertInstanceOf(BookEventPostFlushListener::class, $listener); 51 | 52 | $this->assertFalse($listener->onCreateCalled()); 53 | 54 | $book = new Book('title', 'description'); 55 | static::getEntityManager()->persist($book); 56 | static::getEntityManager()->flush(); 57 | 58 | $this->assertTrue($listener->onCreateCalled()); 59 | } 60 | 61 | public function testManualPreFlush(): void 62 | { 63 | $entityManager = static::getEntityManager(); 64 | $listener = static::getContainer()->get(BookEventPreFlushListener::class); 65 | $this->assertInstanceOf(BookEventPreFlushListener::class, $listener); 66 | 67 | $entityManager->setAutoDispatchDomainEvents(false); 68 | 69 | $this->assertFalse($listener->onCreateCalled()); 70 | 71 | $book = new Book('title', 'description'); 72 | $entityManager->persist($book); 73 | $entityManager->dispatchPreFlushDomainEvents(); 74 | $entityManager->flush(); 75 | $entityManager->clearDomainEvents(); 76 | 77 | $this->assertTrue($listener->onCreateCalled()); 78 | } 79 | 80 | public function testManualPostFlush(): void 81 | { 82 | $entityManager = static::getEntityManager(); 83 | $listener = static::getContainer()->get(BookEventPostFlushListener::class); 84 | $this->assertInstanceOf(BookEventPostFlushListener::class, $listener); 85 | 86 | $entityManager->setAutoDispatchDomainEvents(false); 87 | 88 | $this->assertFalse($listener->onCreateCalled()); 89 | 90 | $book = new Book('title', 'description'); 91 | $entityManager->persist($book); 92 | $entityManager->flush(); 93 | $entityManager->dispatchPostFlushDomainEvents(); 94 | 95 | $this->assertTrue($listener->onCreateCalled()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Framework/Tests/DomainEventTestCase.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Tests; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | use Doctrine\ORM\Tools\SchemaTool; 18 | use Doctrine\Persistence\ManagerRegistry; 19 | use Rekalogika\DomainEvent\Doctrine\DomainEventAwareEntityManager; 20 | use Rekalogika\DomainEvent\DomainEventAwareEntityManagerInterface; 21 | use Rekalogika\DomainEvent\DomainEventAwareManagerRegistry; 22 | use Rekalogika\DomainEvent\Tests\Framework\Kernel; 23 | use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; 24 | use Symfony\Component\HttpKernel\KernelInterface; 25 | 26 | abstract class DomainEventTestCase extends KernelTestCase 27 | { 28 | protected DomainEventAwareEntityManagerInterface $entityManager; 29 | 30 | protected DomainEventAwareManagerRegistry $managerRegistry; 31 | 32 | // @phpstan-ignore-next-line 33 | #[\Override] 34 | protected static function createKernel(array $options = []): KernelInterface 35 | { 36 | return new Kernel(); 37 | } 38 | 39 | #[\Override] 40 | public function setUp(): void 41 | { 42 | parent::setUp(); 43 | 44 | // setup manager registry 45 | 46 | $managerRegistry = static::getContainer()->get('doctrine'); 47 | $this->assertInstanceOf(DomainEventAwareManagerRegistry::class, $managerRegistry); 48 | 49 | $this->managerRegistry = $managerRegistry; 50 | 51 | // create schema 52 | 53 | $managers = $managerRegistry->getManagers(); 54 | 55 | foreach ($managers as $manager) { 56 | $this->assertInstanceOf(EntityManagerInterface::class, $manager); 57 | $schemaTool = new SchemaTool($manager); 58 | $schemaTool->createSchema($manager->getMetadataFactory()->getAllMetadata()); 59 | } 60 | 61 | // save entity manager to class property 62 | 63 | $entityManager = static::getContainer()->get('doctrine.orm.entity_manager'); 64 | $this->assertInstanceOf(DomainEventAwareEntityManagerInterface::class, $entityManager); 65 | 66 | $this->entityManager = $entityManager; 67 | } 68 | 69 | public static function getManagerRegistry(): ManagerRegistry 70 | { 71 | $managerRegistry = static::getContainer()->get(ManagerRegistry::class); 72 | self::assertInstanceOf(ManagerRegistry::class, $managerRegistry); 73 | self::assertInstanceOf(DomainEventAwareManagerRegistry::class, $managerRegistry); 74 | 75 | return $managerRegistry; 76 | } 77 | 78 | public static function getEntityManager(): DomainEventAwareEntityManager 79 | { 80 | $managerRegistry = static::getManagerRegistry(); 81 | 82 | $entityManager = $managerRegistry->getManager(); 83 | self::assertInstanceOf(DomainEventAwareEntityManager::class, $entityManager); 84 | 85 | return $entityManager; 86 | } 87 | 88 | #[\Override] 89 | public function tearDown(): void 90 | { 91 | parent::tearDown(); 92 | 93 | $managers = $this->managerRegistry->getManagers(); 94 | 95 | foreach ($managers as $manager) { 96 | $this->assertInstanceOf(DomainEventAwareEntityManagerInterface::class, $manager); 97 | $manager->clearDomainEvents(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/Framework/Tests/EquatableEventTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Tests; 15 | 16 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Book; 17 | use Rekalogika\DomainEvent\Tests\Framework\EventListener\BookEventPostFlushListener; 18 | use Rekalogika\DomainEvent\Tests\Framework\EventListener\BookEventPreFlushListener; 19 | 20 | final class EquatableEventTest extends DomainEventTestCase 21 | { 22 | public function testWithoutTransaction(): void 23 | { 24 | $preFlushListener = static::getContainer()->get(BookEventPreFlushListener::class); 25 | $this->assertInstanceOf(BookEventPreFlushListener::class, $preFlushListener); 26 | 27 | $postFlushListener = static::getContainer()->get(BookEventPostFlushListener::class); 28 | $this->assertInstanceOf(BookEventPostFlushListener::class, $postFlushListener); 29 | 30 | $book = new Book('Book A', 'Description A'); 31 | 32 | $this->entityManager->persist($book); 33 | $this->entityManager->flush(); 34 | 35 | $book->setTitle('Book B'); 36 | $book->setTitle('Book C'); 37 | $book->setTitle('Book D'); 38 | $book->setTitle('Book E'); 39 | $book->setTitle('Book F'); 40 | 41 | $this->entityManager->flush(); 42 | 43 | $this->assertEquals(1, $preFlushListener->onChangeCalled()); 44 | $this->assertEquals(1, $postFlushListener->onChangeCalled()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Framework/Tests/IntegrationTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Tests; 15 | 16 | use Psr\EventDispatcher\EventDispatcherInterface; 17 | use Rekalogika\DomainEvent\DependencyInjection\Constants; 18 | 19 | final class IntegrationTest extends DomainEventTestCase 20 | { 21 | public function testEventDispatcherWiring(): void 22 | { 23 | $serviceIds = [ 24 | Constants::EVENT_DISPATCHER_IMMEDIATE, 25 | Constants::EVENT_DISPATCHER_PRE_FLUSH, 26 | Constants::EVENT_DISPATCHER_POST_FLUSH, 27 | ]; 28 | 29 | foreach ($serviceIds as $serviceId) { 30 | $this->assertInstanceOf( 31 | EventDispatcherInterface::class, 32 | static::getContainer()->get('test.' . $serviceId), 33 | ); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Framework/Tests/OutboxSetupTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Tests; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | use Doctrine\Persistence\ManagerRegistry; 18 | 19 | final class OutboxSetupTest extends DomainEventTestCase 20 | { 21 | /** 22 | * @dataProvider databaseSetupProvider 23 | */ 24 | public function testDatabaseSetup(string $id): void 25 | { 26 | $managerRegistry = static::getContainer()->get('doctrine'); 27 | self::assertInstanceOf(ManagerRegistry::class, $managerRegistry); 28 | 29 | $entityManager = $managerRegistry->getManager($id); 30 | self::assertInstanceOf(EntityManagerInterface::class, $entityManager); 31 | $connection = $entityManager->getConnection(); 32 | 33 | $queryBuilder = $connection->createQueryBuilder() 34 | ->select('name') 35 | ->from('sqlite_schema') 36 | ->where('type = ?') 37 | ->andWhere('name = ?') 38 | ->setParameter(0, 'table') 39 | ->setParameter(1, 'rekalogika_event_outbox'); 40 | 41 | $queryBuilder->executeStatement(); 42 | 43 | $result = $queryBuilder->fetchAssociative(); 44 | 45 | $this->assertIsArray($result); 46 | $this->assertArrayHasKey('name', $result); 47 | $this->assertEquals('rekalogika_event_outbox', $result['name']); 48 | } 49 | 50 | /** 51 | * @return iterable> 52 | */ 53 | public static function databaseSetupProvider(): iterable 54 | { 55 | $managerRegistry = static::getContainer()->get('doctrine'); 56 | self::assertInstanceOf(ManagerRegistry::class, $managerRegistry); 57 | 58 | foreach ($managerRegistry->getManagers() as $id => $entityManager) { 59 | yield [$id]; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Framework/Tests/PreFlushTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Tests; 15 | 16 | use Rekalogika\DomainEvent\Exception\FlushNotAllowedException; 17 | use Rekalogika\DomainEvent\Exception\SafeguardTriggeredException; 18 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Book; 19 | use Rekalogika\DomainEvent\Tests\Framework\EventListener\BookDummyMethodCalledListener; 20 | use Rekalogika\DomainEvent\Tests\Framework\EventListener\BookDummyMethodForNestedRecordEventListener; 21 | 22 | final class PreFlushTest extends DomainEventTestCase 23 | { 24 | #[\Override] 25 | public function tearDown(): void 26 | { 27 | static::getEntityManager()->clearDomainEvents(); 28 | parent::tearDown(); 29 | } 30 | 31 | public function testFlushInPreFlush(): void 32 | { 33 | $book = new Book('title', 'description'); 34 | $book->dummyMethodForFlush(); 35 | static::getEntityManager()->persist($book); 36 | $this->expectException(FlushNotAllowedException::class); 37 | static::getEntityManager()->flush(); 38 | } 39 | 40 | public function testNestedRecordEvent(): void 41 | { 42 | $dummyMethodCalledListener = static::getContainer()->get(BookDummyMethodCalledListener::class); 43 | $this->assertInstanceOf(BookDummyMethodCalledListener::class, $dummyMethodCalledListener); 44 | 45 | $dummyMethodForNestedRecordEventListener = static::getContainer()->get(BookDummyMethodForNestedRecordEventListener::class); 46 | $this->assertInstanceOf(BookDummyMethodForNestedRecordEventListener::class, $dummyMethodForNestedRecordEventListener); 47 | 48 | $this->assertFalse($dummyMethodCalledListener->isDummyMethodCalled()); 49 | $this->assertFalse($dummyMethodForNestedRecordEventListener->isDummyMethodForNestedRecordEventCalled()); 50 | 51 | $book = new Book('title', 'description'); 52 | static::getEntityManager()->persist($book); 53 | 54 | $book->dummyMethodForNestedRecordEvent(); 55 | static::getEntityManager()->flush(); 56 | 57 | $this->assertTrue($dummyMethodCalledListener->isDummyMethodCalled()); 58 | $this->assertTrue($dummyMethodForNestedRecordEventListener->isDummyMethodForNestedRecordEventCalled()); 59 | } 60 | 61 | public function testInfiniteLoopSafeguard(): void 62 | { 63 | $book = new Book('title', 'description'); 64 | static::getEntityManager()->persist($book); 65 | 66 | $book->dummyMethodForInfiniteLoop(); 67 | $this->expectException(SafeguardTriggeredException::class); 68 | static::getEntityManager()->flush(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Framework/Tests/RemoveTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Tests; 15 | 16 | use Rekalogika\DomainEvent\Tests\Framework\Entity\Book; 17 | use Rekalogika\DomainEvent\Tests\Framework\EventListener\BookEventImmediateListener; 18 | use Rekalogika\DomainEvent\Tests\Framework\EventListener\BookEventPostFlushListener; 19 | use Rekalogika\DomainEvent\Tests\Framework\EventListener\BookEventPreFlushListener; 20 | use Symfony\Component\Uid\Uuid; 21 | 22 | final class RemoveTest extends DomainEventTestCase 23 | { 24 | private function persistBook(): Uuid 25 | { 26 | $entitymanager = static::getEntityManager(); 27 | $book = new Book('title', 'description'); 28 | $entitymanager->persist($book); 29 | $entitymanager->flush(); 30 | $entitymanager->clear(); 31 | 32 | return $book->getId(); 33 | } 34 | 35 | private function findBook(Uuid $id): Book 36 | { 37 | $entitymanager = static::getEntityManager(); 38 | $book = $entitymanager->find(Book::class, $id); 39 | $this->assertInstanceOf(Book::class, $book); 40 | 41 | return $book; 42 | } 43 | 44 | public function testImmediateListener(): void 45 | { 46 | $id = $this->persistBook(); 47 | $book = $this->findBook($id); 48 | 49 | $listener = static::getContainer()->get(BookEventImmediateListener::class); 50 | $this->assertInstanceOf(BookEventImmediateListener::class, $listener); 51 | 52 | $this->assertFalse($listener->onRemoveCalled()); 53 | static::getEntityManager()->remove($book); 54 | $this->assertTrue($listener->onRemoveCalled()); 55 | 56 | static::getEntityManager()->flush(); 57 | } 58 | 59 | public function testPrePostFlushListener(): void 60 | { 61 | $id = $this->persistBook(); 62 | $book = $this->findBook($id); 63 | 64 | $preFlushListener = static::getContainer()->get(BookEventPreFlushListener::class); 65 | $this->assertInstanceOf(BookEventPreFlushListener::class, $preFlushListener); 66 | 67 | $postFlushListener = static::getContainer()->get(BookEventPostFlushListener::class); 68 | $this->assertInstanceOf(BookEventPostFlushListener::class, $postFlushListener); 69 | 70 | $this->assertFalse($preFlushListener->onRemoveCalled()); 71 | $this->assertFalse($postFlushListener->onRemoveCalled()); 72 | 73 | static::getEntityManager()->remove($book); 74 | static::getEntityManager()->flush(); 75 | 76 | $this->assertTrue($preFlushListener->onRemoveCalled()); 77 | $this->assertTrue($postFlushListener->onRemoveCalled()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Framework/Tests/ResetTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Framework\Tests; 15 | 16 | final class ResetTest extends DomainEventTestCase 17 | { 18 | public function testEntityManagerReset(): void 19 | { 20 | $entitymanager = static::getEntityManager(); 21 | $entitymanager->reset(); 22 | } 23 | 24 | public function testManagerRegistryResetManager(): void 25 | { 26 | $managerRegistry = static::getManagerRegistry(); 27 | $managerRegistry->resetManager(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Integration/Event/AbstractEntityDomainEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\Event; 15 | 16 | use Rekalogika\DomainEvent\Tests\Integration\Model\Entity; 17 | 18 | abstract class AbstractEntityDomainEvent 19 | { 20 | final public function __construct(private readonly Entity $entity) {} 21 | 22 | public function getEntity(): Entity 23 | { 24 | return $this->entity; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Integration/Event/EntityCreated.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\Event; 15 | 16 | final class EntityCreated extends AbstractEntityDomainEvent {} 17 | -------------------------------------------------------------------------------- /tests/Integration/Event/EntityNameChanged.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\Event; 15 | 16 | final class EntityNameChanged extends AbstractEntityDomainEvent {} 17 | -------------------------------------------------------------------------------- /tests/Integration/Event/EntityRemoved.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\Event; 15 | 16 | final class EntityRemoved extends AbstractEntityDomainEvent {} 17 | -------------------------------------------------------------------------------- /tests/Integration/Event/EquatableEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\Event; 15 | 16 | use Rekalogika\Contracts\DomainEvent\EquatableDomainEventInterface; 17 | 18 | final class EquatableEvent extends AbstractEntityDomainEvent implements 19 | EquatableDomainEventInterface 20 | { 21 | #[\Override] 22 | public function getSignature(): string 23 | { 24 | return sha1(serialize($this)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Integration/Event/NonEquatableEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\Event; 15 | 16 | final class NonEquatableEvent extends AbstractEntityDomainEvent {} 17 | -------------------------------------------------------------------------------- /tests/Integration/EventListener/DomainEventListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\EventListener; 15 | 16 | final class DomainEventListener 17 | { 18 | private bool $entityCreatedHeard = false; 19 | 20 | private bool $entityRemovedHeard = false; 21 | 22 | private bool $entityNameChangedHeard = false; 23 | 24 | public function onEntityCreated(): void 25 | { 26 | $this->entityCreatedHeard = true; 27 | } 28 | 29 | public function onEntityRemoved(): void 30 | { 31 | $this->entityRemovedHeard = true; 32 | } 33 | 34 | public function onEntityNameChanged(): void 35 | { 36 | $this->entityNameChangedHeard = true; 37 | } 38 | 39 | public function isEntityCreatedHeard(): bool 40 | { 41 | return $this->entityCreatedHeard; 42 | } 43 | 44 | public function isEntityRemovedHeard(): bool 45 | { 46 | return $this->entityRemovedHeard; 47 | } 48 | 49 | public function isEntityNameChangedHeard(): bool 50 | { 51 | return $this->entityNameChangedHeard; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Integration/EventListener/EquatableEventListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\EventListener; 15 | 16 | use Rekalogika\DomainEvent\Tests\Integration\Event\EquatableEvent; 17 | use Rekalogika\DomainEvent\Tests\Integration\Event\NonEquatableEvent; 18 | 19 | final class EquatableEventListener 20 | { 21 | private int $equatableEventHeard = 0; 22 | 23 | private int $nonEquatableEventHeard = 0; 24 | 25 | public function onEquatableEvent(EquatableEvent $event): void 26 | { 27 | $this->equatableEventHeard++; 28 | } 29 | 30 | public function onNonEquatableEvent(NonEquatableEvent $event): void 31 | { 32 | $this->nonEquatableEventHeard++; 33 | } 34 | 35 | public function getEquatableEventHeard(): int 36 | { 37 | return $this->equatableEventHeard; 38 | } 39 | 40 | public function getNonEquatableEventHeard(): int 41 | { 42 | return $this->nonEquatableEventHeard; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Integration/EventListener/FlushingDomainEventListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\EventListener; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | 18 | final class FlushingDomainEventListener 19 | { 20 | public function __construct(private readonly EntityManagerInterface $entityManager) {} 21 | 22 | private bool $entityCreatedHeard = false; 23 | 24 | private bool $entityRemovedHeard = false; 25 | 26 | private bool $entityNameChangedHeard = false; 27 | 28 | public function onEntityCreated(): void 29 | { 30 | $this->entityManager->flush(); 31 | $this->entityCreatedHeard = true; 32 | } 33 | 34 | public function onEntityRemoved(): void 35 | { 36 | $this->entityManager->flush(); 37 | $this->entityRemovedHeard = true; 38 | } 39 | 40 | public function onEntityNameChanged(): void 41 | { 42 | $this->entityManager->flush(); 43 | $this->entityNameChangedHeard = true; 44 | } 45 | 46 | public function isEntityCreatedHeard(): bool 47 | { 48 | return $this->entityCreatedHeard; 49 | } 50 | 51 | public function isEntityRemovedHeard(): bool 52 | { 53 | return $this->entityRemovedHeard; 54 | } 55 | 56 | public function isEntityNameChangedHeard(): bool 57 | { 58 | return $this->entityNameChangedHeard; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Integration/Factory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration; 15 | 16 | use Doctrine\ORM\EntityManagerInterface; 17 | use Doctrine\Persistence\ManagerRegistry; 18 | use Psr\EventDispatcher\EventDispatcherInterface; 19 | 20 | class Factory 21 | { 22 | public static function mockEntityManager(): EntityManagerInterface 23 | { 24 | $entityManager = \Mockery::mock(EntityManagerInterface::class); 25 | \assert($entityManager instanceof EntityManagerInterface); 26 | 27 | return $entityManager; 28 | } 29 | 30 | public static function mockManagerRegistry(): ManagerRegistry 31 | { 32 | $managerRegistry = \Mockery::mock(ManagerRegistry::class); 33 | \assert($managerRegistry instanceof ManagerRegistry); 34 | 35 | return $managerRegistry; 36 | } 37 | 38 | public static function mockEventDispatcher(): EventDispatcherInterface 39 | { 40 | $eventDispatcher = \Mockery::mock(EventDispatcherInterface::class); 41 | \assert($eventDispatcher instanceof EventDispatcherInterface); 42 | 43 | return $eventDispatcher; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Integration/Model/Entity.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\Model; 15 | 16 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterInterface; 17 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterTrait; 18 | use Rekalogika\DomainEvent\Tests\Integration\Event\EntityCreated; 19 | use Rekalogika\DomainEvent\Tests\Integration\Event\EntityNameChanged; 20 | use Rekalogika\DomainEvent\Tests\Integration\Event\EntityRemoved; 21 | use Rekalogika\DomainEvent\Tests\Integration\Event\EquatableEvent; 22 | use Rekalogika\DomainEvent\Tests\Integration\Event\NonEquatableEvent; 23 | 24 | final class Entity implements DomainEventEmitterInterface 25 | { 26 | use DomainEventEmitterTrait; 27 | 28 | private string $id; 29 | 30 | public function __construct( 31 | private string $name, 32 | ) { 33 | $this->id = bin2hex(random_bytes(16)); 34 | 35 | $this->recordEvent(new EntityCreated($this)); 36 | } 37 | 38 | #[\Override] 39 | public function __remove(): void 40 | { 41 | $this->recordEvent(new EntityRemoved($this)); 42 | } 43 | 44 | public function getId(): string 45 | { 46 | return $this->id; 47 | } 48 | 49 | public function getName(): string 50 | { 51 | return $this->name; 52 | } 53 | 54 | public function setName(string $name): self 55 | { 56 | $this->name = $name; 57 | $this->recordEvent(new EntityNameChanged($this)); 58 | 59 | return $this; 60 | } 61 | 62 | public function equatableCheck(): void 63 | { 64 | $this->recordEvent(new NonEquatableEvent($this)); 65 | $this->recordEvent(new NonEquatableEvent($this)); 66 | $this->recordEvent(new NonEquatableEvent($this)); 67 | $this->recordEvent(new EquatableEvent($this)); 68 | $this->recordEvent(new EquatableEvent($this)); 69 | $this->recordEvent(new EquatableEvent($this)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Integration/Service/DomainEventEmitterCollectorStub.phpx: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\DomainEvent\Tests\Integration\Service; 15 | 16 | use Doctrine\ORM\UnitOfWork; 17 | use Rekalogika\Contracts\DomainEvent\DomainEventEmitterInterface; 18 | use Rekalogika\DomainEvent\Doctrine\DomainEventEmitterCollectorInterface; 19 | 20 | final class DomainEventEmitterCollectorStub implements 21 | DomainEventEmitterCollectorInterface 22 | { 23 | /** 24 | * @var iterable 25 | */ 26 | private iterable $entities; 27 | 28 | public function __construct(DomainEventEmitterInterface ...$entities) 29 | { 30 | $this->entities = $entities; 31 | } 32 | 33 | /** 34 | * @return iterable 35 | */ 36 | public function collectEntities(UnitOfWork $unitOfWork): iterable 37 | { 38 | return $this->entities; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 10 | * 11 | * For the full copyright and license information, please view the LICENSE file 12 | * that was distributed with this source code. 13 | */ 14 | 15 | use Rekalogika\DomainEvent\Tests\Framework\Kernel; 16 | use Symfony\Bundle\FrameworkBundle\Console\Application; 17 | 18 | require_once dirname(__DIR__).'/../vendor/autoload_runtime.php'; 19 | 20 | return function (array $context) { 21 | $kernel = new Kernel(); 22 | 23 | return new Application($kernel); 24 | }; 25 | --------------------------------------------------------------------------------