Skip to content

Commit 40845cc

Browse files
committed
feat: add autowiring via attributes to the command queue
1 parent d42e69c commit 40845cc

21 files changed

Lines changed: 458 additions & 41 deletions

CHANGELOG.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,28 @@ All notable changes to this project will be documented in this file. This projec
2323
- Integration events can now be mapped to handlers on an inbound event bus class via the `WithEvent` attribute.
2424
- The default handler can be set on the inbound event bus via the `WithDefault` attribute.
2525
- Middleware can now be added to an inbound event bus via the `Through` attribute.
26-
- New outbound event bus features, when using the publisher handler container:
27-
- Can now use a PSR container for the outbound event bus to resolve both handlers and middleware. Inject the service
28-
container via the first constructor argument.
29-
- Integration events can now be mapped to handlers on a publisher handler container class via the `Publishes`
26+
- New outbound event bus features, when using the component publisher:
27+
- Can now use a PSR container for the outbound event bus to resolve both publishers and middleware. Inject the
28+
service container via the constructor.
29+
- Integration events can now be mapped to publishers on a publisher handler container class via the `Publishes`
3030
attribute.
31-
- The default handler can be set on the outbound event publisher via the `WithDefault` attribute.
32-
- Middleware can now be added to a publisher handler container via the `Through` attribute.
31+
- The default publisher can be set on the outbound event publisher via the `DefaultPublisher` attribute.
32+
- Middleware can now be added to an outbound event publisher via the `Through` attribute.
33+
- New queue features, when using the component queue:
34+
- Can now use a PSR container for the queue to resolve both enqueuers and middleware. Inject the service container
35+
via the constructor.
36+
- Commands can now be mapped to enqueuers on a publisher handler container class via the `Queues` attribute.
37+
- The default enqueuer can be set on the outbound event publisher via the `DefaultEnqueuer` attribute.
38+
- Middleware can now be added to the queue via the `Through` attribute.
3339
- In the Application layer, the `QueryHandlerContainer`, `CommandHandlerContainer` and `EventHandlerContainer` classes
3440
can now fallback to resolving handlers from a PSR service container. Inject the service container via their
3541
constructors.
36-
- In the Infrastructure layer, the `PublisherHandlerContainer` can now fallback to resolving handlers from a PSR service
37-
container. Inject the service container via the constructor.
42+
- In the Infrastructure layer, the `PublisherHandlerContainer` and `EnqueuerContainer` can now fallback to resolving
43+
handlers/enqueuers from a PSR service container. Inject the service container via the constructor.
44+
- The outbound event bus `ClosurePublisher` and the `ClosureQueue` classes now both accept a PSR container for their
45+
middleware. Additionally, middleware can be set on instances of closure publishers via the `Through` attribute.
3846
- The pipeline `PipeContainer` class can now fallback to resolving pipes from a PSR service container. Inject the
3947
service container via the pipe container's only constructor argument.
40-
- The outbound event bus `ClosurePublisher` class now accepts a PSR container for its middleware. Additionally,
41-
middleware can be set on instances of closure publishers via the `Through` attribute.
4248
- The `FakeUnitOfWork` class now has integer properties for the number of attempts, commits and rollbacks.
4349
- New `FakeContainer` class for faking a PSR container in tests.
4450

