diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7b8ce..7d1d244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,28 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased -### [2.0.0-rc.1] - 2024-05-07 +## [2.0.0-rc.2] - 2024-07-27 + +### Added + +- The `Uuid` identifier class now has a `getBytes()` method +- Can now get a nil UUID from the `Uuid::nil()` static method. + +### Changed + +- Made resolution of inner handlers lazy in all buses. In several the handler was immediately resolved, so that the + handler middleware could be calculated. Buses that support handler middleware now first pipe through the bus + middleware, then resolve the inner handler, then pipe through the handler middleware. This allows inner handler + constructor injected dependencies to be lazily resolved after the bus middleware has executed. This is important when + using the setup and teardown middleware for bootstrapping services that may be injected into the inner handler. Buses + that now lazily resolve inner handlers are: + - Command bus + - Query bus + - Inbound integration event bus + - Outbound integration event bus + - Queue bus + +## [2.0.0-rc.1] - 2024-05-07 **Refer to the [Upgrade Guide.](./docs/guide/upgrade.md)** diff --git a/src/Application/Bus/CommandDispatcher.php b/src/Application/Bus/CommandDispatcher.php index 199168c..3b97cc8 100644 --- a/src/Application/Bus/CommandDispatcher.php +++ b/src/Application/Bus/CommandDispatcher.php @@ -67,11 +67,11 @@ public function through(array $pipes): void */ public function dispatch(Command $command): Result { - $handler = $this->handlers->get($command::class); - $pipeline = PipelineBuilder::make($this->middleware) - ->through([...$this->pipes, ...array_values($handler->middleware())]) - ->build(MiddlewareProcessor::wrap($handler)); + ->through($this->pipes) + ->build(new MiddlewareProcessor( + fn (Command $passed): Result => $this->execute($passed), + )); $result = $pipeline->process($command); @@ -97,4 +97,23 @@ public function queue(Command $command): void $this->queue->push($command); } + + /** + * @param Command $command + * @return Result + */ + private function execute(Command $command): Result + { + $handler = $this->handlers->get($command::class); + + $pipeline = PipelineBuilder::make($this->middleware) + ->through($handler->middleware()) + ->build(MiddlewareProcessor::wrap($handler)); + + $result = $pipeline->process($command); + + assert($result instanceof Result, 'Expecting pipeline to return a result object.'); + + return $result; + } } diff --git a/src/Application/Bus/QueryDispatcher.php b/src/Application/Bus/QueryDispatcher.php index 0810373..1152f83 100644 --- a/src/Application/Bus/QueryDispatcher.php +++ b/src/Application/Bus/QueryDispatcher.php @@ -55,11 +55,30 @@ public function through(array $pipes): void * @inheritDoc */ public function dispatch(Query $query): Result + { + $pipeline = PipelineBuilder::make($this->middleware) + ->through($this->pipes) + ->build(new MiddlewareProcessor( + fn (Query $passed): Result => $this->execute($passed), + )); + + $result = $pipeline->process($query); + + assert($result instanceof Result, 'Expecting pipeline to return a result object.'); + + return $result; + } + + /** + * @param Query $query + * @return Result + */ + private function execute(Query $query): Result { $handler = $this->handlers->get($query::class); $pipeline = PipelineBuilder::make($this->middleware) - ->through([...$this->pipes, ...array_values($handler->middleware())]) + ->through($handler->middleware()) ->build(MiddlewareProcessor::wrap($handler)); $result = $pipeline->process($query); diff --git a/src/Application/InboundEventBus/EventDispatcher.php b/src/Application/InboundEventBus/EventDispatcher.php index 48b4dd4..863d2d6 100644 --- a/src/Application/InboundEventBus/EventDispatcher.php +++ b/src/Application/InboundEventBus/EventDispatcher.php @@ -54,11 +54,26 @@ public function through(array $pipes): void * @inheritDoc */ public function dispatch(IntegrationEvent $event): void + { + $pipeline = PipelineBuilder::make($this->middleware) + ->through($this->pipes) + ->build(new MiddlewareProcessor(function (IntegrationEvent $passed): void { + $this->execute($passed); + })); + + $pipeline->process($event); + } + + /** + * @param IntegrationEvent $event + * @return void + */ + private function execute(IntegrationEvent $event): void { $handler = $this->handlers->get($event::class); $pipeline = PipelineBuilder::make($this->middleware) - ->through([...$this->pipes, ...$handler->middleware()]) + ->through($handler->middleware()) ->build(MiddlewareProcessor::call($handler)); $pipeline->process($event); diff --git a/src/Contracts/Toolkit/Identifiers/IdentifierFactory.php b/src/Contracts/Toolkit/Identifiers/IdentifierFactory.php index 0fb1e75..3eedbbb 100644 --- a/src/Contracts/Toolkit/Identifiers/IdentifierFactory.php +++ b/src/Contracts/Toolkit/Identifiers/IdentifierFactory.php @@ -1,6 +1,4 @@ handlers->get($event::class); - $pipeline = PipelineBuilder::make($this->middleware) ->through($this->pipes) - ->build(MiddlewareProcessor::call($handler)); + ->build(new MiddlewareProcessor(function (IntegrationEvent $passed): void { + $handler = $this->handlers->get($passed::class); + $handler($passed); + })); $pipeline->process($event); } diff --git a/src/Infrastructure/Queue/ComponentQueue.php b/src/Infrastructure/Queue/ComponentQueue.php index 12eb5f0..67c1f1b 100644 --- a/src/Infrastructure/Queue/ComponentQueue.php +++ b/src/Infrastructure/Queue/ComponentQueue.php @@ -53,11 +53,12 @@ public function through(array $pipes): void */ public function push(Command $command): void { - $enqueuer = $this->enqueuers->get($command::class); - $pipeline = PipelineBuilder::make($this->middleware) ->through($this->pipes) - ->build(MiddlewareProcessor::call($enqueuer)); + ->build(new MiddlewareProcessor(function (Command $passed): void { + $enqueuer = $this->enqueuers->get($passed::class); + $enqueuer($passed); + })); $pipeline->process($command); } diff --git a/src/Toolkit/Identifiers/Uuid.php b/src/Toolkit/Identifiers/Uuid.php index 37cfa60..572bfa9 100644 --- a/src/Toolkit/Identifiers/Uuid.php +++ b/src/Toolkit/Identifiers/Uuid.php @@ -14,7 +14,8 @@ use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\UuidFactory as IUuidFactory; use JsonSerializable; -use Ramsey\Uuid\UuidInterface as BaseUuid; +use Ramsey\Uuid\Uuid as BaseUuid; +use Ramsey\Uuid\UuidInterface as IBaseUuid; final class Uuid implements Identifier, JsonSerializable { @@ -45,15 +46,15 @@ public static function getFactory(): IUuidFactory } /** - * @param Identifier|BaseUuid|string $value + * @param Identifier|IBaseUuid|string $value * @return self */ - public static function from(Identifier|BaseUuid|string $value): self + public static function from(Identifier|IBaseUuid|string $value): self { $factory = self::getFactory(); return match(true) { - $value instanceof Identifier, $value instanceof BaseUuid => $factory->from($value), + $value instanceof Identifier, $value instanceof IBaseUuid => $factory->from($value), is_string($value) => $factory->fromString($value), }; } @@ -68,12 +69,22 @@ public static function random(): self return self::getFactory()->uuid4(); } + /** + * Create a nil UUID. + * + * @return self + */ + public static function nil(): self + { + return self::from(BaseUuid::NIL); + } + /** * Uuid constructor. * - * @param BaseUuid $value + * @param IBaseUuid $value */ - public function __construct(public readonly BaseUuid $value) + public function __construct(public readonly IBaseUuid $value) { } @@ -94,6 +105,14 @@ public function toString(): string return $this->value->toString(); } + /** + * @return string + */ + public function getBytes(): string + { + return $this->value->getBytes(); + } + /** * @inheritDoc */ diff --git a/tests/Unit/Application/Bus/CommandDispatcherTest.php b/tests/Unit/Application/Bus/CommandDispatcherTest.php index 2819caf..21780f3 100644 --- a/tests/Unit/Application/Bus/CommandDispatcherTest.php +++ b/tests/Unit/Application/Bus/CommandDispatcherTest.php @@ -17,7 +17,7 @@ use CloudCreativity\Modules\Contracts\Application\Messages\Command; use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue\Queue; use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer; -use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; +use CloudCreativity\Modules\Toolkit\Result\Result; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -43,6 +43,11 @@ class CommandDispatcherTest extends TestCase */ private CommandDispatcher $dispatcher; + /** + * @var array + */ + private array $sequence = []; + /** * @return void */ @@ -59,6 +64,15 @@ protected function setUp(): void ); } + /** + * @return void + */ + protected function tearDown(): void + { + unset($this->handlers, $this->middleware, $this->queue, $this->dispatcher, $this->sequence); + parent::tearDown(); + } + /** * @return void */ @@ -78,7 +92,7 @@ public function test(): void ->expects($this->once()) ->method('__invoke') ->with($this->identicalTo($command)) - ->willReturn($expected = $this->createMock(Result::class)); + ->willReturn($expected = Result::ok()); $actual = $this->dispatcher->dispatch($command); @@ -96,32 +110,48 @@ public function testWithMiddleware(): void $command2 = new TestCommand(); $command3 = new TestCommand(); $command4 = new TestCommand(); + $handler = $this->createMock(CommandHandler::class); $middleware1 = function (TestCommand $command, \Closure $next) use ($command1, $command2) { $this->assertSame($command1, $command); - return $next($command2); + $this->sequence[] = 'before1'; + $result = $next($command2); + $this->sequence[] = 'after1'; + return $result; }; $middleware2 = function (TestCommand $command, \Closure $next) use ($command2, $command3) { $this->assertSame($command2, $command); - return $next($command3); + $this->sequence[] = 'before2'; + $result = $next($command3); + $this->sequence[] = 'after2'; + return $result; }; $middleware3 = function (TestCommand $command, \Closure $next) use ($command3, $command4) { $this->assertSame($command3, $command); - return $next($command4); + $this->sequence[] = 'before3'; + $result = $next($command4); + $this->sequence[] = 'after3'; + return $result; }; $this->handlers ->method('get') ->with($command1::class) - ->willReturn($handler = $this->createMock(CommandHandler::class)); + ->willReturnCallback(function () use ($handler) { + $this->assertSame(['before1'], $this->sequence); + return $handler; + }); $this->middleware - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('get') - ->with('MySecondMiddleware') - ->willReturn($middleware2); + ->willReturnCallback(fn (string $name) => match ($name) { + 'MyFirstMiddleware' => $middleware1, + 'MySecondMiddleware' => $middleware2, + default => $this->fail('Unexpected middleware: ' . $name), + }); $handler ->expects($this->once()) @@ -132,12 +162,20 @@ public function testWithMiddleware(): void ->expects($this->once()) ->method('__invoke') ->with($this->identicalTo($command4)) - ->willReturn($expected = $this->createMock(Result::class)); + ->willReturn($expected = Result::ok()); - $this->dispatcher->through([$middleware1]); + $this->dispatcher->through(['MyFirstMiddleware']); $actual = $this->dispatcher->dispatch($command1); $this->assertSame($expected, $actual); + $this->assertSame([ + 'before1', + 'before2', + 'before3', + 'after3', + 'after2', + 'after1', + ], $this->sequence); } /** diff --git a/tests/Unit/Application/Bus/QueryDispatcherTest.php b/tests/Unit/Application/Bus/QueryDispatcherTest.php index 43dfd29..f8a14bd 100644 --- a/tests/Unit/Application/Bus/QueryDispatcherTest.php +++ b/tests/Unit/Application/Bus/QueryDispatcherTest.php @@ -16,7 +16,7 @@ use CloudCreativity\Modules\Contracts\Application\Bus\QueryHandlerContainer; use CloudCreativity\Modules\Contracts\Application\Messages\Query; use CloudCreativity\Modules\Contracts\Toolkit\Pipeline\PipeContainer; -use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; +use CloudCreativity\Modules\Toolkit\Result\Result; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -37,6 +37,11 @@ class QueryDispatcherTest extends TestCase */ private QueryDispatcher $dispatcher; + /** + * @var array + */ + private array $sequence = []; + /** * @return void */ @@ -50,6 +55,15 @@ protected function setUp(): void ); } + /** + * @return void + */ + protected function tearDown(): void + { + unset($this->handlers, $this->middleware, $this->dispatcher, $this->sequence); + parent::tearDown(); + } + /** * @return void */ @@ -67,7 +81,7 @@ public function test(): void ->expects($this->once()) ->method('__invoke') ->with($this->identicalTo($query)) - ->willReturn($expected = $this->createMock(Result::class)); + ->willReturn($expected = Result::ok()); $actual = $this->dispatcher->dispatch($query); @@ -83,33 +97,49 @@ public function testWithMiddleware(): void $query2 = new TestQuery(); $query3 = new TestQuery(); $query4 = new TestQuery(); + $handler = $this->createMock(QueryHandler::class); $middleware1 = function (TestQuery $q, \Closure $next) use ($query1, $query2) { $this->assertSame($query1, $q); - return $next($query2); + $this->sequence[] = 'before1'; + $result = $next($query2); + $this->sequence[] = 'after1'; + return $result; }; $middleware2 = function (TestQuery $q, \Closure $next) use ($query2, $query3) { $this->assertSame($query2, $q); - return $next($query3); + $this->sequence[] = 'before2'; + $result = $next($query3); + $this->sequence[] = 'after2'; + return $result; }; $middleware3 = function (TestQuery $q, \Closure $next) use ($query3, $query4) { $this->assertSame($query3, $q); - return $next($query4); + $this->sequence[] = 'before3'; + $result = $next($query4); + $this->sequence[] = 'after3'; + return $result; }; $this->handlers ->expects($this->once()) ->method('get') ->with(TestQuery::class) - ->willReturn($handler = $this->createMock(QueryHandler::class)); + ->willReturnCallback(function () use ($handler) { + $this->assertSame(['before1'], $this->sequence); + return $handler; + }); $this->middleware - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('get') - ->with('MySecondMiddleware') - ->willReturn($middleware2); + ->willReturnCallback(fn (string $name) => match ($name) { + 'MyFirstMiddleware' => $middleware1, + 'MySecondMiddleware' => $middleware2, + default => $this->fail('Unexpected middleware: ' . $name), + }); $handler ->expects($this->once()) @@ -120,11 +150,19 @@ public function testWithMiddleware(): void ->expects($this->once()) ->method('__invoke') ->with($this->identicalTo($query4)) - ->willReturn($expected = $this->createMock(Result::class)); + ->willReturn($expected = Result::ok()); - $this->dispatcher->through([$middleware1]); + $this->dispatcher->through(['MyFirstMiddleware']); $actual = $this->dispatcher->dispatch($query1); $this->assertSame($expected, $actual); + $this->assertSame([ + 'before1', + 'before2', + 'before3', + 'after3', + 'after2', + 'after1', + ], $this->sequence); } } diff --git a/tests/Unit/Application/InboundEventBus/EventDispatcherTest.php b/tests/Unit/Application/InboundEventBus/EventDispatcherTest.php index fe73a0f..23d8151 100644 --- a/tests/Unit/Application/InboundEventBus/EventDispatcherTest.php +++ b/tests/Unit/Application/InboundEventBus/EventDispatcherTest.php @@ -36,6 +36,11 @@ class EventDispatcherTest extends TestCase */ private EventDispatcher $dispatcher; + /** + * @var array + */ + private array $sequence = []; + /** * @return void */ @@ -49,6 +54,15 @@ protected function setUp(): void ); } + /** + * @return void + */ + protected function tearDown(): void + { + unset($this->handlers, $this->middleware, $this->dispatcher, $this->sequence); + parent::tearDown(); + } + /** * @return void */ @@ -79,32 +93,45 @@ public function testWithMiddleware(): void $event2 = new TestInboundEvent(); $event3 = new TestInboundEvent(); $event4 = new TestInboundEvent(); + $handler = $this->createMock(EventHandler::class); - $middleware1 = function (TestInboundEvent $event, \Closure $next) use ($event1, $event2) { + $middleware1 = function (TestInboundEvent $event, \Closure $next) use ($event1, $event2): void { $this->assertSame($event1, $event); - return $next($event2); + $this->sequence[] = 'before1'; + $next($event2); + $this->sequence[] = 'after1'; }; - $middleware2 = function (TestInboundEvent $event, \Closure $next) use ($event2, $event3) { + $middleware2 = function (TestInboundEvent $event, \Closure $next) use ($event2, $event3): void { $this->assertSame($event2, $event); - return $next($event3); + $this->sequence[] = 'before2'; + $next($event3); + $this->sequence[] = 'after2'; }; - $middleware3 = function (TestInboundEvent $event, \Closure $next) use ($event3, $event4) { + $middleware3 = function (TestInboundEvent $event, \Closure $next) use ($event3, $event4): void { $this->assertSame($event3, $event); - return $next($event4); + $this->sequence[] = 'before3'; + $next($event4); + $this->sequence[] = 'after3'; }; $this->handlers ->method('get') ->with($event1::class) - ->willReturn($handler = $this->createMock(EventHandler::class)); + ->willReturnCallback(function () use ($handler) { + $this->assertSame(['before1'], $this->sequence); + return $handler; + }); $this->middleware - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('get') - ->with('MySecondMiddleware') - ->willReturn($middleware2); + ->willReturnCallback(fn (string $name) => match ($name) { + 'MyFirstMiddleware' => $middleware1, + 'MySecondMiddleware' => $middleware2, + default => $this->fail('Unexpected middleware: ' . $name), + }); $handler ->expects($this->once()) @@ -116,7 +143,16 @@ public function testWithMiddleware(): void ->method('__invoke') ->with($this->identicalTo($event4)); - $this->dispatcher->through([$middleware1]); + $this->dispatcher->through(['MyFirstMiddleware']); $this->dispatcher->dispatch($event1); + + $this->assertSame([ + 'before1', + 'before2', + 'before3', + 'after3', + 'after2', + 'after1', + ], $this->sequence); } } diff --git a/tests/Unit/Infrastructure/OutboundEventBus/ComponentPublisherTest.php b/tests/Unit/Infrastructure/OutboundEventBus/ComponentPublisherTest.php index ac19af1..f0bcd8b 100644 --- a/tests/Unit/Infrastructure/OutboundEventBus/ComponentPublisherTest.php +++ b/tests/Unit/Infrastructure/OutboundEventBus/ComponentPublisherTest.php @@ -36,6 +36,11 @@ class ComponentPublisherTest extends TestCase */ private ComponentPublisher $publisher; + /** + * @var array + */ + private array $sequence = []; + /** * @return void */ @@ -49,6 +54,15 @@ protected function setUp(): void ); } + /** + * @return void + */ + protected function tearDown(): void + { + unset($this->publisher, $this->handlers, $this->middleware); + parent::tearDown(); + } + /** * @return void */ @@ -79,14 +93,18 @@ public function testPublishWithMiddleware(): void $event2 = new TestOutboundEvent(); $event3 = new TestOutboundEvent(); - $middleware1 = function ($actual, Closure $next) use ($event1, $event2) { + $middleware1 = function ($actual, Closure $next) use ($event1, $event2): void { $this->assertSame($event1, $actual); - return $next($event2); + $this->sequence[] = 'before1'; + $next($event2); + $this->sequence[] = 'after1'; }; - $middleware2 = function ($actual, Closure $next) use ($event2, $event3) { + $middleware2 = function ($actual, Closure $next) use ($event2, $event3): void { $this->assertSame($event2, $actual); - return $next($event3); + $this->sequence[] = 'before2'; + $next($event3); + $this->sequence[] = 'after2'; }; $this->middleware @@ -106,9 +124,14 @@ public function testPublishWithMiddleware(): void ->expects($this->once()) ->method('get') ->with($event1::class) - ->willReturn($handler); + ->willReturnCallback(function () use ($handler) { + $this->assertSame(['before1', 'before2'], $this->sequence); + return $handler; + }); $this->publisher->through([$middleware1, 'Middleware2']); $this->publisher->publish($event1); + + $this->assertSame(['before1', 'before2', 'after2', 'after1'], $this->sequence); } } diff --git a/tests/Unit/Infrastructure/Queue/ComponentQueueTest.php b/tests/Unit/Infrastructure/Queue/ComponentQueueTest.php index 724482a..58de167 100644 --- a/tests/Unit/Infrastructure/Queue/ComponentQueueTest.php +++ b/tests/Unit/Infrastructure/Queue/ComponentQueueTest.php @@ -36,6 +36,11 @@ class ComponentQueueTest extends TestCase */ private ComponentQueue $queue; + /** + * @var array + */ + private array $sequence = []; + /** * @return void */ @@ -54,8 +59,8 @@ protected function setUp(): void */ protected function tearDown(): void { + unset($this->queue, $this->enqueuers, $this->middleware, $this->sequence); parent::tearDown(); - unset($this->queue, $this->enqueuers, $this->middleware); } /** @@ -87,22 +92,30 @@ public function testItQueuesThroughMiddleware(): void $command1 = $this->createMock(Command::class); $command2 = $this->createMock(Command::class); $command3 = $this->createMock(Command::class); + $enqueuer = $this->createMock(Enqueuer::class); - $middleware1 = function ($actual, \Closure $next) use ($command1, $command2) { + $middleware1 = function ($actual, \Closure $next) use ($command1, $command2): void { $this->assertSame($command1, $actual); - return $next($command2); + $this->sequence[] = 'before1'; + $next($command2); + $this->sequence[] = 'after1'; }; - $middleware2 = function ($actual, \Closure $next) use ($command2, $command3) { + $middleware2 = function ($actual, \Closure $next) use ($command2, $command3): void { $this->assertSame($command2, $actual); - return $next($command3); + $this->sequence[] = 'before2'; + $next($command3); + $this->sequence[] = 'after2'; }; $this->enqueuers ->expects($this->once()) ->method('get') - ->with($command1::class) - ->willReturn($enqueuer = $this->createMock(Enqueuer::class)); + ->with($command3::class) + ->willReturnCallback(function () use ($enqueuer) { + $this->assertSame(['before1', 'before2'], $this->sequence); + return $enqueuer; + }); $enqueuer ->expects($this->once()) @@ -117,5 +130,7 @@ public function testItQueuesThroughMiddleware(): void $this->queue->through([$middleware1, 'MySecondMiddleware']); $this->queue->push($command1); + + $this->assertSame(['before1', 'before2', 'after2', 'after1'], $this->sequence); } } diff --git a/tests/Unit/Toolkit/Identifiers/UuidTest.php b/tests/Unit/Toolkit/Identifiers/UuidTest.php index 75edb5b..6149f3f 100644 --- a/tests/Unit/Toolkit/Identifiers/UuidTest.php +++ b/tests/Unit/Toolkit/Identifiers/UuidTest.php @@ -45,6 +45,7 @@ public function test(): void $this->assertSame($base->toString(), $id->key()); $this->assertSame($base->toString(), $id->toString()); $this->assertSame((string) $base, (string) $id); + $this->assertSame($base->getBytes(), $id->getBytes()); $this->assertJsonStringEqualsJsonString( json_encode(['id' => $base], \JSON_THROW_ON_ERROR), json_encode(compact('id'), \JSON_THROW_ON_ERROR), @@ -152,4 +153,15 @@ public function testFromWithBaseUuid(): void $this->assertSame($expected, Uuid::from($base)); } + + /** + * @return void + */ + public function testNil(): void + { + $base = RamseyUuid::fromString(RamseyUuid::NIL); + $actual = Uuid::nil(); + + $this->assertTrue($actual->value->equals($base)); + } }