Skip to content

Commit

Permalink
add service handler provider & more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidBadura committed Jan 4, 2025
1 parent c66018b commit 0faa1de
Show file tree
Hide file tree
Showing 18 changed files with 908 additions and 119 deletions.
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ nav:
- Message: message.md
- Store: store.md
- Subscription: subscription.md
- Command Bus: command_bus.md
- Event Bus: event_bus.md
- Advanced:
- Aggregate ID: aggregate_id.md
Expand Down
200 changes: 200 additions & 0 deletions docs/pages/command_bus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Command Bus

You can also use the Command Bus to work with your aggregate root and perform write operations such as creating or editing.
We provide a Command Bus implementation that is sufficient for our use case.
We only support handlers defined in Aggregate and no other handler services.

## Setup

```php
use Patchlevel\EventSourcing\CommandBus\DefaultCommandBus;
use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
use Patchlevel\EventSourcing\Repository\RepositoryManager;

/**
* @var AggregateRootRegistry $aggregateRootRegistry
* @var RepositoryManager $repositoryManager
*/
$commandBus = DefaultCommandBus::createForAggregateHandlers(
$aggregateRootRegistry,
$repositoryManager,
);

$commandBus->dispatch(new CreateProfile($profileId, 'John'));
$commandBus->dispatch(new ChangeProfileName($profileId, 'John Doe'));
```
## Command

```php
final class CreateProfile
{
public function __construct(
public readonly ProfileId $id,
public readonly string $name,
) {
}
}
```



```php
use Patchlevel\EventSourcing\Attribute\Id;

final class ChangeProfileName
{
public function __construct(
#[Id]
public readonly ProfileId $id,
public readonly string $name,
) {
}
}
```
## Handler

```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Handle;
use Patchlevel\EventSourcing\Attribute\Id;

#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
#[Id]
private ProfileId $id;
private string $name;

#[Handle]
public static function create(CreateProfile $command): self
{
$self = new self();

if (!$nameValidator($command->name) {
throw new InvalidArgument();
}

$self->recordThat(new ProfileCreated($command->id, $command->name));

return $self;
}

#[Handle]
public function changeName(ChangeProfileName $command): void
{
if (!$nameValidator($command->name) {
throw new InvalidArgument();
}

$this->recordThat(new NameChanged($command->name));
}

// ... apply methods
}
```
## Dependency Injection

```php
use Patchlevel\EventSourcing\Clock\SystemClock;
use Patchlevel\EventSourcing\CommandBus\DefaultCommandBus;
use Patchlevel\EventSourcing\CommandBus\ServiceLocator;
use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
use Patchlevel\EventSourcing\Repository\RepositoryManager;
use Psr\Clock\ClockInterface;

/**
* @var AggregateRootRegistry $aggregateRootRegistry
* @var RepositoryManager $repositoryManager
*/
$commandBus = DefaultCommandBus::createForAggregateHandlers(
$aggregateRootRegistry,
$repositoryManager,
new ServiceLocator([
ClockInterface::class => new SystemClock(),
]), // or other psr-11 compatible container
);
```
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Handle;
use Psr\Clock\ClockInterface;

#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
#[Handle]
public static function create(
CreateProfile $command,
ClockInterface $clock,
): self {
$self = new self();

$self->recordThat(new ProfileCreated(
$command->id,
$command->name,
$clock->now(),
));

return $self;
}

// ... apply methods
}
```
### Inject

```php
use Patchlevel\EventSourcing\CommandBus\DefaultCommandBus;
use Patchlevel\EventSourcing\CommandBus\ServiceLocator;
use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
use Patchlevel\EventSourcing\Repository\RepositoryManager;

/**
* @var AggregateRootRegistry $aggregateRootRegistry
* @var RepositoryManager $repositoryManager
*/
$commandBus = DefaultCommandBus::createForAggregateHandlers(
$aggregateRootRegistry,
$repositoryManager,
new ServiceLocator([
'name_validator' => new NameValidator(),
]), // or other psr-11 compatible container
);
```
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Handle;
use Patchlevel\EventSourcing\Attribute\Inject;

#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
#[Handle]
public static function create(
CreateProfile $command,
#[Inject('name_validator')]
NameValidator $nameValidator,
): self {
$self = new self();

if (!$nameValidator($command->name) {
throw new InvalidArgument();
}

$self->recordThat(new ProfileCreated($command->id, $command->name));

return $self;
}

// ... apply methods
}
```
## Learn more