src/Infrastructure/OutboundEventBus/ComponentPublisher.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ private function autowire(): void
8080
if ($this->handlers instanceof PublisherHandlerContainer) {
8181
foreach ($reflection->getAttributes(Publishes::class) as $attribute) {
8282
$instance = $attribute->newInstance();
83-
$this->handlers->bind($instance->event, $instance->handler);
83+
$this->handlers->bind($instance->event, $instance->publisher);
8484
}
8585

86-
foreach ($reflection->getAttributes(WithDefault::class) as $attribute) {
87-
$this->handlers->withDefault($attribute->newInstance()->handler);
86+
foreach ($reflection->getAttributes(DefaultPublisher::class) as $attribute) {
87+
$this->handlers->withDefault($attribute->newInstance()->publisher);
8888
}
8989
}
9090

src/Infrastructure/OutboundEventBus/WithDefault.php renamed to src/Infrastructure/OutboundEventBus/DefaultPublisher.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
use Attribute;
1616

1717
#[Attribute(Attribute::TARGET_CLASS)]
18-
final readonly class WithDefault
18+
final readonly class DefaultPublisher
1919
{
2020
/**
21-
* @param class-string $handler
21+
* @param non-empty-string $publisher
2222
*/
23-
public function __construct(public string $handler)
23+
public function __construct(public string $publisher)
2424
{
2525
}
2626
}

src/Infrastructure/OutboundEventBus/Publishes.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
{
2121
/**
2222
* @param class-string<IntegrationEvent> $event
23-
* @param class-string $handler
23+
* @param class-string $publisher
2424
*/
25-
public function __construct(public string $event, public string $handler)
25+
public function __construct(public string $event, public string $publisher)
2626
{
2727
}
2828
}

src/Infrastructure/Queue/ClosureQueue.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer;
1919
use CloudCreativity\Modules\Toolkit\Pipeline\MiddlewareProcessor;
2020
use CloudCreativity\Modules\Toolkit\Pipeline\PipelineBuilder;
21+
use CloudCreativity\Modules\Toolkit\Pipeline\Through;
22+
use Psr\Container\ContainerInterface;
23+
use ReflectionClass;
2124

2225
class ClosureQueue implements Queue
2326
{
@@ -33,8 +36,9 @@ class ClosureQueue implements Queue
3336

3437
public function __construct(
3538
private readonly Closure $fn,
36-
private readonly ?PipeContainer $middleware = null,
39+
private readonly ContainerInterface|PipeContainer|null $middleware = null,
3740
) {
41+
$this->autowire();
3842
}
3943

4044
/**
@@ -67,4 +71,13 @@ public function push(Command $command): void
6771

6872
$pipeline->process($command);
6973
}
74+
75+
private function autowire(): void
76+
{
77+
$reflection = new ReflectionClass($this);
78+
79+
foreach ($reflection->getAttributes(Through::class) as $attribute) {
80+
$this->pipes = $attribute->newInstance()->pipes;
81+
}
82+
}
7083
}

src/Infrastructure/Queue/ComponentQueue.php

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,40 @@
1313
namespace CloudCreativity\Modules\Infrastructure\Queue;
1414

1515
use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue;
16-
use CloudCreativity\Modules\Contracts\Infrastructure\Queue\EnqueuerContainer;
16+
use CloudCreativity\Modules\Contracts\Infrastructure\Queue\EnqueuerContainer as IEnqueuerContainer;
1717
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
18-
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer;
18+
use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer as IPipeContainer;
1919
use CloudCreativity\Modules\Toolkit\Pipeline\MiddlewareProcessor;
20+
use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer;
2021
use CloudCreativity\Modules\Toolkit\Pipeline\PipelineBuilder;
22+
use CloudCreativity\Modules\Toolkit\Pipeline\Through;
23+
use Psr\Container\ContainerInterface;
24+
use ReflectionClass;
2125

2226
class ComponentQueue implements Queue
2327
{
28+
private readonly IEnqueuerContainer $enqueuers;
29+
30+
private readonly ?IPipeContainer $middleware;
31+
2432
/**
2533
* @var list<callable|string>
2634
*/
2735
private array $pipes = [];
2836

2937
public function __construct(
30-
private readonly EnqueuerContainer $enqueuers,
31-
private readonly ?PipeContainer $middleware = null,
38+
ContainerInterface|IEnqueuerContainer $enqueuers,
39+
?IPipeContainer $middleware = null,
3240
) {
41+
$this->enqueuers = $enqueuers instanceof ContainerInterface ?
42+
new EnqueuerContainer(container: $enqueuers) :
43+
$enqueuers;
44+
45+
$this->middleware = $middleware === null && $enqueuers instanceof ContainerInterface
46+
? new PipeContainer($enqueuers)
47+
: $middleware;
48+
49+
$this->autowire();
3350
}
3451

3552
/**
@@ -53,4 +70,24 @@ public function push(Command $command): void
5370

5471
$pipeline->process($command);
5572
}
73+
74+
private function autowire(): void
75+
{
76+
$reflection = new ReflectionClass($this);
77+
78+
if ($this->enqueuers instanceof EnqueuerContainer) {
79+
foreach ($reflection->getAttributes(Queues::class) as $attribute) {
80+
$instance = $attribute->newInstance();
81+
$this->enqueuers->bind($instance->command, $instance->enqueuer);
82+
}
83+
84+
foreach ($reflection->getAttributes(DefaultEnqueuer::class) as $attribute) {
85+
$this->enqueuers->withDefault($attribute->newInstance()->enqueuer);
86+
}
87+
}
88+
89+
foreach ($reflection->getAttributes(Through::class) as $attribute) {
90+
$this->pipes = $attribute->newInstance()->pipes;
91+
}
92+
}
5693
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* Copyright 2026 Cloud Creativity Limited
5+
*
6+
* Use of this source code is governed by an MIT-style
7+
* license that can be found in the LICENSE file or at
8+
* https://opensource.org/licenses/MIT.
9+
*/
10+
11+
declare(strict_types=1);
12+
13+
namespace CloudCreativity\Modules\Infrastructure\Queue;
14+
15+
use Attribute;
16+
17+
#[Attribute(Attribute::TARGET_CLASS)]
18+
final readonly class DefaultEnqueuer
19+
{
20+
/**
21+
* @param non-empty-string $enqueuer
22+
*/
23+
public function __construct(public string $enqueuer)
24+
{
25+
}
26+
}

src/Infrastructure/Queue/EnqueuerContainer.php

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,72 @@
1515
use Closure;
1616
use CloudCreativity\Modules\Contracts\Infrastructure\Queue\EnqueuerContainer as IEnqueuerContainer;
1717
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
18+
use CloudCreativity\Modules\Infrastructure\InfrastructureException;
19+
use Psr\Container\ContainerInterface;
1820

1921
final class EnqueuerContainer implements IEnqueuerContainer
2022
{
2123
/**
22-
* @var array<class-string<Command>, Closure>
24+
* @var array<class-string<Command>, Closure|string>
2325
*/
2426
private array $bindings = [];
2527

2628
/**
27-
* @param Closure(): object $default
29+
* @param (Closure(): object)|string|null $default
2830
*/
29-
public function __construct(private readonly Closure $default)
30-
{
31+
public function __construct(
32+
private Closure|string|null $default = null,
33+
private readonly ?ContainerInterface $container = null,
34+
) {
35+
if (is_string($this->default) && $this->container === null) {
36+
throw new InfrastructureException(
37+
'Cannot bind default enqueuer as a string without a PSR container.',
38+
);
39+
}
3140
}
3241

3342
/**
3443
* Bind an enqueuer factory into the container.
3544
*
3645
* @param class-string<Command> $queueableName
37-
* @param Closure(): object $binding
46+
* @param (Closure(): object)|string $binding
3847
*/
39-
public function bind(string $queueableName, Closure $binding): void
48+
public function bind(string $queueableName, Closure|string $binding): void
4049
{
4150
$this->bindings[$queueableName] = $binding;
4251
}
4352

53+
public function withDefault(Closure|string $binding): void
54+
{
55+
if ($this->default !== null) {
56+
throw new InfrastructureException('Default enqueuer is already set.');
57+
}
58+
59+
if (is_string($binding) && $this->container === null) {
60+
throw new InfrastructureException(
61+
'Cannot bind default enqueuer as a string without a PSR container.',
62+
);
63+
}
64+
65+
$this->default = $binding;
66+
}
67+
4468
public function get(string $command): Enqueuer
4569
{
46-
$factory = $this->bindings[$command] ?? $this->default;
70+
$binding = $this->bindings[$command] ?? $this->default;
4771

48-
$enqueuer = $factory();
72+
if ($binding instanceof Closure) {
73+
$instance = $binding();
74+
assert(is_object($instance), "Enqueuer binding for command {$command} must return an object.");
75+
return new Enqueuer($instance);
76+
}
4977

50-
assert(is_object($enqueuer), "Enqueuer binding for {$command} must return an object.");
78+
if (is_string($binding)) {
79+
$instance = $this->container?->get($binding);
80+
assert(is_object($instance), "PSR container enqueuer binding {$binding} is not an object.");
81+
return new Enqueuer($instance);
82+
}
5183

52-
return new Enqueuer($enqueuer);
84+
throw new InfrastructureException('No enqueuer bound for command: ' . $command);
5385
}
5486
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* Copyright 2026 Cloud Creativity Limited
5+
*
6+
* Use of this source code is governed by an MIT-style
7+
* license that can be found in the LICENSE file or at
8+
* https://opensource.org/licenses/MIT.
9+
*/
10+
11+
declare(strict_types=1);
12+
13+
namespace CloudCreativity\Modules\Infrastructure\Queue;
14+
15+
use Attribute;
16+
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
17+
18+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
19+
final readonly class Queues
20+
{
21+
/**
22+
* @param class-string<Command> $command
23+
* @param class-string $enqueuer
24+
*/
25+
public function __construct(public string $command, public string $enqueuer)
26+
{
27+
}
28+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* Copyright 2026 Cloud Creativity Limited
5+
*
6+
* Use of this source code is governed by an MIT-style
7+
* license that can be found in the LICENSE file or at
8+
* https://opensource.org/licenses/MIT.
9+
*/
10+
11+
declare(strict_types=1);
12+
13+
namespace CloudCreativity\Modules\Tests\Integration\Application\Bus;
14+
15+
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
16+
17+
final readonly class FloorCommand implements Command
18+
{
19+
public function __construct(public float $number)
20+
{
21+
}
22+
}

0 commit comments

Comments
 (0)