Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: 'composer'
directory: '/'
schedule:
interval: 'daily'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
30 changes: 30 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Test

on:
push:
branches:
- main
pull_request: ~

jobs:
test:
name: Test
strategy:
matrix:
php-version: ['8.3']
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer

- name: Install composer dependencies
run: composer install --prefer-dist --no-progress

- name: Run tests
run: composer test
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

###> symfony/framework-bundle ###
/.env.local
/.env.local.php
Expand Down
37 changes: 37 additions & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

$finder = (new PhpCsFixer\Finder())
->in(['src', 'bin', 'config', 'public', 'migrations']);

return (new PhpCsFixer\Config())
->setCacheFile('var/.php-cs-fixer.cache')
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
->setRiskyAllowed(true)
->setRules([
'@PhpCsFixer' => true,
'@Symfony' => true,
'@PER-CS2.0' => true,
'binary_operator_spaces' => false,
'cast_spaces' => true,
'concat_space' => ['spacing' => 'one'],
'declare_strict_types' => true,
'global_namespace_import' => [
'import_classes' => true,
'import_constants' => null,
'import_functions' => null,
],
'multiline_whitespace_before_semicolons' => false,
'phpdoc_align' => ['align' => 'left'],
'phpdoc_order' => true,
'phpdoc_to_comment' => false,
'php_unit_test_class_requires_covers' => false,
'single_line_throw' => false,
'trailing_comma_in_multiline' => [
'elements' => ['arrays', 'arguments', 'parameters'],
],
'void_return' => true,
'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false],
])
->setFinder($finder);
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Gember

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Event Sourcing with DCB in PHP
Example project (Proof of Concept) with Event Sourcing in PHP using the 'Dynamic Consistency Boundary' pattern (DCB).

## Background
'Dynamic Consistency Boundary' pattern is introduced by Sara Pellegrini in 2023 as a thought process (rethinking Event Sourcing).
Explained in her talk: ["The Aggregate is dead. Long live the Aggregate!"](https://sara.event-thinking.io/2023/04/kill-aggregate-chapter-1-I-am-here-to-kill-the-aggregate.html).

Currently, existing Event Sourcing frameworks rely on a consistent boundary within _aggregates_ (primary citizen), as explained in 'The blue book':

> Cluster the ENTITIES and VALUE OBJECTS into AGGREGATES and define boundaries around each.
Choose one ENTITY to be the root of each AGGREGATE, and control all access to the objects inside the boundary through the root. Allow external objects to hold reference to the root only.

_From "Domain-Driven Design: Tackling Complexity in the Heart of Software" by Eric Evans (the 'blue book')._

However, Event Sourcing with DCB has some advantages/solves some pitfalls over a traditional aggregate setup.

### Reusable domain events
Traditionally, a defined boundary enforces a strict 1-on-1 relation between an aggregate and a domain event.
This makes the aggregate internally consistent. The DCB pattern removes this strict 1-on-1 relation. Instead, a domain event is considered as a _Pure event_.

This allows us to create business decision/domain models based on a **subset of specific domain events** as well as from **multiple domain identities**.

These business decision models fit very well in how [EventStorming](https://github.com/ddd-crew/eventstorming-glossary-cheat-sheet) considers aggregates currently.
Instead of aggregates, in EventStorming, these are now called 'system' or 'consistent business rule',
as the term 'aggregate' is difficult to explain for non-technical actors in a software design process.

### No aggregate synchronisation nor domain services needed
As a domain event can be used by multiple business decision models, there is no synchronization needed between aggregates.
Duplicated events as well as domain services (typically to handle business logic spanning multiple aggregates) are not needed anymore.

### Concurrency problems
As an aggregate is based on internal consistency, two concurrent modification are impossible,
even when the modified data is (domain-wise) independent of each other.
The DCB pattern removes this blocking behavior when handled by multiple business decision models.

**But overall it removes accidental complexity which aggregates introduced, allowing to build software closer to the real world.**

More details, pros and cons explained (highly recommended):
- https://sara.event-thinking.io/2023/04/kill-aggregate-chapter-1-I-am-here-to-kill-the-aggregate.html
- https://www.youtube.com/watch?v=wXt54BawI-8
- https://www.axoniq.io/blog/rethinking-microservices-architecture-through-dynamic-consistency-boundaries

## This example project
_The DCB pattern is an interesting concept, but this does not advocate to remove aggregates completely.
Instead, a hybrid solution with aggregates and business decision models is probably more likely, depending on your domain._

This example project is using a fictive domain (taken from Sara Pellegrini's blog) where students can subscribe to courses (of any kind).
Deliberately this is all what is defined for this domain, to focus on how this could be implemented when using Event Sourcing with the DCB pattern in mind.

It contains both classic aggregates (e.g. [Course](src/Domain/Course/Course.php), [Student](src/Domain/Student/Student.php)) as well as business decision models (e.g. [ChangeCourseCapacity](src/Domain/Course/ChangeCourseCapacity.php), [SubscribeStudentToCourse](src/Domain/StudentToCourseSubscription/SubscribeStudentToCourse.php), [UnsubscribeStudentFromCourse](src/Domain/StudentToCourseSubscription/UnsubscribeStudentFromCourse.php)).

Inspired by other PHP libraries such as [Broadway](https://github.com/broadway), [EventSauce](https://github.com/EventSaucePHP), [Prooph](https://github.com/prooph) and [Ecotone](https://github.com/ecotoneframework) as well as [Axon Framework](https://github.com/AxonFramework) for Java.

### How to run
First start docker compose for a database
```
docker compose up
```

Then run migrations on your local machine:
```
bin/console doctrine:migrations:migrate
```

You're all set, see what commands you can run:
```
bin/console gember
```
19 changes: 19 additions & 0 deletions captainhook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"pre-push": {
"enabled": true,
"actions": [
{
"action": "composer cs:dry-run"
},
{
"action": "composer rector:dry-run"
},
{
"action": "composer phpstan"
},
{
"action": "composer dependency-analyser"
}
]
}
}
28 changes: 22 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,17 @@
"symfony/dotenv": "7.1.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "7.1.*",
"symfony/property-access": "7.1.*",
"symfony/runtime": "7.1.*",
"symfony/yaml": "7.1.*"
},
"require-dev": {
"captainhook/captainhook": "^5.23",
"friendsofphp/php-cs-fixer": "^3.58",
"phpstan/phpstan": "^1.11",
"rector/rector": "^1.2",
"shipmonk/composer-dependency-analyser": "^1.5"
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
Expand All @@ -54,11 +62,6 @@
"Gember\\ExampleEventSourcingDcb\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Gember\\ExampleEventSourcingDcb\\Test\\": "tests/"
}
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
Expand All @@ -83,6 +86,19 @@
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
}
},
"cs": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php",
"cs:dry-run": "vendor/bin/php-cs-fixer fix --diff --dry-run --config=.php-cs-fixer.php",
"dependency-analyser": "vendor/bin/composer-dependency-analyser",
"phpstan": "vendor/bin/phpstan analyse -c phpstan.neon",
"phpstan:baseline": "vendor/bin/phpstan analyse -c phpstan.neon --generate-baseline phpstan-baseline.php src",
"rector": "vendor/bin/rector process --ansi",
"rector:dry-run": "vendor/bin/rector process --ansi --dry-run",
"test": [
"@rector:dry-run",
"@cs:dry-run",
"@phpstan",
"@dependency-analyser"
]
}
}
Loading
Loading