* [How to use aggregates](aggregate.md)
* [How to use events](events.md)
* [How to use clock](clock.md)
* [How to use aggregate id](aggregate_id.md)
2 changes: 1 addition & 1 deletion src/CommandBus/AggregateHandlerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function handlerForCommand(string $commandClass): iterable
private function initialize(): void
{
foreach ($this->aggregateRootRegistry->aggregateClasses() as $aggregateClass) {
foreach (AggregateHandlerFinder::find($aggregateClass) as $handler) {
foreach (HandlerFinder::findInClass($aggregateClass) as $handler) {
if ($handler->static) {
$this->handlers[$handler->commandClass][] = new HandlerDescriptor(
new CreateAggregateHandler(
Expand Down
33 changes: 33 additions & 0 deletions src/CommandBus/ChainHandlerProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\CommandBus;

final class ChainHandlerProvider implements HandlerProvider
{
/** @param iterable<HandlerProvider> $providers */
public function __construct(
private readonly iterable $providers,
) {
}

/**
* @param class-string $commandClass
*
* @return iterable<HandlerDescriptor>
*/
public function handlerForCommand(string $commandClass): iterable
{
$handlers = [];

foreach ($this->providers as $provider) {
$handlers = [
...$handlers,
...$provider->handlerForCommand($commandClass),

Check failure on line 27 in src/CommandBus/ChainHandlerProvider.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

InvalidOperand

src/CommandBus/ChainHandlerProvider.php:27:20: InvalidOperand: Cannot use spread operator on iterable with key type mixed (see https://psalm.dev/058)
];
}

return $handlers;
}
}
11 changes: 10 additions & 1 deletion src/CommandBus/DefaultCommandBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,23 @@

final class DefaultCommandBus implements CommandBus
{
private readonly HandlerProvider $handlerProvider;

/** @var array<object> */
private array $queue;
private bool $processing;

/** @param iterable<HandlerProvider>|HandlerProvider $handlerProviders */
public function __construct(
private readonly HandlerProvider $handlerProvider,
iterable|HandlerProvider $handlerProviders,
private readonly LoggerInterface|null $logger = null,
) {
if (!$handlerProviders instanceof HandlerProvider) {
$this->handlerProvider = new ChainHandlerProvider($handlerProviders);
} else {
$this->handlerProvider = $handlerProviders;
}

$this->queue = [];
$this->processing = false;
}
Expand Down
4 changes: 1 addition & 3 deletions src/CommandBus/HandlerDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
use Closure;
use ReflectionFunction;

use function method_exists;

final class HandlerDescriptor
{
private readonly Closure $callable;
Expand All @@ -34,7 +32,7 @@ private static function closureName(Closure $closure): string
{
$reflectionFunction = new ReflectionFunction($closure);

if (method_exists($reflectionFunction, 'isAnonymous') && $reflectionFunction->isAnonymous()) {
if ($reflectionFunction->isAnonymous()) {
return 'Closure';
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,22 @@

namespace Patchlevel\EventSourcing\CommandBus;

use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
use Patchlevel\EventSourcing\Attribute\Handle;
use ReflectionClass;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;

use function class_exists;

/** @internal */
final class AggregateHandlerFinder
final class HandlerFinder
{
/**
* @param class-string<AggregateRoot> $aggregateClass
* @param class-string $classString
*
* @return iterable<AggregateHandler>
* @return iterable<HandlerReference>
*/
public static function find(string $aggregateClass): iterable
public static function findInClass(string $classString): iterable
{
$typeResolver = TypeResolver::create();
$reflectionClass = new ReflectionClass($aggregateClass);
$reflectionClass = new ReflectionClass($classString);

foreach ($reflectionClass->getMethods() as $reflectionMethod) {
$handleAttributes = $reflectionMethod->getAttributes(Handle::class);
Expand All @@ -35,7 +31,7 @@ public static function find(string $aggregateClass): iterable
$handle = $handleAttributes[0]->newInstance();

if ($handle->commandClass !== null) {
yield new AggregateHandler(
yield new HandlerReference(
$handle->commandClass,
$reflectionMethod->getName(),
$reflectionMethod->isStatic(),
Expand Down Expand Up @@ -71,16 +67,10 @@ public static function find(string $aggregateClass): iterable
);
}

/** @var class-string $commandClass */
$commandClass = $type->getClassName();

if (!class_exists($commandClass)) {
throw InvalidHandleMethod::incompatibleType(
$reflectionMethod->getDeclaringClass()->getName(),
$reflectionMethod->getName(),
);
}

yield new AggregateHandler(
yield new HandlerReference(
$commandClass,
$reflectionMethod->getName(),
$reflectionMethod->isStatic(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

namespace Patchlevel\EventSourcing\CommandBus;

/** @internal */
final class AggregateHandler
final class HandlerReference
{
/** @param class-string $commandClass */
public function __construct(
Expand Down
Loading

0 comments on commit 0faa1de

Please sign in to comment.