Skip to content

Commit 7b82867

Browse files
committed
Merge branch 'release/5.1.0'
2 parents 85fbcd7 + 62620e0 commit 7b82867

304 files changed

Lines changed: 3047 additions & 449 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,53 @@ All notable changes to this project will be documented in this file. This projec
55

66
## Unreleased
77

8+
## [5.1.0] - 2026-01-01
9+
10+
### Added
11+
12+
- New command bus features:
13+
- Can now use a PSR container for the command bus to resolve both handlers and middleware. Inject the service
14+
container via the first constructor argument.
15+
- Commands can now be mapped to handlers on a command bus class via the `WithCommand` attribute.
16+
- Middleware can now be added to a command bus via the `Through` attribute.
17+
- New query bus features:
18+
- Can now use a PSR container for the query bus to resolve both handlers and middleware. Inject the service
19+
container via the first constructor argument.
20+
- Queries can now be mapped to handlers on a query bus class via the `WithQuery` attribute.
21+
- Middleware can now be added to a query bus via the `Through` attribute.
22+
- New inbound event bus features:
23+
- Can now use a PSR container for the inbound event bus to resolve both handlers and middleware. Inject the service
24+
container via the first constructor argument.
25+
- Integration events can now be mapped to handlers on an inbound event bus class via the `WithEvent` attribute.
26+
- The default handler can be set on the inbound event bus via the `WithDefault` attribute.
27+
- Middleware can now be added to an inbound event bus via the `Through` attribute.
28+
- New outbound event bus features, when using the component publisher:
29+
- Can now use a PSR container for the outbound event bus to resolve both publishers and middleware. Inject the
30+
service container via the constructor.
31+
- Integration events can now be mapped to publishers on a publisher handler container class via the `Publishes`
32+
attribute.
33+
- The default publisher can be set on the outbound event publisher via the `DefaultPublisher` attribute.
34+
- Middleware can now be added to an outbound event publisher via the `Through` attribute.
35+
- New queue features, when using the component queue:
36+
- Can now use a PSR container for the queue to resolve both enqueuers and middleware. Inject the service container
37+
via the constructor.
38+
- Commands can now be mapped to enqueuers on a publisher handler container class via the `Queues` attribute.
39+
- The default enqueuer can be set on the outbound event publisher via the `DefaultEnqueuer` attribute.
40+
- Middleware can now be added to the queue via the `Through` attribute.
41+
- In the Application layer, the `QueryHandlerContainer`, `CommandHandlerContainer` and `EventHandlerContainer` classes
42+
can now fall back to resolving handlers from a PSR service container. Inject the service container via their
43+
constructors.
44+
- In the Infrastructure layer, the `PublisherHandlerContainer` and `EnqueuerContainer` can now fall back to resolving
45+
handlers/enqueuers from a PSR service container. Inject the service container via the constructor.
46+
- The outbound event bus `ClosurePublisher` and the `ClosureQueue` classes now both accept a PSR container for their
47+
middleware. Additionally, middleware can be set on instances of closure publishers via the `Through` attribute.
48+
- The pipeline `PipeContainer` class can now fall back to resolving pipes from a PSR service container. Inject the
49+
service container via the pipe container's only constructor argument.
50+
- The `FakeUnitOfWork` class now has integer properties for the number of attempts, commits and rollbacks.
51+
- New `FakeContainer` class for faking a PSR container in tests.
52+
- Added `UuidV4` and `UuidV7` identifiers, for use by implementations that need to enforce use of specific UUID
53+
versions.
54+
855
## [5.0.0] - 2025-12-09
956

1057
### Added
@@ -97,7 +144,7 @@ All notable changes to this project will be documented in this file. This projec
97144
- **BREAKING** The error and error list interfaces now accept `UnitEnum` instead of `BackedEnum` for error codes.
98145
Although technically breaking, this will only affect your implementation if you have implemented these interfaces. All
99146
concrete classes provided by this package have been updated.
100-
- **BREAKING**: The key of an error can now be a enum - previously only strings were accepted. This is only breaking if
147+
- **BREAKING**: The key of an error can now be an enum - previously only strings were accepted. This is only breaking if
101148
you have implemented the interface yourself.
102149
- Updated `KeyedSetOfErrors` to handle error keys now being strings or enums.
103150
- **BREAKING**: The `Guid::make()` method will now convert a string that is a UUID to a UUID GUID. Previously it would
@@ -306,8 +353,8 @@ All notable changes to this project will be documented in this file. This projec
306353
the `Contracts\Application\Ports` namespace, with them differentiated between driving and driven ports.
307354
- **BREAKING** As a number of interfaces had to be moved to a `Ports` namespace, we've tidied them all up by removing
308355
the `Interface` suffix and moving them to a `Contracts` namespace.
309-
- **BREAKING** We've also removed the `Trait` suffix from traits. To avoid collisions with interfaces, we've use `Is` a
310-
prefix where it makes sense. For example, `EntityTrait` has become `IsEntity`.
356+
- **BREAKING** We've also removed the `Trait` suffix from traits. To avoid collisions with interfaces, we've used an
357+
`Is` prefix where it makes sense. For example, `EntityTrait` has become `IsEntity`.
311358
- **BREAKING** The `DomainEventDispatching` namespace has been moved from `Infrastructure` to `Application`. This was
312359
needed for the new hexagonal architecture approach, but also makes it a lot clearer that domain events are the way the
313360
domain layer communicates with the application layer.
@@ -558,6 +605,8 @@ All notable changes to this project will be documented in this file. This projec
558605

