diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc21cd..8349b5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +## [2.0.0] - 2024-12-07 + +### Changed + +- **BREAKING** Removed the sub-namespaces for ports provided by this package, i.e.: + - `Contracts\Application\Ports\Driven` all interfaces are no longer in sub-namespaces; and + - `Contracts\Application\Ports\Driven` also has the same change. +- **BREAKING** Renamed the `InboundEventBus\EventDispatcher` port to `InboundEventDispatcher`. +- **BREAKING** Renamed the `OutboundEventBus\EventPublisher` port to `OutboundEventPublisher`. +- Upgraded to PHPStan v2. + ## [2.0.0-rc.3] - 2024-10-13 ### Added @@ -16,6 +27,10 @@ All notable changes to this project will be documented in this file. This projec is already implemented. - New `AggregateRoot` interface so that an aggregate root can be distinguished from a regular aggregate or entity. +### Changed + +- Remove deprecation message in PHP 8.4. + ## [2.0.0-rc.2] - 2024-07-27 ### Added @@ -312,3 +327,5 @@ All notable changes to this project will be documented in this file. This projec ## [0.1.0] - 2023-11-18 Initial release. + +[2.0.0]: https://github.com/cloudcreativity/docs.dancecloud.com/compare/v2.0.0-rc.3...v2.0.0 \ No newline at end of file diff --git a/composer.json b/composer.json index 0654157..13cedba 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ }, "require-dev": { "laravel/pint": "^1.15", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.4" }, "autoload": { diff --git a/docs/guide/application/asynchronous-processing.md b/docs/guide/application/asynchronous-processing.md index df04b72..587ae24 100644 --- a/docs/guide/application/asynchronous-processing.md +++ b/docs/guide/application/asynchronous-processing.md @@ -47,7 +47,7 @@ For example, an endpoint that triggers a recalculation of our sales report: namespace App\Http\Controllers\Api\AttendanceReport; use App\Modules\EventManagement\Application\{ - Ports\Driving\Commands\CommandBus, + Ports\Driving\CommandBus, UsesCases\Commands\RecalculateSalesAtEvent\RecalculateSalesAtEventCommand, }; use CloudCreativity\Modules\Toolkit\Identifiers\IntegerId; @@ -95,7 +95,7 @@ processing. Some examples of where your application layer might need to push int Or anything else that fits with the specific use case of your bounded context! Our approach is to define this work as _internal_ command messages. These are queued and dispatched by a specific -_internal_ command bus - separating them from the command bus that is an adapter of the driving port in the application +_internal_ command bus - separating them from the command bus that implements the driving port in the application layer. This means internal commands are not exposed as use cases of our module - making them an internal implementation detail @@ -177,12 +177,12 @@ command is queued by an infrastructure adapter, it has left the application laye queue for processing, it needs to re-enter the application layer via a driving port. However, by defining this as a separate port to our public command bus, we can ensure that internal commands are only -dispatched by the internal command bus adapter. +dispatched by the internal command bus. -Define the port as follows: +Define the internal command bus as follows: ```php -namespace App\Modules\EventManagement\Application\Ports\Driving\CommandBus; +namespace App\Modules\EventManagement\Application\Ports\Driving; use CloudCreativity\Modules\Application\Ports\Driving\CommandBus\CommandDispatcher; @@ -191,15 +191,15 @@ interface InternalCommandBus extends CommandDispatcher } ``` -And then our adapter (the concrete implementation of the port) is as follows: +And then our port implementation is as follows: ```php -namespace App\Modules\EventManagement\Application\Adapters\CommandBus; +namespace App\Modules\EventManagement\Application\Bus; -use App\Modules\EventManagement\Application\Ports\Driving\CommandBus\InternalCommandBus; +use App\Modules\EventManagement\Application\Ports\Driving\InternalCommandBus; use CloudCreativity\Modules\Application\Bus\CommandDispatcher; -final class InternalCommandBusAdapter extends CommandDispatcher implements +final class InternalCommandBusService extends CommandDispatcher implements InternalCommandBus { } @@ -221,7 +221,7 @@ public commands. I.e.: ```php namespace App\Modules\EventManagement\Application\Ports\Driven\Queue; -use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue\Queue as Port; +use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue as Port; // queues public commands interface Queue extends Port diff --git a/docs/guide/application/commands.md b/docs/guide/application/commands.md index 65f6984..6152796 100644 --- a/docs/guide/application/commands.md +++ b/docs/guide/application/commands.md @@ -126,32 +126,31 @@ Although there is a _generic_ command bus interface, our bounded context needs t We do this by defining an interface in our application's driving ports. ```php -namespace App\Modules\EventManagement\Application\Ports\Driving\CommandBus; +namespace App\Modules\EventManagement\Application\Ports\Driving; -use CloudCreativity\Modules\Application\Ports\Driving\CommandBus\CommandDispatcher; +use CloudCreativity\Modules\Application\Ports\Driving\CommandDispatcher; interface CommandBus extends CommandDispatcher { } ``` -And then our adapter (the concrete implementation of the port) is as follows: +And then our implementation is as follows: ```php -namespace App\Modules\EventManagement\Application\Adapters\CommandBus; +namespace App\Modules\EventManagement\Application\Bus; -use App\Modules\EventManagement\Application\Ports\Driving\CommandBus\CommandBus; +use App\Modules\EventManagement\Application\Ports\Driving\CommandBus as Port; use CloudCreativity\Modules\Application\Bus\CommandDispatcher; -final class CommandBusAdapter extends CommandDispatcher implements - CommandBus +final class CommandBus extends CommandDispatcher implements Port { } ``` ### Creating a Command Bus -The command dispatcher class that your adapter extended (in the above example) allows you to build a command bus +The command dispatcher class that your implementation extends (in the above example) allows you to build a command bus specific to your domain. You do this by: 1. Binding command handler factories into the command dispatcher; and @@ -164,29 +163,29 @@ handler or middleware are actually being used. For example: ```php -namespace App\Modules\EventManagement\Application\Adapters\CommandBus; +namespace App\Modules\EventManagement\Application\Bus; use App\Modules\EventManagement\Application\UsesCases\Commands\{ CancelAttendeeTicket\CancelAttendeeTicketCommand, CancelAttendeeTicket\CancelAttendeeTicketHandler, }; -use App\Modules\EventManagement\Application\Ports\Driving\CommandBus\CommandBus; +use App\Modules\EventManagement\Application\Ports\Driving\CommandBus as CommandBusPort; use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies; use CloudCreativity\Modules\Application\Bus\CommandHandlerContainer; use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork; use CloudCreativity\Modules\Application\Bus\Middleware\LogMessageDispatch; use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; -final class CommandBusAdapterProvider +final class CommandBusProvider { public function __construct( private readonly ExternalDependencies $dependencies, ) { } - public function getCommandBus(): CommandBus + public function getCommandBus(): CommandBusPort { - $bus = new CommandBusAdapter( + $bus = new CommandBus( handlers: $handlers = new CommandHandlerContainer(), middleware: $middleware = new PipeContainer(), ); @@ -222,15 +221,15 @@ final class CommandBusAdapterProvider } ``` -As the presentation and delivery layer is the user of the driving ports, we can now bind the port and its adapter into a -service container. For example, in Laravel: +Adapters in the presentation and delivery layer will use the driving ports. Typically this means we need to bind the +port into a service container. For example, in Laravel: ```php namespace App\Providers; use App\Modules\EventManagement\Application\{ - Adapters\CommandBus\CommandBusAdapterProvider, - Ports\Driving\CommandBus\CommandBus, + Bus\CommandBusProvider, + Ports\Driving\CommandBus, }; use Illuminate\Contracts\Container\Container; use Illuminate\Support\ServiceProvider; @@ -242,13 +241,12 @@ final class EventManagementServiceProvider extends ServiceProvider $this->app->bind( CommandBus::class, static function (Container $app) { - $provider = $app->make(CommandBusAdapterProvider::class); + $provider = $app->make(CommandBusProvider::class); return $provider->getCommandBus(); }, ); } } - ``` ### Dispatching Commands @@ -260,8 +258,8 @@ a single action controller to handle a HTTP request in a Laravel application, we namespace App\Http\Controllers\Api\Attendees; use App\Modules\EventManagement\Application\{ - Ports\Driving\CommandBus\CommandBus, - UsesCases\Commands\CancelAttendeeTicket\CancelAttendeeTicketCommand, + Ports\Driving\CommandBus, + UseCases\Commands\CancelAttendeeTicket\CancelAttendeeTicketCommand, }; use CloudCreativity\Modules\Toolkit\Identifiers\IntegerId; use Illuminate\Validation\Rule; @@ -322,8 +320,8 @@ updated to return a `202 Accepted` response to indicate the command has been que namespace App\Http\Controllers\Api\Attendees; use App\Modules\EventManagement\Application\{ - Ports\Driving\CommandBus\CommandBus, - UsesCases\Commands\CancelAttendeeTicket\CancelAttendeeTicketCommand, + Ports\Driving\CommandBus, + UseCases\Commands\CancelAttendeeTicket\CancelAttendeeTicketCommand, }; use CloudCreativity\Modules\Toolkit\Identifiers\IntegerId; use Illuminate\Validation\Rule; @@ -573,7 +571,7 @@ You can write your own middleware to suit your specific needs. Middleware is a s following signature: ```php -namespace App\Modules\EventManagement\Application\Adapters\Middleware; +namespace App\Modules\EventManagement\Application\Bus\Middleware; use Closure; use CloudCreativity\Modules\Contracts\Application\Bus\CommandMiddleware; @@ -615,7 +613,7 @@ If you want to write middleware that can be used with both commands and queries, instead: ```php -namespace App\Modules\EventManagement\Application\Adapters\Middleware; +namespace App\Modules\EventManagement\Application\Bus\Middleware; use Closure; use CloudCreativity\Modules\Contracts\Application\Bus\BusMiddleware; diff --git a/docs/guide/application/domain-events.md b/docs/guide/application/domain-events.md index 283a947..d847065 100644 --- a/docs/guide/application/domain-events.md +++ b/docs/guide/application/domain-events.md @@ -169,9 +169,9 @@ Although this sounds like a lot of work, we provide the tools to make this easy. above: ```php -namespace App\Modules\EventManagement\Application\Adapters\CommandBus; +namespace App\Modules\EventManagement\Application\Bus; -use App\Modules\EventManagement\Application\Ports\Driving\CommandBus\CommandBus; +use App\Modules\EventManagement\Application\Ports\Driving\CommandBus; use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies; use App\Modules\EventManagement\Application\Internal\DomainEvents\DomainEventDispatcher; use App\Modules\EventManagement\Application\Internal\DomainEvents\DomainEventDispatcherProvider; @@ -182,7 +182,7 @@ use CloudCreativity\Modules\Application\Bus\Middleware\SetupBeforeDispatch; use CloudCreativity\Modules\Application\UnitOfWork\UnitOfWorkManager; use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; -final class CommandBusAdapterProvider +final class CommandBusProvider { /** * @var UnitOfWorkManager|null @@ -202,7 +202,7 @@ final class CommandBusAdapterProvider public function getCommandBus(): CommandBus { - $bus = new CommandBusAdapter( + $bus = new CommandBus( handlers: $handlers = new CommandHandlerContainer(), middleware: $middleware = new PipeContainer(), ); @@ -407,9 +407,9 @@ into the middleware that flushes deferred events. Here's an example: ```php -namespace App\Modules\EventManagement\Application\Adapters\CommandBus; +namespace App\Modules\EventManagement\Application\Bus; -use App\Modules\EventManagement\Application\Ports\Driving\CommandBus\CommandBus; +use App\Modules\EventManagement\Application\Ports\Driving\CommandBus as CommandBusPort; use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies; use App\Modules\EventManagement\Application\Internal\DomainEvents\DomainEventDispatcher; use App\Modules\EventManagement\Application\Internal\DomainEvents\DomainEventDispatcherProvider; @@ -419,7 +419,7 @@ use CloudCreativity\Modules\Application\Bus\Middleware\FlushDeferredEvents; use CloudCreativity\Modules\Application\Bus\Middleware\SetupBeforeDispatch; use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; -final class CommandBusAdapterProvider +final class CommandBusProvider { /** * @var DomainEventDispatcher|null @@ -432,9 +432,9 @@ final class CommandBusAdapterProvider ) { } - public function getCommandBus(): CommandBus + public function getCommandBus(): CommandBusPort { - $bus = new CommandBusAdapter( + $bus = new CommandBus( handlers: $handlers = new CommandHandlerContainer(), middleware: $middleware = new PipeContainer(), ); diff --git a/docs/guide/application/integration-events.md b/docs/guide/application/integration-events.md index 28e07e1..c74f845 100644 --- a/docs/guide/application/integration-events.md +++ b/docs/guide/application/integration-events.md @@ -12,8 +12,8 @@ and any consuming bounded contexts. Integration events are bidirectional. They are both _published_ by a bounded context, and _consumed_ by other bounded contexts. This means we can refer to them in terms of their direction - specifically: -- **Inbound** integration events, are those a bounded context _consumes_ via a driving port and an adapter - implementation within the application layer. +- **Inbound** integration events, are those a bounded context _consumes_ via a driving port that is implemented by a + service in the application layer. - **Outbound** integration events, are those _published_ by a bounded context. Publishing occurs via a driven port, with the infrastructure layer implementing the adapter. @@ -151,9 +151,9 @@ Your application layer should define the driven port: ```php namespace App\Modules\EventManagement\Application\Ports\Driven\OutboundEventBus; -use CloudCreativity\Modules\Contracts\Application\Ports\Driven\OutboundEventBus\EventPublisher; +use CloudCreativity\Modules\Contracts\Application\Ports\Driven\OutboundEventPublisher; -interface OutboundEventBus extends EventPublisher +interface OutboundEventBus extends OutboundEventPublisher { } ``` @@ -401,33 +401,32 @@ needs to expose its _specific_ inbound event bus. We do this by defining an interface in our application's driving ports: ```php -namespace App\Modules\EventManagement\Application\Ports\Driving\InboundEventBus; +namespace App\Modules\EventManagement\Application\Ports\Driving; -use CloudCreativity\Modules\Contracts\Application\Ports\Driving\InboundEvents\EventDispatcher; +use CloudCreativity\Modules\Contracts\Application\Ports\Driving\InboundEventDispatcher; -interface InboundEventBus extends EventDispatcher +interface InboundEventBus extends InboundEventDispatcher { } ``` -And then our adapter (the concrete implementation of the port) is as follows: +And then our implementation is as follows: ```php -namespace App\Modules\EventManagement\Application\Adapters\InboundEventBus; +namespace App\Modules\EventManagement\Application\Bus; -use App\Modules\EventManagement\Application\Ports\Driving\InboundEventBus\InboundEventBus; -use CloudCreativity\Modules\Application\InboundEventBus\EventDispatcher; +use App\Modules\EventManagement\Application\Ports\Driving\InboundEventBus as Port; +use CloudCreativity\Modules\Application\InboundEventBus\InboundEventDispatcher; -final class InboundEventBusAdapter extends EventDispatcher implements - InboundEventBus +final class InboundEventBus extends InboundEventDispatcher implements Port { } ``` ### Creating a Bus -The event dispatcher class that your adapter extended (in the above example) allows you to build an inbound event bus -specific to your domain. You do this by: +The event dispatcher class that your implementation extends (in the above example) allows you to build an inbound event +bus specific to your domain. You do this by: 1. Binding event handler factories into the event dispatcher; and 2. Binding factories for any middleware used by your bounded context; and @@ -439,11 +438,11 @@ or middleware are actually being used. For example: ```php -namespace App\Modules\EventManagement\Application\Adapters\InboundEventBus; +namespace App\Modules\EventManagement\Application\Bus; -use App\Modules\EventManagement\Application\Adapters\CommandBus\CommandBusAdapterProvider; +use App\Modules\EventManagement\Application\Bus\CommandBusProvider; use App\Modules\EventManagement\Application\UsesCases\InboundEvents\OrderWasFulfilledHandler; -use App\Modules\EventManagement\Application\Ports\Driving\InboundEventBus\InboundEventBus; +use App\Modules\EventManagement\Application\Ports\Driving\InboundEventBus as InboundEventBusPort; use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies; use CloudCreativity\Modules\Application\InboundEventBus\EventHandlerContainer; use CloudCreativity\Modules\Application\InboundEventBus\Middleware\HandleInUnitOfWork; @@ -451,17 +450,17 @@ use CloudCreativity\Modules\Application\InboundEventBus\Middleware\LogInboundEve use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; use VendorName\Ordering\Shared\IntegrationEvents\V1\OrderWasFulfilled; -final class InboundEventBusAdapterProvider +final class InboundEventBusProvider { public function __construct( - private readonly CommandBusAdapterProvider $commandBusProvider, + private readonly CommandBusProvider $commandBusProvider, private readonly ExternalDependencies $dependencies, ) { } - public function getEventBus(): InboundEventBus + public function getEventBus(): InboundEventBusPort { - $bus = new InboundEventBusAdapter( + $bus = new InboundEventBus( handlers: $handlers = new EventHandlerContainer(), middleware: $middleware = new PipeContainer(), ); @@ -498,15 +497,15 @@ final class InboundEventBusAdapterProvider ``` Inbound events are received by the presentation and delivery layer of your application. For example, a controller that -receives a push message from Google Cloud Pub/Sub. We therefore need to bind the driving port and its adapter into a +receives a push message from Google Cloud Pub/Sub. Typically this means we need to bind the driving port into a service container. For example, in Laravel: ```php namespace App\Providers; use App\Modules\EventManagement\Application\{ - Adapters\InboundEventBus\InboundEventBusAdapterProvider, - Ports\Driving\InboundEvents\InboundEventBus, + Bus\InboundEventBusProvider, + Ports\Driving\InboundEventBus, }; use Illuminate\Contracts\Container\Container; use Illuminate\Support\ServiceProvider; @@ -518,7 +517,7 @@ final class EventManagementServiceProvider extends ServiceProvider $this->app->bind( InboundEventBus::class, static function (Container $app) { - $provider = $app->make(InboundEventBusAdapterProvider::class); + $provider = $app->make(InboundEventBusProvider::class); return $provider->getEventBus(); }, ); @@ -532,7 +531,7 @@ Integration events published by other bounded contexts will arrive in your prese a controller for an endpoint that Google Cloud Pub/Sub pushes events to. The implementation pattern here is to deserialize the incoming event data, converting it to the defined integration -event message. Then this is pushed into your bounded context via its event bus interface - i.e. the entry point for +event message. Then this is pushed into your bounded context via its inbound event bus port - i.e. the entry point for the bounded context. Here is an example controller from a Laravel application to demonstrate the pattern: @@ -540,7 +539,7 @@ Here is an example controller from a Laravel application to demonstrate the patt ```php namespace App\Http\Controllers\Api\PubSub; -use App\Modules\EventManagement\Application\Ports\Driving\InboundEvents\InboundEventBus; +use App\Modules\EventManagement\Application\Ports\Driving\InboundEventBus; use CloudCreativity\Modules\Contracts\Application\Messages\IntegrationEvent; use VendorName\Ordering\Shared\IntegrationEvents\V1\Serializers\JsonSerializer; @@ -594,7 +593,7 @@ the `SwallowInboundEvent` handler for this purpose: use CloudCreativity\Modules\Application\InboundEventBus\EventHandlerContainer; use CloudCreativity\Modules\Application\InboundEventBus\SwallowInboundEvent; -$bus = new InboundEventBusAdapter( +$bus = new InboundEventBus( handlers: $handlers = new EventHandlerContainer( default: fn() => new SwallowInboundEvent(), ), @@ -610,7 +609,7 @@ use CloudCreativity\Modules\Application\InboundEventBus\EventHandlerContainer; use CloudCreativity\Modules\Application\InboundEventBus\SwallowInboundEvent; use Psr\Log\LogLevel; -$bus = new InboundEventBusAdapter( +$bus = new InboundEventBus( handlers: $handlers = new EventHandlerContainer( default: fn() => new SwallowInboundEvent( logger: $this->dependencies->getLogger(), @@ -647,7 +646,7 @@ Firstly, our application layer will need an inbox driving port. This will allow example: ```php -namespace App\Modules\EventManagement\Application\Ports\Driving\InboundEvents; +namespace App\Modules\EventManagement\Application\Ports\Driving; use CloudCreativity\Modules\Contracts\Application\Messages\IntegrationEvent; @@ -657,7 +656,7 @@ interface Inbox } ``` -The adapter implementation would then check if the event has been received before, and if not, persist the event in the +The implementation would then check if the event has been received before, and if not, persist the event in the inbox. For both these actions - checking whether it exists, and storing - the adapter will need a driven port. That might look like this: @@ -887,7 +886,7 @@ You can write your own middleware to suit your specific needs. Middleware is a s following signature: ```php -namespace App\Modules\EventManagement\Application\Adapters\Middleware; +namespace App\Modules\EventManagement\Application\Bus\Middleware; use Closure; use CloudCreativity\Modules\Contracts\Application\InboundEventBus\InboundEventMiddleware; diff --git a/docs/guide/application/queries.md b/docs/guide/application/queries.md index 130f1ba..2171e56 100644 --- a/docs/guide/application/queries.md +++ b/docs/guide/application/queries.md @@ -104,33 +104,32 @@ Although there is a _generic_ query bus interface, our bounded context needs to We do this by defining an interface in our application's driving ports: ```php -namespace App\Modules\EventManagement\Application\Ports\Driving\QueryBus; +namespace App\Modules\EventManagement\Application\Ports\Driving; -use CloudCreativity\Modules\Contracts\Application\Ports\Driving\Queries\QueryDispatcher; +use CloudCreativity\Modules\Contracts\Application\Ports\Driving\QueryDispatcher; interface QueryBus extends QueryDispatcher { } ``` -And then our adapter (the concrete implementation of the port) is as follows: +And then our implementation is as follows: ```php -namespace App\Modules\EventManagement\Application\Adapters\QueryBus; +namespace App\Modules\EventManagement\Application\Bus; -use App\Modules\EventManagement\Application\Ports\Driving\QueryBus\QueryBus; +use App\Modules\EventManagement\Application\Ports\Driving\QueryBus as Port; use CloudCreativity\Modules\Application\Bus\QueryDispatcher; -final class EventManagementQueryBus extends QueryDispatcher implements - QueryBus +final class QueryBus extends QueryDispatcher implements Port { } ``` ### Creating a Query Bus -The query dispatcher class that your adapter extended (in the above example) allows you to build a query bus specific to -your domain. You do this by: +The query dispatcher class that your implementation extends (in the above example) allows you to build a query bus +specific to your domain. You do this by: 1. Binding query handler factories into the query dispatcher; and 2. Binding factories for any middleware used by your bounded context; and @@ -142,28 +141,28 @@ handler or middleware are actually being used. For example: ```php -namespace App\Modules\EventManagement\Application\Adapters\QueryBus; +namespace App\Modules\EventManagement\Application\Bus; use App\Modules\EventManagement\Application\UseCases\Queries\{ GetAttendeeTickets\GetAttendeeTicketsQuery, GetAttendeeTickets\GetAttendeeTicketsHandler, }; -use App\Modules\EventManagement\Application\Ports\Driving\QueryBus\QueryBus; +use App\Modules\EventManagement\Application\Ports\Driving\QueryBus as QueryBusPort; use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies; use CloudCreativity\Modules\Application\Bus\QueryHandlerContainer; use CloudCreativity\Modules\Application\Bus\Middleware\LogMessageDispatch; use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; -final class QueryBusAdapterProvider +final class QueryBusProvider { public function __construct( private readonly ExternalDependencies $dependencies, ) { } - public function getQueryBus(): QueryBus + public function getQueryBus(): QueryBusPort { - $bus = new EventManagementQueryBus( + $bus = new QueryBus( handlers: $handlers = new QueryHandlerContainer(), middleware: $middleware = new PipeContainer(), ); @@ -194,15 +193,14 @@ final class QueryBusAdapterProvider } ``` -As the presentation and delivery layer is the user of the driving ports, we can now bind the port and its adapter into a -service container. For example, in Laravel: +Adapters in the presentation and delivery layer will use the driving ports. Typically this means we need to bind the port into a service container. For example, in Laravel: ```php namespace App\Providers; use App\Modules\EventManagement\Application\{ - Adapters\QueryBus\QueryBusAdapterProvider, - Ports\Driving\QueryBus\QueryBus, + Bus\QueryBusProvider, + Ports\Driving\QueryBus, }; use Illuminate\Contracts\Container\Container; use Illuminate\Support\ServiceProvider; @@ -214,7 +212,7 @@ final class EventManagementServiceProvider extends ServiceProvider $this->app->bind( QueryBus::class, static function (Container $app) { - $provider = $app->make(QueryBusAdapterProvider::class); + $provider = $app->make(QueryBusProvider::class); return $provider->getQueryBus(); }, ); @@ -399,7 +397,7 @@ You can write your own middleware to suit your specific needs. Middleware is a s following signature: ```php -namespace App\Modules\EventManagement\Application\Adapters\Middleware; +namespace App\Modules\EventManagement\Application\Bus\Middleware; use Closure; use CloudCreativity\Modules\Contracts\Application\Bus\QueryMiddleware; @@ -441,7 +439,7 @@ If you want to write middleware that can be used with both commands and queries, instead: ```php -namespace App\Modules\EventManagement\Application\Adapters\Middleware; +namespace App\Modules\EventManagement\Application\Bus\Middleware; use Closure; use CloudCreativity\Modules\Contracts\Application\Bus\BusMiddleware; diff --git a/docs/guide/application/units-of-work.md b/docs/guide/application/units-of-work.md index c3157bc..4e1449b 100644 --- a/docs/guide/application/units-of-work.md +++ b/docs/guide/application/units-of-work.md @@ -93,7 +93,7 @@ Given the above example, the correct order from the application perspective is: 1. Unit of work begins by starting a transaction. 2. The aggregate root state change occurs and the domain event is emitted by the aggregate. -3. The domain event dispatching is deferred until later in the unit of work. This means side-effects via event listeners +3. The domain event dispatching is deferred until later in the unit of work. This means side effects via event listeners will be triggered later. 4. The aggregate root is persisted via the repository port. Internally within the adapter this may result in multiple database writes, that are within the transaction. @@ -189,7 +189,7 @@ implementation for Laravel could look like this: namespace App\Modules\Shared\Infrastructure; use Closure; -use CloudCreativity\Modules\Contracts\Application\Ports\Driven\UnitOfWork\UnitOfWork; +use CloudCreativity\Modules\Contracts\Application\Ports\Driven\UnitOfWork; use Illuminate\Database\ConnectionInterface; final readonly class IlluminateUnitOfWork implements UnitOfWork diff --git a/docs/guide/concepts/layers.md b/docs/guide/concepts/layers.md index 562f58b..724f516 100644 --- a/docs/guide/concepts/layers.md +++ b/docs/guide/concepts/layers.md @@ -35,15 +35,22 @@ this must never be exposed outside of the infrastructure layer. The application layer contains the use cases of the bounded context. These are the business processes that can be executed by passing information _into_ the application, and the information that can be read _out_ of the application. -This package embraces hexagonal architecture to define the boundary of the application layer. This boundary is expressed +This package embraces Hexagonal Architecture to define the boundary of the application layer. This boundary is expressed by _ports_ - interfaces that define the use cases of the module - and _adapters_ - the implementations of these interfaces. There are two types of ports: -- **Driving Ports** (aka _primary_ or _input_ ports) - interfaces that define the use cases of the bounded context. The - adapters that implement these interfaces are in the application layer and are used by the outside world to interact - with the module. +- **Driving Ports** (aka _primary_ or _input_ ports) - interfaces that define the use cases of the bounded context. + These are implemented by application services, and are used by adapters in the outside world to initiate interactions + with the application. For example, an adapter could be a HTTP controller that takes input from a request and passes it + to the application via a driving port. - **Driven Ports** (aka _secondary_ or _output_ ports) - interfaces that define the dependencies of the application - layer. The adapters that implement these interfaces are in the infrastructure layer. + layer. The adapters that implement these interfaces are in the infrastructure layer. For example, a persistence port + that has an adapter to read and write data to a database. + +:::tip +For a more detailed explanation of Hexagonal Architecture - along with some excellent diagrams - we +recommend [this article.](https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c) +::: When defining the driving ports in the application layer, we follow the Command Query Responsibility Segregation (CQRS) pattern. This pattern separates read (query) and write (command) operations, which makes it completely clear what is diff --git a/docs/guide/concepts/modularisation.md b/docs/guide/concepts/modularisation.md index c8645d3..7d4ee16 100644 --- a/docs/guide/concepts/modularisation.md +++ b/docs/guide/concepts/modularisation.md @@ -145,7 +145,7 @@ The application namespace can be structured as follows: - Queue - Persistence - ... - - Adapters + - Bus - CommandBus - QueryBus - InboundEventBus @@ -165,7 +165,7 @@ The namespaces shown here are as follows: - **Ports** - the driving and driven ports of the application layer expressed as interfaces. The driving ports are the interfaces that the application layer uses to interact with the outside world. The driven ports are the interfaces that the application layer expects to be implemented by the infrastructure layer. -- **Adapters** - contains the implementations of the driving ports. The concrete implementations are the command bus, +- **Bus** - contains the implementations of the driving ports. The concrete implementations are the command bus, query bus, and inbound event bus. Each bus ensures a message is dispatched to the correct handler. - **Use Cases** - the implementation of the business logic of the application layer. Use cases are expressed as the command and query messages that can enter the application, and the handlers that implement what happens when a diff --git a/docs/guide/infrastructure/dependency-injection.md b/docs/guide/infrastructure/dependency-injection.md index 544f774..2491935 100644 --- a/docs/guide/infrastructure/dependency-injection.md +++ b/docs/guide/infrastructure/dependency-injection.md @@ -47,7 +47,7 @@ This external dependencies port in effect provides other driven ports. For examp namespace App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection; use App\Modules\EventManagement\Application\Ports\Driven\Persistence\AttendeeRepository; -use App\Modules\EventManagement\Application\Ports\Driven\Queue\Queue; +use App\Modules\EventManagement\Application\Ports\Driven\Queue; use Psr\Log\LoggerInterface; interface ExternalDependencies @@ -66,29 +66,29 @@ These external dependencies can then be type-hinted wherever the application lay creating a command bus: ```php -namespace App\Modules\EventManagement\Application\Adapters\CommandBus; +namespace App\Modules\EventManagement\Application\Bus; use App\Modules\EventManagement\Application\UsesCases\Commands\{ CancelAttendeeTicket\CancelAttendeeTicketCommand, CancelAttendeeTicket\CancelAttendeeTicketHandler, }; -use App\Modules\EventManagement\Application\Ports\Driving\CommandBus\CommandBus; +use App\Modules\EventManagement\Application\Ports\Driving\CommandBus as CommandBusPort; use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies; use CloudCreativity\Modules\Application\Bus\CommandHandlerContainer; use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork; use CloudCreativity\Modules\Application\Bus\Middleware\LogMessageDispatch; use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; -final class CommandBusAdapterProvider +final class CommandBusProvider { public function __construct( private readonly ExternalDependencies $dependencies, ) { } - public function getCommandBus(): CommandBus + public function getCommandBus(): CommandBusPort { - $bus = new CommandBusAdapter( + $bus = new CommandBus( handlers: $handlers = new CommandHandlerContainer(), middleware: $middleware = new PipeContainer(), ); @@ -131,7 +131,7 @@ on a `RepositoryProvider` interface, that can be accessed via the external depen namespace App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection; use App\Modules\EventManagement\Application\Ports\Driven\Persistence\RepositoryProvider; -use App\Modules\EventManagement\Application\Ports\Driven\Queue\Queue; +use App\Modules\EventManagement\Application\Ports\Driven\Queue; use Psr\Log\LoggerInterface; interface ExternalDependencies diff --git a/docs/guide/infrastructure/exception-reporting.md b/docs/guide/infrastructure/exception-reporting.md index ebb4b8c..d869d3f 100644 --- a/docs/guide/infrastructure/exception-reporting.md +++ b/docs/guide/infrastructure/exception-reporting.md @@ -59,7 +59,7 @@ try { This package provides a driven port in the application layer that allows that layer to report exceptions: ```php -namespace CloudCreativity\Modules\Application\Ports\Driven\Exceptions; +namespace CloudCreativity\Modules\Application\Ports\Driven; use Throwable; @@ -86,7 +86,7 @@ implementation looks like this: ```php namespace App\Modules\Shared\Infrastructure\Exceptions; -use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Exceptions\ExceptionReporter; +use CloudCreativity\Modules\Contracts\Application\Ports\Driven\ExceptionReporter; use Illuminate\Contracts\Debug\ExceptionHandler; use Throwable; diff --git a/docs/guide/infrastructure/outbox.md b/docs/guide/infrastructure/outbox.md index 10802ee..c0f65f0 100644 --- a/docs/guide/infrastructure/outbox.md +++ b/docs/guide/infrastructure/outbox.md @@ -126,7 +126,7 @@ port `Queue` - as suggested by the [Queues chapter](./queues) - call it `Outbox` ```php namespace App\Modules\EventManagement\Application\Ports\Driven\Queue; -use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue\Queue; +use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue; interface Outbox extends Queue { diff --git a/docs/guide/infrastructure/publishing-events.md b/docs/guide/infrastructure/publishing-events.md index c372962..12e772f 100644 --- a/docs/guide/infrastructure/publishing-events.md +++ b/docs/guide/infrastructure/publishing-events.md @@ -16,9 +16,9 @@ The following is an example port: ```php namespace App\Modules\EventManagement\Application\Ports\Driven\OutboundEventBus; -use CloudCreativity\Modules\Contracts\Application\Ports\Driven\OutboundEventBus\EventPublisher; +use CloudCreativity\Modules\Contracts\Application\Ports\Driven\OutboundEventPublisher; -interface OutboundEventBus extends EventPublisher +interface OutboundEventBus extends OutboundEventPublisher { } ``` diff --git a/docs/guide/infrastructure/queues.md b/docs/guide/infrastructure/queues.md index 0c5e331..d8666a9 100644 --- a/docs/guide/infrastructure/queues.md +++ b/docs/guide/infrastructure/queues.md @@ -17,7 +17,7 @@ We do this by defining an interface in our application's driven ports: ```php namespace App\Modules\EventManagement\Application\Ports\Driven\Queue; -use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue\Queue as Port; +use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue as Port; interface Queue extends Port { @@ -28,7 +28,7 @@ This port is injected into a command bus via a closure factory that ensures the lazy. For example: ```php -$bus = new CommandBusAdapter( +$bus = new CommandBus( handlers: $handlers = new CommandHandlerContainer(), middleware: $middleware = new PipeContainer(), queue: fn() => $this->dependencies->getQueue(), @@ -55,7 +55,7 @@ In this scenario, define another driven port in your application layer: ```php namespace App\Modules\EventManagement\Application\Ports\Driven\Queue; -use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue\Queue as Port; +use CloudCreativity\Modules\Contracts\Application\Ports\Driven\Queue as Port; interface InternalQueue extends Port { @@ -65,7 +65,7 @@ interface InternalQueue extends Port And then ensure the adapter of this internal port is injected into your internal command bus: ```php -$bus = new InternalCommandBusAdapter( +$bus = new InternalCommandBus( handlers: $handlers = new CommandHandlerContainer(), middleware: $middleware = new PipeContainer(), queue: fn() => $this->dependencies->getInternalQueue(), @@ -299,7 +299,7 @@ For example, a default Laravel job for queuing and dispatching commands would be ```php namespace App\Modules\EventManagement\Infrastructure\Queue; -use App\Modules\EventManagement\Application\Ports\Driving\Commands\CommandBus; +use App\Modules\EventManagement\Application\Ports\Driving\CommandBus; use CloudCreativity\Modules\Contracts\Application\Messages\Command; use CloudCreativity\Modules\Toolkit\Result\FailedResultException; use Illuminate\Bus\Queueable; @@ -340,7 +340,7 @@ An example of a queue job for a specific command might be: ```php namespace App\Modules\EventManagement\Infrastructure\Queue; -use App\Modules\EventManagement\Application\Ports\Driving\Commands\CommandBus; +use App\Modules\EventManagement\Application\Ports\Driving\CommandBus; use App\Modules\EventManagement\Application\UseCases\Commands\{ RecalculateSalesAtEvent\ErrorCodeEnum, RecalculateSalesAtEvent\RecalculateSalesAtEventCommand, diff --git a/docs/guide/upgrade.md b/docs/guide/upgrade.md index bbd064d..5e9e4e3 100644 --- a/docs/guide/upgrade.md +++ b/docs/guide/upgrade.md @@ -27,7 +27,7 @@ While the `1.x` version was good, the main problem it had was the relationship b infrastructure layers. The layering of domain, infrastructure then application did not quite work - it always felt like the right relationship was domain, application and then infrastructure as an external concern. -We have solved this problem by switching to _hexagonal architecture_. +We have solved this problem by switching to _Hexagonal Architecture_. The domain layer remains the core of your bounded context's implementation. This is wrapped by the application layer, i.e. the infrastructure layer is no longer between the domain and the application layers. @@ -36,17 +36,19 @@ Instead, the application layer has a clearly defined boundary. This boundary is define the use cases of the module - and _adapters_ - the implementations of these interfaces. There are two types of ports: -- **Driving Ports** (aka _primary_ or _input_ ports) - interfaces that define the use cases of the bounded context. The - adapters that implement these interfaces are in the application layer and are used by the outside world to interact - with the module. +- **Driving Ports** (aka _primary_ or _input_ ports) - interfaces that define the use cases of the bounded context. + These are implemented by application services, and are used by adapters in the outside world to initiate interactions + with the application. For example, an adapter could be a HTTP controller that takes input from a request and passes it + to the application via a driving port. - **Driven Ports** (aka _secondary_ or _output_ ports) - interfaces that define the dependencies of the application - layer. The adapters that implement these interfaces are in the infrastructure layer. + layer. The adapters that implement these interfaces are in the infrastructure layer. For example, a persistence port + that has an adapter to read and write data to a database. The _driving ports_ in this package continue to use the CQRS pattern. So they are your command bus and query bus, plus inbound integration events via an inbound event bus. The _driven ports_ define the boundary between the application and infrastructure layer. This uses a _dependency -inversion_ principle. The application layer defines the port as an interface, which is then implemented by the adapter +inversion_ principle. The application layer defines the port as an interface, which is then implemented by an adapter in the infrastructure layer. #### Interface Changes @@ -71,7 +73,7 @@ For example, `EntityTrait` is now `IsEntity`. Command messages must now implement the `Contracts\Application\Messages\Command` interface. -The command dispatcher interface is now `Contracts\Application\Ports\Driving\Commands\CommandDispatcher`. +The command dispatcher interface is now `Contracts\Application\Ports\Driving\CommandDispatcher`. The concrete implementation has been moved from `Bus` to `Application\Bus`. The constructor argument for the middleware pipe container has been renamed `middleware` for clarity. This will only affect your implementation if you are using @@ -85,7 +87,7 @@ the `Application\Bus\Middleware` namespace. Query messages must now implement the `Contracts\Application\Messages\Query` interface. -The query dispatcher interface is now `Contracts\Application\Ports\Driving\Queries\QueryDispatcher`. +The query dispatcher interface is now `Contracts\Application\Ports\Driving\QueryDispatcher`. The concrete implementation has been moved from `Bus` to `Application\Bus`. The constructor argument for the middleware pipe container has been renamed `middleware` for clarity. This will only affect your implementation if you are using @@ -104,7 +106,7 @@ The previous event bus implementation has been split in two. This is due to the inbound events is now a _driving port_, whereas publishing outbound events occurs via a _driven port_. The new inbound implementation (previously referred to as a _notifier_) is now in the `Application\InboundEventBus` -namespace. The driving port is `Contracts\Application\Ports\Driving\InboundEvents\EventDispatcher`. +namespace. The driving port is `Contracts\Application\Ports\Driving\EventDispatcher`. The new outbound implementation (referred to as a _publisher_) is now in the `Infrastructure\OutboundEventBus` namespace. The driven port is `Contracts\Application\Ports\Driven\OutboundEvents\EventPublisher`. diff --git a/phpstan.neon b/phpstan.neon index 42888f2..bb836b2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 9 + level: 10 paths: - src - tests diff --git a/pint.json b/pint.json index 3a56850..3de4dce 100644 --- a/pint.json +++ b/pint.json @@ -4,6 +4,7 @@ "declare_strict_types": true, "trailing_comma_in_multiline": { "elements": ["arguments", "arrays", "match", "parameters"] - } + }, + "nullable_type_declaration": true } } diff --git a/src/Application/Bus/CommandDispatcher.php b/src/Application/Bus/CommandDispatcher.php index 3b97cc8..0997f18 100644 --- a/src/Application/Bus/CommandDispatcher.php +++ b/src/Application/Bus/CommandDispatcher.php @@ -1,4 +1,5 @@ handler::class, )); - return $this->handler->execute($command); + $result = $this->handler->execute($command); + + assert($result instanceof Result, 'Expecting command handler to return a result.'); + + return $result; } /** diff --git a/src/Application/Bus/CommandHandlerContainer.php b/src/Application/Bus/CommandHandlerContainer.php index f09fb29..6cff681 100644 --- a/src/Application/Bus/CommandHandlerContainer.php +++ b/src/Application/Bus/CommandHandlerContainer.php @@ -1,4 +1,5 @@ handler::class, )); - return $this->handler->execute($query); + $result = $this->handler->execute($query); + + assert($result instanceof Result, 'Expecting query handler to return a result.'); + + return $result; } /** diff --git a/src/Application/Bus/QueryHandlerContainer.php b/src/Application/Bus/QueryHandlerContainer.php index 8edb9e0..cc379b2 100644 --- a/src/Application/Bus/QueryHandlerContainer.php +++ b/src/Application/Bus/QueryHandlerContainer.php @@ -1,4 +1,5 @@ bindings[$event] ?? []; - foreach ((array) $listener as $name) { + foreach (is_array($listener) ? $listener : [$listener] as $name) { if ($this->canAttach($name)) { $bindings[] = $name; continue; diff --git a/src/Application/DomainEventDispatching/EventHandler.php b/src/Application/DomainEventDispatching/EventHandler.php index 65ac6b2..ceddb66 100644 --- a/src/Application/DomainEventDispatching/EventHandler.php +++ b/src/Application/DomainEventDispatching/EventHandler.php @@ -1,4 +1,5 @@ diff --git a/src/Application/InboundEventBus/Middleware/FlushDeferredEvents.php b/src/Application/InboundEventBus/Middleware/FlushDeferredEvents.php index c5268ac..0f7593f 100644 --- a/src/Application/InboundEventBus/Middleware/FlushDeferredEvents.php +++ b/src/Application/InboundEventBus/Middleware/FlushDeferredEvents.php @@ -1,4 +1,5 @@ + * @extends IteratorAggregate */ interface ListIterator extends IteratorAggregate, Countable { diff --git a/src/Contracts/Toolkit/Iterables/NonEmptyList.php b/src/Contracts/Toolkit/Iterables/NonEmptyList.php index 015201a..8eeb303 100644 --- a/src/Contracts/Toolkit/Iterables/NonEmptyList.php +++ b/src/Contracts/Toolkit/Iterables/NonEmptyList.php @@ -1,4 +1,5 @@ , Closure> diff --git a/src/Infrastructure/OutboundEventBus/ComponentPublisher.php b/src/Infrastructure/OutboundEventBus/ComponentPublisher.php index f1e4e07..f90fe82 100644 --- a/src/Infrastructure/OutboundEventBus/ComponentPublisher.php +++ b/src/Infrastructure/OutboundEventBus/ComponentPublisher.php @@ -1,4 +1,5 @@ @@ -47,7 +48,7 @@ public function through(array $pipes): void { assert(array_is_list($pipes), 'Expecting an array list of middleware.'); - $this->pipes = array_values($pipes); + $this->pipes = $pipes; } /** diff --git a/src/Infrastructure/OutboundEventBus/Middleware/LogOutboundEvent.php b/src/Infrastructure/OutboundEventBus/Middleware/LogOutboundEvent.php index 5d2c78f..d873701 100644 --- a/src/Infrastructure/OutboundEventBus/Middleware/LogOutboundEvent.php +++ b/src/Infrastructure/OutboundEventBus/Middleware/LogOutboundEvent.php @@ -1,4 +1,5 @@ stack = $identifiers; + $this->stack = array_values($identifiers); } /** diff --git a/src/Toolkit/Identifiers/PossiblyNumericId.php b/src/Toolkit/Identifiers/PossiblyNumericId.php index a5743f8..61d83d7 100644 --- a/src/Toolkit/Identifiers/PossiblyNumericId.php +++ b/src/Toolkit/Identifiers/PossiblyNumericId.php @@ -1,4 +1,5 @@ baseFactory, 'uuid7')) { - return new Uuid($this->baseFactory->uuid7($dateTime)); + $base = $this->baseFactory->uuid7($dateTime); + assert($base instanceof UuidInterface); + return new Uuid($base); } throw new RuntimeException('UUID version 7 is not supported by the underlying factory.'); @@ -157,7 +160,9 @@ public function uuid7(?DateTimeInterface $dateTime = null): Uuid public function uuid8(string $bytes): Uuid { if (method_exists($this->baseFactory, 'uuid8')) { - return new Uuid($this->baseFactory->uuid8($bytes)); + $base = $this->baseFactory->uuid8($bytes); + assert($base instanceof UuidInterface); + return new Uuid($base); } throw new RuntimeException('UUID version 8 is not supported by the underlying factory.'); diff --git a/src/Toolkit/Iterables/IsKeyedSet.php b/src/Toolkit/Iterables/IsKeyedSet.php index 86149c5..2078a56 100644 --- a/src/Toolkit/Iterables/IsKeyedSet.php +++ b/src/Toolkit/Iterables/IsKeyedSet.php @@ -1,4 +1,5 @@ + * @var list */ private array $stack = []; diff --git a/src/Toolkit/Iterables/IsNonEmptyList.php b/src/Toolkit/Iterables/IsNonEmptyList.php index 9df89e5..923c22d 100644 --- a/src/Toolkit/Iterables/IsNonEmptyList.php +++ b/src/Toolkit/Iterables/IsNonEmptyList.php @@ -1,4 +1,5 @@ */ - private array $stack = []; + private array $stack; /** * @return Generator diff --git a/src/Toolkit/Loggable/ObjectContext.php b/src/Toolkit/Loggable/ObjectContext.php index a2ba981..530fbf3 100644 --- a/src/Toolkit/Loggable/ObjectContext.php +++ b/src/Toolkit/Loggable/ObjectContext.php @@ -1,4 +1,5 @@ destination, ); + assert(is_callable($pipeline)); + return $pipeline($payload); } diff --git a/src/Toolkit/Pipeline/PipeContainer.php b/src/Toolkit/Pipeline/PipeContainer.php index 948a744..2f125a8 100644 --- a/src/Toolkit/Pipeline/PipeContainer.php +++ b/src/Toolkit/Pipeline/PipeContainer.php @@ -1,4 +1,5 @@ pipes[$pipeName] ?? null; - if ($factory) { - return $factory(); + if (is_callable($factory)) { + $pipe = $factory(); + assert(is_callable($pipe), 'Expecting pipe from factory to be callable.'); + return $pipe; } throw new RuntimeException('Unrecognised pipe name: ' . $pipeName); diff --git a/src/Toolkit/Pipeline/Pipeline.php b/src/Toolkit/Pipeline/Pipeline.php index 1214126..1497289 100644 --- a/src/Toolkit/Pipeline/Pipeline.php +++ b/src/Toolkit/Pipeline/Pipeline.php @@ -1,4 +1,5 @@ container) { + if ($this->container) { return new LazyPipe($this->container, $stage); } diff --git a/src/Toolkit/Pipeline/SimpleProcessor.php b/src/Toolkit/Pipeline/SimpleProcessor.php index ee6513f..bcbebb5 100644 --- a/src/Toolkit/Pipeline/SimpleProcessor.php +++ b/src/Toolkit/Pipeline/SimpleProcessor.php @@ -1,4 +1,5 @@ $errors) { - assert(is_string($key) && $errors instanceof IListOfErrors); $copy->stack[$key] = $copy->get($key)->merge($errors); } diff --git a/src/Toolkit/Result/ListOfErrors.php b/src/Toolkit/Result/ListOfErrors.php index e5c4ac5..a1f462b 100644 --- a/src/Toolkit/Result/ListOfErrors.php +++ b/src/Toolkit/Result/ListOfErrors.php @@ -1,4 +1,5 @@ stack = $errors; + $this->stack = array_values($errors); } /** diff --git a/src/Toolkit/Result/Meta.php b/src/Toolkit/Result/Meta.php index fd7a5b2..33eefe6 100644 --- a/src/Toolkit/Result/Meta.php +++ b/src/Toolkit/Result/Meta.php @@ -1,4 +1,5 @@ $message + * @param class-string $message * @return void * @dataProvider messageProvider */ public function test(string $message): void { - /** @var Command|Query $query */ + /** @var (Command&MockObject)|(Query&MockObject) $query */ $query = $this->createMock($message); $error1 = new Error(null, 'Message 1'); $error2 = new Error(null, 'Message 2'); diff --git a/tests/Unit/Application/DomainEventDispatching/DeferredDispatcherTest.php b/tests/Unit/Application/DomainEventDispatching/DeferredDispatcherTest.php index de77ea9..a18c22a 100644 --- a/tests/Unit/Application/DomainEventDispatching/DeferredDispatcherTest.php +++ b/tests/Unit/Application/DomainEventDispatching/DeferredDispatcherTest.php @@ -1,4 +1,5 @@ createMock(TestListener::class); $listener4 = $this->createMock(TestListener::class); - $listener2Closure = static fn ($event) => $listener2->handle($event); + $listener2Closure = static fn (DomainEvent $event) => $listener2->handle($event); $this->listeners ->expects($this->exactly(2)) @@ -130,7 +131,7 @@ public function testItFlushesDeferredEvents(): void $listener3 = $this->createMock(TestListener::class); $listener4 = $this->createMock(TestListener::class); - $listener2Closure = static fn ($event) => $listener2->handle($event1); + $listener2Closure = static fn (DomainEvent $event) => $listener2->handle($event1); $this->listeners ->expects($this->exactly(3)) @@ -399,11 +400,13 @@ public function testItDispatchesThroughMiddleware(): void $a = function ($actual, Closure $next) use ($event1, $event2): DomainEvent { $this->assertSame($event1, $actual); + /** @phpstan-ignore-next-line */ return $next($event2); }; $b = function ($actual, Closure $next) use ($event2, $event3): DomainEvent { $this->assertSame($event2, $actual); + /** @phpstan-ignore-next-line */ return $next($event3); }; diff --git a/tests/Unit/Application/DomainEventDispatching/DispatcherTest.php b/tests/Unit/Application/DomainEventDispatching/DispatcherTest.php index 79b2893..4a94106 100644 --- a/tests/Unit/Application/DomainEventDispatching/DispatcherTest.php +++ b/tests/Unit/Application/DomainEventDispatching/DispatcherTest.php @@ -1,4 +1,5 @@ createMock(TestListener::class); $listener4 = $this->createMock(TestListener::class); - $listener2Closure = static fn ($event) => $listener2->handle($event); + $listener2Closure = static fn (DomainEvent $event) => $listener2->handle($event); $this->listeners ->expects($this->exactly(2)) @@ -136,11 +137,13 @@ public function testItDispatchesThroughMiddleware(): void $a = function ($actual, Closure $next) use ($event1, $event2): DomainEvent { $this->assertSame($event1, $actual); + /** @phpstan-ignore-next-line */ return $next($event2); }; $b = function ($actual, Closure $next) use ($event2, $event3): DomainEvent { $this->assertSame($event2, $actual); + /** @phpstan-ignore-next-line */ return $next($event3); }; diff --git a/tests/Unit/Application/DomainEventDispatching/ListenerContainerTest.php b/tests/Unit/Application/DomainEventDispatching/ListenerContainerTest.php index 939dd03..75e5594 100644 --- a/tests/Unit/Application/DomainEventDispatching/ListenerContainerTest.php +++ b/tests/Unit/Application/DomainEventDispatching/ListenerContainerTest.php @@ -1,4 +1,5 @@ createMock(TestListenerAfterCommit::class); $listener7 = $this->createMock(TestListener::class); - $listener2Closure = static fn ($event) => $listener2->handle($event); + $listener2Closure = static fn (DomainEvent $event) => $listener2->handle($event); $this->listeners ->expects($this->exactly(5)) @@ -327,11 +328,13 @@ public function testItDispatchesThroughMiddleware(): void $a = function ($actual, Closure $next) use ($event1, $event2): DomainEvent { $this->assertSame($event1, $actual); + /** @phpstan-ignore-next-line */ return $next($event2); }; $b = function ($actual, Closure $next) use ($event2, $event3): DomainEvent { $this->assertSame($event2, $actual); + /** @phpstan-ignore-next-line */ return $next($event3); }; diff --git a/tests/Unit/Application/InboundEventBus/EventHandlerContainerTest.php b/tests/Unit/Application/InboundEventBus/EventHandlerContainerTest.php index e51f454..64f96a4 100644 --- a/tests/Unit/Application/InboundEventBus/EventHandlerContainerTest.php +++ b/tests/Unit/Application/InboundEventBus/EventHandlerContainerTest.php @@ -1,4 +1,5 @@ @@ -48,7 +49,7 @@ protected function setUp(): void { parent::setUp(); - $this->dispatcher = new EventDispatcher( + $this->dispatcher = new InboundEventDispatcher( handlers: $this->handlers = $this->createMock(EventHandlerContainer::class), middleware: $this->middleware = $this->createMock(PipeContainer::class), ); diff --git a/tests/Unit/Application/InboundEventBus/Middleware/FlushDeferredEventsTest.php b/tests/Unit/Application/InboundEventBus/Middleware/FlushDeferredEventsTest.php index 4491d23..c75e2dd 100644 --- a/tests/Unit/Application/InboundEventBus/Middleware/FlushDeferredEventsTest.php +++ b/tests/Unit/Application/InboundEventBus/Middleware/FlushDeferredEventsTest.php @@ -1,4 +1,5 @@ handle(new TestInboundEvent()); + /** @phpstan-ignore-next-line */ $this->assertTrue(true); } diff --git a/tests/Unit/Application/InboundEventBus/TestEventHandler.php b/tests/Unit/Application/InboundEventBus/TestEventHandler.php index cd1c861..a28d5ed 100644 --- a/tests/Unit/Application/InboundEventBus/TestEventHandler.php +++ b/tests/Unit/Application/InboundEventBus/TestEventHandler.php @@ -1,4 +1,5 @@ assertTrue(true); } diff --git a/tests/Unit/Toolkit/Identifiers/GuidTest.php b/tests/Unit/Toolkit/Identifiers/GuidTest.php index 0c0e878..2abc8a0 100644 --- a/tests/Unit/Toolkit/Identifiers/GuidTest.php +++ b/tests/Unit/Toolkit/Identifiers/GuidTest.php @@ -1,4 +1,5 @@ new StringId($expected), - is_int($expected) => new IntegerId($expected), - default => $this->fail('Unexpected value.'), - }; + $expectedId = is_string($expected) ? new StringId($expected) : new IntegerId($expected); $this->assertSame($expected, $actual->value); $this->assertSame($expected, PossiblyNumericId::from($value)->value); diff --git a/tests/Unit/Toolkit/Identifiers/StringIdTest.php b/tests/Unit/Toolkit/Identifiers/StringIdTest.php index bb29078..6a672b7 100644 --- a/tests/Unit/Toolkit/Identifiers/StringIdTest.php +++ b/tests/Unit/Toolkit/Identifiers/StringIdTest.php @@ -1,4 +1,5 @@ + */ + $list = new class (...$expected) implements NonEmptyList { + /** @use IsNonEmptyList */ + use IsNonEmptyList; + + public function __construct(string $value, string ...$values) + { + $this->stack = [$value, ...array_values($values)]; + } + }; + + $this->assertSame($expected, iterator_to_array($list)); + $this->assertSame($expected, $list->all()); + $this->assertCount(count($expected), $list); + } +} diff --git a/tests/Unit/Toolkit/Loggable/ObjectContextTest.php b/tests/Unit/Toolkit/Loggable/ObjectContextTest.php index b768a80..804fdeb 100644 --- a/tests/Unit/Toolkit/Loggable/ObjectContextTest.php +++ b/tests/Unit/Toolkit/Loggable/ObjectContextTest.php @@ -1,4 +1,5 @@ 0 === (intval($value) % 10), + static fn (int|float $value): bool => 0 === (intval($value) % 10), ); $a = static fn (int $value): int => $value * 10; diff --git a/tests/Unit/Toolkit/Pipeline/LazyPipeTest.php b/tests/Unit/Toolkit/Pipeline/LazyPipeTest.php index ccd3102..711ba3d 100644 --- a/tests/Unit/Toolkit/Pipeline/LazyPipeTest.php +++ b/tests/Unit/Toolkit/Pipeline/LazyPipeTest.php @@ -1,4 +1,5 @@ middleware = static function (string $step): \Closure { - return static function (array $values, \Closure $next) use ($step) { - $values[] = "{$step}1"; - $result = $next($values); - $result[] = "{$step}2"; - return $result; - }; - }; - } - public function test(): void { $processor = new MiddlewareProcessor(); $result = $processor->process( [], - ($this->middleware)('A'), - ($this->middleware)('B'), - ($this->middleware)('C'), + $this->createMiddleware('A'), + $this->createMiddleware('B'), + $this->createMiddleware('C'), ); $this->assertSame(['A1', 'B1', 'C1', 'C2', 'B2', 'A2'], $result); @@ -52,14 +35,17 @@ public function test(): void public function testWithDestination(): void { $processor = new MiddlewareProcessor( - static fn (array $values): array => array_map('strtolower', $values), + static fn (array $values): array => array_map( + static fn (string $v): string => strtolower($v), /** @phpstan-ignore-line */ + $values, + ), ); $result = $processor->process( [], - ($this->middleware)('A'), - ($this->middleware)('B'), - ($this->middleware)('C'), + $this->createMiddleware('A'), + $this->createMiddleware('B'), + $this->createMiddleware('C'), ); $this->assertSame(['a1', 'b1', 'c1', 'C2', 'B2', 'A2'], $result); @@ -84,4 +70,19 @@ public function testNoStagesWithDestination(): void $this->assertSame('FOO', $result); } + + /** + * @param string $step + * @return Closure + */ + private function createMiddleware(string $step): Closure + { + return static function (array $values, Closure $next) use ($step) { + $values[] = "{$step}1"; + $result = $next($values); + assert(is_array($result)); + $result[] = "{$step}2"; + return $result; + }; + } } diff --git a/tests/Unit/Toolkit/Pipeline/PipeContainerTest.php b/tests/Unit/Toolkit/Pipeline/PipeContainerTest.php index 17e8f34..b8ac25d 100644 --- a/tests/Unit/Toolkit/Pipeline/PipeContainerTest.php +++ b/tests/Unit/Toolkit/Pipeline/PipeContainerTest.php @@ -1,4 +1,5 @@