diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e6e5e8..c4a99c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/composer.json b/composer.json
index caf5551..87b1cfa 100644
--- a/composer.json
+++ b/composer.json
@@ -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",
diff --git a/deptrac.yaml b/deptrac.yaml
index 2287983..7ce16fe 100644
--- a/deptrac.yaml
+++ b/deptrac.yaml
@@ -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
@@ -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
diff --git a/phpunit.xml b/phpunit.xml
index 2968136..c830534 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -19,6 +19,9 @@
./tests/Unit/
+
+ ./tests/Integration/
+
diff --git a/src/Application/Bus/CommandDispatcher.php b/src/Application/Bus/CommandDispatcher.php
index 2a33820..3d975b9 100644
--- a/src/Application/Bus/CommandDispatcher.php
+++ b/src/Application/Bus/CommandDispatcher.php
@@ -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
*/
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();
}
/**
@@ -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;
+ }
+ }
}
diff --git a/src/Application/Bus/CommandHandler.php b/src/Application/Bus/CommandHandler.php
index 5ce85a6..5853e6a 100644
--- a/src/Application/Bus/CommandHandler.php
+++ b/src/Application/Bus/CommandHandler.php
@@ -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)
{
}
@@ -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 [];
- }
}
diff --git a/src/Application/Bus/CommandHandlerContainer.php b/src/Application/Bus/CommandHandlerContainer.php
index 5c74c2b..ca7b505 100644
--- a/src/Application/Bus/CommandHandlerContainer.php
+++ b/src/Application/Bus/CommandHandlerContainer.php
@@ -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, Closure>
+ * @var array, class-string|Closure>
*/
private array $bindings = [];
+ public function __construct(private readonly ?ContainerInterface $container = null)
+ {
+ }
+
/**
* Bind a command handler into the container.
*
* @param class-string $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);
diff --git a/src/Application/Bus/QueryDispatcher.php b/src/Application/Bus/QueryDispatcher.php
index a19ec4d..dbc9e10 100644
--- a/src/Application/Bus/QueryDispatcher.php
+++ b/src/Application/Bus/QueryDispatcher.php
@@ -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
*/
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();
}
/**
@@ -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;
+ }
+ }
}
diff --git a/src/Application/Bus/QueryHandler.php b/src/Application/Bus/QueryHandler.php
index 8b4470c..9d0dd50 100644
--- a/src/Application/Bus/QueryHandler.php
+++ b/src/Application/Bus/QueryHandler.php
@@ -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)
{
}
@@ -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 [];
- }
}
diff --git a/src/Application/Bus/QueryHandlerContainer.php b/src/Application/Bus/QueryHandlerContainer.php
index 4352fe5..892df79 100644
--- a/src/Application/Bus/QueryHandlerContainer.php
+++ b/src/Application/Bus/QueryHandlerContainer.php
@@ -16,33 +16,48 @@
use CloudCreativity\Modules\Application\ApplicationException;
use CloudCreativity\Modules\Contracts\Application\Bus\QueryHandlerContainer as IQueryHandlerContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Query;
+use Psr\Container\ContainerInterface;
final class QueryHandlerContainer implements IQueryHandlerContainer
{
/**
- * @var array,Closure>
+ * @var array, class-string|Closure>
*/
private array $bindings = [];
+ public function __construct(private readonly ?ContainerInterface $container = null)
+ {
+ }
+
/**
* Bind a query handler into the container.
*
* @param class-string $queryClass
- * @param Closure(): object $binding
+ * @param class-string|(Closure(): object) $binding
*/
- public function bind(string $queryClass, Closure $binding): void
+ public function bind(string $queryClass, Closure|string $binding): void
{
+ if (is_string($binding) && !$this->container) {
+ throw new ApplicationException('Cannot use a string query handler binding without a PSR container.');
+ }
+
$this->bindings[$queryClass] = $binding;
}
public function get(string $queryClass): QueryHandler
{
- $factory = $this->bindings[$queryClass] ?? null;
+ $binding = $this->bindings[$queryClass] ?? null;
+
+ if ($binding instanceof Closure) {
+ $instance = $binding();
+ assert(is_object($instance), "Query handler binding for {$queryClass} must return an object.");
+ return new QueryHandler($instance);
+ }
- if ($factory) {
- $innerHandler = $factory();
- assert(is_object($innerHandler), "Query handler binding for {$queryClass} must return an object.");
- return new QueryHandler($innerHandler);
+ if (is_string($binding)) {
+ $instance = $this->container?->get($binding);
+ assert(is_object($instance), "PSR container query handler binding {$binding} is not an object.");
+ return new QueryHandler($instance);
}
throw new ApplicationException('No query handler bound for query class: ' . $queryClass);
diff --git a/src/Application/Bus/WithCommand.php b/src/Application/Bus/WithCommand.php
new file mode 100644
index 0000000..d689ce4
--- /dev/null
+++ b/src/Application/Bus/WithCommand.php
@@ -0,0 +1,28 @@
+ $command
+ * @param class-string $handler
+ */
+ public function __construct(public string $command, public string $handler)
+ {
+ }
+}
diff --git a/src/Application/Bus/WithQuery.php b/src/Application/Bus/WithQuery.php
new file mode 100644
index 0000000..4dafba1
--- /dev/null
+++ b/src/Application/Bus/WithQuery.php
@@ -0,0 +1,28 @@
+ $query
+ * @param class-string $handler
+ */
+ public function __construct(public string $query, public string $handler)
+ {
+ }
+}
diff --git a/src/Application/InboundEventBus/EventHandler.php b/src/Application/InboundEventBus/EventHandler.php
index 1f86055..8e0def6 100644
--- a/src/Application/InboundEventBus/EventHandler.php
+++ b/src/Application/InboundEventBus/EventHandler.php
@@ -12,12 +12,14 @@
namespace CloudCreativity\Modules\Application\InboundEventBus;
+use CloudCreativity\Modules\Application\Messages\HandlesMessages;
use CloudCreativity\Modules\Contracts\Application\InboundEventBus\EventHandler as IEventHandler;
-use CloudCreativity\Modules\Contracts\Application\Messages\DispatchThroughMiddleware;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\IntegrationEvent;
final readonly class EventHandler implements IEventHandler
{
+ use HandlesMessages;
+
public function __construct(private object $handler)
{
}
@@ -32,13 +34,4 @@ public function __invoke(IntegrationEvent $event): void
$this->handler->handle($event);
}
-
- public function middleware(): array
- {
- if ($this->handler instanceof DispatchThroughMiddleware) {
- return $this->handler->middleware();
- }
-
- return [];
- }
}
diff --git a/src/Application/InboundEventBus/EventHandlerContainer.php b/src/Application/InboundEventBus/EventHandlerContainer.php
index 5a9cef1..e5007bb 100644
--- a/src/Application/InboundEventBus/EventHandlerContainer.php
+++ b/src/Application/InboundEventBus/EventHandlerContainer.php
@@ -16,40 +16,68 @@
use CloudCreativity\Modules\Application\ApplicationException;
use CloudCreativity\Modules\Contracts\Application\InboundEventBus\EventHandlerContainer as IEventHandlerContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\IntegrationEvent;
+use Psr\Container\ContainerInterface;
final class EventHandlerContainer implements IEventHandlerContainer
{
/**
- * @var array, Closure>
+ * @var array, Closure|non-empty-string>
*/
private array $bindings = [];
/**
- * @param ?Closure(): object $default
+ * @param (Closure(): object)|non-empty-string|null $default
*/
- public function __construct(private readonly ?Closure $default = null)
- {
+ public function __construct(
+ private Closure|string|null $default = null,
+ private readonly ?ContainerInterface $container = null,
+ ) {
}
/**
* Bind a handler factory into the container.
*
* @param class-string $eventName
- * @param Closure(): object $binding
+ * @param (Closure(): object)|non-empty-string $binding
*/
- public function bind(string $eventName, Closure $binding): void
+ public function bind(string $eventName, Closure|string $binding): void
{
+ if (is_string($binding) && $this->container === null) {
+ throw new ApplicationException('Cannot use a string event handler binding without a PSR container.');
+ }
+
$this->bindings[$eventName] = $binding;
}
+ /**
+ * Bind a default handler factory into the container.
+ *
+ * @param (Closure(): object)|non-empty-string $binding
+ */
+ public function withDefault(Closure|string $binding): void
+ {
+ if ($this->default === null) {
+ $this->default = $binding;
+ return;
+ }
+
+ throw new ApplicationException('Default event handler binding is already set.');
+ }
+
public function get(string $eventName): EventHandler
{
- $factory = $this->bindings[$eventName] ?? $this->default;
+ $binding = $this->bindings[$eventName] ?? $this->default;
+
+ if ($binding instanceof Closure) {
+ $instance = $binding();
+ assert(is_object($instance), "Handler binding for integration event {$eventName} must return an object.");
+ return new EventHandler($instance);
+ }
- if ($factory) {
- $handler = $factory();
- assert(is_object($handler), "Handler binding for integration event {$eventName} must return an object.");
- return new EventHandler($handler);
+ if (is_string($binding)) {
+ $instance = $this->container?->get($binding);
+ assert(is_object($instance), "PSR container event handler binding {$binding} is not an object.");
+ return new EventHandler($instance);
}
throw new ApplicationException('No handler bound for integration event: ' . $eventName);
diff --git a/src/Application/InboundEventBus/InboundEventDispatcher.php b/src/Application/InboundEventBus/InboundEventDispatcher.php
index 691905c..60f4da5 100644
--- a/src/Application/InboundEventBus/InboundEventDispatcher.php
+++ b/src/Application/InboundEventBus/InboundEventDispatcher.php
@@ -12,24 +12,40 @@
namespace CloudCreativity\Modules\Application\InboundEventBus;
-use CloudCreativity\Modules\Contracts\Application\InboundEventBus\EventHandlerContainer;
+use CloudCreativity\Modules\Contracts\Application\InboundEventBus\EventHandlerContainer as IEventHandlerContainer;
use CloudCreativity\Modules\Contracts\Application\Ports\Driving\InboundEventDispatcher as IInboundEventDispatcher;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\IntegrationEvent;
-use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer;
+use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer as IPipeContainer;
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;
class InboundEventDispatcher implements IInboundEventDispatcher
{
+ private readonly IEventHandlerContainer $handlers;
+
+ private readonly ?IPipeContainer $middleware;
+
/**
* @var array
*/
private array $pipes = [];
public function __construct(
- private readonly EventHandlerContainer $handlers,
- private readonly ?PipeContainer $middleware = null,
+ ContainerInterface|IEventHandlerContainer $handlers,
+ ?IPipeContainer $middleware = null,
) {
+ $this->handlers = $handlers instanceof ContainerInterface ?
+ new EventHandlerContainer(container: $handlers) :
+ $handlers;
+
+ $this->middleware = $middleware === null && $handlers instanceof ContainerInterface ?
+ new PipeContainer(container: $handlers) :
+ $middleware;
+
+ $this->autowire();
}
/**
@@ -65,4 +81,26 @@ private function execute(IntegrationEvent $event): void
$pipeline->process($event);
}
+
+ private function autowire(): void
+ {
+ $reflection = new \ReflectionClass($this);
+
+ if ($this->handlers instanceof EventHandlerContainer) {
+ foreach ($reflection->getAttributes(WithEvent::class) as $attribute) {
+ $instance = $attribute->newInstance();
+ $this->handlers->bind($instance->event, $instance->handler);
+ }
+
+ foreach ($reflection->getAttributes(WithDefault::class) as $attribute) {
+ $instance = $attribute->newInstance();
+ $this->handlers->withDefault($instance->handler);
+ }
+ }
+
+ foreach ($reflection->getAttributes(Through::class) as $attribute) {
+ $instance = $attribute->newInstance();
+ $this->pipes = $instance->pipes;
+ }
+ }
}
diff --git a/src/Application/InboundEventBus/WithDefault.php b/src/Application/InboundEventBus/WithDefault.php
new file mode 100644
index 0000000..2b91975
--- /dev/null
+++ b/src/Application/InboundEventBus/WithDefault.php
@@ -0,0 +1,26 @@
+ $event
+ * @param class-string $handler
+ */
+ public function __construct(public string $event, public string $handler)
+ {
+ }
+}
diff --git a/src/Application/Messages/HandlesMessages.php b/src/Application/Messages/HandlesMessages.php
new file mode 100644
index 0000000..67b3b72
--- /dev/null
+++ b/src/Application/Messages/HandlesMessages.php
@@ -0,0 +1,36 @@
+handler instanceof DispatchThroughMiddleware) {
+ return $this->handler->middleware();
+ }
+
+ $reflection = new ReflectionClass($this->handler);
+
+ foreach ($reflection->getAttributes(Through::class) as $attribute) {
+ $instance = $attribute->newInstance();
+ return $instance->pipes;
+ }
+
+ return [];
+ }
+}
diff --git a/src/Infrastructure/OutboundEventBus/ClosurePublisher.php b/src/Infrastructure/OutboundEventBus/ClosurePublisher.php
index a541298..3b661cd 100644
--- a/src/Infrastructure/OutboundEventBus/ClosurePublisher.php
+++ b/src/Infrastructure/OutboundEventBus/ClosurePublisher.php
@@ -18,6 +18,9 @@
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer;
use CloudCreativity\Modules\Toolkit\Pipeline\MiddlewareProcessor;
use CloudCreativity\Modules\Toolkit\Pipeline\PipelineBuilder;
+use CloudCreativity\Modules\Toolkit\Pipeline\Through;
+use Psr\Container\ContainerInterface;
+use ReflectionClass;
class ClosurePublisher implements OutboundEventPublisher
{
@@ -33,8 +36,9 @@ class ClosurePublisher implements OutboundEventPublisher
public function __construct(
private readonly Closure $fn,
- private readonly ?PipeContainer $middleware = null,
+ private readonly ContainerInterface|PipeContainer|null $middleware = null,
) {
+ $this->autowire();
}
/**
@@ -67,4 +71,14 @@ public function publish(IntegrationEvent $event): void
$pipeline->process($event);
}
+
+ private function autowire(): void
+ {
+ $reflection = new ReflectionClass($this);
+
+ foreach ($reflection->getAttributes(Through::class) as $attribute) {
+ $instance = $attribute->newInstance();
+ $this->pipes = $instance->pipes;
+ }
+ }
}
diff --git a/src/Infrastructure/OutboundEventBus/ComponentPublisher.php b/src/Infrastructure/OutboundEventBus/ComponentPublisher.php
index d876d2e..4d9e715 100644
--- a/src/Infrastructure/OutboundEventBus/ComponentPublisher.php
+++ b/src/Infrastructure/OutboundEventBus/ComponentPublisher.php
@@ -13,23 +13,40 @@
namespace CloudCreativity\Modules\Infrastructure\OutboundEventBus;
use CloudCreativity\Modules\Contracts\Application\Ports\Driven\OutboundEventPublisher;
-use CloudCreativity\Modules\Contracts\Infrastructure\OutboundEventBus\PublisherHandlerContainer;
+use CloudCreativity\Modules\Contracts\Infrastructure\OutboundEventBus\PublisherHandlerContainer as IPublisherHandlerContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\IntegrationEvent;
-use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer;
+use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer as IPipeContainer;
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 ComponentPublisher implements OutboundEventPublisher
{
+ private readonly IPublisherHandlerContainer $handlers;
+
+ private readonly ?IPipeContainer $middleware;
+
/**
* @var array
*/
private array $pipes = [];
public function __construct(
- private readonly PublisherHandlerContainer $handlers,
- private readonly ?PipeContainer $middleware = null,
+ ContainerInterface|IPublisherHandlerContainer $handlers,
+ ?IPipeContainer $middleware = null,
) {
+ $this->handlers = $handlers instanceof ContainerInterface ?
+ new PublisherHandlerContainer(container: $handlers) :
+ $handlers;
+
+ $this->middleware = $middleware === null && $handlers instanceof ContainerInterface ?
+ new PipeContainer(container: $handlers) :
+ $middleware;
+
+ $this->autowire();
}
/**
@@ -55,4 +72,24 @@ public function publish(IntegrationEvent $event): void
$pipeline->process($event);
}
+
+ private function autowire(): void
+ {
+ $reflection = new ReflectionClass($this);
+
+ if ($this->handlers instanceof PublisherHandlerContainer) {
+ foreach ($reflection->getAttributes(Publishes::class) as $attribute) {
+ $instance = $attribute->newInstance();
+ $this->handlers->bind($instance->event, $instance->publisher);
+ }
+
+ foreach ($reflection->getAttributes(DefaultPublisher::class) as $attribute) {
+ $this->handlers->withDefault($attribute->newInstance()->publisher);
+ }
+ }
+
+ foreach ($reflection->getAttributes(Through::class) as $attribute) {
+ $this->pipes = $attribute->newInstance()->pipes;
+ }
+ }
}
diff --git a/src/Infrastructure/OutboundEventBus/DefaultPublisher.php b/src/Infrastructure/OutboundEventBus/DefaultPublisher.php
new file mode 100644
index 0000000..d9ff172
--- /dev/null
+++ b/src/Infrastructure/OutboundEventBus/DefaultPublisher.php
@@ -0,0 +1,26 @@
+, Closure>
+ * @var array, Closure|string>
*/
private array $bindings = [];
- public function __construct(private readonly ?Closure $default = null)
- {
+ public function __construct(
+ private Closure|string|null $default = null,
+ private readonly ?ContainerInterface $container = null,
+ ) {
+ if (is_string($this->default) && $this->container === null) {
+ throw new InfrastructureException(
+ 'Cannot bind default event publisher handler as a string without a PSR container.',
+ );
+ }
}
/**
@@ -34,19 +42,46 @@ public function __construct(private readonly ?Closure $default = null)
*
* @param class-string $eventName
*/
- public function bind(string $eventName, Closure $binding): void
+ public function bind(string $eventName, Closure|string $binding): void
{
+ if (is_string($binding) && $this->container === null) {
+ throw new InfrastructureException(
+ 'Cannot bind event publisher handler as a string without a PSR container.',
+ );
+ }
+
$this->bindings[$eventName] = $binding;
}
+ public function withDefault(Closure|string $binding): void
+ {
+ if ($this->default !== null) {
+ throw new InfrastructureException('Default event publisher handler is already set.');
+ }
+
+ if (is_string($binding) && $this->container === null) {
+ throw new InfrastructureException(
+ 'Cannot bind default event publisher handler as a string without a PSR container.',
+ );
+ }
+
+ $this->default = $binding;
+ }
+
public function get(string $eventName): PublisherHandler
{
- $factory = $this->bindings[$eventName] ?? $this->default;
+ $binding = $this->bindings[$eventName] ?? $this->default;
+
+ if ($binding instanceof Closure) {
+ $instance = $binding();
+ assert(is_object($instance), "Handler binding for integration event {$eventName} must return an object.");
+ return new PublisherHandler($instance);
+ }
- if ($factory) {
- $handler = $factory();
- assert(is_object($handler), "Handler binding for integration event {$eventName} must return an object.");
- return new PublisherHandler($handler);
+ if (is_string($binding)) {
+ $instance = $this->container?->get($binding);
+ assert(is_object($instance), "PSR container event publisher handler binding {$binding} is not an object.");
+ return new PublisherHandler($instance);
}
throw new InfrastructureException('No handler bound for integration event: ' . $eventName);
diff --git a/src/Infrastructure/OutboundEventBus/Publishes.php b/src/Infrastructure/OutboundEventBus/Publishes.php
new file mode 100644
index 0000000..77d68c0
--- /dev/null
+++ b/src/Infrastructure/OutboundEventBus/Publishes.php
@@ -0,0 +1,28 @@
+ $event
+ * @param class-string $publisher
+ */
+ public function __construct(public string $event, public string $publisher)
+ {
+ }
+}
diff --git a/src/Infrastructure/Queue/ClosureQueue.php b/src/Infrastructure/Queue/ClosureQueue.php
index 88a22aa..385dcc1 100644
--- a/src/Infrastructure/Queue/ClosureQueue.php
+++ b/src/Infrastructure/Queue/ClosureQueue.php
@@ -18,6 +18,9 @@
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer;
use CloudCreativity\Modules\Toolkit\Pipeline\MiddlewareProcessor;
use CloudCreativity\Modules\Toolkit\Pipeline\PipelineBuilder;
+use CloudCreativity\Modules\Toolkit\Pipeline\Through;
+use Psr\Container\ContainerInterface;
+use ReflectionClass;
class ClosureQueue implements Queue
{
@@ -33,8 +36,9 @@ class ClosureQueue implements Queue
public function __construct(
private readonly Closure $fn,
- private readonly ?PipeContainer $middleware = null,
+ private readonly ContainerInterface|PipeContainer|null $middleware = null,
) {
+ $this->autowire();
}
/**
@@ -67,4 +71,13 @@ public function push(Command $command): void
$pipeline->process($command);
}
+
+ private function autowire(): void
+ {
+ $reflection = new ReflectionClass($this);
+
+ foreach ($reflection->getAttributes(Through::class) as $attribute) {
+ $this->pipes = $attribute->newInstance()->pipes;
+ }
+ }
}
diff --git a/src/Infrastructure/Queue/ComponentQueue.php b/src/Infrastructure/Queue/ComponentQueue.php
index 6bc4f18..dbc2938 100644
--- a/src/Infrastructure/Queue/ComponentQueue.php
+++ b/src/Infrastructure/Queue/ComponentQueue.php
@@ -13,23 +13,40 @@
namespace CloudCreativity\Modules\Infrastructure\Queue;
use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue;
-use CloudCreativity\Modules\Contracts\Infrastructure\Queue\EnqueuerContainer;
+use CloudCreativity\Modules\Contracts\Infrastructure\Queue\EnqueuerContainer as IEnqueuerContainer;
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\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 ComponentQueue implements Queue
{
+ private readonly IEnqueuerContainer $enqueuers;
+
+ private readonly ?IPipeContainer $middleware;
+
/**
* @var list
*/
private array $pipes = [];
public function __construct(
- private readonly EnqueuerContainer $enqueuers,
- private readonly ?PipeContainer $middleware = null,
+ ContainerInterface|IEnqueuerContainer $enqueuers,
+ ?IPipeContainer $middleware = null,
) {
+ $this->enqueuers = $enqueuers instanceof ContainerInterface ?
+ new EnqueuerContainer(container: $enqueuers) :
+ $enqueuers;
+
+ $this->middleware = $middleware === null && $enqueuers instanceof ContainerInterface
+ ? new PipeContainer($enqueuers)
+ : $middleware;
+
+ $this->autowire();
}
/**
@@ -53,4 +70,24 @@ public function push(Command $command): void
$pipeline->process($command);
}
+
+ private function autowire(): void
+ {
+ $reflection = new ReflectionClass($this);
+
+ if ($this->enqueuers instanceof EnqueuerContainer) {
+ foreach ($reflection->getAttributes(Queues::class) as $attribute) {
+ $instance = $attribute->newInstance();
+ $this->enqueuers->bind($instance->command, $instance->enqueuer);
+ }
+
+ foreach ($reflection->getAttributes(DefaultEnqueuer::class) as $attribute) {
+ $this->enqueuers->withDefault($attribute->newInstance()->enqueuer);
+ }
+ }
+
+ foreach ($reflection->getAttributes(Through::class) as $attribute) {
+ $this->pipes = $attribute->newInstance()->pipes;
+ }
+ }
}
diff --git a/src/Infrastructure/Queue/DefaultEnqueuer.php b/src/Infrastructure/Queue/DefaultEnqueuer.php
new file mode 100644
index 0000000..1ae67c2
--- /dev/null
+++ b/src/Infrastructure/Queue/DefaultEnqueuer.php
@@ -0,0 +1,26 @@
+, Closure>
+ * @var array, Closure|string>
*/
private array $bindings = [];
/**
- * @param Closure(): object $default
+ * @param (Closure(): object)|string|null $default
*/
- public function __construct(private readonly Closure $default)
- {
+ public function __construct(
+ private Closure|string|null $default = null,
+ private readonly ?ContainerInterface $container = null,
+ ) {
+ if (is_string($this->default) && $this->container === null) {
+ throw new InfrastructureException(
+ 'Cannot bind default enqueuer as a string without a PSR container.',
+ );
+ }
}
/**
* Bind an enqueuer factory into the container.
*
* @param class-string $queueableName
- * @param Closure(): object $binding
+ * @param (Closure(): object)|string $binding
*/
- public function bind(string $queueableName, Closure $binding): void
+ public function bind(string $queueableName, Closure|string $binding): void
{
$this->bindings[$queueableName] = $binding;
}
+ public function withDefault(Closure|string $binding): void
+ {
+ if ($this->default !== null) {
+ throw new InfrastructureException('Default enqueuer is already set.');
+ }
+
+ if (is_string($binding) && $this->container === null) {
+ throw new InfrastructureException(
+ 'Cannot bind default enqueuer as a string without a PSR container.',
+ );
+ }
+
+ $this->default = $binding;
+ }
+
public function get(string $command): Enqueuer
{
- $factory = $this->bindings[$command] ?? $this->default;
+ $binding = $this->bindings[$command] ?? $this->default;
- $enqueuer = $factory();
+ if ($binding instanceof Closure) {
+ $instance = $binding();
+ assert(is_object($instance), "Enqueuer binding for command {$command} must return an object.");
+ return new Enqueuer($instance);
+ }
- assert(is_object($enqueuer), "Enqueuer binding for {$command} must return an object.");
+ if (is_string($binding)) {
+ $instance = $this->container?->get($binding);
+ assert(is_object($instance), "PSR container enqueuer binding {$binding} is not an object.");
+ return new Enqueuer($instance);
+ }
- return new Enqueuer($enqueuer);
+ throw new InfrastructureException('No enqueuer bound for command: ' . $command);
}
}
diff --git a/src/Infrastructure/Queue/Queues.php b/src/Infrastructure/Queue/Queues.php
new file mode 100644
index 0000000..6977fa6
--- /dev/null
+++ b/src/Infrastructure/Queue/Queues.php
@@ -0,0 +1,28 @@
+ $command
+ * @param class-string $enqueuer
+ */
+ public function __construct(public string $command, public string $enqueuer)
+ {
+ }
+}
diff --git a/src/Testing/FakeContainer.php b/src/Testing/FakeContainer.php
new file mode 100644
index 0000000..7c6564c
--- /dev/null
+++ b/src/Testing/FakeContainer.php
@@ -0,0 +1,84 @@
+, 2025
+ */
+
+declare(strict_types=1);
+
+namespace CloudCreativity\Modules\Testing;
+
+use Closure;
+use CloudCreativity\Modules\Contracts\Application\Ports\Driven\ExceptionReporter;
+use CloudCreativity\Modules\Contracts\Application\Ports\Driven\UnitOfWork;
+use Exception;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use Psr\Log\LoggerInterface;
+
+final class FakeContainer implements ContainerInterface
+{
+ public readonly FakeLogger $logger;
+ public readonly FakeExceptionReporter $reporter;
+ public readonly FakeUnitOfWork $unitOfWork;
+
+ /**
+ * @var array
+ */
+ private array $bindings = [];
+
+ public function __construct()
+ {
+ $this->logger = new FakeLogger();
+ $this->reporter = new FakeExceptionReporter();
+ $this->unitOfWork = new FakeUnitOfWork($this->reporter);
+ }
+
+ public function bind(string $id, Closure $binding): void
+ {
+ $this->bindings[$id] = $binding;
+ }
+
+ public function instance(string $id, mixed $instance): void
+ {
+ $this->bindings[$id] = fn () => $instance;
+ }
+
+ public function get(string $id)
+ {
+ $stub = match ($id) {
+ ExceptionReporter::class => $this->reporter,
+ LoggerInterface::class => $this->logger,
+ UnitOfWork::class => $this->unitOfWork,
+ default => null,
+ };
+
+ if ($stub === null) {
+ $binding = $this->bindings[$id] ?? null;
+ $stub = $binding ? $binding() : null;
+ }
+
+ return $stub ?? $this->abort($id);
+ }
+
+ public function has(string $id): bool
+ {
+ try {
+ $this->get($id);
+ return true;
+ } catch (NotFoundExceptionInterface) {
+ return false;
+ }
+ }
+
+ protected function abort(string $id): never
+ {
+ $message = 'Test service not found: ' . $id;
+
+ throw new class ($message) extends Exception implements NotFoundExceptionInterface {};
+ }
+}
diff --git a/src/Testing/FakeLogger.php b/src/Testing/FakeLogger.php
new file mode 100644
index 0000000..f46615b
--- /dev/null
+++ b/src/Testing/FakeLogger.php
@@ -0,0 +1,53 @@
+
+ */
+ public array $log = [];
+
+ public function log($level, string|Stringable $message, array $context = []): void
+ {
+ $this->log[] = [
+ 'level' => $level,
+ 'message' => (string) $message,
+ 'context' => $context,
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public function messages(): array
+ {
+ return array_map(
+ fn (array $entry): string => implode(': ', array_filter([
+ is_scalar($entry['level']) ? strtoupper((string) $entry['level']) : null,
+ $entry['message'],
+ ])),
+ $this->log,
+ );
+ }
+
+ public function count(): int
+ {
+ return count($this->log);
+ }
+}
diff --git a/src/Testing/FakeUnitOfWork.php b/src/Testing/FakeUnitOfWork.php
index a3ae33e..adbbbca 100644
--- a/src/Testing/FakeUnitOfWork.php
+++ b/src/Testing/FakeUnitOfWork.php
@@ -20,6 +20,12 @@
final class FakeUnitOfWork implements UnitOfWork
{
+ public int $attempts = 0;
+
+ public int $commits = 0;
+
+ public int $rollbacks = 0;
+
/**
* @var list
*/
@@ -38,11 +44,14 @@ public function execute(Closure $callback, int $attempts = 1): mixed
for ($i = 1; $i <= $attempts; $i++) {
try {
$this->sequence[] = 'attempt:' . $i;
+ $this->attempts++;
$result = $callback();
$this->sequence[] = 'commit:' . $i;
+ $this->commits++;
return $result;
} catch (Throwable $ex) {
$this->sequence[] = 'rollback:' . $i;
+ $this->rollbacks++;
$this->exceptions->report($ex);
if ($i === $attempts) {
diff --git a/src/Toolkit/Pipeline/PipeContainer.php b/src/Toolkit/Pipeline/PipeContainer.php
index eff2526..96d403a 100644
--- a/src/Toolkit/Pipeline/PipeContainer.php
+++ b/src/Toolkit/Pipeline/PipeContainer.php
@@ -14,6 +14,7 @@
use Closure;
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer as IPipeContainer;
+use Psr\Container\ContainerInterface;
use RuntimeException;
final class PipeContainer implements IPipeContainer
@@ -23,6 +24,10 @@ final class PipeContainer implements IPipeContainer
*/
private array $pipes = [];
+ public function __construct(private readonly ?ContainerInterface $container = null)
+ {
+ }
+
/**
* Bind a pipe into the container.
*/
@@ -37,7 +42,13 @@ public function get(string $pipeName): callable
if (is_callable($factory)) {
$pipe = $factory();
- assert(is_callable($pipe), 'Expecting pipe from factory to be callable.');
+ assert(is_callable($pipe), "Expecting pipe {$pipeName} from factory to be callable.");
+ return $pipe;
+ }
+
+ if ($this->container) {
+ $pipe = $this->container->get($pipeName);
+ assert(is_callable($pipe), "Expecting pipe {$pipeName} from PSR container to be callable.");
return $pipe;
}
diff --git a/src/Toolkit/Pipeline/PipelineBuilder.php b/src/Toolkit/Pipeline/PipelineBuilder.php
index 7b901e4..15daa1b 100644
--- a/src/Toolkit/Pipeline/PipelineBuilder.php
+++ b/src/Toolkit/Pipeline/PipelineBuilder.php
@@ -12,9 +12,10 @@
namespace CloudCreativity\Modules\Toolkit\Pipeline;
-use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer;
+use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer as IPipeContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipelineBuilder as IPipelineBuilder;
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\Processor;
+use Psr\Container\ContainerInterface;
use RuntimeException;
final class PipelineBuilder implements IPipelineBuilder
@@ -24,12 +25,16 @@ final class PipelineBuilder implements IPipelineBuilder
*/
private array $stages = [];
- public static function make(?PipeContainer $container = null): self
+ public static function make(ContainerInterface|IPipeContainer|null $container = null): self
{
+ if ($container instanceof ContainerInterface) {
+ return new self(new PipeContainer($container));
+ }
+
return new self($container);
}
- public function __construct(private readonly ?PipeContainer $container = null)
+ public function __construct(private readonly ?IPipeContainer $container = null)
{
}
diff --git a/src/Toolkit/Pipeline/Through.php b/src/Toolkit/Pipeline/Through.php
new file mode 100644
index 0000000..090a7dc
--- /dev/null
+++ b/src/Toolkit/Pipeline/Through.php
@@ -0,0 +1,32 @@
+
+ */
+ public array $pipes;
+
+ /**
+ * @param non-empty-string ...$pipes
+ */
+ public function __construct(string ...$pipes)
+ {
+ $this->pipes = array_values($pipes);
+ }
+}
diff --git a/tests/Integration/Application/Bus/AddCommand.php b/tests/Integration/Application/Bus/AddCommand.php
new file mode 100644
index 0000000..2596e23
--- /dev/null
+++ b/tests/Integration/Application/Bus/AddCommand.php
@@ -0,0 +1,22 @@
+
+ */
+ public function execute(AddCommand $command): Result
+ {
+ return Result::ok($command->a + $command->b + $this->c);
+ }
+}
diff --git a/tests/Integration/Application/Bus/DivideQuery.php b/tests/Integration/Application/Bus/DivideQuery.php
new file mode 100644
index 0000000..e439eba
--- /dev/null
+++ b/tests/Integration/Application/Bus/DivideQuery.php
@@ -0,0 +1,22 @@
+
+ */
+ public function execute(DivideQuery $query): Result
+ {
+ return Result::ok($query->a / $query->b);
+ }
+}
diff --git a/tests/Integration/Application/Bus/FloorCommand.php b/tests/Integration/Application/Bus/FloorCommand.php
new file mode 100644
index 0000000..fc79999
--- /dev/null
+++ b/tests/Integration/Application/Bus/FloorCommand.php
@@ -0,0 +1,22 @@
+bind(AddCommandHandler::class, fn () => new AddCommandHandler(3));
+ $container->bind(MultiplyCommandHandler::class, fn () => new MultiplyCommandHandler());
+ $container->bind(LogMessageDispatch::class, fn () => new LogMessageDispatch($container->logger));
+ $container->bind(ExecuteInUnitOfWork::class, fn () => new ExecuteInUnitOfWork(
+ new UnitOfWorkManager($container->unitOfWork),
+ ));
+
+ $bus = new MathCommandBus($container);
+
+ $add = $bus->dispatch(new AddCommand(1, 2));
+ $multiply = $bus->dispatch(new MultiplyCommand(10, 11));
+
+ $this->assertSame(6, $add->value());
+ $this->assertSame(110, $multiply->value());
+ $this->assertCount(4, $container->logger);
+ $this->assertSame(1, $container->unitOfWork->commits);
+ }
+}
diff --git a/tests/Integration/Application/Bus/MathQueryBus.php b/tests/Integration/Application/Bus/MathQueryBus.php
new file mode 100644
index 0000000..0cf831c
--- /dev/null
+++ b/tests/Integration/Application/Bus/MathQueryBus.php
@@ -0,0 +1,25 @@
+bind(SubtractQueryHandler::class, fn () => new SubtractQueryHandler(3));
+ $container->bind(DivideQueryHandler::class, fn () => new DivideQueryHandler());
+ $container->bind(LogMessageDispatch::class, fn () => new LogMessageDispatch($container->logger));
+ $container->bind('division-modifier', fn () => function (DivideQuery $query, Closure $next): mixed {
+ $query = new DivideQuery($query->a * 10, $query->b);
+ return $next($query);
+ });
+
+ $bus = new MathQueryBus($container);
+
+ $add = $bus->dispatch(new SubtractQuery(1, 2));
+ $multiply = $bus->dispatch(new DivideQuery(12, 3));
+
+ $this->assertSame(1 - 2 - 3, $add->value());
+ $this->assertEquals(120 / 3, $multiply->value());
+ $this->assertCount(4, $container->logger);
+ }
+}
diff --git a/tests/Integration/Application/Bus/MultiplyCommand.php b/tests/Integration/Application/Bus/MultiplyCommand.php
new file mode 100644
index 0000000..fe63747
--- /dev/null
+++ b/tests/Integration/Application/Bus/MultiplyCommand.php
@@ -0,0 +1,22 @@
+
+ */
+ public function execute(MultiplyCommand $command): Result
+ {
+ return Result::ok($command->a * $command->b);
+ }
+}
diff --git a/tests/Integration/Application/Bus/SubtractQuery.php b/tests/Integration/Application/Bus/SubtractQuery.php
new file mode 100644
index 0000000..9983742
--- /dev/null
+++ b/tests/Integration/Application/Bus/SubtractQuery.php
@@ -0,0 +1,22 @@
+
+ */
+ public function execute(SubtractQuery $query): Result
+ {
+ return Result::ok($query->a - $query->b - $this->c);
+ }
+}
diff --git a/tests/Integration/Application/InboundEventBus/DefaultHandler.php b/tests/Integration/Application/InboundEventBus/DefaultHandler.php
new file mode 100644
index 0000000..73f6f21
--- /dev/null
+++ b/tests/Integration/Application/InboundEventBus/DefaultHandler.php
@@ -0,0 +1,28 @@
+
+ */
+ public array $handled = [];
+
+ public function handle(IntegrationEvent $event): void
+ {
+ $this->handled[] = $event;
+ }
+}
diff --git a/tests/Integration/Application/InboundEventBus/MathEventBus.php b/tests/Integration/Application/InboundEventBus/MathEventBus.php
new file mode 100644
index 0000000..cd56b7d
--- /dev/null
+++ b/tests/Integration/Application/InboundEventBus/MathEventBus.php
@@ -0,0 +1,27 @@
+bind(NumbersAddedHandler::class, fn () => $a);
+ $container->bind(NumbersSubtractedHandler::class, fn () => $b);
+ $container->bind(DefaultHandler::class, fn () => $c);
+ $container->bind(LogInboundEvent::class, fn () => new LogInboundEvent($container->logger));
+ $container->bind(HandleInUnitOfWork::class, fn () => new HandleInUnitOfWork(
+ new UnitOfWorkManager($container->unitOfWork),
+ ));
+
+ $bus = new MathEventBus($container);
+
+ $bus->dispatch($ev1 = new NumbersAdded(1, 2, 3));
+ $bus->dispatch($ev2 = new NumbersSubtracted(10, 6, 4));
+ $bus->dispatch($ev3 = new NumbersDivided(12, 3, 4));
+
+ $this->assertSame([$ev1], $a->handled);
+ $this->assertSame([$ev2], $b->handled);
+ $this->assertSame([$ev3], $c->handled);
+ $this->assertCount(6, $container->logger);
+ $this->assertSame(1, $container->unitOfWork->commits);
+ }
+}
diff --git a/tests/Integration/Application/InboundEventBus/NumbersAdded.php b/tests/Integration/Application/InboundEventBus/NumbersAdded.php
new file mode 100644
index 0000000..271af6d
--- /dev/null
+++ b/tests/Integration/Application/InboundEventBus/NumbersAdded.php
@@ -0,0 +1,43 @@
+uuid = Uuid::random();
+ }
+
+ public function getUuid(): Uuid
+ {
+ return $this->uuid;
+ }
+
+ public function getOccurredAt(): DateTimeImmutable
+ {
+ return $this->calculatedAt;
+ }
+}
diff --git a/tests/Integration/Application/InboundEventBus/NumbersAddedHandler.php b/tests/Integration/Application/InboundEventBus/NumbersAddedHandler.php
new file mode 100644
index 0000000..686dd94
--- /dev/null
+++ b/tests/Integration/Application/InboundEventBus/NumbersAddedHandler.php
@@ -0,0 +1,26 @@
+
+ */
+ public array $handled = [];
+
+ public function handle(NumbersAdded $event): void
+ {
+ $this->handled[] = $event;
+ }
+}
diff --git a/tests/Integration/Application/InboundEventBus/NumbersDivided.php b/tests/Integration/Application/InboundEventBus/NumbersDivided.php
new file mode 100644
index 0000000..020aae9
--- /dev/null
+++ b/tests/Integration/Application/InboundEventBus/NumbersDivided.php
@@ -0,0 +1,43 @@
+uuid = Uuid::random();
+ }
+
+ public function getUuid(): Uuid
+ {
+ return $this->uuid;
+ }
+
+ public function getOccurredAt(): DateTimeImmutable
+ {
+ return $this->calculatedAt;
+ }
+}
diff --git a/tests/Integration/Application/InboundEventBus/NumbersSubtracted.php b/tests/Integration/Application/InboundEventBus/NumbersSubtracted.php
new file mode 100644
index 0000000..112fd56
--- /dev/null
+++ b/tests/Integration/Application/InboundEventBus/NumbersSubtracted.php
@@ -0,0 +1,43 @@
+uuid = Uuid::random();
+ }
+
+ public function getUuid(): Uuid
+ {
+ return $this->uuid;
+ }
+
+ public function getOccurredAt(): DateTimeImmutable
+ {
+ return $this->calculatedAt;
+ }
+}
diff --git a/tests/Integration/Application/InboundEventBus/NumbersSubtractedHandler.php b/tests/Integration/Application/InboundEventBus/NumbersSubtractedHandler.php
new file mode 100644
index 0000000..e298768
--- /dev/null
+++ b/tests/Integration/Application/InboundEventBus/NumbersSubtractedHandler.php
@@ -0,0 +1,30 @@
+
+ */
+ public array $handled = [];
+
+ public function handle(NumbersSubtracted $event): void
+ {
+ $this->handled[] = $event;
+ }
+}
diff --git a/tests/Integration/Infrastructure/OutboundEventBus/MathEventPublisher.php b/tests/Integration/Infrastructure/OutboundEventBus/MathEventPublisher.php
new file mode 100644
index 0000000..7fa6d56
--- /dev/null
+++ b/tests/Integration/Infrastructure/OutboundEventBus/MathEventPublisher.php
@@ -0,0 +1,29 @@
+bind(NumbersAddedPublisher::class, fn () => $a);
+ $container->bind(NumbersSubtractedPublisher::class, fn () => $b);
+ $container->bind(TestDefaultPublisher::class, fn () => $c);
+ $container->bind(LogOutboundEvent::class, fn () => new LogOutboundEvent($container->logger));
+
+ $publisher = new MathEventPublisher($container);
+ $publisher->publish($ev1 = new NumbersAdded(1, 2, 3));
+ $publisher->publish($ev2 = new NumbersSubtracted(10, 6, 4));
+ $publisher->publish($ev3 = new NumbersDivided(12, 3, 4));
+
+ $this->assertSame([$ev1], $a->published);
+ $this->assertSame([$ev2], $b->published);
+ $this->assertSame([$ev3], $c->published);
+ $this->assertCount(6, $container->logger);
+ }
+}
diff --git a/tests/Integration/Infrastructure/OutboundEventBus/NumbersAddedPublisher.php b/tests/Integration/Infrastructure/OutboundEventBus/NumbersAddedPublisher.php
new file mode 100644
index 0000000..c3f97a8
--- /dev/null
+++ b/tests/Integration/Infrastructure/OutboundEventBus/NumbersAddedPublisher.php
@@ -0,0 +1,28 @@
+
+ */
+ public array $published = [];
+
+ public function publish(NumbersAdded $event): void
+ {
+ $this->published[] = $event;
+ }
+}
diff --git a/tests/Integration/Infrastructure/OutboundEventBus/NumbersSubtractedPublisher.php b/tests/Integration/Infrastructure/OutboundEventBus/NumbersSubtractedPublisher.php
new file mode 100644
index 0000000..f9379a7
--- /dev/null
+++ b/tests/Integration/Infrastructure/OutboundEventBus/NumbersSubtractedPublisher.php
@@ -0,0 +1,28 @@
+
+ */
+ public array $published = [];
+
+ public function publish(NumbersSubtracted $event): void
+ {
+ $this->published[] = $event;
+ }
+}
diff --git a/tests/Integration/Infrastructure/OutboundEventBus/TestClosurePublisher.php b/tests/Integration/Infrastructure/OutboundEventBus/TestClosurePublisher.php
new file mode 100644
index 0000000..3497b9f
--- /dev/null
+++ b/tests/Integration/Infrastructure/OutboundEventBus/TestClosurePublisher.php
@@ -0,0 +1,22 @@
+bind(LogOutboundEvent::class, fn () => new LogOutboundEvent($container->logger));
+
+ $publisher = new TestClosurePublisher(
+ fn: function ($event) use (&$published) {
+ $published[] = $event;
+ },
+ middleware: $container,
+ );
+
+ $event = new NumbersAdded(1, 2, 3);
+ $publisher->publish($event);
+
+ $this->assertSame([$event], $published);
+ $this->assertCount(2, $container->logger);
+ }
+}
diff --git a/tests/Integration/Infrastructure/OutboundEventBus/TestDefaultPublisher.php b/tests/Integration/Infrastructure/OutboundEventBus/TestDefaultPublisher.php
new file mode 100644
index 0000000..36b1a21
--- /dev/null
+++ b/tests/Integration/Infrastructure/OutboundEventBus/TestDefaultPublisher.php
@@ -0,0 +1,28 @@
+
+ */
+ public array $published = [];
+
+ public function publish(IntegrationEvent $event): void
+ {
+ $this->published[] = $event;
+ }
+}
diff --git a/tests/Integration/Infrastructure/Queue/AddCommandEnqueuer.php b/tests/Integration/Infrastructure/Queue/AddCommandEnqueuer.php
new file mode 100644
index 0000000..5bb7646
--- /dev/null
+++ b/tests/Integration/Infrastructure/Queue/AddCommandEnqueuer.php
@@ -0,0 +1,28 @@
+
+ */
+ public array $queued = [];
+
+ public function push(AddCommand $command): void
+ {
+ $this->queued[] = $command;
+ }
+}
diff --git a/tests/Integration/Infrastructure/Queue/MathQueue.php b/tests/Integration/Infrastructure/Queue/MathQueue.php
new file mode 100644
index 0000000..4cd8fb0
--- /dev/null
+++ b/tests/Integration/Infrastructure/Queue/MathQueue.php
@@ -0,0 +1,29 @@
+bind(AddCommandEnqueuer::class, fn () => $a);
+ $container->bind(MultiplyCommandEnqueuer::class, fn () => $b);
+ $container->bind(TestDefaultEnqueuer::class, fn () => $c);
+ $container->bind(LogPushedToQueue::class, fn () => new LogPushedToQueue($container->logger));
+
+ $publisher = new MathQueue($container);
+ $publisher->push($command1 = new AddCommand(1, 2));
+ $publisher->push($command2 = new MultiplyCommand(10, 6));
+ $publisher->push($command3 = new FloorCommand(99.9));
+
+ $this->assertSame([$command1], $a->queued);
+ $this->assertSame([$command2], $b->queued);
+ $this->assertSame([$command3], $c->queued);
+ $this->assertCount(6, $container->logger);
+ }
+}
diff --git a/tests/Integration/Infrastructure/Queue/MultiplyCommandEnqueuer.php b/tests/Integration/Infrastructure/Queue/MultiplyCommandEnqueuer.php
new file mode 100644
index 0000000..d2f9ee9
--- /dev/null
+++ b/tests/Integration/Infrastructure/Queue/MultiplyCommandEnqueuer.php
@@ -0,0 +1,28 @@
+
+ */
+ public array $queued = [];
+
+ public function push(MultiplyCommand $command): void
+ {
+ $this->queued[] = $command;
+ }
+}
diff --git a/tests/Integration/Infrastructure/Queue/TestClosureQueue.php b/tests/Integration/Infrastructure/Queue/TestClosureQueue.php
new file mode 100644
index 0000000..d690220
--- /dev/null
+++ b/tests/Integration/Infrastructure/Queue/TestClosureQueue.php
@@ -0,0 +1,22 @@
+bind(LogPushedToQueue::class, fn () => new LogPushedToQueue($container->logger));
+
+ $publisher = new TestClosureQueue(
+ fn: function ($event) use (&$queued) {
+ $queued[] = $event;
+ },
+ middleware: $container,
+ );
+
+ $command = new AddCommand(1, 2);
+ $publisher->push($command);
+
+ $this->assertSame([$command], $queued);
+ $this->assertCount(2, $container->logger);
+ }
+}
diff --git a/tests/Integration/Infrastructure/Queue/TestDefaultEnqueuer.php b/tests/Integration/Infrastructure/Queue/TestDefaultEnqueuer.php
new file mode 100644
index 0000000..6bec9c3
--- /dev/null
+++ b/tests/Integration/Infrastructure/Queue/TestDefaultEnqueuer.php
@@ -0,0 +1,28 @@
+
+ */
+ public array $queued = [];
+
+ public function push(Command $command): void
+ {
+ $this->queued[] = $command;
+ }
+}
diff --git a/tests/Unit/Application/Bus/CommandHandlerContainerTest.php b/tests/Unit/Application/Bus/CommandHandlerContainerTest.php
index d14d64a..7bacf1d 100644
--- a/tests/Unit/Application/Bus/CommandHandlerContainerTest.php
+++ b/tests/Unit/Application/Bus/CommandHandlerContainerTest.php
@@ -17,13 +17,14 @@
use CloudCreativity\Modules\Application\Bus\CommandHandlerContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
class CommandHandlerContainerTest extends TestCase
{
- public function test(): void
+ public function testItResolvesUsingClosureBindings(): void
{
$a = new TestCommandHandler();
- $b = $this->createMock(TestCommandHandler::class);
+ $b = $this->createStub(TestCommandHandler::class);
$command1 = new class () implements Command {};
$command2 = new class () implements Command {};
@@ -41,4 +42,36 @@ public function test(): void
$container->get($command3::class);
}
+
+ public function testItResolvesViaPsrContainer(): void
+ {
+ $a = new TestCommandHandler();
+ $b = $this->createStub(TestCommandHandler::class);
+
+ $command1 = new class () implements Command {};
+ $command2 = new class () implements Command {};
+ $command3 = new class () implements Command {};
+
+ $psrContainer = $this->createMock(ContainerInterface::class);
+ $psrContainer
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->willReturnCallback(fn (string $id) => match ($id) {
+ $a::class => $a,
+ $b::class => $b,
+ default => $this->fail('Unexpected container id: ' . $id),
+ });
+
+ $container = new CommandHandlerContainer($psrContainer);
+ $container->bind($command1::class, $a::class);
+ $container->bind($command2::class, $b::class);
+
+ $this->assertEquals(new CommandHandler($a), $container->get($command1::class));
+ $this->assertEquals(new CommandHandler($b), $container->get($command2::class));
+
+ $this->expectException(ApplicationException::class);
+ $this->expectExceptionMessage('No command handler bound for command class: ' . $command3::class);
+
+ $container->get($command3::class);
+ }
}
diff --git a/tests/Unit/Application/Bus/QueryHandlerContainerTest.php b/tests/Unit/Application/Bus/QueryHandlerContainerTest.php
index 2348927..23ac92b 100644
--- a/tests/Unit/Application/Bus/QueryHandlerContainerTest.php
+++ b/tests/Unit/Application/Bus/QueryHandlerContainerTest.php
@@ -17,13 +17,14 @@
use CloudCreativity\Modules\Application\Bus\QueryHandlerContainer;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Query;
use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
class QueryHandlerContainerTest extends TestCase
{
- public function test(): void
+ public function testItResolvesClosureBindings(): void
{
$a = new TestQueryHandler();
- $b = $this->createMock(TestQueryHandler::class);
+ $b = $this->createStub(TestQueryHandler::class);
$query1 = new class () implements Query {};
$query2 = new class () implements Query {};
@@ -41,4 +42,36 @@ public function test(): void
$container->get($query3::class);
}
+
+ public function testItResolvesViaPsrContainer(): void
+ {
+ $a = new TestQueryHandler();
+ $b = $this->createStub(TestQueryHandler::class);
+
+ $query1 = new class () implements Query {};
+ $query2 = new class () implements Query {};
+ $query3 = new class () implements Query {};
+
+ $psrContainer = $this->createMock(ContainerInterface::class);
+ $psrContainer
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->willReturnCallback(fn (string $id) => match ($id) {
+ $a::class => $a,
+ $b::class => $b,
+ default => $this->fail('Unexpected container id: ' . $id),
+ });
+
+ $container = new QueryHandlerContainer($psrContainer);
+ $container->bind($query1::class, $a::class);
+ $container->bind($query2::class, $b::class);
+
+ $this->assertEquals(new QueryHandler($a), $container->get($query1::class));
+ $this->assertEquals(new QueryHandler($b), $container->get($query2::class));
+
+ $this->expectException(ApplicationException::class);
+ $this->expectExceptionMessage('No query handler bound for query class: ' . $query3::class);
+
+ $container->get($query3::class);
+ }
}
diff --git a/tests/Unit/Application/InboundEventBus/EventHandlerContainerTest.php b/tests/Unit/Application/InboundEventBus/EventHandlerContainerTest.php
index 335520e..f82069d 100644
--- a/tests/Unit/Application/InboundEventBus/EventHandlerContainerTest.php
+++ b/tests/Unit/Application/InboundEventBus/EventHandlerContainerTest.php
@@ -12,17 +12,19 @@
namespace CloudCreativity\Modules\Tests\Unit\Application\InboundEventBus;
+use CloudCreativity\Modules\Application\ApplicationException;
use CloudCreativity\Modules\Application\InboundEventBus\EventHandler;
use CloudCreativity\Modules\Application\InboundEventBus\EventHandlerContainer;
use CloudCreativity\Modules\Tests\Unit\Infrastructure\OutboundEventBus\TestOutboundEvent;
use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
class EventHandlerContainerTest extends TestCase
{
- public function testItHasHandlers(): void
+ public function testItHasHandlerBindings(): void
{
$a = new TestEventHandler();
- $b = $this->createMock(TestEventHandler::class);
+ $b = $this->createStub(TestEventHandler::class);
$container = new EventHandlerContainer();
$container->bind(TestInboundEvent::class, fn () => $a);
@@ -32,10 +34,33 @@ public function testItHasHandlers(): void
$this->assertEquals(new EventHandler($b), $container->get(TestOutboundEvent::class));
}
- public function testItHasDefaultHandler(): void
+ public function testItUsesPsrContainer(): void
{
$a = new TestEventHandler();
- $b = $this->createMock(TestEventHandler::class);
+ $b = $this->createStub(TestEventHandler::class);
+
+ $psrContainer = $this->createMock(ContainerInterface::class);
+ $psrContainer
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->willReturnCallback(fn (string $class) => match ($class) {
+ $a::class => $a,
+ $b::class => $b,
+ default => $this->fail('Unexpected class requested: ' . $class),
+ });
+
+ $container = new EventHandlerContainer(container: $psrContainer);
+ $container->bind(TestInboundEvent::class, $a::class);
+ $container->bind(TestOutboundEvent::class, $b::class);
+
+ $this->assertEquals(new EventHandler($a), $container->get(TestInboundEvent::class));
+ $this->assertEquals(new EventHandler($b), $container->get(TestOutboundEvent::class));
+ }
+
+ public function testItHasBoundDefaultHandler(): void
+ {
+ $a = new TestEventHandler();
+ $b = $this->createStub(TestEventHandler::class);
$container = new EventHandlerContainer(default: fn () => $b);
$container->bind(TestInboundEvent::class, fn () => $a);
@@ -44,13 +69,32 @@ public function testItHasDefaultHandler(): void
$this->assertEquals(new EventHandler($b), $container->get(TestOutboundEvent::class));
}
+ public function testItHasDefaultHandlerInPsrContainer(): void
+ {
+ $a = new TestEventHandler();
+ $b = $this->createStub(TestEventHandler::class);
+
+ $psrContainer = $this->createMock(ContainerInterface::class);
+ $psrContainer
+ ->expects($this->once())
+ ->method('get')
+ ->with('default-handler')
+ ->willReturn($b);
+
+ $container = new EventHandlerContainer(default: 'default-handler', container: $psrContainer);
+ $container->bind(TestInboundEvent::class, fn () => $a);
+
+ $this->assertEquals(new EventHandler($a), $container->get(TestInboundEvent::class));
+ $this->assertEquals(new EventHandler($b), $container->get(TestOutboundEvent::class));
+ }
+
public function testItDoesNotHaveHandler(): void
{
$container = new EventHandlerContainer();
$container->bind(TestInboundEvent::class, fn () => new TestEventHandler());
- $this->expectException(\RuntimeException::class);
+ $this->expectException(ApplicationException::class);
$this->expectExceptionMessage(
'No handler bound for integration event: ' . TestOutboundEvent::class,
);
diff --git a/tests/Unit/Infrastructure/OutboundEventBus/ClosurePublisherTest.php b/tests/Unit/Infrastructure/OutboundEventBus/ClosurePublisherTest.php
index 338d799..bf8366c 100644
--- a/tests/Unit/Infrastructure/OutboundEventBus/ClosurePublisherTest.php
+++ b/tests/Unit/Infrastructure/OutboundEventBus/ClosurePublisherTest.php
@@ -13,55 +13,40 @@
namespace CloudCreativity\Modules\Tests\Unit\Infrastructure\OutboundEventBus;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\IntegrationEvent;
-use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer;
+use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer as IPipeContainer;
use CloudCreativity\Modules\Infrastructure\OutboundEventBus\ClosurePublisher;
-use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
class ClosurePublisherTest extends TestCase
{
- private MockObject&PipeContainer $middleware;
-
/**
* @var array
*/
private array $actual = [];
- private ClosurePublisher $publisher;
-
- protected function setUp(): void
- {
- parent::setUp();
-
- $this->publisher = new ClosurePublisher(
- function (IntegrationEvent $event): void {
- $this->actual[] = $event;
- },
- $this->middleware = $this->createMock(PipeContainer::class),
- );
- }
-
protected function tearDown(): void
{
parent::tearDown();
- unset($this->publisher, $this->middleware, $this->actual);
+ unset($this->actual);
}
public function test(): void
{
- $event = $this->createMock(IntegrationEvent::class);
+ $event = $this->createStub(IntegrationEvent::class);
- $this->publisher->publish($event);
+ $publisher = $this->createPublisher();
+ $publisher->publish($event);
$this->assertSame([$event], $this->actual);
}
public function testWithMiddleware(): void
{
- $event1 = $this->createMock(IntegrationEvent::class);
- $event2 = $this->createMock(IntegrationEvent::class);
- $event3 = $this->createMock(IntegrationEvent::class);
- $event4 = $this->createMock(IntegrationEvent::class);
+ $event1 = $this->createStub(IntegrationEvent::class);
+ $event2 = $this->createStub(IntegrationEvent::class);
+ $event3 = $this->createStub(IntegrationEvent::class);
+ $event4 = $this->createStub(IntegrationEvent::class);
$middleware1 = function ($event, \Closure $next) use ($event1, $event2) {
$this->assertSame($event1, $event);
@@ -78,44 +63,92 @@ public function testWithMiddleware(): void
return $next($event4);
};
- $this->middleware
+ $middleware = $this->createMock(IPipeContainer::class);
+ $middleware
->expects($this->once())
->method('get')
->with('MySecondMiddleware')
->willReturn($middleware2);
- $this->publisher->through([
+ $publisher = $this->createPublisher($middleware);
+ $publisher->through([
$middleware1,
'MySecondMiddleware',
$middleware3,
]);
- $this->publisher->publish($event1);
+ $publisher->publish($event1);
$this->assertSame([$event4], $this->actual);
}
+ public function testWithMiddlewareViaPsrContainer(): void
+ {
+ $event1 = $this->createStub(IntegrationEvent::class);
+ $event2 = $this->createStub(IntegrationEvent::class);
+ $event3 = $this->createStub(IntegrationEvent::class);
+
+ $middleware1 = function ($event, \Closure $next) use ($event1, $event2) {
+ $this->assertSame($event1, $event);
+ return $next($event2);
+ };
+
+ $middleware2 = function ($event, \Closure $next) use ($event2, $event3) {
+ $this->assertSame($event2, $event);
+ return $next($event3);
+ };
+
+ $psrContainer = $this->createMock(ContainerInterface::class);
+ $psrContainer
+ ->expects($this->once())
+ ->method('get')
+ ->with('MySecondMiddleware')
+ ->willReturn($middleware2);
+
+ $publisher = $this->createPublisher($psrContainer);
+ $publisher->through([
+ $middleware1,
+ 'MySecondMiddleware',
+ ]);
+
+ $publisher->publish($event1);
+
+ $this->assertSame([$event3], $this->actual);
+ }
+
public function testWithAlternativeHandlers(): void
{
$expected = new TestOutboundEvent();
- $mock = $this->createMock(IntegrationEvent::class);
+ $stub = $this->createStub(IntegrationEvent::class);
$actual = null;
- $this->publisher->bind($mock::class, function (): never {
+ $publisher = $this->createPublisher();
+
+ $publisher->bind($stub::class, function (): never {
$this->fail('Not expecting this closure to be called.');
});
- $this->publisher->bind(
+ $publisher->bind(
TestOutboundEvent::class,
function (TestOutboundEvent $in) use (&$actual) {
$actual = $in;
},
);
- $this->publisher->publish($expected);
+ $publisher->publish($expected);
$this->assertEmpty($this->actual);
$this->assertSame($expected, $actual);
}
+
+ private function createPublisher(ContainerInterface|IPipeContainer|null $middleware = null): ClosurePublisher
+ {
+ return new ClosurePublisher(
+ function (IntegrationEvent $event): void {
+ $this->actual[] = $event;
+ },
+ $middleware,
+ );
+ }
}
diff --git a/tests/Unit/Infrastructure/OutboundEventBus/PublisherHandlerContainerTest.php b/tests/Unit/Infrastructure/OutboundEventBus/PublisherHandlerContainerTest.php
index 009bc0d..3c07902 100644
--- a/tests/Unit/Infrastructure/OutboundEventBus/PublisherHandlerContainerTest.php
+++ b/tests/Unit/Infrastructure/OutboundEventBus/PublisherHandlerContainerTest.php
@@ -15,14 +15,15 @@
use CloudCreativity\Modules\Infrastructure\OutboundEventBus\PublisherHandler;
use CloudCreativity\Modules\Infrastructure\OutboundEventBus\PublisherHandlerContainer;
use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
use RuntimeException;
class PublisherHandlerContainerTest extends TestCase
{
- public function testItDoesNotHaveDefaultHandler(): void
+ public function testItPublishesViaBindingsWithoutDefaultHandler(): void
{
$a = new TestPublisher();
- $b = $this->createMock(TestPublisher::class);
+ $b = $this->createStub(TestPublisher::class);
$event1 = new class () extends TestOutboundEvent {};
$event2 = new class () extends TestOutboundEvent {};
@@ -41,10 +42,10 @@ public function testItDoesNotHaveDefaultHandler(): void
$container->get($event3::class);
}
- public function testItHasDefaultHandler(): void
+ public function testItPublishesViaBindingsWithDefaultHandler(): void
{
$a = new TestPublisher();
- $b = $this->createMock(TestPublisher::class);
+ $b = $this->createStub(TestPublisher::class);
$event1 = new class () extends TestOutboundEvent {};
$event2 = new class () extends TestOutboundEvent {};
@@ -57,4 +58,63 @@ public function testItHasDefaultHandler(): void
$this->assertEquals($default = new PublisherHandler($b), $container->get($event2::class));
$this->assertEquals($default, $container->get($event3::class));
}
+
+ public function testItPublishesViaPsrContainerWithoutDefaultHandler(): void
+ {
+ $a = new TestPublisher();
+ $b = $this->createStub(TestPublisher::class);
+
+ $event1 = new class () extends TestOutboundEvent {};
+ $event2 = new class () extends TestOutboundEvent {};
+ $event3 = new class () extends TestOutboundEvent {};
+
+ $psrContainer = $this->createMock(ContainerInterface::class);
+ $psrContainer
+ ->expects($this->exactly(2))
+ ->method('get')
+ ->willReturnCallback(fn (string $id) => match ($id) {
+ $a::class => $a,
+ $b::class => $b,
+ default => $this->fail('Unexpected container id: ' . $id),
+ });
+
+ $container = new PublisherHandlerContainer(container: $psrContainer);
+ $container->bind($event1::class, $a::class);
+ $container->bind($event2::class, $b::class);
+
+ $this->assertEquals(new PublisherHandler($a), $container->get($event1::class));
+ $this->assertEquals(new PublisherHandler($b), $container->get($event2::class));
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('No handler bound for integration event: ' . $event3::class);
+
+ $container->get($event3::class);
+ }
+
+ public function testItPublishesViaPsrContainerWithDefaultHandler(): void
+ {
+ $a = new TestPublisher();
+ $b = $this->createStub(TestPublisher::class);
+
+ $event1 = new class () extends TestOutboundEvent {};
+ $event2 = new class () extends TestOutboundEvent {};
+ $event3 = new class () extends TestOutboundEvent {};
+
+ $psrContainer = $this->createMock(ContainerInterface::class);
+ $psrContainer
+ ->expects($this->exactly(3))
+ ->method('get')
+ ->willReturnCallback(fn (string $id) => match ($id) {
+ $a::class => $a,
+ $b::class => $b,
+ default => $this->fail('Unexpected container id: ' . $id),
+ });
+
+ $container = new PublisherHandlerContainer(default: $b::class, container: $psrContainer);
+ $container->bind($event1::class, $a::class);
+
+ $this->assertEquals(new PublisherHandler($a), $container->get($event1::class));
+ $this->assertEquals($default = new PublisherHandler($b), $container->get($event2::class));
+ $this->assertEquals($default, $container->get($event3::class));
+ }
}
diff --git a/tests/Unit/Infrastructure/Queue/EnqueuerContainerTest.php b/tests/Unit/Infrastructure/Queue/EnqueuerContainerTest.php
index 6226e78..31757c4 100644
--- a/tests/Unit/Infrastructure/Queue/EnqueuerContainerTest.php
+++ b/tests/Unit/Infrastructure/Queue/EnqueuerContainerTest.php
@@ -17,17 +17,18 @@
use CloudCreativity\Modules\Infrastructure\Queue\EnqueuerContainer;
use CloudCreativity\Modules\Tests\Unit\Application\Bus\TestCommand;
use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
class EnqueuerContainerTest extends TestCase
{
- public function test(): void
+ public function testItUsesBindings(): void
{
$command1 = new class () implements Command {};
$command2 = new class () implements Command {};
$a = new TestEnqueuer();
- $b = $this->createMock(TestEnqueuer::class);
- $default = $this->createMock(TestEnqueuer::class);
+ $b = $this->createStub(TestEnqueuer::class);
+ $default = $this->createStub(TestEnqueuer::class);
$container = new EnqueuerContainer(fn () => $default);
$container->bind($command1::class, fn () => $a);
@@ -37,4 +38,33 @@ public function test(): void
$this->assertEquals(new Enqueuer($b), $container->get($command2::class));
$this->assertEquals(new Enqueuer($default), $container->get(TestCommand::class));
}
+
+ public function testItUsesPsrContainer(): void
+ {
+ $command1 = new class () implements Command {};
+ $command2 = new class () implements Command {};
+
+ $a = new TestEnqueuer();
+ $b = $this->createStub(TestEnqueuer::class);
+ $default = $this->createStub(TestEnqueuer::class);
+
+ $psrContainer = $this->createMock(ContainerInterface::class);
+ $psrContainer
+ ->expects($this->exactly(3))
+ ->method('get')
+ ->willReturnCallback(fn (string $id) => match ($id) {
+ $a::class => $a,
+ $b::class => $b,
+ $default::class => $default,
+ default => $this->fail('Unexpected binding: ' . $id),
+ });
+
+ $container = new EnqueuerContainer(default: $default::class, container: $psrContainer);
+ $container->bind($command1::class, $a::class);
+ $container->bind($command2::class, $b::class);
+
+ $this->assertEquals(new Enqueuer($a), $container->get($command1::class));
+ $this->assertEquals(new Enqueuer($b), $container->get($command2::class));
+ $this->assertEquals(new Enqueuer($default), $container->get(TestCommand::class));
+ }
}
diff --git a/tests/Unit/Testing/FakeUnitOfWorkTest.php b/tests/Unit/Testing/FakeUnitOfWorkTest.php
index 3889bf3..d5bceab 100644
--- a/tests/Unit/Testing/FakeUnitOfWorkTest.php
+++ b/tests/Unit/Testing/FakeUnitOfWorkTest.php
@@ -28,6 +28,9 @@ public function testItIsSuccessfulOnFirstAttempt(): void
$this->assertSame('result', $result);
$this->assertSame(['attempt:1', 'commit:1'], $unitOfWork->sequence);
$this->assertEmpty($unitOfWork->exceptions->reported);
+ $this->assertSame(1, $unitOfWork->attempts);
+ $this->assertSame(1, $unitOfWork->commits);
+ $this->assertSame(0, $unitOfWork->rollbacks);
}
public function testItIsSuccessfulBeforeMaxAttempts(): void
@@ -56,6 +59,9 @@ public function testItIsSuccessfulBeforeMaxAttempts(): void
'commit:3',
], $unitOfWork->sequence);
$this->assertSame($unitOfWork->exceptions->reported, [$ex1, $ex2]);
+ $this->assertSame(3, $unitOfWork->attempts);
+ $this->assertSame(1, $unitOfWork->commits);
+ $this->assertSame(2, $unitOfWork->rollbacks);
}
public function testItIsSuccessfulOnMaxAttempts(): void
@@ -84,6 +90,9 @@ public function testItIsSuccessfulOnMaxAttempts(): void
'commit:3',
], $unitOfWork->sequence);
$this->assertSame($unitOfWork->exceptions->reported, [$ex1, $ex2]);
+ $this->assertSame(3, $unitOfWork->attempts);
+ $this->assertSame(1, $unitOfWork->commits);
+ $this->assertSame(2, $unitOfWork->rollbacks);
}
public function testItIsNotSuccessful(): void
@@ -108,6 +117,9 @@ public function testItIsNotSuccessful(): void
'rollback:2',
], $unitOfWork->sequence);
$this->assertSame($unitOfWork->exceptions->reported, [$ex1, $ex2]);
+ $this->assertSame(2, $unitOfWork->attempts);
+ $this->assertSame(0, $unitOfWork->commits);
+ $this->assertSame(2, $unitOfWork->rollbacks);
}
}
}
diff --git a/tests/Unit/Toolkit/Pipeline/PipeContainerTest.php b/tests/Unit/Toolkit/Pipeline/PipeContainerTest.php
index d2ca32a..6da2fea 100644
--- a/tests/Unit/Toolkit/Pipeline/PipeContainerTest.php
+++ b/tests/Unit/Toolkit/Pipeline/PipeContainerTest.php
@@ -14,10 +14,11 @@
use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer;
use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
class PipeContainerTest extends TestCase
{
- public function test(): void
+ public function testItResolvesBoundPipes(): void
{
$a = fn () => 1;
$b = fn () => 2;
@@ -34,4 +35,27 @@ public function test(): void
$container->get('PipeC');
}
+
+ public function testItFallsBackToPsrContainer(): void
+ {
+ $psrContainer = $this->createMock(ContainerInterface::class);
+
+ $a = fn () => 1;
+ $b = fn () => 2;
+ $c = fn () => 3;
+
+ $container = new PipeContainer($psrContainer);
+ $container->bind('PipeA', fn () => $a);
+ $container->bind('PipeB', fn () => $b);
+
+ $psrContainer
+ ->expects($this->once())
+ ->method('get')
+ ->with('PipeC')
+ ->willReturn($c);
+
+ $this->assertSame($a, $container->get('PipeA'));
+ $this->assertSame($b, $container->get('PipeB'));
+ $this->assertSame($c, $container->get('PipeC'));
+ }
}