559606
Initial release.
560607

608+
[5.0.0]: https://github.com/cloudcreativity/ddd-modules/compare/v5.0.0-rc.4...v5.0.0
609+
561610
[5.0.0-rc.4]: https://github.com/cloudcreativity/ddd-modules/compare/v5.0.0-rc.3...v5.0.0-rc.4
562611

563612
[5.0.0-rc.3]: https://github.com/cloudcreativity/ddd-modules/compare/v5.0.0-rc.2...v5.0.0-rc.3

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
"require": {
2424
"php": "^8.2",
2525
"ext-json": "*",
26+
"psr/container": "^2.0",
2627
"psr/log": "^2.0 || ^3.0",
2728
"ramsey/uuid": "^4.7",
28-
"symfony/polyfill-php84": "^1.33"
29+
"symfony/polyfill-php84": "^1.33",
30+
"symfony/polyfill-php85": "^1.33"
2931
},
3032
"require-dev": {
3133
"deptrac/deptrac": "^4.4",

deptrac.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ deptrac:
2828
value: CloudCreativity\\Modules\\Contracts\\Infrastructure\\*
2929
- type: classLike
3030
value: CloudCreativity\\Modules\\Infrastructure\\*
31+
- name: PSR Container
32+
collectors:
33+
- type: classLike
34+
value: Psr\\Container\\*
3135
- name: PSR Log
3236
collectors:
3337
- type: classLike
@@ -39,17 +43,20 @@ deptrac:
3943
ruleset:
4044
Toolkit:
4145
- Attributes
46+
- PSR Container
4247
Domain:
4348
- Toolkit
4449
- Attributes
4550
Application:
4651
- Toolkit
4752
- Domain
53+
- PSR Container
4854
- PSR Log
4955
- Attributes
5056
Infrastructure:
5157
- Toolkit
5258
- Domain
5359
- Application
60+
- PSR Container
5461
- PSR Log
5562
- Attributes

phpunit.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
<testsuite name="Unit">
2020
<directory suffix="Test.php">./tests/Unit/</directory>
2121
</testsuite>
22+
<testsuite name="Integration">
23+
<directory suffix="Test.php">./tests/Integration/</directory>
24+
</testsuite>
2225
</testsuites>
2326
<php>
2427
<ini name="error_reporting" value="E_ALL"/>

src/Application/ApplicationException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/*
4-
* Copyright 2025 Cloud Creativity Limited
4+
* Copyright 2026 Cloud Creativity Limited
55
*
66
* Use of this source code is governed by an MIT-style
77
* license that can be found in the LICENSE file or at

src/Application/Bus/CommandDispatcher.php

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/*
4-
* Copyright 2025 Cloud Creativity Limited
4+
* Copyright 2026 Cloud Creativity Limited
55
*
66
* Use of this source code is governed by an MIT-style
77
* license that can be found in the LICENSE file or at
@@ -12,25 +12,42 @@
1212

1313
namespace CloudCreativity\Modules\Application\Bus;
1414

15-
use CloudCreativity\Modules\Contracts\Application\Bus\CommandHandlerContainer;
15+
use CloudCreativity\Modules\Contracts\Application\Bus\CommandHandlerContainer as ICommandHandlerContainer;
1616
use CloudCreativity\Modules\Contracts\Application\Ports\Driving\CommandDispatcher as ICommandDispatcher;
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\Contracts\Toolkit\Result\Result;
2020
use CloudCreativity\Modules\Toolkit\Pipeline\MiddlewareProcessor;
21+
use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer;
2122
use CloudCreativity\Modules\Toolkit\Pipeline\PipelineBuilder;
23+
use CloudCreativity\Modules\Toolkit\Pipeline\Through;
24+
use Psr\Container\ContainerInterface;
25+
use ReflectionClass;
2226

2327
class CommandDispatcher implements ICommandDispatcher
2428
{
29+
private readonly ICommandHandlerContainer $handlers;
30+
31+
private readonly ?IPipeContainer $middleware;
32+
2533
/**
2634
* @var array<callable|string>
2735
*/
2836
private array $pipes = [];
2937

3038
public function __construct(
31-
private readonly CommandHandlerContainer $handlers,
32-
private readonly ?PipeContainer $middleware = null,
39+
ContainerInterface|ICommandHandlerContainer $handlers,
40+
?IPipeContainer $middleware = null,
3341
) {
42+
$this->handlers = $handlers instanceof ContainerInterface ?
43+
new CommandHandlerContainer($handlers) :
44+
$handlers;
45+
46+
$this->middleware = $middleware === null && $handlers instanceof ContainerInterface
47+
? new PipeContainer($handlers)
48+
: $middleware;
49+
50+
$this->autowire();
3451
}
3552

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

7895
return $result;
7996
}
97+
98+
private function autowire(): void
99+
{
100+
$reflection = new ReflectionClass($this);
101+
102+
if ($this->handlers instanceof CommandHandlerContainer) {
103+
foreach ($reflection->getAttributes(WithCommand::class) as $attribute) {
104+
$instance = $attribute->newInstance();
105+
$this->handlers->bind($instance->command, $instance->handler);
106+
}
107+
}
108+
109+
foreach ($reflection->getAttributes(Through::class) as $attribute) {
110+
$instance = $attribute->newInstance();
111+
$this->pipes = $instance->pipes;
112+
}
113+
}
80114
}

