Skip to content
Merged
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
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file. This projec

## Unreleased

### Added

- New command bus features:
- Can now use a PSR container for the command bus to resolve both handlers and middleware. Inject the service
container via the first constructor argument.
- Commands can now be mapped to handlers on a command bus class via the `WithCommand` attribute.
- Middleware can now be added to a command bus via the `Through` attribute.
- New query bus features:
- Can now use a PSR container for the query bus to resolve both handlers and middleware. Inject the service
container via the first constructor argument.
- Queries can now be mapped to handlers on a query bus class via the `WithQuery` attribute.
- Middleware can now be added to a query bus via the `Through` attribute.
- New inbound event bus features:
- Can now use a PSR container for the inbound event bus to resolve both handlers and middleware. Inject the service
container via the first constructor argument.
- Integration events can now be mapped to handlers on an inbound event bus class via the `WithEvent` attribute.
- The default handler can be set on the inbound event bus via the `WithDefault` attribute.
- Middleware can now be added to an inbound event bus via the `Through` attribute.
- New outbound event bus features, when using the component publisher:
- Can now use a PSR container for the outbound event bus to resolve both publishers and middleware. Inject the
service container via the constructor.
- Integration events can now be mapped to publishers on a publisher handler container class via the `Publishes`
attribute.
- The default publisher can be set on the outbound event publisher via the `DefaultPublisher` attribute.
- Middleware can now be added to an outbound event publisher via the `Through` attribute.
- New queue features, when using the component queue:
- Can now use a PSR container for the queue to resolve both enqueuers and middleware. Inject the service container
via the constructor.
- Commands can now be mapped to enqueuers on a publisher handler container class via the `Queues` attribute.
- The default enqueuer can be set on the outbound event publisher via the `DefaultEnqueuer` attribute.
- Middleware can now be added to the queue via the `Through` attribute.
- In the Application layer, the `QueryHandlerContainer`, `CommandHandlerContainer` and `EventHandlerContainer` classes
can now fallback to resolving handlers from a PSR service container. Inject the service container via their
constructors.
- In the Infrastructure layer, the `PublisherHandlerContainer` and `EnqueuerContainer` can now fallback to resolving
handlers/enqueuers from a PSR service container. Inject the service container via the constructor.
- The outbound event bus `ClosurePublisher` and the `ClosureQueue` classes now both accept a PSR container for their
middleware. Additionally, middleware can be set on instances of closure publishers via the `Through` attribute.
- The pipeline `PipeContainer` class can now fallback to resolving pipes from a PSR service container. Inject the
service container via the pipe container's only constructor argument.
- The `FakeUnitOfWork` class now has integer properties for the number of attempts, commits and rollbacks.
- New `FakeContainer` class for faking a PSR container in tests.

## [5.0.0] - 2025-12-09

