Skip to content

Commit

Permalink
Major overhaul
Browse files Browse the repository at this point in the history
  • Loading branch information
bwaidelich committed Dec 10, 2024
1 parent c5eb67c commit d591a98
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 149 deletions.
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,23 @@ This package models the example of this presentation (with a few deviations) usi

### Important Classes / Concepts

* [Command](src%2FCommand) are just a concept of this example package. They implement the [Command Marker Interface](src%2FCommand%2FCommand.php)
* [Commands](src%2FCommand) are just a concept of this example package. They implement the [Command Marker Interface](src%2FCommand%2FCommand.php)
* The [CommandHandler](src/CommandHandler.php) is the central authority, handling and verifying incoming Command
* ...it uses in-memory [Projections](src%2FProjection%2FProjection.php) to enforce hard constraints
* The [Projections](src%2FProjection%2FProjection.php) are surprisingly small because they focus on a single responsibility
* It uses in-memory [Projections](src%2FProjection%2FProjection.php) to enforce hard constraints
* For each command handler a [DecisionModel](src%2FDecisionModel%2FDecisionModel.php) instance is built that contains the state of those in-memory projections and the [AppendCondition](https://github.com/bwaidelich/dcb-eventstore/blob/main/Specification.md#AppendCondition) for new events
* The [EventSerializer](src%2FEventSerializer.php) can convert [DomainEvent](src%2FEvent%2DDomainEvent.php) instances to writable events, vice versa
* This package contains no Read Model (i.e. classic projections) yet
* *Note:* This package contains no Read Model (i.e. classic projections) yet

### Considerations / Findings

I always had the feeling, that the focus on Event Streams is a distraction to Domain-driven design. So I was very happy to come across this concept.
So far I didn't have the chance to test it in a real world scenario, but it makes a lot of sense to me and IMO this example shows, that the approach
really works out in practice (in spite of some minor caveats in the current implementation).
In the meantime I have had the chance to test it in multiple real world scenarios, and it works really well for me and simplifies things (in spite of some minor caveats in the current implementation):

* It becomes trivial to enforce constraints involving multiple entities (like in this example).
* Global uniqueness (aka "the unique username problem") can easily be achieved with DCB
* Consecutive sequences (e.g. invoice number) can be done without reservation patterns and by only reading a single event per constraint check
* When using composition like in this example, phe in-memory projections are surprisingly small because they focus on a single responsibility
* ...and more

## Usage

Expand Down
5 changes: 2 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
],
"require": {
"php": ">=8.4",
"ramsey/uuid": "^4.7",
"webmozart/assert": "^1.11",
"wwwision/dcb-eventstore": "@dev",
"wwwision/dcb-eventstore-doctrine": "@dev"
"wwwision/dcb-eventstore": "^4",
"wwwision/dcb-eventstore-doctrine": "^4"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
Expand Down
7 changes: 6 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
parameters:
level: 9
paths:
- src
- src
services:
-
class: Wwwision\DCBExample\Tests\PHPStan\DecisionModelPhpStanExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
227 changes: 101 additions & 126 deletions src/CommandHandler.php

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions src/DecisionModel/DecisionModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Wwwision\DCBExample\DecisionModel;

use Wwwision\DCBEventStore\Types\AppendCondition;

/**
* @template S of object
*/
final readonly class DecisionModel
{
/**
* @param S $state
*/
public function __construct(
public mixed $state,
public AppendCondition $appendCondition,
) {
}
}
2 changes: 1 addition & 1 deletion src/Projection/CompositeProjection.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function initialState(): object
public function apply(mixed $state, DomainEvent $domainEvent, EventEnvelope $eventEnvelope): object
{
foreach ($this->projections as $projectionKey => $projection) {
if ($projection instanceof StreamCriteriaAware && !$projection->getCriteria()->hashes()->intersect($eventEnvelope->criterionHashes)) {
if ($projection instanceof StreamCriteriaAware && !$projection->getCriteria()->matchesEvent($eventEnvelope->event)) {
continue;
}
$state->{$projectionKey} = $projection->apply($state->{$projectionKey}, $domainEvent, $eventEnvelope);
Expand Down
3 changes: 0 additions & 3 deletions src/Projection/TaggedProjection.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,6 @@ public function getCriteria(): Criteria
$criteria = [];
if ($this->wrapped instanceof StreamCriteriaAware) {
foreach ($this->wrapped->getCriteria() as $criterion) {
if (!$criterion instanceof EventTypesAndTagsCriterion) {
throw new RuntimeException(sprintf('%s only supports criteria of type %s, given: %s', self::class, EventTypesAndTagsCriterion::class, get_debug_type($criterion)));
}
$criteria[] = EventTypesAndTagsCriterion::create(
eventTypes: $criterion->eventTypes,
tags: $criterion->tags !== null ? $criterion->tags->merge($this->tags) : $this->tags,
Expand Down
13 changes: 4 additions & 9 deletions tests/Behat/Bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,48 +12,43 @@
use Doctrine\DBAL\Platforms\SqlitePlatform;
use InvalidArgumentException;
use PHPUnit\Framework\Assert;
use RuntimeException;
use Throwable;
use Wwwision\DCBEventStore\EventStore;
use Wwwision\DCBEventStore\EventStream;
use Wwwision\DCBEventStore\Helpers\InMemoryEventStore;
use Wwwision\DCBEventStore\Helpers\InMemoryEventStream;
use Wwwision\DCBEventStore\Types\AppendCondition;
use Wwwision\DCBEventStore\Types\Event;
use Wwwision\DCBEventStore\Types\Events;
use Wwwision\DCBEventStore\Types\ReadOptions;
use Wwwision\DCBEventStore\Types\SequenceNumber;
use Wwwision\DCBEventStore\Types\StreamQuery\StreamQuery;
use Wwwision\DCBEventStoreDoctrine\DoctrineEventStore;
use Wwwision\DCBExample\CommandHandler;
use Wwwision\DCBExample\Command\Command;
use Wwwision\DCBExample\Command\CreateCourse;
use Wwwision\DCBExample\Command\RegisterStudent;
use Wwwision\DCBExample\Command\RenameCourse;
use Wwwision\DCBExample\Command\SubscribeStudentToCourse;
use Wwwision\DCBExample\Command\UnsubscribeStudentFromCourse;
use Wwwision\DCBExample\Command\UpdateCourseCapacity;
use Wwwision\DCBExample\CommandHandler;
use Wwwision\DCBExample\Event\CourseCreated;
use Wwwision\DCBExample\Event\DomainEvent;
use Wwwision\DCBExample\EventSerializer;
use Wwwision\DCBExample\Event\StudentRegistered;
use Wwwision\DCBExample\Event\StudentSubscribedToCourse;
use Wwwision\DCBExample\Event\StudentUnsubscribedFromCourse;
use Wwwision\DCBExample\EventSerializer;
use Wwwision\DCBExample\Exception\ConstraintException;
use Wwwision\DCBExample\Types\CourseCapacity;
use Wwwision\DCBExample\Types\CourseId;
use Wwwision\DCBExample\Types\CourseTitle;
use Wwwision\DCBExample\Types\StudentId;

use function array_diff;
use function array_keys;
use function array_map;
use function explode;
use function func_get_args;
use function get_debug_type;
use function implode;
use function json_decode;
use function reset;
use function sprintf;

use const JSON_THROW_ON_ERROR;

final class FeatureContext implements Context
Expand Down
43 changes: 43 additions & 0 deletions tests/PHPStan/DecisionModelPhpStanExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Wwwision\DCBExample\Tests\PHPStan;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectShapeType;
use PHPStan\Type\Type;
use Wwwision\DCBExample\CommandHandler;
use Wwwision\DCBExample\DecisionModel\DecisionModel;
use Wwwision\DCBExample\Projection\Projection;

final readonly class DecisionModelPhpStanExtension implements DynamicMethodReturnTypeExtension
{

public function getClass(): string
{
return CommandHandler::class;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'buildDecisionModel';
}

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
$args = $methodCall->getArgs();
$properties = [];
foreach ($args as $argExpression) {
$nameOfParam = $argExpression->getAttributes()['originalArg']->name->name;
/** @var GenericObjectType $projectionObjectType */
$projectionObjectType = $scope->getType($argExpression->value);
$properties[$nameOfParam] = $projectionObjectType->getTemplateType(Projection::class, 'S');
}
return new GenericObjectType(DecisionModel::class, [new ObjectShapeType($properties, [])]);
}
}

0 comments on commit d591a98

Please sign in to comment.