├── .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