### Added
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
"require": {
"php": "^8.2",
"ext-json": "*",
"psr/container": "^2.0",
"psr/log": "^2.0 || ^3.0",
"ramsey/uuid": "^4.7",
"symfony/polyfill-php84": "^1.33"
"symfony/polyfill-php84": "^1.33",
"symfony/polyfill-php85": "^1.33"
},
"require-dev": {
"deptrac/deptrac": "^4.4",
Expand Down
7 changes: 7 additions & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ deptrac:
value: CloudCreativity\\Modules\\Contracts\\Infrastructure\\*
- type: classLike
value: CloudCreativity\\Modules\\Infrastructure\\*
- name: PSR Container
collectors:
- type: classLike
value: Psr\\Container\\*
- name: PSR Log
collectors:
- type: classLike
Expand All @@ -39,17 +43,20 @@ deptrac:
ruleset:
Toolkit:
- Attributes
- PSR Container
Domain:
- Toolkit
- Attributes
Application:
- Toolkit
- Domain
- PSR Container
- PSR Log
- Attributes
Infrastructure:
- Toolkit
- Domain
- Application
- PSR Container
- PSR Log
- Attributes
3 changes: 3 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit/</directory>
</testsuite>
<testsuite name="Integration">
<directory suffix="Test.php">./tests/Integration/</directory>
</testsuite>
</testsuites>
<php>
<ini name="error_reporting" value="E_ALL"/>
Expand Down
42 changes: 38 additions & 4 deletions src/Application/Bus/CommandDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,42 @@

namespace CloudCreativity\Modules\Application\Bus;

use CloudCreativity\Modules\Contracts\Application\Bus\CommandHandlerContainer;
use CloudCreativity\Modules\Contracts\Application\Bus\CommandHandlerContainer as ICommandHandlerContainer;
use CloudCreativity\Modules\Contracts\Application\Ports\Driving\CommandDispatcher as ICommandDispatcher;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer as IPipeContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Result\Result;
use CloudCreativity\Modules\Toolkit\Pipeline\MiddlewareProcessor;
use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer;
use CloudCreativity\Modules\Toolkit\Pipeline\PipelineBuilder;
use CloudCreativity\Modules\Toolkit\Pipeline\Through;
use Psr\Container\ContainerInterface;
use ReflectionClass;

class CommandDispatcher implements ICommandDispatcher
{
private readonly ICommandHandlerContainer $handlers;

private readonly ?IPipeContainer $middleware;

/**
* @var array<callable|string>
*/
private array $pipes = [];

public function __construct(
private readonly CommandHandlerContainer $handlers,
private readonly ?PipeContainer $middleware = null,
ContainerInterface|ICommandHandlerContainer $handlers,
?IPipeContainer $middleware = null,
) {
$this->handlers = $handlers instanceof ContainerInterface ?
new CommandHandlerContainer($handlers) :
$handlers;

$this->middleware = $middleware === null && $handlers instanceof ContainerInterface
? new PipeContainer($handlers)
: $middleware;

$this->autowire();
}

/**
Expand Down Expand Up @@ -77,4 +94,21 @@ private function execute(Command $command): Result

return $result;
}

private function autowire(): void
{
$reflection = new ReflectionClass($this);

if ($this->handlers instanceof CommandHandlerContainer) {
foreach ($reflection->getAttributes(WithCommand::class) as $attribute) {
$instance = $attribute->newInstance();
$this->handlers->bind($instance->command, $instance->handler);
}
}

foreach ($reflection->getAttributes(Through::class) as $attribute) {
$instance = $attribute->newInstance();
$this->pipes = $instance->pipes;
}
}
}
13 changes: 3 additions & 10 deletions src/Application/Bus/CommandHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@

namespace CloudCreativity\Modules\Application\Bus;

use CloudCreativity\Modules\Application\Messages\HandlesMessages;
use CloudCreativity\Modules\Contracts\Application\Bus\CommandHandler as ICommandHandler;
use CloudCreativity\Modules\Contracts\Application\Messages\DispatchThroughMiddleware;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
use CloudCreativity\Modules\Contracts\Toolkit\Result\Result;

final readonly class CommandHandler implements ICommandHandler
{
use HandlesMessages;

public function __construct(private object $handler)
{
}
Expand All @@ -37,13 +39,4 @@ public function __invoke(Command $command): Result

return $result;
}

public function middleware(): array
{
if ($this->handler instanceof DispatchThroughMiddleware) {
return $this->handler->middleware();
}

return [];
}
}
31 changes: 23 additions & 8 deletions src/Application/Bus/CommandHandlerContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,48 @@
use CloudCreativity\Modules\Application\ApplicationException;
use CloudCreativity\Modules\Contracts\Application\Bus\CommandHandlerContainer as ICommandHandlerContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
use Psr\Container\ContainerInterface;

final class CommandHandlerContainer implements ICommandHandlerContainer
{
/**
* @var array<class-string<Command>, Closure>
* @var array<class-string<Command>, class-string|Closure>
*/
private array $bindings = [];

public function __construct(private readonly ?ContainerInterface $container = null)
{
}

/**
* Bind a command handler into the container.
*
* @param class-string<Command> $commandClass
* @param Closure(): object $binding
* @param class-string|(Closure(): object) $binding
*/
public function bind(string $commandClass, Closure $binding): void
public function bind(string $commandClass, Closure|string $binding): void
{
if (is_string($binding) && $this->container === null) {
throw new ApplicationException('Cannot use a string command handler binding without a PSR container.');
}

$this->bindings[$commandClass] = $binding;
}

public function get(string $commandClass): CommandHandler
{
$factory = $this->bindings[$commandClass] ?? null;
$binding = $this->bindings[$commandClass] ?? null;

if ($binding instanceof Closure) {
$instance = $binding();
assert(is_object($instance), "Command handler binding for {$commandClass} must return an object.");
return new CommandHandler($instance);
}

if ($factory) {
$innerHandler = $factory();
assert(is_object($innerHandler), "Command handler binding for {$commandClass} must return an object.");
return new CommandHandler($innerHandler);
if (is_string($binding)) {
$instance = $this->container?->get($binding);
assert(is_object($instance), "PSR container command handler binding {$binding} is not an object.");
return new CommandHandler($instance);
}

throw new ApplicationException('No command handler bound for command class: ' . $commandClass);
Expand Down
43 changes: 39 additions & 4 deletions src/Application/Bus/QueryDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,42 @@

namespace CloudCreativity\Modules\Application\Bus;

use CloudCreativity\Modules\Contracts\Application\Bus\QueryHandlerContainer;
use CloudCreativity\Modules\Contracts\Application\Bus\QueryHandlerContainer as IQueryHandlerContainer;
use CloudCreativity\Modules\Contracts\Application\Ports\Driving\QueryDispatcher as IQueryDispatcher;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Query;
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer as IPipeContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Result\Result;
use CloudCreativity\Modules\Toolkit\Pipeline\MiddlewareProcessor;
use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer;
use CloudCreativity\Modules\Toolkit\Pipeline\PipelineBuilder;
use CloudCreativity\Modules\Toolkit\Pipeline\Through;
use Psr\Container\ContainerInterface;
use ReflectionClass;

class QueryDispatcher implements IQueryDispatcher
{
private readonly IQueryHandlerContainer $handlers;

private readonly ?IPipeContainer $middleware;

/**
* @var array<callable|string>
*/
private array $pipes = [];

public function __construct(
private readonly QueryHandlerContainer $handlers,
private readonly ?PipeContainer $middleware = null,
ContainerInterface|IQueryHandlerContainer $handlers,
?IPipeContainer $middleware = null,
) {
$this->handlers = $handlers instanceof ContainerInterface ?
new QueryHandlerContainer($handlers) :
$handlers;

$this->middleware = $middleware === null && $handlers instanceof ContainerInterface
? new PipeContainer($handlers)
: $middleware;

$this->autowire();
}

/**
Expand Down Expand Up @@ -77,4 +94,22 @@ private function execute(Query $query): Result

return $result;
}


private function autowire(): void
{
$reflection = new ReflectionClass($this);

if ($this->handlers instanceof QueryHandlerContainer) {
foreach ($reflection->getAttributes(WithQuery::class) as $attribute) {
$instance = $attribute->newInstance();
$this->handlers->bind($instance->query, $instance->handler);
}
}

foreach ($reflection->getAttributes(Through::class) as $attribute) {
$instance = $attribute->newInstance();
$this->pipes = $instance->pipes;
}
}
}
13 changes: 3 additions & 10 deletions src/Application/Bus/QueryHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@

namespace CloudCreativity\Modules\Application\Bus;

use CloudCreativity\Modules\Application\Messages\HandlesMessages;
use CloudCreativity\Modules\Contracts\Application\Bus\QueryHandler as IQueryHandler;
use CloudCreativity\Modules\Contracts\Application\Messages\DispatchThroughMiddleware;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Query;
use CloudCreativity\Modules\Contracts\Toolkit\Result\Result;

final readonly class QueryHandler implements IQueryHandler
{
use HandlesMessages;

public function __construct(private object $handler)
{
}
Expand All @@ -37,13 +39,4 @@ public function __invoke(Query $query): Result

return $result;
}

public function middleware(): array
{
if ($this->handler instanceof DispatchThroughMiddleware) {
return $this->handler->middleware();
}

return [];
}
}
Loading