botilka / botilka
CQRS & Event Sourcing framework
Installs: 2 815
Dependents: 0
Suggesters: 0
Security: 0
Stars: 34
Watchers: 4
Forks: 4
Open Issues: 1
Requires
- php: >=7.1
- doctrine/common: ^2.9
- doctrine/doctrine-cache-bundle: >=1.3.5
- ramsey/uuid: ^3.8
- symfony/console: ^4.2
- symfony/dependency-injection: ^4.2
- symfony/messenger: ^4.3
- symfony/monolog-bundle: ^3.3
- symfony/serializer: ^4.2
- symfony/translation: ^4.2
- symfony/validator: ^4.2
Requires (Dev)
- api-platform/core: ^2.3
- doctrine/doctrine-bundle: ^1.10
- doctrine/orm: ^2.6.3
- jangregor/phpstan-prophecy: ^0.3.0
- mongodb/mongodb: ^1.4.2
- phpstan/phpstan: ^0.11.0
- phpstan/phpstan-deprecation-rules: ^0.11.2
- phpstan/phpstan-phpunit: ^0.11.0
- phpunit/phpunit: 7.3.*
- ramsey/uuid-doctrine: ^1.5
- symfony/framework-bundle: ^4.2
- symfony/http-kernel: ^4.3
- symfony/yaml: ^4.2
Suggests
- api-platform/core: To expose your commands & queries via REST
- doctrine/orm: To use Doctrine as an EventStore/SnapshotStore
- mongodb/mongodb: To use MongoDB as an EventStore/SnapshotStore
This package is auto-updated.
Last update: 2024-04-08 13:20:47 UTC
README
An modern & easy-to-use Event Sourcing & CQRS library. It's shipped with implementations built on top of Symfony components.
It can leverage API Platform to expose yours Commands
and Queries
via REST.
Features
- EventStore implementation with Doctrine or MongoDB.
- Snapshot store for performance (Doctrine & MongoDB).
- Swagger commands & queries description (via API Platform UI).
- REST API access to commands & queries.
- Sync or async event handling is a matter of configuration.
- Event replaying (allow to test domain changes).
- Projection re-play on demand (ie. when you add a ReadModel).
- Safe commands concurrency (optimistic locking).
- Tested, good code coverage.
Configuration
An event store should (must) be persisted and the default implementation is not! Choose between:
Botilka\Infrastructure\Doctrine\EventStoreDoctrine
Botilka\Infrastructure\MongoDB\EventStoreMongoDB
# config/packages/botilka.yaml botilka: # default implementation is 'Botilka\Infrastructure\InMemory\EventStoreInMemory', not persisted!! event_store: Botilka\Infrastructure\Doctrine\EventStoreDoctrine # or 'Botilka\Infrastructure\MongoDB\EventStoreMongoDB'
Botilka provide a command to create & configure the event store:
bin/console botilka:store:initialize event doctrine # or mongodb
You can force recreate, but be carefull, you will lost all the previous events:
bin/console botilka:store:initialize event doctrine -f
Usage
CQRS & EventSourcing
You'll need to create Commands, Queries, Events and so on. Read the documentation.
Async command handling
You may need to handle some or all commands asychonously. Using Symfony Messenger, you'll need to configure it:
# config/packages/messenger.yaml framework: messenger: transports: async: "%env(MESSENGER_TRANSPORT_DSN)%" routing: 'Botilka\Application\Command\Command': async
Snapshots
When dealing with a lots of events, snapshotting is a good way to keep up on performance. Read the documentation.
Event replaying
It you've added or changed a business rule, you may want to see how it would have behaved with the event stream, this is a use case for event replaying.
You can replay event by aggregate id or by domain.
Let's say your a bank. The BI team said they want to send a SMS each time withdrawal is made if amount is more than a value and they want to know how many SMS would have been sent. You have to:
- create the new event handler
- re-play events
The event
final class SendSMSOnWithdrawalPerformed implements EventHandler { public function onWithdrawalPerformed(WithdrawalPerformed $event): void { $user = $this->userRepository->getOwner($event->getgetAccountId()); if ($this->isMobilePhone($phoneNumber = $user->getPhoneNumber()) && $event->getAmount() > self::ALERT_AMOUNT) { // record the calls count somewhere, now you know how many SMS would have been sent $this->smsSender->send($phoneNumber); } } }
Replay:
# by domain bin/console botilka:event_store:replay --domain [domain name] # or by id bin/console botilka:event_store:replay --id [aggregate root id] # you can limit the scope with --from/-f & --to/-t
Projection replay
In the same way than replaying events, you can replay projection. If you've added a projection
and you want to replay only this projection, use the --matching/-m
options.
Matching is a regex matched against
[ProjectorFQCN]::[method]
, ie.App\BankAccount\Projection\Doctrine\BankAccountProjector::sumOfDeposit
The projection
namespace App\BankAccount\Projection\Doctrine; final class BankAccountProjector implements Projector { public function sumOfDeposit(DepositPerformed $event): void { $stmt = $this->connection->prepare('UPDATE all_the_sums SET value = value + :amount WHERE type = :type'); $stmt->prepare(['amount' => $event->getAmount(), 'type' => 'deposit']); $stmt->execute(); } public static function getSubscribedEvents() { return [ DepositPerformed::class => 'sumOfDeposit', ]; } }
Replay projection:
# by domain bin/console botilka:projector:build domain [domain name] # or by id bin/console botilka:projector:build id [aggregate root id] --matching sumOfDeposit # you can limit the scope with --from/-f & --to/-t
API Platform bridge
See the API Platform bridge documentation.
Testing
This project uses PHP Unit: vendor/bin/phpunit
.
Functionals tests are grouped under the tag functional
: vendor/bin/phpunit --group functional
.
How it works
Have a look here to better understand the design choices made and how the magic stuff happens.
todo
- Event upcasting.
- (maybe) Saga / Process manager.
- (maybe) Smart command retry on concurrency exception.
Resources
- https://github.com/dddinphp/blog-cqrs
- https://github.com/broadway/broadway
- https://github.com/jorge07/symfony-4-es-cqrs-boilerplate (uses Broadway)
- https://github.com/CodelyTV/cqrs-ddd-php-example
- https://github.com/mnavarrocarter/ddd
- https://github.com/prooph/
- https://www.youtube.com/watch?v=qBLtZN3p3FU [french]
- https://www.youtube.com/watch?v=VpzSMz_XbqM [french]