src/Application/Bus/CommandHandler.php

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/*
4-
* Copyright 2025 Cloud Creativity Limited
4+
* Copyright 2026 Cloud Creativity Limited
55
*
66
* Use of this source code is governed by an MIT-style
77
* license that can be found in the LICENSE file or at
@@ -12,13 +12,15 @@
1212

1313
namespace CloudCreativity\Modules\Application\Bus;
1414

15+
use CloudCreativity\Modules\Application\Messages\HandlesMessages;
1516
use CloudCreativity\Modules\Contracts\Application\Bus\CommandHandler as ICommandHandler;
16-
use CloudCreativity\Modules\Contracts\Application\Messages\DispatchThroughMiddleware;
1717
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
1818
use CloudCreativity\Modules\Contracts\Toolkit\Result\Result;
1919

2020
final readonly class CommandHandler implements ICommandHandler
2121
{
22+
use HandlesMessages;
23+
2224
public function __construct(private object $handler)
2325
{
2426
}
@@ -37,13 +39,4 @@ public function __invoke(Command $command): Result
3739

3840
return $result;
3941
}
40-
41-
public function middleware(): array
42-
{
43-
if ($this->handler instanceof DispatchThroughMiddleware) {
44-
return $this->handler->middleware();
45-
}
46-
47-
return [];
48-
}
4942
}

src/Application/Bus/CommandHandlerContainer.php

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/*
4-
* Copyright 2025 Cloud Creativity Limited
4+
* Copyright 2026 Cloud Creativity Limited
55
*
66
* Use of this source code is governed by an MIT-style
77
* license that can be found in the LICENSE file or at
@@ -16,33 +16,48 @@
1616
use CloudCreativity\Modules\Application\ApplicationException;
1717
use CloudCreativity\Modules\Contracts\Application\Bus\CommandHandlerContainer as ICommandHandlerContainer;
1818
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
19+
use Psr\Container\ContainerInterface;
1920

2021
final class CommandHandlerContainer implements ICommandHandlerContainer
2122
{
2223
/**
23-
* @var array<class-string<Command>, Closure>
24+
* @var array<class-string<Command>, class-string|Closure>
2425
*/
2526
private array $bindings = [];
2627

28+
public function __construct(private readonly ?ContainerInterface $container = null)
29+
{
30+
}
31+
2732
/**
2833
* Bind a command handler into the container.
2934
*
3035
* @param class-string<Command> $commandClass
31-
* @param Closure(): object $binding
36+
* @param class-string|(Closure(): object) $binding
3237
*/
33-
public function bind(string $commandClass, Closure $binding): void
38+
public function bind(string $commandClass, Closure|string $binding): void
3439
{
40+
if (is_string($binding) && $this->container === null) {
41+
throw new ApplicationException('Cannot use a string command handler binding without a PSR container.');
42+
}
43+
3544
$this->bindings[$commandClass] = $binding;
3645
}
3746

3847
public function get(string $commandClass): CommandHandler
3948
{
40-
$factory = $this->bindings[$commandClass] ?? null;
49+
$binding = $this->bindings[$commandClass] ?? null;
50+
51+
if ($binding instanceof Closure) {
52+
$instance = $binding();
53+
assert(is_object($instance), "Command handler binding for {$commandClass} must return an object.");
54+
return new CommandHandler($instance);
55+
}
4156

42-
if ($factory) {
43-
$innerHandler = $factory();
44-
assert(is_object($innerHandler), "Command handler binding for {$commandClass} must return an object.");
45-
return new CommandHandler($innerHandler);
57+
if (is_string($binding)) {
58+
$instance = $this->container?->get($binding);
59+
assert(is_object($instance), "PSR container command handler binding {$binding} is not an object.");
60+
return new CommandHandler($instance);
4661
}
4762

4863
throw new ApplicationException('No command handler bound for command class: ' . $commandClass);

src/Application/Bus/CommandQueuer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/*
4-
* Copyright 2025 Cloud Creativity Limited
4+
* Copyright 2026 Cloud Creativity Limited
55
*
66
* Use of this source code is governed by an MIT-style
77
* license that can be found in the LICENSE file or at

src/Application/Bus/Exceptions/AbortOnFailureException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/*
4-
* Copyright 2025 Cloud Creativity Limited
4+
* Copyright 2026 Cloud Creativity Limited
55
*
66
* Use of this source code is governed by an MIT-style
77
* license that can be found in the LICENSE file or at

0 commit comments

Comments
 (0)