From 2475432f0c42bfb40cdd5bb045f70299b6947a8b Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Fri, 8 May 2026 16:13:37 +0100 Subject: [PATCH] docs: update docs for v6 --- .npmrc | 1 + docs/.vitepress/config.mts | 1 - .../application/asynchronous-processing.md | 36 +- docs/guide/application/commands.md | 315 ++++-------- docs/guide/application/domain-events.md | 456 ++---------------- docs/guide/application/integration-events.md | 334 +++++-------- docs/guide/application/queries.md | 194 +++----- docs/guide/application/units-of-work.md | 124 ++--- docs/guide/concepts/encapsulation.md | 26 +- docs/guide/concepts/layers.md | 2 +- docs/guide/concepts/modularisation.md | 251 +++------- docs/guide/domain/entities.md | 69 ++- docs/guide/domain/events.md | 16 +- docs/guide/domain/services.md | 10 +- .../infrastructure/dependency-injection.md | 168 ------- .../infrastructure/exception-reporting.md | 9 +- docs/guide/infrastructure/outbox.md | 10 +- docs/guide/infrastructure/persistence.md | 50 +- .../guide/infrastructure/publishing-events.md | 135 ++---- docs/guide/infrastructure/queues.md | 111 ++--- docs/guide/installation.md | 1 - docs/guide/upgrade.md | 52 +- package-lock.json | 7 +- 23 files changed, 691 insertions(+), 1687 deletions(-) create mode 100644 .npmrc delete mode 100644 docs/guide/infrastructure/dependency-injection.md diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..97b895e2 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +ignore-scripts=true diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 6ccc22bf..53eba9f6 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -62,7 +62,6 @@ export default defineConfig({ text: 'Infrastructure Layer', collapsed: false, items: [ - {text: 'Dependency Injection', link: '/guide/infrastructure/dependency-injection'}, {text: 'Exception Reporting', link: '/guide/infrastructure/exception-reporting'}, {text: 'Persistence', link: '/guide/infrastructure/persistence'}, {text: 'Publishing Events', link: '/guide/infrastructure/publishing-events'}, diff --git a/docs/guide/application/asynchronous-processing.md b/docs/guide/application/asynchronous-processing.md index 45b58b51..a62bcbbe 100644 --- a/docs/guide/application/asynchronous-processing.md +++ b/docs/guide/application/asynchronous-processing.md @@ -49,9 +49,9 @@ For example, an endpoint that triggers a recalculation of our sales report: ```php namespace App\Http\Controllers\Api\AttendanceReport; -use App\Modules\EventManagement\Application\{ - Ports\Driving\CommandQueuer, - UsesCases\Commands\RecalculateSalesAtEvent\RecalculateSalesAtEventCommand, +use App\Modules\EventManagement\Api\{ + CommandQueuer, + Input\RecalculateSalesAtEventCommand, }; use CloudCreativity\Modules\Toolkit\Identifiers\IntegerId; use Illuminate\Validation\Rule; @@ -115,12 +115,10 @@ i.e. derived data can be out-of-date for a short amount of time, as long as it i We could push this internal work to a queue via a domain event listener: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents\Listeners; +namespace App\Modules\EventManagement\Application\Orchestration\Listeners; -use App\Modules\EventManagement\Application\Ports\Driven\Queue\{ - InternalQueue, - Commands\RecalculateSalesAtEventCommand, -}; +use App\Modules\EventManagement\Application\Ports\InternalQueue; +use App\Modules\EventManagement\Application\Internal\RecalculateSalesAtEventCommand; use App\Modules\EventManagement\Domain\Events\AttendeeTicketWasCancelled; final readonly class QueueTicketSalesReportRecalculation @@ -138,11 +136,6 @@ final readonly class QueueTicketSalesReportRecalculation } ``` -:::tip -Notice that as this is an internal command, the command class is defined in the queue driven port namespace. This is to -ensure that the command is not exposed to the outside world. -::: - ### Workflow Orchestration When you have a complex process that needs to be executed asynchronously, you can define a workflow that orchestrates @@ -174,16 +167,13 @@ commands that could cancel or retry the workflow. ### Internal Command Bus If you are implementing internal commands, you will need an internal command bus that is separate from your _driving_ -port command bus. - -We deal with this by defining the internal command bus as a _driven_ port. This is technically correct, as commands -cannot be queued unless we have infrastructure to support queuing messages. Therefore, the internal command bus works -nicely as a driven port. +port command bus. As this is not defined in the `Api` namespace, this signals that it cannot be used by the outside +world - it is an internal implementation detail of the module's application layer. Define the internal command bus as follows: ```php -namespace App\Modules\EventManagement\Application\Ports\Driven\Queue; +namespace App\Modules\EventManagement\Application\Internal; use CloudCreativity\Modules\Application\Ports\Driving\CommandBus\CommandDispatcher; @@ -195,9 +185,9 @@ interface InternalCommandBus extends CommandDispatcher And then our port adapter is as follows: ```php -namespace App\Modules\EventManagement\Application\Bus; +namespace App\Modules\EventManagement\Application\Internal; -use App\Modules\EventManagement\Application\Ports\Driven\Queue\InternalCommandBus;use CloudCreativity\Modules\Bus\CommandDispatcher; +use CloudCreativity\Modules\Bus\CommandDispatcher; final class InternalCommandBusAdapter extends CommandDispatcher implements InternalCommandBus @@ -207,7 +197,7 @@ final class InternalCommandBusAdapter extends CommandDispatcher implements :::info See the [commands chapter](./commands) for details on how to create the adapter. This covers binding command handlers -and middleware into the command bus. +and middleware into the command bus via PHP attributes. ::: You will also need a queue driven port that allows you to queue these internal commands. This means there must also be a @@ -218,7 +208,7 @@ One approach is to define a port specifically for queuing internal commands - ra public commands. I.e.: ```php -namespace App\Modules\EventManagement\Application\Ports\Driven\Queue; +namespace App\Modules\EventManagement\Application\Ports\Queue; use CloudCreativity\Modules\Contracts\Application\Ports\Queue as Port; diff --git a/docs/guide/application/commands.md b/docs/guide/application/commands.md index 6c135d8e..2f9a8730 100644 --- a/docs/guide/application/commands.md +++ b/docs/guide/application/commands.md @@ -16,9 +16,11 @@ action. I.e. it defines the data contract for the action. Commands must be immut For example: ```php -namespace App\Modules\EventManagement\Application\UseCases\Commands\CancelAttendeeTicket; +namespace App\Modules\EventManagement\Api\Input; -use CloudCreativity\Modules\Contracts\Messaging\Command;use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier;use VendorName\EventManagement\Shared\Enums\CancellationReasonEnum; +use App\Modules\EventManagement\Api\Input\Enums\CancellationReasonEnum; +use CloudCreativity\Modules\Contracts\Messaging\Command; +use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; final readonly class CancelAttendeeTicketCommand implements Command { @@ -31,6 +33,11 @@ final readonly class CancelAttendeeTicketCommand implements Command } ``` +Whether you use enums or any value objects on your commands is up to you. We prefer to properly define the data contract +for a command, as our view is that the presentation and delivery layer should not be able to dispatch a command unless +it is properly formed. Defining values and enums to describe the data contract for a command is a good way to achieve +this. However, we are aware of other teams who prefer to only use internal PHP types on commands. + :::tip Some commands will only need to hold a few values to perform the action - such as in the example above, where the action can be described by two identifiers and an enum. @@ -48,12 +55,15 @@ the action, and updating the state of the bounded context. For example: ```php -namespace App\Modules\EventManagement\Application\UseCases\Commands\CancelAttendeeTicket; +namespace App\Modules\EventManagement\Application\UseCases; -use App\Modules\EventManagement\Application\Ports\Driven\Persistence\AttendeeRepository;use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork;use CloudCreativity\Modules\Contracts\Bus\DispatchThroughMiddleware;use CloudCreativity\Modules\Toolkit\Results\Result; +use App\Modules\EventManagement\Application\Ports\Persistence\AttendeeRepository; +use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork; +use CloudCreativity\Modules\Toolkit\Pipeline\Through; +use CloudCreativity\Modules\Toolkit\Results\Result; -final readonly class CancelAttendeeTicketHandler implements - DispatchThroughMiddleware +#[Through(ExecuteInUnitOfWork::class)] +final readonly class CancelAttendeeTicketHandler { public function __construct( private AttendeeRepository $attendees, @@ -77,23 +87,15 @@ final readonly class CancelAttendeeTicketHandler implements return Result::ok(); } - - public function middleware(): array - { - return [ - ExecuteInUnitOfWork::class, - ]; - } } ``` :::tip -You'll notice from the example above that our command handlers support [middleware](#middleware). In this example, we -are ensuring that the handler executes within a [unit of work](#unit-of-work) - i.e. the action is -performed within a single transaction. +You'll notice from the example above that our command handlers support [middleware](#middleware) via the `Through` +attribute. In this example, we are ensuring that the handler executes within a [unit of work](#unit-of-work) - i.e. the +action is performed within a single transaction. -Middleware is optional - if you do not need to use any middleware specific to the handler, your handler does not need to -implement the `DispatchThroughMiddleware` interface. +Middleware is optional. ::: ### Results @@ -123,9 +125,9 @@ 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; +namespace App\Modules\EventManagement\Api; -use CloudCreativity\Modules\Application\Ports\Driving\CommandDispatcher; +use CloudCreativity\Modules\Contracts\Messaging\CommandDispatcher; interface CommandBus extends CommandDispatcher { @@ -135,105 +137,48 @@ interface CommandBus extends CommandDispatcher And then our implementation is as follows: ```php -namespace App\Modules\EventManagement\Application\Bus; +namespace App\Modules\EventManagement\Application\Adapters; -use App\Modules\EventManagement\Application\Ports\Driving\CommandBus as Port;use CloudCreativity\Modules\Bus\CommandDispatcher; +use App\Modules\EventManagement\Api\CommandBus as Port; +use App\Modules\EventManagement\Api\Input\CancelAttendeeTicketCommand; +use App\Modules\EventManagement\Application\UseCases\CancelAttendeeTicketHandler; +use CloudCreativity\Modules\Bus\CommandDispatcher; +use CloudCreativity\Modules\Bus\Middleware\LogMessageDispatch; +use CloudCreativity\Modules\Bus\WithCommand; +use CloudCreativity\Modules\Toolkit\Pipeline\Through; +#[Through(LogMessageDispatch::class)] +#[WithCommand(CancelAttendeeTicketCommand::class, CancelAttendeeTicketHandler::class)] final class CommandBus extends CommandDispatcher implements Port { } ``` -### Creating 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 -2. Binding factories for any middleware used by your bounded context; and -3. Optionally, attaching middleware that runs for all commands dispatched through the command bus. +Notice that the command dispatcher can have middleware - attached using the `Through` attribute. The `WithCommand` +attribute is used to map a command from the `Api\Input` namespace to the handler in the `Application\UseCases` +namespace. -Factories must always be _lazy_, so that the cost of instantiating command handlers or middleware only occurs if the -handler or middleware are actually being used. +### Creating a Command Bus -For example: +The command dispatcher class that your implementation extends (in the above example) requires you to inject a PSR +container. This container is then used to resolve any middleware and command handlers that you've attached to the +dispatcher via the `Through` and `WithCommand` attributes. ```php -namespace App\Modules\EventManagement\Application\Bus; - -use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies;use App\Modules\EventManagement\Application\Ports\Driving\CommandBus as CommandBusPort;use App\Modules\EventManagement\Application\UsesCases\Commands\{CancelAttendeeTicket\CancelAttendeeTicketCommand,CancelAttendeeTicket\CancelAttendeeTicketHandler,};use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork;use CloudCreativity\Modules\Bus\CommandHandlerContainer;use CloudCreativity\Modules\Bus\Middleware\LogMessageDispatch;use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; - -final class CommandBusProvider -{ - public function __construct( - private readonly ExternalDependencies $dependencies, - ) { - } - - public function getCommandBus(): CommandBusPort - { - $bus = new CommandBus( - handlers: $handlers = new CommandHandlerContainer(), - middleware: $middleware = new PipeContainer(), - ); - - /** Bind commands to handler factories */ - $handlers->bind( - CancelAttendeeTicketCommand::class, - fn() => new CancelAttendeeTicketHandler( - $this->dependencies->getAttendeeRepository(), - ), - ); - - /** Bind middleware factories */ - $middleware->bind( - ExecuteInUnitOfWork::class, - fn () => new ExecuteInUnitOfWork($this->dependencies->getUnitOfWorkManager()), - ); - - $middleware->bind( - LogMessageDispatch::class, - fn () => new LogMessageDispatch( - $this->dependencies->getLogger(), - ), - ); - - /** Attach middleware that runs for all commands */ - $bus->through([ - LogMessageDispatch::class, - ]); - - return $bus; - } -} +$dispatcher = new CommandBus($psrContainer); ``` -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: +So, for example in a Laravel application, you would bind this to the interface as follows in your service provider: ```php -namespace App\Providers; - -use App\Modules\EventManagement\Application\{ - Bus\CommandBusProvider, - Ports\Driving\CommandBus, -}; -use Illuminate\Contracts\Container\Container; -use Illuminate\Support\ServiceProvider; +use App\Modules\EventManagement\Api\CommandBus; +use App\Modules\EventManagement\Application\Adapters\CommandBusAdapter; +use Illuminate\Contracts\Foundation\Application; -final class EventManagementServiceProvider extends ServiceProvider -{ - public function register() - { - $this->app->bind( - CommandBus::class, - static function (Container $app) { - $provider = $app->make(CommandBusProvider::class); - return $provider->getCommandBus(); - }, - ); - } -} +$this->app->bind( + CommandBus::class, + static fn (Application $app) => new CommandBusAdapter($app), +); ``` ### Dispatching Commands @@ -244,13 +189,13 @@ a single action controller to handle a HTTP request in a Laravel application, we ```php namespace App\Http\Controllers\Api\Attendees; -use App\Modules\EventManagement\Application\{ - Ports\Driving\CommandBus, - UseCases\Commands\CancelAttendeeTicket\CancelAttendeeTicketCommand, +use App\Modules\EventManagement\Api\{ + CommandBus, + Input\CancelAttendeeTicketCommand, + Input\Enums\CancellationReasonEnum, }; use CloudCreativity\Modules\Toolkit\Identifiers\IntegerId; use Illuminate\Validation\Rule; -use VendorName\EventManagement\Shared\Enums\CancellationReasonEnum; class CancellationController extends Controller { @@ -314,9 +259,9 @@ queuer. We do this by defining an interface in our application's driving ports. ```php -namespace App\Modules\EventManagement\Application\Ports\Driving; +namespace App\Modules\EventManagement\Api; -use CloudCreativity\Modules\Application\Ports\Driving\CommandQueuer as ICommandQueuer; +use CloudCreativity\Modules\Contracts\Bus\CommandQueuer as ICommandQueuer; interface CommandQueuer extends ICommandQueuer { @@ -326,9 +271,11 @@ interface CommandQueuer extends ICommandQueuer And then our implementation is as follows: ```php -namespace App\Modules\EventManagement\Application\Bus; +namespace App\Modules\EventManagement\Api; -use App\Modules\EventManagement\Application\Ports\Driven\Queue;use App\Modules\EventManagement\Application\Ports\Driving\CommandQueuer as Port;use CloudCreativity\Modules\Application\Bus\CommandQueuer as Queuer; +use App\Modules\EventManagement\Application\Ports\Queue; +use App\Modules\EventManagement\Api\CommandQueuer as Port; +use CloudCreativity\Modules\Application\Bus\CommandQueuer as Queuer; final class CommandQueuer extends Queuer implements Port { @@ -350,28 +297,6 @@ See the [Queue chapter](../infrastructure/queues.md) for more information on how Creating a command queuer is simple, as it is just a thin wrapper around the queue - i.e. it immediately hands off to a driven port. This is because queuing a command is an infrastructure concern. -```php -namespace App\Modules\EventManagement\Application\Bus; - -use App\Modules\EventManagement\Application\Ports\Driving\CommandQueuer as CommandQueuerPort; -use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies; - -final class CommandBusProvider -{ - public function __construct( - private readonly ExternalDependencies $dependencies, - ) { - } - - public function getCommandQueuer(): CommandQueuerPort - { - return new CommandQueuer( - queue: $this->dependencies->getQueue(), - ); - } -} -``` - :::tip The queue supports middleware to add cross-cutting concerns, such as logging. This means there is no need to add any middleware to the command queuer. @@ -386,13 +311,13 @@ queued: ```php namespace App\Http\Controllers\Api\Attendees; -use App\Modules\EventManagement\Application\{ - Ports\Driving\CommandQueuer, - UseCases\Commands\CancelAttendeeTicket\CancelAttendeeTicketCommand, +use App\Modules\EventManagement\Api\{ + CommandQueuer, + Input\CancelAttendeeTicketCommand, + Input\Enums\CancellationReasonEnum, }; use CloudCreativity\Modules\Toolkit\Identifiers\IntegerId; use Illuminate\Validation\Rule; -use VendorName\EventManagement\Shared\Enums\CancellationReasonEnum; class CancellationController extends Controller { @@ -427,12 +352,12 @@ transaction management, and so on. Middleware can be added either to the command bus (so it runs for every command) or to individual command handlers. -To apply middleware to the command bus, you can use the `through()` method on the bus - as shown in the example above. +To apply middleware to the command bus, you can use the `Through` attribute on the bus - as shown in the examples above. Middleware is executed in the order it is added to the bus. -To apply middleware to a specific command handler, the handler must implement the `DispatchThroughMiddleware` interface, -as shown in the example handler above. The `middleware()` method should return an array of middleware to run, in the -order they should be executed. Handler middleware are always executed _after_ the bus middleware. +To apply middleware to a specific command handler, use the `Through` attribute on the handler class - as shown in the +example handler above. Again, middleware is executed in the order it is added to the handler. Handler middleware will +run _after_ middleware attached to the command bus. This package provides a number of command middleware, which are described below. Additionally, you can write your own middleware to suit your specific needs. @@ -446,29 +371,26 @@ This allows you to set up any state and guarantee that the state is cleaned up, command. The primary use case for this is to boostrap [Domain Services](../domain/services) and to garbage collect any singleton instances of dependencies. -For example: +For example, bind an instance of the middleware into your service container: ```php -use App\Modules\EventManagement\Domain\Services;use CloudCreativity\Modules\Bus\Middleware\SetupBeforeDispatch; +use App\Modules\EventManagement\Domain\Services; +use CloudCreativity\Modules\Bus\Middleware\SetupBeforeDispatch; -$middleware->bind( - SetupBeforeDispatch::class, - fn () => new SetupBeforeDispatch(function (): Closure { +$container->bind( + 'event-management:setup', + fn () => new SetupBeforeDispatch(function () use ($container): Closure { // setup domain services - Services::setEvents(fn() => $this->getDomainEventDispatcher()); + Services::setEvents(fn () => $container->get(DomainEventDispatcher::class)); return function (): void { - // clean up a singleton instance of a unit of work manager. - $this->unitOfWorkManager = null; // teardown the domain services Services::tearDown(); }; }), ); -$bus->through([ - LogMessageDispatch::class, - SetupBeforeDispatch::class, -]); +// use #[Through('event-management:setup')] +// on your command bus to apply the middleware. ``` Here our setup middleware takes a setup closure as its only constructor argument. This setup closure can optionally @@ -481,18 +403,15 @@ closure as its only constructor argument: ```php use CloudCreativity\Modules\Bus\Middleware\TeardownAfterDispatch; -$middleware->bind( - TeardownAfterDispatch::class, +$container->bind( + 'event:management:teardown', fn () => new TeardownAfterDispatch(function (): Closure { - // clean up a singleton instance of a unit of work manager. - $this->unitOfWorkManager = null; + // clean up work goes here. }), ); -$bus->through([ - LogMessageDispatch::class, - TearDownAfterDispatch::class, -]); +// use #[Through('event-management:teardown')] +// on your command bus to apply the middleware. ``` ### Unit of Work @@ -505,19 +424,6 @@ implement this as handler middleware - because typically you need it to be the f handler is invoked. It also makes it clear to developers looking at the command handler that it is expected to run in a unit of work. The example `CancelAttendeeTicketHandler` above demonstrates this. -An example binding for this middleware is: - -```php -use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork; - -$middleware->bind( - ExecuteInUnitOfWork::class, - fn () => new ExecuteInUnitOfWork( - $this->dependencies->getUnitOfWorkManager(), - ), -); -``` - :::warning If you're using a unit of work, you should be combining this with our "unit of work domain event dispatcher". One really important thing to note is that you **must inject both the middleware and the domain event dispatcher with @@ -536,19 +442,6 @@ When using this dispatcher, you will need to use our `FlushDeferredEvents` middl implement this as handler middleware - because typically you need it to be the final middleware that runs before a handler is invoked. I.e. this is an equivalent middleware to the unit of work middleware. -An example binding for this middleware is: - -```php -use CloudCreativity\Modules\Application\Bus\Middleware\FlushDeferredEvents; - -$middleware->bind( - FlushDeferredEvents::class, - fn () => new FlushDeferredEvents( - $this->eventDispatcher, - ), -); -``` - :::warning When using this middleware, it is important that you inject it with a singleton instance of the deferred event dispatcher. This must be the same instance that is exposed to your domain layer as a service. @@ -559,18 +452,8 @@ dispatcher. This must be the same instance that is exposed to your domain layer Use our `LogMessageDispatch` middleware to log the dispatch of a command, and the result. The middleware takes a [PSR Logger](https://php-fig.org/psr/psr-3/). -```php -use CloudCreativity\Modules\Bus\Middleware\LogMessageDispatch; - -$middleware->bind( - LogMessageDispatch::class, - fn (): LogMessageDispatch => new LogMessageDispatch( - $this->dependencies->getLogger(), - ), -); - -$bus->through([LogMessageDispatch::class]); -``` +> Typically, a service container will know how to resolve a PSR Logger, so if you're using an auto-wiring container ( +> like the Laravel container), you typically will not need to define anything for the container to make this middleware. The middleware will log a message before executing the command, with a log level of _debug_. It will then log a message after the command has executed, with a log level of _info_. @@ -580,17 +463,16 @@ _info_, and the _dispatched_ message to be _notice_: ```php use Psr\Log\LogLevel; +use Psr\Log\LoggerInterface; -$middleware->bind( +$container->bind( LogMessageDispatch::class, fn (): LogMessageDispatch => new LogMessageDispatch( - logger: $this->dependencies->getLogger(), + logger: $container->get(LoggerInterface::class), dispatchLevel: LogLevel::INFO, dispatchedLevel: LogLevel::NOTICE, ), ); - -$bus->through([LogMessageDispatch::class]); ``` #### Log Context @@ -602,7 +484,9 @@ However, there may be scenarios where a property should not be logged, e.g. beca In this scenario, use the `Sensitive` attribute on the property, and it will not be logged: ```php -use CloudCreativity\Modules\Toolkit\Sensitive;use CloudCreativity\Modules\Contracts\Messaging\Command;use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; +use CloudCreativity\Modules\Toolkit\Sensitive; +use CloudCreativity\Modules\Contracts\Messaging\Command; +use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; final readonly class CancelAttendeeTicketCommand implements Command { @@ -615,14 +499,16 @@ final readonly class CancelAttendeeTicketCommand implements Command } ``` -If you need full control over the log context, implement the `ContextProvider` interface on your command message: +If you need full control over the log context, implement the `Contextual` interface on your command message: ```php -use CloudCreativity\Modules\Contracts\Bus\Loggable\ContextProvider;use CloudCreativity\Modules\Contracts\Messaging\Command;use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; +use CloudCreativity\Modules\Contracts\Toolkit\Contextual; +use CloudCreativity\Modules\Contracts\Messaging\Command; +use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; final readonly class CancelAttendeeTicketCommand implements Command, - ContextProvider + Contextual { public function __construct( public Identifier $attendeeId, @@ -646,9 +532,12 @@ You can write your own middleware to suit your specific needs. Middleware is a s following signature: ```php -namespace App\Modules\EventManagement\Application\Bus\Middleware; +namespace App\Modules\EventManagement\Application\Adapters\Middleware; -use Closure;use CloudCreativity\Modules\Contracts\Bus\Middleware\CommandMiddleware;use CloudCreativity\Modules\Contracts\Messaging\Command;use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; +use Closure; +use CloudCreativity\Modules\Contracts\Bus\Middleware\CommandMiddleware; +use CloudCreativity\Modules\Contracts\Messaging\Command; +use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; final class MyMiddleware implements CommandMiddleware { @@ -685,9 +574,13 @@ If you want to write middleware that can be used with both commands and queries, instead: ```php -namespace App\Modules\EventManagement\Application\Bus\Middleware; +namespace App\Modules\EventManagement\Application\Adapters\Middleware; -use Closure;use CloudCreativity\Modules\Contracts\Bus\Middleware\BusMiddleware;use CloudCreativity\Modules\Contracts\Messaging\Command;use CloudCreativity\Modules\Contracts\Messaging\Query;use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; +use Closure; +use CloudCreativity\Modules\Contracts\Bus\Middleware\BusMiddleware; +use CloudCreativity\Modules\Contracts\Messaging\Command; +use CloudCreativity\Modules\Contracts\Messaging\Query; +use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; class MyBusMiddleware implements BusMiddleware { diff --git a/docs/guide/application/domain-events.md b/docs/guide/application/domain-events.md index b55aaa8e..126e724c 100644 --- a/docs/guide/application/domain-events.md +++ b/docs/guide/application/domain-events.md @@ -49,213 +49,44 @@ This chapter covers both of these dispatchers. To use this dispatcher, create a concrete implementation of your domain layer's dispatcher interface: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents; +namespace App\Modules\EventManagement\Application\Orchestration; -use App\Modules\EventManagement\Domain\Events\DomainEventDispatcher; +use App\Modules\EventManagement\Domain\Events\DomainEventDispatcher as IDomainEventDispatcher; use CloudCreativity\Modules\Application\DomainEventDispatching\UnitOfWorkAwareDispatcher; - -final class DomainEventDispatcherAdapter extends UnitOfWorkAwareDispatcher implements - DomainEventDispatcher +use CloudCreativity\Modules\Application\DomainEventDispatching\ListenTo; +use CloudCreativity\Modules\Toolkit\Pipeline\Through; + +#[Through(LogDomainEventDispatch::class)] +#[ListenTo(SomeDomainEvent::class, FooListener::class)] +#[ListenTo(SomeOtherDomainEvent::class, [BarListener::class, BazListener::class])] +final class DomainEventDispatcher extends UnitOfWorkAwareDispatcher implements + IDomainEventDispatcher { } ``` +Notice that middleware is bound to the dispatcher using the `Through` attribute. + +Events are mapped to listeners via the `ListenTo` attribute. You can specify a single listener or an array of listeners +for each event. + ### Creating a Dispatcher -To create a unit of work aware dispatcher, you need to provide it with: +To create a unit of work aware dispatcher, you need to provide it with a unit of work manager. As described in +the [unit of work chapter](units-of-work.md), this MUST be a singleton instance. I.e. the instance that is provided to +your dispatcher must also be the same instance that is provided to unit of work middleware. -1. A unit of work manager. As described in the [unit of work chapter](units-of-work.md), this must be singleton - instance. I.e. the instance that is provided to your dispatcher must also be the same instance that is provided to - unit of work middleware. -2. A listener container, that allows you to bind the factories for listeners that the dispatcher will need to - instantiate. -3. Optionally, middleware factories for any middleware your dispatcher uses. +You also need to provide a PSR container, so that the dispatcher can resolve any listeners and middleware. For example: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents; - -use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies; -use App\Modules\EventManagement\Domain\Events\{ - AttendeeTicketWasCancelled, - DomainEventDispatcher, -}; -use CloudCreativity\Modules\Application\DomainEventDispatching\ListenerContainer; -use CloudCreativity\Modules\Application\DomainEventDispatching\Middleware\LogDomainEventDispatch; use CloudCreativity\Modules\Contracts\Application\UnitOfWork\UnitOfWorkManager; -use CloudCreativity\Modules\Contracts\Domain\Events\DomainEvent; -use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; - -final readonly class DomainEventDispatcherProvider -{ - /** - * @var array, list> - */ - private array $subscriptions = [ - AttendeeTicketWasCancelled::class => [ - Listeners\UpdateTicketSalesReport::class, - Listeners\QueueTicketCancellationEmail::class, - ], - // ...other events - ]; - - public function __construct( - private ExternalDependencies $dependencies, - ) { - } - - public function getEventDispatcher(UnitOfWorkManager $unitOfWorkManager): DomainEventDispatcher - { - $dispatcher = new DomainEventDispatcherAdapter( - unitOfWorkManager: $unitOfWorkManager, - listeners: $listeners = new ListenerContainer(), - middleware: $middleware = new PipeContainer(), - ); - - /** Bind listener factories */ - $listeners->bind( - Listeners\UpdateTicketSalesReport::class, - fn () => new Listeners\UpdateTicketSalesReport( - $this->dependencies->getTicketSalesReportRepository(), - ), - ); - - $listeners->bind( - Listeners\QueueTicketCancellationEmail::class, - fn () => new Listeners\QueueTicketCancellationEmail( - $this->dependencies->getMailer(), - ), - ); - - /** Subscribe listeners to events */ - foreach ($this->subscriptions as $event => $listeners) { - $dispatcher->listen($event, $listeners); - } - - /** Bind middleware factories */ - $middleware->bind( - LogDomainEventDispatch::class, - fn () => new LogDomainEventDispatch( - $this->dependencies->getLogger(), - ), - ); - - /** Attach middleware for all events */ - $dispatcher->through([ - LogDomainEventDispatch::class, - ]); - - return $dispatcher; - } -} -``` - -### Bootstrapping - -We've now got everything we need to use the unit of work aware dispatcher in our application layer. There our however a -few things we need to do to ensure it is correctly bootstrapped. - -For example, when creating a command bus there's a few things we'll need to do: - -1. Ensure we have a singleton instance of the unit of work manager. -2. Inject this instance into the domain event dispatcher. -3. Ensure that our domain layer can access this dispatcher as a domain service. -4. Ensure our command handlers are wrapped in a unit of work, by injecting the manager into the unit of work command - middleware. -5. Once a command has been dispatched, reliably tear down the unit of work. - -Although this sounds like a lot of work, we provide the tools to make this easy. Here's an example that does all of the -above: - -```php -namespace App\Modules\EventManagement\Application\Bus; - -use App\Modules\EventManagement\Application\Internal\DomainEvents\DomainEventDispatcher;use App\Modules\EventManagement\Application\Internal\DomainEvents\DomainEventDispatcherProvider;use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies;use App\Modules\EventManagement\Application\Ports\Driving\CommandBus;use App\Modules\EventManagement\Domain\Services as DomainServices;use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork;use CloudCreativity\Modules\Application\UnitOfWork\UnitOfWorkManager;use CloudCreativity\Modules\Bus\CommandHandlerContainer;use CloudCreativity\Modules\Bus\Middleware\SetupBeforeDispatch;use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; - -final class CommandBusProvider -{ - /** - * @var UnitOfWorkManager|null - */ - private ?UnitOfWorkManager $unitOfWorkManager = null; - - /** - * @var DomainEventDispatcher|null - */ - private ?DomainEventDispatcher $eventDispatcher = null; - public function __construct( - private readonly ExternalDependencies $dependencies, - private readonly DomainEventDispatcherProvider $eventDispatcherProvider, - ) { - } - - public function getCommandBus(): CommandBus - { - $bus = new CommandBus( - handlers: $handlers = new CommandHandlerContainer(), - middleware: $middleware = new PipeContainer(), - ); - - // ...handler bindings. - - $middleware->bind( - SetupBeforeDispatch::class, - fn () => new SetupBeforeDispatch(function (): Closure { - $this->setUp(); - return function (): void { - $this->tearDown(); - }; - }), - ); - - $middleware->bind( - ExecuteInUnitOfWork::class, - fn () => new ExecuteInUnitOfWork($this->unitOfWorkManager), - ); - - $bus->through([ - SetupBeforeDispatch::class, - ]); - - return $bus; - } - - /** - * Set up command handling state. - * - * @return void - */ - private function setUp(): void - { - $this->unitOfWorkManager = new UnitOfWorkManager( - $this->dependencies->getUnitOfWork(), - ); - - DomainServices::setEvents(function () { - if ($this->eventDispatcher) { - return $this->eventDispatcher; - } - - return $this->eventDispatcher = $this->eventDispatcherProvider->getEventDispatcher( - $this->unitOfWorkManager - ); - }); - } - - /** - * Tear down command handling state. - * - * @return void - */ - private function tearDown(): void - { - DomainServices::tearDown(); - $this->eventDispatcher = null; - $this->unitOfWorkManager = null; - } -} +$dispatcher = new DomainEventDispatcher( + $container->get(UnitOfWorkManager::class), + $container, +); ``` ### Deferred Events @@ -286,7 +117,7 @@ As well as implementing your domain layer's dispatcher interface, you also need interface. Combine these two as an interface in your application layer: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents; +namespace App\Modules\EventManagement\Application\Orchestration; use App\Modules\EventManagement\Domain\Events\DomainEventDispatcher; use CloudCreativity\Modules\Contracts\Application\DomainEventDispatching\DeferredDispatcher; @@ -299,7 +130,7 @@ interface DeferredDomainEventDispatcher extends DomainEventDispatcher, DeferredD Then create a concrete implementation of your domain layer's dispatcher interface: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents; +namespace App\Modules\EventManagement\Application\Orchestration; use CloudCreativity\Modules\Application\DomainEventDispatching\DeferredDispatcher; @@ -311,167 +142,20 @@ final class DomainEventDispatcherAdapter extends DeferredDispatcher implements ### Creating a Dispatcher -The following example shows how to create this dispatcher: - -```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents; - -use App\Modules\EventManagement\Domain\Events\{ - DomainEventDispatcher, - AttendeeTicketWasCancelled, -}; -use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies; -use CloudCreativity\Modules\Application\DomainEventDispatching\ListenerContainer; -use CloudCreativity\Modules\Application\DomainEventDispatching\Middleware\LogDomainEventDispatch; -use CloudCreativity\Modules\Contracts\Domain\Events\DomainEvent; -use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; - -final readonly class DomainEventDispatcherProvider -{ - /** - * @var array, list> - */ - private array $subscriptions = [ - AttendeeTicketWasCancelled::class => [ - Listeners\UpdateTicketSalesReport::class, - Listeners\QueueTicketCancellationEmail::class, - ], - // ...other events - ]; +To create a deferred dispatcher, you need to provide a PSR container. This allows the dispatcher to resolve any +listeners and middleware. - public function __construct( - private ExternalDependencies $dependencies, - ) { - } - - public function getEventDispatcher(): DeferredDomainEventDispatcher - { - $dispatcher = new DomainEventDispatcherAdapter( - listeners: $listeners = new ListenerContainer(), - middleware: $middleware = new PipeContainer(), - ); - - /** Bind listener factories */ - $listeners->bind( - Listeners\UpdateTicketSalesReport::class, - fn () => new Listeners\UpdateTicketSalesReport( - $this->dependencies->getTicketSalesReportRepository(), - ), - ); - - $listeners->bind( - Listeners\QueueTicketCancellationEmail::class, - fn () => new Listeners\QueueTicketCancellationEmail( - $this->dependencies->getMailer(), - ), - ); - - /** Subscribe listeners to events */ - foreach ($this->subscriptions as $event => $listeners) { - $dispatcher->listen($event, $listeners); - } - - /** Bind middleware factories */ - $middleware->bind( - LogDomainEventDispatch::class, - fn () => new LogDomainEventDispatch( - $this->dependencies->getLogger(), - ), - ); - - /** Attach middleware for all events */ - $dispatcher->through([ - LogDomainEventDispatch::class, - ]); - - return $dispatcher; - } -} -``` - -### Bootstrapping - -Bootstrapping is simpler for the deferred dispatcher. The main thing you need to ensure is that you keep a singleton -instance of the dispatcher. Your domain layer will need access to this instance, plus the same instance must be injected -into the middleware that flushes deferred events. - -Here's an example: +For example: ```php -namespace App\Modules\EventManagement\Application\Bus; - -use App\Modules\EventManagement\Application\Internal\DomainEvents\DomainEventDispatcher;use App\Modules\EventManagement\Application\Internal\DomainEvents\DomainEventDispatcherProvider;use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies;use App\Modules\EventManagement\Application\Ports\Driving\CommandBus as CommandBusPort;use App\Modules\EventManagement\Domain\Services as DomainServices;use CloudCreativity\Modules\Application\Bus\Middleware\FlushDeferredEvents;use CloudCreativity\Modules\Bus\CommandHandlerContainer;use CloudCreativity\Modules\Bus\Middleware\SetupBeforeDispatch;use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; - -final class CommandBusProvider -{ - /** - * @var DomainEventDispatcher|null - */ - private ?DomainEventDispatcher $eventDispatcher = null; - - public function __construct( - private readonly ExternalDependencies $dependencies, - private readonly DomainEventDispatcherProvider $eventDispatcherProvider, - ) { - } - - public function getCommandBus(): CommandBusPort - { - $bus = new CommandBus( - handlers: $handlers = new CommandHandlerContainer(), - middleware: $middleware = new PipeContainer(), - ); - - // ...handler bindings. - - $middleware->bind( - FlushDeferredEvents::class, - fn () => new ExecuteInUnitOfWork($this->eventDispatcher), - ); - - $middleware->bind( - SetupBeforeDispatch::class, - fn () => new SetupBeforeDispatch(function (): Closure { - $this->setUp(); - return function (): void { - $this->tearDown(); - }; - }), - ); - - $bus->through([ - SetupBeforeDispatch::class, - ]); - - return $bus; - } - - /** - * Set up command handling state. - * - * @return void - */ - private function setUp(): void - { - $this->eventDispatcher = $this->eventDispatcherProvider - ->getEventDispatcher(); - DomainServices::setEvents(fn () => $this->eventDispatcher); - } - - /** - * Tear down command handling state. - * - * @return void - */ - private function tearDown(): void - { - DomainServices::tearDown(); - $this->eventDispatcher = null; - } -} +$dispatcher = new DomainEventDispatcher($container); ``` +The main thing you need to ensure is that your instance of this domain event dispatcher is a singleton. This ensures +that the same instance is used by the domain to dispatch events, plus by middleware to flush the dispatcher at the +correct moment. + ### Deferred Events This dispatcher works by not immediately dispatching events the domain layer asks it to emit. Instead, events are @@ -525,9 +209,9 @@ There are several examples in the [Use Cases section of the domain layer chapter one such example: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents\Listeners; +namespace App\Modules\EventManagement\Application\Orchestration\Listeners; -use App\Modules\EventManagement\Application\Ports\Driven\Persistence\TicketSalesReportRepository; +use App\Modules\EventManagement\Application\Ports\Persistence\TicketSalesReportRepository; use App\Modules\EventManagement\Domain\Events\AttendeeTicketWasCancelled; final readonly class UpdateTicketSalesReport @@ -554,56 +238,14 @@ roots outside the control of the emitting aggregate root, these domain layer sid via a driven port. ::: -Class-based listeners are bound into a listener container that is given to the event dispatcher. This is shown in the -examples above, but as a reminder: - -```php -$dispatcher = new DomainEventDispatcherAdapter( - listeners: $listeners = new ListenerContainer(), -); - -/** Bind listener factories */ -$listeners->bind( - Listeners\UpdateTicketSalesReport::class, - fn () => new Listeners\UpdateTicketSalesReport( - $this->dependencies->getTicketSalesReportRepository(), - ), -); - -/** Then subscribe it to events */ -$dispatcher->listen( - AttendeeTicketWasCancelled::class, - Listeners\UpdateTicketSalesReport::class, -); -``` - -### Closure Listeners - -Our implementation also allows you to use closures as listeners. This can be useful for simple side effects that do not -require a class. However, we recommend using class-based listeners as they are easier to unit test. - -Closure listeners are attached to events via the dispatcher, so are not bound into a listener container. - -```php -$dispatcher = new DomainEventDispatcher(); - -$dispatcher->listen( - AttendeeTicketWasCancelled::class, - function (AttendeeTicketWasCancelled $event): void { - $notifier = $this->dependencies - ->getNotifiers() - ->getTicketCancellationNotifier(); - $notifier->notify($event->ticketId); - }, -); -``` +Use the `ListenTo` attribute on the dispatcher class to bind listeners to events. ## Middleware Middleware can be attached to the dispatcher to perform actions before and/or after a domain event is emitted. This can be useful for cross-cutting concerns, such as logging. -To apply middleware to the event dispatcher, you can use the `through()` method - as shown in the examples earlier in +To apply middleware to the event dispatcher, you can use the `Through` attribute - as shown in the examples earlier in this chapter. Middleware is executed in the order it is added to the dispatcher. ### Logging @@ -611,28 +253,6 @@ this chapter. Middleware is executed in the order it is added to the dispatcher. Use our `LogDomainEventDispatch` middleware to log when an aggregate root emits an event. This middleware logs the event name when it is dispatched, and when it has been dispatched. -For example: - -```php -use CloudCreativity\Modules\Application\DomainEventDispatching\Middleware\LogDomainEventDispatch; - -$dispatcher = new DomainEventDispatcher( - listeners: $listeners = new ListenerContainer(), - middleware: $middleware = new PipeContainer(), -); - -$middleware->bind( - LogDomainEventDispatch::class, - fn () => new LogDomainEventDispatch( - $this->dependencies->getLogger(), - ), -); - -$dispatcher->through([ - LogDomainEventDispatch::class, -]); -``` - This works exactly like the logging middleware described in the [commands chapter.](../application/commands#logging) You can provide a custom logging level for the before and after dispatch log messages. @@ -645,7 +265,7 @@ You can write your own middleware to suit your specific needs. Middleware is a s following signature: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents\Middleware; +namespace App\Modules\EventManagement\Application\Orchestration\Middleware; use Closure; use CloudCreativity\Modules\Contracts\Application\DomainEventDispatching\DomainEventMiddleware; diff --git a/docs/guide/application/integration-events.md b/docs/guide/application/integration-events.md index 9aa47dea..c8c7290c 100644 --- a/docs/guide/application/integration-events.md +++ b/docs/guide/application/integration-events.md @@ -12,8 +12,7 @@ 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 that is implemented by a - service in the application layer. +- **Inbound** integration events, are those a bounded context _consumes_ via an inbound event bus driving port. - **Outbound** integration events, are those _published_ by a bounded context. Publishing occurs via a driven port, with the infrastructure layer implementing the adapter. @@ -34,9 +33,12 @@ The integration event interface is light-weight and defines only two methods: For example: ```php -namespace VendorName\EventManagement\Shared\IntegrationEvents\V1; +namespace App\Modules\EventManagement\Output\V1\Events; -use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent;use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier;use CloudCreativity\Modules\Toolkit\Identifiers\Uuid;use VendorName\EventManagement\Shared\Enums\CancellationReasonEnum; +use App\Modules\EventManagement\Output\V1\Enums\CancellationReasonEnum; +use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; +use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; +use CloudCreativity\Modules\Toolkit\Identifiers\Uuid; final readonly class AttendeeTicketWasCancelled implements IntegrationEvent @@ -73,12 +75,10 @@ up to it. This means that the data contract for an integration event is _shared_ between the bounded contexts. :::tip -The above example integration event places the message in a shared package. As the data contract is shared, when a +The above example integration event places the message in the `Api\Output` namespace. This means other modules can +receive it directly, because it is defined as API output for that module. This means the data contract is shared; when a bounded context publishes an event the expectation is that consuming bounded contexts will receive exactly the same information. - -Therefore, the integration event message must be defined in a shared package that is accessible to both the publishing -and consuming bounded contexts. ::: ### Symmetrical Serialization @@ -92,7 +92,7 @@ deserialization, the result will always be an identical integration event messag This can be expressed via an interface. To illustrate the point, a JSON serializer might look like this: ```php -namespace VendorName\Ordering\Shared\IntegrationEvents\V1\Serializers; +namespace App\Modules\Ordering\Output\V1\Events\Serializers; use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; @@ -125,7 +125,7 @@ without updating every single consumer to use the new contract. In large systems, this can be a significant challenge. To mitigate this, you can version your integration events. This allows you to introduce breaking changes to the data contract, while still supporting older versions of the event. For -example, our integration events could be in `IntegrationEvents\V1` and `IntegrationEvents\V2` namespaces. +example, our integration events could be in `Api\Output\V1` and `Api\Output\V2` namespaces. This allows you to introduce a new version of the event, while retaining the event name. Retaining the event name is important because it is an expression of your domain, using the ubiquitous language of your bounded context. If you do @@ -146,7 +146,7 @@ implementing the adapter. Your application layer should define the driven port: ```php -namespace App\Modules\EventManagement\Application\Ports\Driven\OutboundEventBus; +namespace App\Modules\EventManagement\Application\Ports\OutboundEventBus; use CloudCreativity\Modules\Contracts\Application\Ports\OutboundEventPublisher; @@ -167,12 +167,12 @@ event, you will need a domain event listener in your application layer that publ For example: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents\Listeners; +namespace App\Modules\EventManagement\Application\Orchestration\Listeners; -use App\Modules\EventManagement\Application\Ports\Driven\OutboundEvents\OutboundEventBus; +use App\Modules\EventManagement\Application\Ports\OutboundEvents\OutboundEventBus; use App\Modules\EventManagement\Domain\Events\AttendeeTicketWasCancelled; +use App\Modules\EventManagement\Api\Output\V1\Events as IntegrationEvents; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\UuidFactory; -use VendorName\EventManagement\Shared\IntegrationEvents\V1 as IntegrationEvents; final readonly class PublishAttendeeTicketWasCancelled { @@ -210,12 +210,12 @@ work is committed. In this scenario, the above listener would be changed to use the outbox instead: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents\Listeners; +namespace App\Modules\EventManagement\Application\Orchestration\Listeners; -use App\Modules\EventManagement\Application\Ports\Driven\OutboundEvents\Outbox; +use App\Modules\EventManagement\Application\Ports\OutboundEvents\Outbox; use App\Modules\EventManagement\Domain\Events\AttendeeTicketWasCancelled; +use App\Modules\EventManagement\Api\Output\V1\Events as IntegrationEvents; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\UuidFactory; -use VendorName\EventManagement\Shared\IntegrationEvents\V1 as IntegrationEvents; final readonly class PublishAttendeeTicketWasCancelled { @@ -277,13 +277,11 @@ There are example handlers for each of these strategies below. An inbound event handler that dispatches a command that is a use case in your application layer would look like this: ```php -namespace App\Modules\EventManagement\Application\UseCases\InboundEvents; +namespace App\Modules\EventManagement\Application\UseCases\Events; -use App\Modules\EventManagement\Application\Ports\Driving\CommandBus\CommandBus; -use App\Modules\EventManagement\Application\UseCases\Commands\{ - RecalculateSalesAtEvent\RecalculateSalesAtEventCommand, -}; -use VendorName\Ordering\Shared\IntegrationEvents\V1\OrderWasFulfilled; +use App\Modules\EventManagement\Api\CommandBus\CommandBus; +use App\Modules\EventManagement\Api\Input\RecalculateSalesAtEventCommand; +use App\Modules\Ordering\Api\Output\V1\Events\OrderWasFulfilled; final readonly class OrderWasFulfilledHandler { @@ -312,17 +310,15 @@ command handler to do this. This is almost identical to the previous example. However, in this case the command is internal to the bounded context. I.e. it is not intended to be exposed as a use case that the outside world can dispatch. -This means the command message and command bus are in the application layer's internal namespace. Otherwise, the +This means the command message and command bus are in the application layer's driven ports namespace. Otherwise, the approach is identical to the previous strategy. ```php -namespace App\Modules\EventManagement\Application\UseCases\InboundEvents; +namespace App\Modules\EventManagement\Application\UseCases\Events; -use App\Modules\EventManagement\Application\Internal\Commands\{ - RecalculateSalesAtEvent\RecalculateSalesAtEventCommand, -}; -use App\Modules\EventManagement\Application\Ports\Driving\CommandBus\InternalCommandBus; -use VendorName\Ordering\Shared\IntegrationEvents\V1\OrderWasFulfilled; +use App\Modules\EventManagement\Application\Internal\RecalculateSalesAtEventCommand; +use App\Modules\EventManagement\Application\Internal\InternalCommandBus; +use App\Modules\Ordering\Api\Output\V1\Events\OrderWasFulfilled; final readonly class OrderWasFulfilledHandler { @@ -333,7 +329,7 @@ final readonly class OrderWasFulfilledHandler public function handle(OrderWasFulfilled $event): void { - // alternatively we could use `queue()` to process the command asynchronously + // alternatively we could queue this to process it asynchronously $this->bus->dispatch(new RecalculateSalesAtEventCommand( eventId: $event->eventId, )); @@ -351,12 +347,15 @@ that is already in use by your domain, e.g. emitted by an aggregate. Reusing the side effects are triggered. ```php -namespace App\Modules\EventManagement\Application\UseCases\InboundEvents; +namespace App\Modules\EventManagement\Application\UseCases\Events; -use App\Modules\EventManagement\Domain\Events\DomainEventDispatcher;use App\Modules\EventManagement\Domain\Events\SalesAtEventDidChange;use CloudCreativity\Modules\Application\InboundEventBus\Middleware\HandleInUnitOfWork;use CloudCreativity\Modules\Contracts\Bus\DispatchThroughMiddleware;use VendorName\Ordering\Shared\IntegrationEvents\V1\OrderWasFulfilled; +use App\Modules\EventManagement\Domain\Events\DomainEventDispatcher; +use App\Modules\EventManagement\Domain\Events\SalesAtEventDidChange; +use App\Modules\Ordering\Output\V1\Events\OrderWasFulfilled; +use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork; -final readonly class OrderWasFulfilledHandler implements - DispatchThroughMiddleware +#[Through(ExecuteInUnitOfWork::class)] +final readonly class OrderWasFulfilledHandler { public function __construct( private DomainEventDispatcher $domainEvents, @@ -369,13 +368,6 @@ final readonly class OrderWasFulfilledHandler implements eventId: $event->eventId, )); } - - public function middleware(): array - { - return [ - HandleInUnitOfWork::class, - ]; - } } ``` @@ -394,7 +386,7 @@ 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; +namespace App\Modules\EventManagement\Api; use CloudCreativity\Modules\Contracts\Messaging\InboundEventDispatcher; @@ -406,107 +398,51 @@ interface InboundEventBus extends InboundEventDispatcher And then our implementation is as follows: ```php -namespace App\Modules\EventManagement\Application\Bus; - -use App\Modules\EventManagement\Application\Ports\Driving\InboundEventBus as Port;use CloudCreativity\Modules\Bus\InboundEventDispatcher; - -final class InboundEventBus extends InboundEventDispatcher implements Port +namespace App\Modules\EventManagement\Application\Adapters; + +use App\Modules\EventManagement\Api\InboundEventBus as Port; +use App\Modules\EventManagement\Application\UseCases\Events\DefaultEventHandler; +use App\Modules\EventManagement\Application\UseCases\Events\OrderWasFulfilledHandler; +use App\Modules\Ordering\Output\V1\Events\OrderWasFulfilled; +use CloudCreativity\Modules\Bus\InboundEventDispatcher; +use CloudCreativity\Modules\Bus\WithDefault; +use CloudCreativity\Modules\Bus\WithEvent; +use CloudCreativity\Modules\Bus\Middleware\LogMessageDispatch; + +#[Through(LogMessageDispatch::class)] +#[WithDefault(DefaultEventHandler::class)] +#[WithEvent(OrderWasFulfilled::class, OrderWasFulfilledHandler::class)] +final class InboundEventBusAdapter extends InboundEventDispatcher implements Port { } ``` -### Creating a Bus +Notice that the inbound event dispatcher can have middleware - attached using the `Through` attribute. The `WithEvent` +attribute is used to map an integration event from an `Api\Output` namespace to the handler in the +`Application\UseCases\Events` namespace. The `WithDefault` attribute does exactly as it describes - any events that do +not have a specific mapping via `WithEvent` will use the specified default handler. -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 -3. Optionally, attaching middleware that runs for all inbound events dispatched through the event bus. +### Creating a Bus -Factories must always be lazy, so that the cost of instantiating event handlers or middleware only occurs if the handler -or middleware are actually being used. - -For example: +The inbound event bus dispatcher class that your implementation extends (in the above example) requires you to inject a +PSR container. This container is then used to resolve any middleware and event handlers that you've attached to the +dispatcher via the `Through`, `WithDefault` and `WithEvent` attributes. ```php -namespace App\Modules\EventManagement\Application\Bus; - -use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies;use App\Modules\EventManagement\Application\Ports\Driving\InboundEventBus as InboundEventBusPort;use App\Modules\EventManagement\Application\UsesCases\InboundEvents\OrderWasFulfilledHandler;use CloudCreativity\Modules\Application\InboundEventBus\Middleware\HandleInUnitOfWork;use CloudCreativity\Modules\Bus\EventHandlerContainer;use CloudCreativity\Modules\Bus\Middleware\LogInboundEvent;use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer;use VendorName\Ordering\Shared\IntegrationEvents\V1\OrderWasFulfilled; - -final class InboundEventBusProvider -{ - public function __construct( - private readonly CommandBusProvider $commandBusProvider, - private readonly ExternalDependencies $dependencies, - ) { - } - - public function getEventBus(): InboundEventBusPort - { - $bus = new InboundEventBus( - handlers: $handlers = new EventHandlerContainer(), - middleware: $middleware = new PipeContainer(), - ); - - /** Bind integration events to handler factories */ - $handlers->bind( - OrderWasFulfilled::class, - fn() => new OrderWasFulfilledHandler( - $this->commandBusProvider->getCommandBus(), - ), - ); - - /** Bind middleware factories */ - $middleware->bind( - HandleInUnitOfWork::class, - fn () => new HandleInUnitOfWork($this->dependencies->getUnitOfWorkManager()), - ); - - $middleware->bind( - LogInboundEvent::class, - fn () => new LogInboundEvent( - $this->dependencies->getLogger(), - ), - ); - - /** Attach middleware that runs for all events */ - $bus->through([ - LogInboundEvent::class, - ]); - - return $bus; - } -} +$dispatcher = new InboundEventBus($psrContainer); ``` -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. Typically this means we need to bind the driving port into a -service container. For example, in Laravel: +So, for example in a Laravel application, you would bind this to the interface as follows in your service provider: ```php -namespace App\Providers; - -use App\Modules\EventManagement\Application\{ - Bus\InboundEventBusProvider, - Ports\Driving\InboundEventBus, -}; -use Illuminate\Contracts\Container\Container; -use Illuminate\Support\ServiceProvider; +use App\Modules\EventManagement\Api\InboundEventBus; +use App\Modules\EventManagement\Application\Adapters\InboundEventBusAdapter; +use Illuminate\Contracts\Foundation\Application; -final class EventManagementServiceProvider extends ServiceProvider -{ - public function register() - { - $this->app->bind( - InboundEventBus::class, - static function (Container $app) { - $provider = $app->make(InboundEventBusProvider::class); - return $provider->getEventBus(); - }, - ); - } -} +$this->app->bind( + InboundEventBus::class, + static fn (Application $app) => new InboundEventBusAdapter($app), +); ``` ### Consuming Events @@ -523,7 +459,9 @@ 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\InboundEventBus;use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent;use VendorName\Ordering\Shared\IntegrationEvents\V1\Serializers\JsonSerializer; +use App\Modules\EventManagement\Api\InboundEventBus; +use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; +use App\Modules\Ordering\Api\Output\V1\Events\Serializers\JsonSerializer; class InboundEventController extends Controller { @@ -568,35 +506,31 @@ _opportunity_ to consume the event. In this scenario, we need to configure the inbound event bus to _swallow_ events that it does not have a handler for. This is because the event bus will throw an exception if it does not have a handler for an event. -To do this, we configure a default handler on the handler container that is given to the event bus. Use -the `SwallowInboundEvent` handler for this purpose: +To do this, we configure a default handler via the `WithDefault` attribute on the inbound event bus instance - use the +`SwallowInboundEvent` handler for this purpose. ```php -use CloudCreativity\Modules\Bus\EventHandlerContainer;use CloudCreativity\Modules\Bus\SwallowInboundEvent; - -$bus = new InboundEventBus( - handlers: $handlers = new EventHandlerContainer( - default: fn() => new SwallowInboundEvent(), - ), -); +namespace App\Modules\EventManagement\Application\Adapters; + +use App\Modules\EventManagement\Api\InboundEventBus as Port; +use App\Modules\EventManagement\Application\UseCases\Events\OrderWasFulfilledHandler; +use App\Modules\Ordering\Output\V1\Events\OrderWasFulfilled; +use CloudCreativity\Modules\Bus\InboundEventDispatcher; +use CloudCreativity\Modules\Bus\WithDefault; +use CloudCreativity\Modules\Bus\WithEvent; +use CloudCreativity\Modules\Bus\Middleware\LogMessageDispatch; +use CloudCreativity\Modules\Bus\SwallowInboundEvent; + +#[Through(LogMessageDispatch::class)] +#[WithDefault(SwallowInboundEvent::class)] +#[WithEvent(OrderWasFulfilled::class, OrderWasFulfilledHandler::class)] +final class InboundEventBusAdapter extends InboundEventDispatcher implements Port +{ +} ``` -Notice we provide the event handler container with a factory that creates a default handler. In this case, -the `SwallowInboundEvent` handler will do nothing with the event. You can also provide a logger and log level to -the `SwallowInboundEvent` handler, so that it logs that the event was swallowed: - -```php -use CloudCreativity\Modules\Bus\EventHandlerContainer;use CloudCreativity\Modules\Bus\SwallowInboundEvent;use Psr\Log\LogLevel; - -$bus = new InboundEventBus( - handlers: $handlers = new EventHandlerContainer( - default: fn() => new SwallowInboundEvent( - logger: $this->dependencies->getLogger(), - level: LogLevel::INFO, // optional, defaults to debug - ), - ), -); -``` +The `SwallowInboundEvent` handler can be injected with a PSR logger, so that it logs any inbound events that are +swallowed. Alternatively, you can write your own default handler if desired. @@ -625,11 +559,11 @@ Firstly, our application layer will need an inbox driving port. This will allow example: ```php -namespace App\Modules\EventManagement\Application\Ports\Driving; +namespace App\Modules\EventManagement\Api; use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; -interface Inbox +interface InboundEventInbox { public function push(IntegrationEvent $event): void; } @@ -640,7 +574,7 @@ inbox. For both these actions - checking whether it exists, and storing - the ad might look like this: ```php -namespace App\Modules\EventManagement\Application\Ports\Driven\Inbox; +namespace App\Modules\EventManagement\Api\Ports\Inbox; use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; @@ -661,7 +595,9 @@ This means we can now update the previous controller example to use the inbox in ```php namespace App\Http\Controllers\Api\PubSub; -use App\Modules\EventManagement\Application\Ports\Driving\InboundEvents\Inbox;use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent;use VendorName\Ordering\Shared\IntegrationEvents\V1\Serializers\JsonSerializer; +use App\Modules\EventManagement\Api\InboundEventInbox; +use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; +use App\Modules\Ordering\Api\Output\V1\Events\Serializers\JsonSerializer; class InboundEventController extends Controller { @@ -709,12 +645,13 @@ via middleware. Middleware is a powerful way to add cross-cutting concerns to yo Middleware can be added either to the inbound event bus (so it runs for every event) or to individual event handlers. -To apply middleware to the inbound event bus, use the `through()` method - as shown in the earlier examples. +To apply middleware to the inbound event bus, use the `Through` attribute - as shown in the earlier examples. Middleware is executed in the order it is added. -Additionally, you can add middleware to individual handler classes. To do this, implement the -`DispatchThroughMiddleware` interface. The `middleware()` method should then return an array of middleware to run, in -the order they should be executed. Handler middleware are always executed _after_ the event bus middleware. +Additionally, you can add middleware to individual handler classes. To do this, use the `Through` attribute on the +handler class. This allows you to apply middleware to specific handlers, rather than all handlers that are registered +with the inbound event bus. Middleware is executed in the order it is added; additionally, handler middleware is +executed _after_ inbound event bus middleware. This package provides several useful middleware, which are described below. Additionally, you can write your own middleware to suit your specific needs. @@ -732,8 +669,8 @@ For example: ```php use CloudCreativity\Modules\Bus\Middleware\SetupBeforeEvent; -$middleware->bind( - SetupBeforeEvent::class, +$container->bind( + 'event-management:setup-events', fn () => new SetupBeforeEvent(function (): Closure { // setup singletons, dependencies etc here. return function (): void { @@ -742,11 +679,6 @@ $middleware->bind( }; }), ); - -$bus->through([ - LogInboundEvent::class, - SetupBeforeEvent::class, -]); ``` Here our setup middleware takes a setup closure as its only constructor argument. This setup closure can optionally @@ -759,17 +691,12 @@ closure as its only constructor argument: ```php use CloudCreativity\Modules\Bus\Middleware\TearDownAfterEvent; -$middleware->bind( - TearDownAfterEvent::class, +$container->bind( + 'event-management:teardown-events', fn () => new TearDownAfterEvent(function (): Closure { // teardown here }), ); - -$bus->through([ - LogInboundEvent::class, - TearDownAfterEvent::class, -]); ``` ### Unit of Work @@ -782,22 +709,11 @@ If your consumer only dispatches a command, then it will not need to be wrapped command itself should use a unit of work. ::: -To consume an event in a unit of work, you will need to use our `HandleInUnitOfWork` middleware. You should always +To consume an event in a unit of work, you will need to use our `ExecuteInUnitOfWork` middleware. You should always implement this as handler middleware - because typically you need it to be the final middleware that runs before a handler is invoked. It also makes it clear to developers looking at the handler that it is expected to run in a unit of work. The example `OrderWasFulfilledHandler` above demonstrates this. -An example binding for this middleware is: - -```php -use CloudCreativity\Modules\Application\InboundEventBus\Middleware\HandleInUnitOfWork; - -$middleware->bind( - HandleInUnitOfWork::class, - fn () => new HandleInUnitOfWork($this->dependencies->getUnitOfWorkManager()), -); -``` - :::warning If you're using a unit of work, you should be combining this with our "unit of work domain event dispatcher". One really important thing to note is that you **must inject both the middleware and the domain event dispatcher with @@ -816,19 +732,6 @@ When using this dispatcher, you will need to use our `FlushDeferredEvents` middl implement this as handler middleware - because typically you need it to be the final middleware that runs before a handler is invoked. I.e. this is an equivalent middleware to the unit of work middleware. -An example binding for this middleware is: - -```php -use CloudCreativity\Modules\Application\InboundEventBus\Middleware\FlushDeferredEvents; - -$middleware->bind( - FlushDeferredEvents::class, - fn () => new FlushDeferredEvents( - $this->eventDispatcher, - ), -); -``` - :::warning When using this middleware, it is important that you inject it with a singleton instance of the deferred event dispatcher. This must be the same instance that is exposed to your domain layer as a service. @@ -836,26 +739,15 @@ dispatcher. This must be the same instance that is exposed to your domain layer ### Logging -Use our `LogInboundEvent` middleware to log when an integration event is consumed. It takes -a [PSR Logger](https://php-fig.org/psr/psr-3/). - -```php -use CloudCreativity\Modules\Bus\Middleware\LogInboundEvent; - -$middleware->bind( - LogInboundEvent::class, - fn () => new LogInboundEvent( - $this->dependencies->getLogger(), - ), -); -``` +Use our `LogMessageDispatch` middleware to log the dispatch of an inbound event. The middleware takes a +[PSR Logger](https://php-fig.org/psr/psr-3/). The use of this middleware is identical to that described in the [Commands chapter.](./commands#logging) See those instructions for more information, such as configuring the log levels. Additionally, you can customise the context that is logged for an event. To exclude properties, mark them with the -`Sensitive` attribute. Alternatively, if you need full control over the context, implement the `ContextProvider` -interface on your integration event. See the examples in the [Commands chapter.](./commands#logging) +`Sensitive` attribute. Alternatively, if you need full control over the context, implement the `Contextual` +interface on your integration event message. See the examples in the [Commands chapter.](./commands#logging) ### Writing Middleware @@ -865,7 +757,9 @@ following signature: ```php namespace App\Modules\EventManagement\Application\Bus\Middleware; -use Closure;use CloudCreativity\Modules\Contracts\Bus\Middleware\IntegrationEventMiddleware;use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; +use Closure; +use CloudCreativity\Modules\Contracts\Bus\Middleware\IntegrationEventMiddleware; +use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; final class MyMiddleware implements IntegrationEventMiddleware { diff --git a/docs/guide/application/queries.md b/docs/guide/application/queries.md index 8f032ec1..6249cdd7 100644 --- a/docs/guide/application/queries.md +++ b/docs/guide/application/queries.md @@ -16,9 +16,10 @@ I.e. it defines the data contract for the request. For example: ```php -namespace App\Modules\EventManagement\Application\UseCases\Queries\GetAttendeeTickets; +namespace App\Modules\EventManagement\Api\Input\GetAttendeeTickets; -use CloudCreativity\Modules\Contracts\Messaging\Query;use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; +use CloudCreativity\Modules\Contracts\Messaging\Query; +use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; final readonly class GetAttendeeTicketsQuery implements Query { @@ -38,16 +39,18 @@ the data collection, and returning the result. Your query handler defines the use case - by type-hinting the query input and the result output. For example: ```php -namespace App\Modules\EventManagement\Application\UseCases\Queries\GetAttendeeTickets; +namespace App\Modules\EventManagement\Application\UseCases; -use App\Modules\EventManagement\Application\Ports\Driven\Persistence\ReadModels\V1\TicketModelRepository; +use App\Modules\EventManagement\Api\Output\V1\Models\TicketModel; +use App\Modules\EventManagement\Application\Ports\Persistence\ReadModels\V1\TicketModelRepository; +use App\Modules\EventManagement\Application\Factories\ReadModels\V1\TicketModelFactory; use CloudCreativity\Modules\Toolkit\Results\Result; -use VendorName\EventManagement\Shared\ReadModels\V1\TicketModel; final readonly class GetAttendeeTicketsHandler { public function __construct( private TicketModelRepository $repository, + private TicketModelFactory $factory, ) { } @@ -59,12 +62,14 @@ final readonly class GetAttendeeTicketsHandler */ public function handle(GetAttendeeTicketsQuery $query): Result { - $models = $this->repository->findByAttendeeId($query->attendeeId); + $state = $this->repository->findByAttendeeId($query->attendeeId); - if (count($models) === 0) { + if (count($state) === 0) { return Result::failed('The provided attendee does not exist.'); } + $models = $this->factory->make($state); + return Result::ok($models); } } @@ -79,12 +84,13 @@ that alter the state. A query is a request to _read_ the state, and a command sh :::tip You'll notice here that the example is very simple. The application layer hands off the request to the infrastructure -layer via a driven port, and returns the result. This is a common pattern for queries, as the logic is often very -simple. +layer via a driven port, maps the values returned by persistence to read models, then returns these models as the +result. This is a common pattern for queries where state can be read directly from persistence. An alternative pattern +would be to load a domain aggregate or service, read domain state and map that back to read models that are returned as +the result. -There may be times when your query handlers need to do a lot more work. For instance, there is an example in the -[domain services chapter](../domain/services#query-handlers) that shows a query handler executing business logic and -returning a result representing the outcome of that logic. +For instance, there is an example in the [domain services chapter](../domain/services#query-handlers) that shows a query +handler executing business logic and returning a result representing the outcome of that logic. ::: ### Results @@ -93,7 +99,8 @@ Just like commands, queries handlers return a result object - which contains the See the [Results chapter for information on using this object.](../toolkit/results) Unlike command results, query results can contain complex data structures as their return value. It is best to define -these data structures - which is why our recommended pattern is to return [read models.](#read-models) +these data structures - which is why our recommended pattern is to return [read models.](#read-models) Values returned +are placed in the `Api\Output` namespace to make it clear that these are values that can be returned by the module. ## Query Bus @@ -103,7 +110,7 @@ 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; +namespace App\Modules\EventManagement\Api; use CloudCreativity\Modules\Contracts\Messaging\QueryDispatcher; @@ -115,100 +122,47 @@ interface QueryBus extends QueryDispatcher And then our implementation is as follows: ```php -namespace App\Modules\EventManagement\Application\Bus; +namespace App\Modules\EventManagement\Application\Adapters; -use App\Modules\EventManagement\Application\Ports\Driving\QueryBus as Port;use CloudCreativity\Modules\Bus\QueryDispatcher; +use App\Modules\EventManagement\Api\QueryBus as Port; +use App\Modules\EventManagement\Api\Input\GetAttendeeTicketsQuery; +use App\Modules\EventManagement\Application\UseCases\GetAttendeeTicketsHandler; +use CloudCreativity\Modules\Bus\QueryDispatcher; +use CloudCreativity\Modules\Bus\Middleware\LogMessageDispatch; +use CloudCreativity\Modules\Bus\WithQuery; +#[Through(LogMessageDispatch::class)] +#[WithQuery(GetAttendeeTicketsQuery::class, GetAttendeeTicketsHandler::class)] final class QueryBus extends QueryDispatcher implements Port { } ``` -### Creating a Query Bus - -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 -3. Optionally, attaching middleware that runs for all queries dispatched through the query bus. +Notice that the query dispatcher can have middleware - attached using the `Through` attribute. The `WithQuery` +attribute is used to map a query from the `Api\Input` namespace to the handler in the `Application\UseCases` +namespace. -Factories must always be _lazy_, so that the cost of instantiating command handlers or middleware only occurs if the -handler or middleware are actually being used. +### Creating a Query Bus -For example: +The query dispatcher class that your implementation extends (in the above example) requires you to inject a PSR +container. This container is then used to resolve any middleware and query handlers that you've attached to the +dispatcher via the `Through` and `WithQuery` attributes. ```php -namespace App\Modules\EventManagement\Application\Bus; - -use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies;use App\Modules\EventManagement\Application\Ports\Driving\QueryBus as QueryBusPort;use App\Modules\EventManagement\Application\UseCases\Queries\{GetAttendeeTickets\GetAttendeeTicketsHandler,GetAttendeeTickets\GetAttendeeTicketsQuery,};use CloudCreativity\Modules\Bus\Middleware\LogMessageDispatch;use CloudCreativity\Modules\Bus\QueryHandlerContainer;use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; - -final class QueryBusProvider -{ - public function __construct( - private readonly ExternalDependencies $dependencies, - ) { - } - - public function getQueryBus(): QueryBusPort - { - $bus = new QueryBus( - handlers: $handlers = new QueryHandlerContainer(), - middleware: $middleware = new PipeContainer(), - ); - - /** Bind queries to handler factories */ - $handlers->bind( - GetAttendeeTicketsQuery::class, - fn() => new GetAttendeeTicketsHandler( - $this->dependencies->getTicketModelRepository(), - ), - ); - - /** Bind middleware factories */ - $middleware->bind( - LogMessageDispatch::class, - fn () => new LogMessageDispatch( - $this->dependencies->getLogger(), - ), - ); - - /** Attach middleware that runs for all queries */ - $bus->through([ - LogMessageDispatch::class, - ]); - - return $bus; - } -} +$dispatcher = new QueryBus($psrContainer); ``` -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: +So, for example in a Laravel application, you would bind this to the interface as follows in your service provider: ```php -namespace App\Providers; - -use App\Modules\EventManagement\Application\{ - Bus\QueryBusProvider, - Ports\Driving\QueryBus, -}; -use Illuminate\Contracts\Container\Container; -use Illuminate\Support\ServiceProvider; +use App\Modules\EventManagement\Api\QueryBus; +use App\Modules\EventManagement\Application\Adapters\QueryBusAdapter; +use Illuminate\Contracts\Foundation\Application; -final class EventManagementServiceProvider extends ServiceProvider -{ - public function register(): void - { - $this->app->bind( - QueryBus::class, - static function (Container $app) { - $provider = $app->make(QueryBusProvider::class); - return $provider->getQueryBus(); - }, - ); - } -} +$this->app->bind( + QueryBus::class, + static fn (Application $app) => new QueryBusAdapter($app), +); ``` ### Dispatching Queries @@ -219,15 +173,15 @@ a single action controller to handle a HTTP request in a Laravel application, we ```php namespace App\Http\Controllers\Api\Attendees; -use App\Modules\EventManagement\Application\{ - Ports\Driving\QueryBus\QueryBus, - UsesCases\Queries\GetAttendeeTickets\GetAttendeeTicketsQuery, +use App\Modules\EventManagement\Api\{ + QueryBus, + Input\GetAttendeeTicketsQuery, + Output\V1\Models\TicketModel, }; use App\Http\Resources\Attendees\TicketsResource; use CloudCreativity\Modules\Toolkit\Identifiers\IntegerId; use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; use Illuminate\Validation\Rule; -use VendorName\EventManagement\Shared\ReadModels\V1\TicketModel; class TicketsController extends Controller { @@ -262,7 +216,7 @@ represents some current state of the bounded context. They are _read-only_ i.e. For example, our model returned by our "get attendee tickets" query might look like this: ```php -namespace VendorName\EventManagement\Shared\ReadModels\V1; +namespace App\Modules\EventManagement\Output\V1\Models; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; @@ -294,10 +248,9 @@ This is not unusual - and in fact, it is actually good design to have different operations. This gives a clear separation of concerns. Aggregate roots and entities represent the data structure that is required to determine _if_ the state of the domain can -be -changed, and _what_ to change it to - plus what domain events should be emitted as a result. In our example domain, the -attendee aggregate root controls changes to its tickets - therefore the tickets are always contained within the attendee -aggregate root. +be changed, and _what_ to change it to - plus what domain events should be emitted as a result. In our example domain, +the attendee aggregate root controls changes to its tickets - therefore the tickets are always contained within the +attendee aggregate root. Read models represent the answer to a question posed by a query, and are structured in a way that we can understand the state of the domain. In our example, it makes sense for tickets to be retrieved independently of the attendee - e.g. if @@ -325,7 +278,7 @@ contract. In large systems, this can be a significant challenge. To mitigate this, you can version your read models. This allows you to introduce breaking changes to the data contract, while still supporting older versions. For -example, our read models could be in `ReadModels\V1` and `ReadModels\V2` namespaces. +example, our read models could be in `Api\Output\V1\Models` and `...\V2\Models` namespaces. This allows you to introduce a new version of the model, while retaining the model name. Retaining the model name is important because it is an expression of your domain, using the ubiquitous language of your bounded context. If you do @@ -344,12 +297,13 @@ middleware. Middleware is a powerful way to add cross-cutting concerns to your c Middleware can be added either to the query bus (so it runs for every query) or to individual query handlers. -To apply middleware to the query bus, you can use the `through()` method on the bus - as shown in the example above. +To apply middleware to the query bus, you can use the `Through` attribute on the bus - as shown in the example above. Middleware is executed in the order it is added to the bus. -To apply middleware to a specific query handler, the handler must implement the `DispatchThroughMiddleware` interface. -The `middleware()` method should then return an array of middleware to run, in the order they should be executed. -Handler middleware are always executed _after_ the bus middleware. +To apply middleware to a specific query handler, use the `Through` attribute on the handler class. This allows you to +add middleware that is only executed for a specific query. You can add as many middleware as you like to a handler, and +they will be executed in the order they are added to the handler. Handler middleware are executed _after_ any bus +middleware. This package provides several useful middleware, which are described below. Additionally, you can write your own middleware to suit your specific needs. @@ -365,22 +319,11 @@ Their use is identical for queries. Use our `LogMessageDispatch` middleware to log the dispatch of a query, and the result. The middleware takes a [PSR Logger](https://php-fig.org/psr/psr-3/). -```php -use CloudCreativity\Modules\Bus\Middleware\LogMessageDispatch; - -$middleware->bind( - LogMessageDispatch::class, - fn (): LogMessageDispatch => new LogMessageDispatch( - $this->dependencies->getLogger(), - ), -); -``` - The use of this middleware is identical to that described in the [Commands chapter.](./commands#logging) See those instructions for more information, such as configuring the log levels. Additionally, you can customise the context that is logged for a query. To exclude properties, mark them with the -`Sensitive` attribute. Alternatively, if you need full control over the context, implement the `ContextProvider` +`Sensitive` attribute. Alternatively, if you need full control over the context, implement the `Contextual` interface on your query message. See the examples in the [Commands chapter.](./commands#logging) ### Writing Middleware @@ -389,9 +332,12 @@ You can write your own middleware to suit your specific needs. Middleware is a s following signature: ```php -namespace App\Modules\EventManagement\Application\Bus\Middleware; +namespace App\Modules\EventManagement\Application\Adapters\Middleware; -use Closure;use CloudCreativity\Modules\Contracts\Bus\Middleware\QueryMiddleware;use CloudCreativity\Modules\Contracts\Messaging\Query;use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; +use Closure; +use CloudCreativity\Modules\Contracts\Bus\Middleware\QueryMiddleware; +use CloudCreativity\Modules\Contracts\Messaging\Query; +use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; final class MyMiddleware implements QueryMiddleware { @@ -428,9 +374,13 @@ If you want to write middleware that can be used with both commands and queries, instead: ```php -namespace App\Modules\EventManagement\Application\Bus\Middleware; +namespace App\Modules\EventManagement\Application\Adapters\Middleware; -use Closure;use CloudCreativity\Modules\Contracts\Bus\Middleware\BusMiddleware;use CloudCreativity\Modules\Contracts\Messaging\Command;use CloudCreativity\Modules\Contracts\Messaging\Query;use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; +use Closure; +use CloudCreativity\Modules\Contracts\Bus\Middleware\BusMiddleware; +use CloudCreativity\Modules\Contracts\Messaging\Command; +use CloudCreativity\Modules\Contracts\Messaging\Query; +use CloudCreativity\Modules\Contracts\Toolkit\Result\Result; class MyBusMiddleware implements BusMiddleware { diff --git a/docs/guide/application/units-of-work.md b/docs/guide/application/units-of-work.md index 4f08346c..b0a98f0b 100644 --- a/docs/guide/application/units-of-work.md +++ b/docs/guide/application/units-of-work.md @@ -20,9 +20,10 @@ because our domain logic says that state of the attendee may change when the tic Our command handler might look like this: ```php -namespace App\Modules\EventManagement\Application\UseCases\Commands\CancelAttendeeTicket; +namespace App\Modules\EventManagement\Application\UseCases; -use App\Modules\EventManagement\Application\Ports\Driven\Persistence\AttendeeRepository; +use App\Modules\EventManagement\Api\Input\CancelAttendeeTicketCommand; +use App\Modules\EventManagement\Application\Ports\Persistence\AttendeeRepository; use CloudCreativity\Modules\Toolkit\Results\Result; final readonly class CancelAttendeeTicketHandler @@ -123,12 +124,16 @@ that actually starts, commits and rolls back the transaction. Our previous example can be updated to add a unit of work that wraps the command handler execution via middleware: ```php -namespace App\Modules\EventManagement\Application\UseCases\Commands\CancelAttendeeTicket; +namespace App\Modules\EventManagement\Application\UseCases; -use App\Modules\EventManagement\Application\Ports\Driven\Persistence\AttendeeRepository;use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork;use CloudCreativity\Modules\Contracts\Bus\DispatchThroughMiddleware;use CloudCreativity\Modules\Toolkit\Results\Result; +use App\Modules\EventManagement\Api\Input\CancelAttendeeTicketCommand; +use App\Modules\EventManagement\Application\Ports\Persistence\AttendeeRepository; +use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork; +use CloudCreativity\Modules\Toolkit\Results\Result; +use CloudCreativity\Modules\Toolkit\Pipeline\Through; -final readonly class CancelAttendeeTicketHandler implements - DispatchThroughMiddleware +#[Through(ExecuteInUnitOfWork::class)] +final readonly class CancelAttendeeTicketHandler { public function __construct( private AttendeeRepository $attendees, @@ -148,32 +153,28 @@ final readonly class CancelAttendeeTicketHandler implements return Result::ok(); } - - public function middleware(): array - { - return [ - // the last middleware to be executed before the command handler - ExecuteInUnitOfWork::class, - ]; - } } ``` ## Unit of Work -To implement a unit of work, you need an adapter in your infrastructure layer that implements the following driven port: +To implement a unit of work, you need an adapter in your infrastructure layer that implements our `UnitOfWork` +interface. ```php -namespace CloudCreativity\Modules\Application\Ports\Driven\UnitOfWork; +namespace CloudCreativity\Modules\Contracts\Application\Ports; + +use Closure; interface UnitOfWork { /** * Execute the callback in a transaction. * - * @param \Closure $callback - * @param int $attempts - * @return mixed + * @template TReturn + * @param Closure(): TReturn $callback + * @param int<1, max> $attempts + * @return TReturn */ public function execute(Closure $callback, int $attempts = 1): mixed; } @@ -183,9 +184,11 @@ This allows you to plug our unit of work manager into any database solution you implementation for Laravel could look like this: ```php -namespace App\Modules\Shared\Infrastructure; +namespace App\Modules\Infrastructure; -use Closure;use CloudCreativity\Modules\Contracts\Application\Ports\UnitOfWork;use Illuminate\Database\ConnectionInterface; +use Closure; +use CloudCreativity\Modules\Contracts\Application\Ports\UnitOfWork; +use Illuminate\Database\ConnectionInterface; final readonly class IlluminateUnitOfWork implements UnitOfWork { @@ -213,10 +216,12 @@ The adapter just requires your concrete unit of work implementation: ```php use CloudCreativity\Modules\Application\UnitOfWork\UnitOfWorkManager; +use CloudCreativity\Modules\Contracts\Application\Ports\UnitOfWork; +use CloudCreativity\Modules\Contracts\Application\Ports\ExceptionReporter; $manager = new UnitOfWorkManager( - db: $this->dependencies->getUnitOfWork(), - reporter: $this->dependencies->getExceptionReporter(), + db: $container->get(UnitOfWork::class), + reporter: $container->get(ExceptionReporter::class), ); ``` @@ -234,30 +239,6 @@ duration of the unit of work. The same instance must be injected both into the domain event dispatcher, plus the middleware that wraps command handlers and integration event consumers. -You can (and should) dispose of this instance once the unit of work is complete. To do this, we provide middleware that -allows you to setup and tear down the unit of work manager for each operation. - -For example, we can use the setup before dispatch middleware on our command bus: - -```php -use CloudCreativity\Modules\Bus\Middleware\SetupBeforeDispatch; - -$middleware->bind( - SetupBeforeDispatch::class, - fn () => new SetupBeforeDispatch(function (): Closure { - // setup - $this->unitOfWorkManager = new UnitOfWorkManager( - db: $this->dependencies->getUnitOfWork(), - reporter: $this->dependencies->getExceptionReporter(), - ); - // tear down - return function (): void { - $this->unitOfWorkManager = null; - }; - }), -); -``` - :::tip Middleware is documented in the relevant chapters for [commands](../application/commands#middleware) and [inbound integration events.](../application/integration-events#inbound-middleware) @@ -336,7 +317,7 @@ This immediate dispatch of the domain events allows listeners to be triggered im listeners can actually be deferred? Indicate this by implementing the `DispatchBeforeCommit` interface on the listener: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents\Listeners; +namespace App\Modules\EventManagement\Application\Orchestration\Listeners; use CloudCreativity\Modules\Contracts\Application\UnitOfWork\DispatchBeforeCommit; @@ -362,7 +343,7 @@ To indicate that a listener should be deferred to after the unit of work commits interface: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents\Listeners; +namespace App\Modules\EventManagement\Application\Orchestration\Listeners; use CloudCreativity\Modules\Contracts\Application\UnitOfWork\DispatchAfterCommit; @@ -398,12 +379,15 @@ This means you should implement it on the handler itself, rather than adding it Use the `ExecuteInUnitOfWork` middleware to wrap command handlers in a unit of work: ```php -namespace App\Modules\EventManagement\Application\UseCases\Commands\CancelAttendeeTicket; +namespace App\Modules\EventManagement\Api\Input\CancelAttendeeTicket; -use App\Modules\EventManagement\Application\Ports\Driven\Persistence\AttendeeRepository;use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork;use CloudCreativity\Modules\Contracts\Bus\DispatchThroughMiddleware;use CloudCreativity\Modules\Toolkit\Results\Result; +use App\Modules\EventManagement\Application\Ports\Persistence\AttendeeRepository; +use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork; +use CloudCreativity\Modules\Toolkit\Results\Result; +use CloudCreativity\Modules\Toolkit\Pipeline\Through; -final readonly class CancelAttendeeTicketHandler implements - DispatchThroughMiddleware +#[Through(ExecuteInUnitOfWork::class)] +final readonly class CancelAttendeeTicketHandler { public function __construct( private AttendeeRepository $attendees, @@ -423,14 +407,6 @@ final readonly class CancelAttendeeTicketHandler implements return Result::ok(); } - - public function middleware(): array - { - return [ - // the last middleware to be executed before the command handler - ExecuteInUnitOfWork::class, - ]; - } } ``` @@ -447,15 +423,20 @@ However, an alternative approach is to map the inbound integration event to a do will need to wrap the dispatch of the domain event in a unit of work. This ensures side effects are properly orchestrated by the unit of work manager and are atomic. -This can be achieved via the `HandleInUnitOfWork` middleware on the inbound event handler: +This can be achieved via the `ExecuteInUnitOfWork` middleware on the inbound event handler: ```php -namespace App\Modules\EventManagement\Application\UseCases\InboundEvents; +namespace App\Modules\EventManagement\Application\UseCases\Events; -use App\Modules\EventManagement\Domain\Events\DomainEventDispatcher;use App\Modules\EventManagement\Domain\Events\SalesAtEventDidChange;use CloudCreativity\Modules\Application\InboundEventBus\Middleware\HandleInUnitOfWork;use CloudCreativity\Modules\Contracts\Bus\DispatchThroughMiddleware;use VendorName\Ordering\Shared\IntegrationEvents\V1\OrderWasFulfilled; +use App\Modules\EventManagement\Domain\Events\DomainEventDispatcher; +use App\Modules\EventManagement\Domain\Events\SalesAtEventDidChange; +use App\Modules\Ordering\Api\Output\V1\Events\OrderWasFulfilled; +use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork; +use CloudCreativity\Modules\Contracts\Bus\DispatchThroughMiddleware; +use CloudCreativity\Modules\Toolkit\Pipeline\Through; -final readonly class OrderWasFulfilledHandler implements - DispatchThroughMiddleware +#[Through(ExecuteInUnitOfWork::class)] +final readonly class OrderWasFulfilledHandler { public function __construct( private DomainEventDispatcher $domainEvents, @@ -468,14 +449,6 @@ final readonly class OrderWasFulfilledHandler implements eventId: $event->eventId, )); } - - public function middleware(): array - { - return [ - // the last middleware to be executed before the event is handled - HandleInUnitOfWork::class, - ]; - } } ``` @@ -498,6 +471,9 @@ $unitOfWork = new FakeUnitOfWork(); // execute work that uses the unit of work +$this->assertSame(2, $unitOfWork->attempts); +$this->assertSame(1, $unitOfWork->commits); +$this->assertSame(1, $unitOfWork->rollbacks); $this->assertSame( ['attempt:1', 'rollback:1', 'attempt:2', 'commit:2'], $unitOfWork->sequence, diff --git a/docs/guide/concepts/encapsulation.md b/docs/guide/concepts/encapsulation.md index fb6658a1..f76ffe97 100644 --- a/docs/guide/concepts/encapsulation.md +++ b/docs/guide/concepts/encapsulation.md @@ -68,26 +68,18 @@ The application layer of a bounded context defines its uses cases. These are the dispatched to the bounded context, and the _integration events_ that the bounded context consumes. We follow a hexagonal architecture, where the application layer defines the _driving ports_ that it exposes to the -outside world. +outside world. We place these in the module's `Api` namespace, as these define the public interface of the bounded context. This means your bounded context's interface could be expressed as follows: ```php -namespace App\Modules\EventManagement\Application\Ports\Driving; +namespace App\Modules\Ticketing\Api; -use App\Modules\EventManagement\Application\Ports\Driving\{ - CommandBus\CommandBus, - InboundEventBus\EventBus, - QueryBus\QueryBus, -}; - -interface Application +interface Ticketing { public function getCommandBus(): CommandBus; - public function getQueryBus(): QueryBus; - - public function getEventBus(): EventBus; + public function getInboundEventBus(): InboundEventBus; } ``` @@ -99,16 +91,16 @@ handle. Everything else - e.g. domain entities containing business logic, coordi etc - is hidden as an internal implementation detail of your bounded context. :::tip -In the above example interface, it is important to note that there is a specific interface for the event management's +In the above example interface, it is important to note that there is a specific interface for the ticketing module's command, query and event buses. This is intentional. Although there are _generic_ command, query and event bus -interfaces, the purpose of the event management application is to expose the _specific_ buses for the event management -bounded context. Therefore, there are _specific_ event management bus interfaces. +interfaces, the purpose of the ticketing module is to expose the _specific_ buses for the ticketing +bounded context. Therefore, there are _specific_ ticketing bus interfaces. ::: ## Coupling Although bounded contexts are encapsulated, there are times when context-to-context communication is required. For -example, our "event management" bounded context may need to amend its attendee totals when a customer completes an +example, our "ticketing" bounded context may need to amend its attendee totals when a customer completes an order. However, completing orders is a concern of the "ordering" bounded context. Bounded contexts should have clear _boundaries_ that define how they communicate with other contexts. This is @@ -117,7 +109,7 @@ achieved either by _loose_ or _direct_ coupling - with _loose_ coupling being th ### Loose Coupling Bounded contexts are loosely coupled via integration events, as described above. These events allow loose coupling -because all a bounded context needs to do is publish the event to an event bus. What happens as a result of this +because all a bounded context needs to do is publish the event to an outbound event bus. What happens as a result of this publishing is not the concern of the bounded context - it is the concern of the other bounded contexts that _consume_ that event by subscribing to it. diff --git a/docs/guide/concepts/layers.md b/docs/guide/concepts/layers.md index 724f5169..53d9cdca 100644 --- a/docs/guide/concepts/layers.md +++ b/docs/guide/concepts/layers.md @@ -64,7 +64,7 @@ This means that there are three types of _messages_ that define the use cases of 1. **Commands** - that mutate the state of the domain; 2. **Queries** - that read the state of the domain; and -3. **Integration Events** - that are emitted and consumed by other bounded contexts. +3. **Integration Events** - that are emitted and consumed by other modules, and/or the presentation and delivery layer. ## Infrastructure Layer diff --git a/docs/guide/concepts/modularisation.md b/docs/guide/concepts/modularisation.md index 7d4ee165..8ef171a8 100644 --- a/docs/guide/concepts/modularisation.md +++ b/docs/guide/concepts/modularisation.md @@ -46,29 +46,29 @@ The top-level namespace of the `Modules` namespace looks like this: ``` - Modules - + - Api + - Input + - Output - Application - Domain - Infrastructure - - Consumer - - Shared - + - Api - Application - Domain - Infrastructure - - Consumer - - Shared - ``` As a top-level summary, the namespaces in each module are: 1. **Domain** - the domain business logic, expressed in aggregates, entities, value objects etc. -2. **Application** - the use cases of the module, along with the driving and driven ports. +2. **Application** - orchestrates coordination of the domain and infrastructure layers, as well as defining how the use + cases of the module are implemented (e.g. via command and query handlers). 3. **Infrastructure** - the adapters that implement the application's driven ports. -4. **Consumer** - the contracts that define the coupling between the module and others, in particular defining - the data contracts for information exchange. -5. **Shared** - code that is shared between the module and the consumer. This should be limited to - shared data models - i.e. integration events and read models - and any value objects needed by these models. +4. **Api** - the public interface for interacting with the module, i.e. by the presentation and delivery layer. This + holds the _input_ contracts and value objects that can be used externally (including the driving ports) and the _ + output) values that are received as a result of interacting with the module. :::tip Note that there is no _presentation layer_ here. Presentation and delivery is _outside_ the `Modules` namespace. This is @@ -81,53 +81,53 @@ outside `App\Modules` is a concern of the presentation and delivery layer. ## Microservices In a microservice, we would also have a `Modules` namespace. This would contain the one or more subdomains that -the microservice represents. +the microservice represents. This would use the same structure as described above. -The structure here is as follows: +The use of the same structure means there is a clear pathway from a modular monolith to a microservice, by _lifting and +shifting_ the code for the module from the monolith into the microservice. As the module is fully encapsulated and +loosely coupled, this can be done with minimal changes to the code. Typically, the only changes would be to the +infrastructure layer, as the module might need to use different implementations of the driven ports in the microservice +than it did in the monolith. -``` -- Modules - - - - Application - - Domain - - Infrastructure - - - - Application - - Domain - - Infrastructure - - -``` - -So where have `Shared` and `Consumer` gone? - -The consumer namespace is not required by the microservice - as it cannot consume itself! Instead, this is a Composer -package that is installed wherever another module needs to consume this one. For example, this could be in a module in -another microservice, or in your modular monolith. Consumption is either loose via integration events, or direct via a -client interface that internally calls the microservice. - -This means we also put the shared namespace in a separate Composer package. This is so that the shared data models can -be required by both the microservice and the consumer package. - -For example, the microservice would publish an integration event defined in the shared package. As consumers subscribe -to the integration event, they would also depend on the shared package. +## Layers -### Transitioning to Microservices +### API Namespace -This means there is a clear pathway from a modular monolith to a microservice, by _lifting and shifting_ the code for -the module from the monolith. +The API namespace defines the public interface for interacting with the module. This is the point of interaction between +the presentation and delivery layer and the module. It contains the data contracts for interacting with the module, as +well as the driving ports that the presentation and delivery layer can call to interact with the module. -If the module has a **shared** namespace, it is moved to a Composer package. This means it can be required by both -the microservice that contains the module and the consumer code. +Our structure for this namespace is to put the driving ports in the root of the namespace, then split other objects into +either `Input` or `Output` namespaces. The `Input` namespace contains the data contracts for interacting with the +module, e.g. the command and query messages that can be dispatched to the application layer. The `Output` namespace +contains the data contracts for receiving data from the module, e.g. the read models that can be returned from queries. -The **application, domain and infrastructure namespaces** would be _lifted and shifted_ to the `Modules` namespace in -the microservice codebase. If there is a shared package, that can be installed into the microservice via Composer. +For example: -The **consumer namespace** would be moved to a Composer package. This means any other module (including those split to -other microservices) can require it as needed. This consumer package represents the allowed coupling to the -microservice, and the client defines the interface for accessing the microservice directly. The consumer package would -require the shared package. +``` +- Api + - Input + - Enums + - Values + FooCommand + BarCommand + BazQuery + BatQuery + - Output + - Enums + - Values + - Models + - CommandBus <- driving port + - QueryBus <- driving port + - InboundEventBus <- driving port +``` -## Layers +:::tip +The `Api` namespace makes it extremely easy to enforce your architectural layers in the presentation and delivery layer. +If you're using a tool like [Deptrac](https://github.com/deptrac/deptrac) you can specify that the presentation and +delivery layer can only consume classes from the API namespace of the module. This ensures that there is no incorrect +use of the application, domain and infrastrucute layers - enforcing encapsulation. +::: ### Application Namespace @@ -135,96 +135,53 @@ The application namespace can be structured as follows: ``` - Application - - Ports - - Driving - - CommandBus - - QueryBus - - InboundEventBus - - Driven - - OutboundEvents - - Queue - - Persistence - - ... - - Bus - - CommandBus - - QueryBus - - InboundEventBus - - UseCases - - Commands - - Queries - - InboundEvents - - Internal - - Commands - - DomainEvents - - Listeners + - Ports <- driven ports + - OutboundEvents + - Queue + - Persistence - ... + - Adapters <- driving port adapters + - CommandBusAdapter + - QueryBusAdapter + - InboundEventBusAdapter + - UseCases + - Internal <- internal use cases, e.g. for asynchronous processing + - ... + - FooCommandHandler <- uses cases + - BarQueryHandler + - Orchestration <- orchestration of domain and infrastructure layers ``` 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 +- **Ports** - the driven ports of the application layer expressed as interfaces. The driven ports are the interfaces that the application layer expects to be implemented by the infrastructure layer. -- **Bus** - contains the implementations of the driving ports. The concrete implementations are the command bus, +- **Adapter** - 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 - command, query or inbound integration event is dispatched. -- **Internal** - contains any internal concerns of the application layer, that are not exposed as ports. For example, - domain event listeners, internal commands for asynchronous processing, etc. +- **Use Cases** - the implementation of the business logic of the application layer, i.e. how the application layer + handles inbound commands, queries and integration events. Additionally we use an `Internal` namespace in here for + organising the use cases that are for internal use by the module only - i.e. asynchronous processing. +- **Orchestration** - any classes required to help coordinate with the domain layer. For example, this is where the + concrete implementation of the domain event dispatcher will go, along with domain event listeners. ### Domain Namespace -The domain namespace can be structured as follows: - -``` -- Domain - - Enums - - Events - - ValueObjects - . Aggregate1 - . Aggregate2 - . Entity1 - . ... -``` - -We are however less prescriptive about the structure of the domain namespace, as each domain is unique. - -For example, the above structure places aggregate roots and entities at the top level. However, you may prefer to group -them by aggregate root - particularly if your domain has a large number of aggregates and entities. That could result in -a structure like this: - - ``` - - Domain - - - - Enums - - ValueObjects - . AggrateRoot1 - . ContainedEntity1 - . ContainedEntity2 - - - - Enums - - ValueObjects - . AggrateRoot2 - . ContainedEntity1 - . ContainedEntity2 - ``` +The domain namespace can be structured as you wish. It's your domain, so only you know how best to organise it! ### Infrastructure Namespace The infrastructure namespace contains the adapters that implement the driven ports of the application layer. We would -structure this according to the structure of the ports in the application namespace, so that it's easy to conceptually -tie the two together. +structure this according to the structure of the driven ports in the application namespace, so that it's easy to +conceptually tie the two together. For example, if our application driven ports looked like this: ``` - Application - Ports - - Driven - - OutboundEventBus - - Persistence - - Queue + - OutboundEventBus + - Persistence + - Queue ``` Then our infrastructure namespace would look like this: @@ -233,61 +190,5 @@ Then our infrastructure namespace would look like this: - Infrastructure - OutboundEventBus - Persistence - - Queue + - Queue ``` - -## Packages - -### Shared - -The shared namespace is optional, and is only required if the module is consumed by other modules. Where this is the -case, the package contains data models that are shared between the module and its consumers. - -There are two types of shared data models: - -- **Integration events**: these are shared so that the module can publish them, while consumers can receive and react to - them. This is loose coupling via a data contract defined on the integration event. This contract is identical at the - point it is published by the module and when it is received by the consumer. -- **Read models**: these share the current state of a module between the module and its consumers. In the module, - queries dispatched by the query bus can return read models representing the current state. The same read model might - need to be shared with a consumer. For example, if the client interface the consumer can call returns the same read - model - e.g. an HTTP JSON response containing a serialised read model. - -Your shared package may also contain enums and value objects, where these help to define and describe the data model on -integration events and/or read models. - -One thing to note is that as these data models are shared between the module and the consumer, you cannot make -breaking changes to the data contract without updating every single consumer. In large systems, this can be challenging. -Therefore, it is sensible to version the integration events and read models - allowing you to incrementally update -consumers to the new version. - -Therefore the shared namespace could look like this: - -``` -- Shared - - Enums - - IntegrationEvents - - V1 - - V2 - - ReadModels - - V1 - - V2 - - ValueObjects -``` - -### Consumer - -The consumer namespace is optional, and is only required if the module is consumed by others. Typically -you should loosely couple modules by using integration events. However, there are scenarios where one module would -need to call another module directly. - -:::info -In a microservice architecture, this would be the point where one microservice representing a bounded context or -subdomain calls another microservice representing a separate bounded context or subdomain, e.g. via HTTP or gRPC. -::: - -The consumer namespace must not contain any business logic - therefore it must not depend on anything from the domain, -application or infrastructure layers. Instead it contains the interfaces for the direct consumption of the module by -other modules. I.e. it is the _client_ or Software Development Kit (SDK) for the module. - - diff --git a/docs/guide/domain/entities.md b/docs/guide/domain/entities.md index cdc7266b..eea54f69 100644 --- a/docs/guide/domain/entities.md +++ b/docs/guide/domain/entities.md @@ -25,15 +25,15 @@ use CloudCreativity\Modules\Contracts\Domain\Entity; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; use CloudCreativity\Modules\Domain\IsEntity; -class BookableEvent implements Entity +final class BookableEvent implements Entity { use IsEntity; public function __construct( Identifier $id, - private readonly \DateTimeImmutable $startsAt, - private readonly \DateTimeImmutable $endsAt, - private bool $isCancelled = false, + public readonly \DateTimeImmutable $startsAt, + public readonly \DateTimeImmutable $endsAt, + private(set) bool $isCancelled = false, ) { $this->id = $id; } @@ -56,15 +56,15 @@ use CloudCreativity\Modules\Contracts\Domain\Entity; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; use CloudCreativity\Modules\Domain\IsEntityWithNullableId; -class BookableEvent implements Entity +final class BookableEvent implements Entity { use IsEntityWithNullableId; public function __construct( - private readonly ?Identifier $id, - private readonly \DateTimeImmutable $startsAt, - private readonly \DateTimeImmutable $endsAt, - private bool $isCancelled = false, + ?Identifier $id, + public readonly \DateTimeImmutable $startsAt, + public readonly \DateTimeImmutable $endsAt, + private(set) bool $isCancelled = false, ) { $this->id = $id; } @@ -87,14 +87,14 @@ use CloudCreativity\Modules\Contracts\Domain\AggregateRoot; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; use CloudCreativity\Modules\Domain\IsEntity; -class Attendee implements AggregateRoot +final class Attendee implements AggregateRoot { use IsEntity; public function __construct( - private readonly Identifier $id, - private readonly Customer $customer, - private readonly ListOfTickets $tickets, + Identifier $id, + public readonly Customer $customer, + private(set) readonly ListOfTickets $tickets, ) { $this->id = $id; } @@ -113,28 +113,21 @@ list of `Ticket` entities. ## Identifiers -In both the entity and the aggregate root, the identifier is type-hinted as the `Identifier` interface. This is -intentional, as it prevents a concern of the infrastructure layer's persistence implementation from leaking into your -domain. - -For example, it can be tempting to type-hint the identifier as `int` if your persistence implementation uses an -auto-incrementing integer as the primary key. However, this is a leaky abstraction. It means that the domain layer is -now coupled to the persistence implementation as it knows how identifiers are issued and persisted. This coupling is the -wrong way around: the domain layer should not be coupled to any other layer. +In both the entity and the aggregate root examples above, the identifier is type-hinted as the `Identifier` interface. +There are two different schools of thought on how to implement entity identifiers: -To prevent this coupling, this package provides an `Identifier` interface that can be used in the domain layer. It then -provides tools for working with identifiers in other layers, where you need to work with _expected identifier types_, -e.g. an integer where we know the persistence implementation uses an auto-incrementing integer as the primary key. +1. Type-hint the `Identifier` interface. This leaves the identifier type open to the persistence implementation, and + prevents coupling between the domain layer and the persistence implementation. +2. Type-hint the expected identifier type, e.g. `Uuid` or even more specific like `UuidV7`. This is more explicit, and + can be useful where you want to constrain the identifier type. -See the [Identifiers chapter](../toolkit/identifiers) for more details. +Both approaches work. However, we'd generally recommend that if using the second approach, you use globally unique +identifiers rather than something like an auto-incrementing integer. This is because globally unique identifiers are +more flexible, and can be used across different persistence implementations without coupling the domain layer to a +specific implementation, such as a MySQL auto-incrementing key. -:::info -This is our recommended approach for handling identifiers in the domain layer. However, you may have a good reason to -take a different approach. - -For example, if your implementation used UUIDs _everywhere_, you may prefer to just type-hint the `Uuid` class instead. -This particularly makes sense with UUIDs, which are globally unique. -::: +If you want to use the generic identifier interface, see the [Identifiers chapter](../toolkit/identifiers) for more +details on how to handle identifiers. ## Invariants @@ -156,14 +149,14 @@ use CloudCreativity\Modules\Contracts\Domain\AggregateRoot; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; use CloudCreativity\Modules\Domain\IsEntity; -class Attendee implements AggregateRoot +final class Attendee implements AggregateRoot { use IsEntity; public function __construct( - private readonly Identifier $id, - private readonly Customer $customer, - private readonly ListOfTickets $tickets, + Identifier $id, + public readonly Customer $customer, + private(set) ListOfTickets $tickets, ) { Contracts::assert( $this->tickets->isNotEmpty(), @@ -207,7 +200,7 @@ public function cancelTicket( { $ticket = $this->tickets->findOrFail($ticketId); - if ($ticket->isNotCancelled()) { + if ($ticket->cancelled === false) { $ticket->markAsCancelled($reason); Services::getEvents()->dispatch(new AttendeeTicketWasCancelled( @@ -253,4 +246,4 @@ should return immutable read models that represent the data model of the current read models that should be serialized to JSON. Learn about [read models in the Query chapter.](../application/queries#read-models) -::: \ No newline at end of file +::: diff --git a/docs/guide/domain/events.md b/docs/guide/domain/events.md index 6cbc2d12..2e74996a 100644 --- a/docs/guide/domain/events.md +++ b/docs/guide/domain/events.md @@ -54,13 +54,13 @@ final readonly class AttendeeTicketWasCancelled implements public Identifier $attendeeId, public Identifier $ticketId, public CancellationReasonEnum $reason, - public \DateTimeImmutable $occurredAt = new \DateTimeImmutable(), + public \DateTimeImmutable $cancelledAt = new \DateTimeImmutable(), ) { } public function getOccurredAt(): DateTimeImmutable { - return $this->occurredAt; + return $this->cancelledAt; } } ``` @@ -181,7 +181,7 @@ class Attendee implements AggregateRoot { $ticket = $this->tickets->findOrFail($ticketId); - if ($ticket->isNotCancelled()) { + if ($ticket->cancelled === false) { $ticket->markAsCancelled($reason); Services::getEvents()->dispatch(new AttendeeTicketWasCancelled( @@ -221,10 +221,10 @@ aggregate to update its state as a side-effect. Our listener for this scenario might look like this: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents\Listeners; +namespace App\Modules\EventManagement\Application\Orchestration\Listeners; use App\Modules\EventManagement\Domain\Events\AttendeeTicketWasCancelled; -use App\Modules\EventManagement\Application\Ports\Driven\Persistence\TicketSalesReportRepository; +use App\Modules\EventManagement\Application\Ports\Persistence\TicketSalesReportRepository; final readonly class UpdateTicketSalesReport { @@ -313,7 +313,7 @@ Our listener to do this might look like this: ```php namespace App\Modules\EventManagement\Application\Internal\DomainEvents\Listeners; -use App\Modules\EventManagement\Application\Ports\Driven\OutboundEventBus\OutboundEventBus; +use App\Modules\EventManagement\Application\Ports\OutboundEventBus\OutboundEventBus; use App\Modules\EventManagement\Domain\Events\AttendeeTicketWasCancelled; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\UuidFactory; use VendorName\EventManagement\Shared\IntegrationEvents\V1 as IntegrationEvents; @@ -372,9 +372,9 @@ class AttendeeTest extends TestCase protected function setUp(): void { parent::setUp(); - + $this->dispatcher = new class () extends FakeDomainEventDispatcher implements IDomainEventDispatcher {}; - + Services::setEvents(fn () => $this->dispatcher); } diff --git a/docs/guide/domain/services.md b/docs/guide/domain/services.md index 30461902..98db82b7 100644 --- a/docs/guide/domain/services.md +++ b/docs/guide/domain/services.md @@ -25,9 +25,9 @@ Here, the domain service can be injected into the handler via constructor depend look something like this: ```php -namespace App\Modules\BankAccounts\Application\UseCases\Commands\TransferFunds; +namespace App\Modules\BankAccounts\Application\UseCases; -use App\Modules\BankAccounts\Application\Ports\Driven\Persistence\BankAccountRepository; +use App\Modules\BankAccounts\Application\Ports\Persistence\BankAccountRepository; use App\Modules\BankAccounts\Domain\Services\TransferFundsService; use CloudCreativity\Modules\Toolkit\Result\Result; @@ -41,7 +41,7 @@ final readonly class TransferFundsHandler /** * Execute the command. - * + * * @param TransferFundsCommand $command * @return Result */ @@ -86,9 +86,9 @@ transfer funds service that we used in the command handler. In this case, we can again use constructor dependency injection, with our query handler looking something like this: ```php -namespace App\Modules\BankAccounts\Application\UseCases\Queries\CanTransferFunds; +namespace App\Modules\BankAccounts\Application\UseCases; -use App\Modules\BankAccounts\Application\Ports\Driven\Persistence\BankAccountRepository; +use App\Modules\BankAccounts\Application\Ports\Persistence\BankAccountRepository; use App\Modules\BankAccounts\Domain\Services\TransferFundsService; use VendorName\BankAccounts\Shared\ReadModels\V1\CannotTransferFundsModel; use CloudCreativity\Modules\Toolkit\Result\Result; diff --git a/docs/guide/infrastructure/dependency-injection.md b/docs/guide/infrastructure/dependency-injection.md deleted file mode 100644 index e173ee16..00000000 --- a/docs/guide/infrastructure/dependency-injection.md +++ /dev/null @@ -1,168 +0,0 @@ -# Dependency Injection - -In our hexagonal architecture, the application layer defines _driven ports_ for the external dependencies it needs to -interact with. The adapters of these ports - the concrete implementations - are found in the infrastructure layer. - -Which raises the question - how does the application get access to the adapters from the infrastructure layer? - -For example, when constructing a command bus (the adapter for a _driving port_), the application layer will have to -inject the infrastructure adapters into command handlers. - -:::info -This chapter covers our approach to this dependency injection, to illustrate how you can solve this problem. However, it -is not the only approach - so feel free to take a different approach if you prefer. -::: - -## Service Locators - -Surely the solution is as simple as injecting a _service locator_, aka _service container_, into the application layer? - -You could take this approach, but we choose not to. - -While there is a whole discourse on whether or not service locators are anti-pattern, our rationale for not using a -service locator is essentially that it breaks the _encapsulation principle_. - -By following the techniques described in this package, you will have constructed fully encapsulated domain and -application layers - with really clear boundaries defined by ports. - -However, if we inject a service locator into our application layer, we arguably break that encapsulation. Why? Because -the service locator would allow our application layer to resolve _any_ service. This is particularly the case for a lot -of modern service locator implementations that use Reflection to build any requested service - e.g. the Laravel -container. - -But our application layer should not rely on _any_ service - it can only depend on the _specific_ infrastructure -services, defined by ports. Therefore, we never expose a service locator to any of our bounded contexts. - -## External Dependencies - -Instead, we define the dependencies that the application needs via an external dependencies _driven port_. - -By using a driven port, the application expects the infrastructure layer to provide an adapter for this port. So in -effect, we push the logic for creating driven port adapters into the infrastructure layer. This feels like the best -place for it as this is where the adapters live. - -This external dependencies port in effect provides other driven ports. For example: - -```php -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; -use Psr\Log\LoggerInterface; - -interface ExternalDependencies -{ - public function getLogger(): LoggerInterface; - - public function getQueue(): Queue; - - public function getAttendeeRepository(): AttendeeRepository; - - // ...other methods -} -``` - -These external dependencies can then be type-hinted wherever the application layer needs to use them. For example, when -creating a command bus: - -```php -namespace App\Modules\EventManagement\Application\Bus; - -use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies;use App\Modules\EventManagement\Application\Ports\Driving\CommandBus as CommandBusPort;use App\Modules\EventManagement\Application\UsesCases\Commands\{CancelAttendeeTicket\CancelAttendeeTicketCommand,CancelAttendeeTicket\CancelAttendeeTicketHandler,};use CloudCreativity\Modules\Bus\CommandHandlerContainer;use CloudCreativity\Modules\Bus\Middleware\LogMessageDispatch;use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; - -final class CommandBusProvider -{ - public function __construct( - private readonly ExternalDependencies $dependencies, - ) { - } - - public function getCommandBus(): CommandBusPort - { - $bus = new CommandBus( - handlers: $handlers = new CommandHandlerContainer(), - middleware: $middleware = new PipeContainer(), - ); - - /** Bind commands to handler factories */ - $handlers->bind( - CancelAttendeeTicketCommand::class, - fn() => new CancelAttendeeTicketHandler( - $this->dependencies->getAttendeeRepository(), - ), - ); - - $middleware->bind( - LogMessageDispatch::class, - fn () => new LogMessageDispatch( - $this->dependencies->getLogger(), - ), - ); - - /** Attach middleware that runs for all commands */ - $bus->through([ - LogMessageDispatch::class, - ]); - - return $bus; - } -} -``` - -### Many Dependencies - -If your application layer ends up with a lot of driven ports, then you will find that this external dependencies -interface gets very long. We handle this by grouping dependencies into several interfaces, accessed via the external -dependencies interface. - -For example, you often end up with a lot of repositories as your bounded context grows in complexity. We would put these -on a `RepositoryProvider` interface, that can be accessed via the external dependencies interface: - -```php -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; -use Psr\Log\LoggerInterface; - -interface ExternalDependencies -{ - public function getLogger(): LoggerInterface; - - public function getQueue(): Queue; - - public function getRepositories(): RepositoryProvider; -} -``` - -Then in our persistence layer ports: - -```php -namespace App\Modules\EventManagement\Application\Ports\Driven\Persistence; - -interface RepositoryProvider -{ - public function getAttendees(): AttendeeRepository; - - public function getSalesReports(): SalesReportRepository; - - // ...other repositories -} -``` - -This means will still only need to inject the external dependencies port wherever the application needs to access -dependencies. - -### Singleton Instances - -Our approach is that the external dependencies interface always returns a new instance for whatever dependency is needed -by the application layer. - -If the application layer needs a singleton instance of an external dependency, we always handle this in the application -layer. This is because the application layer _has knowledge_ that it needs a singleton instance. So it should handle the -lifetime of that instance - allowing it to set it up and tear it down as needed. - -This also helps make the external dependencies port _predictable_. If some methods returned singletons and others did -not, how does the application layer know what it has been given - a singleton, or a new instance? Also, the application -layer would then not be able to tear down any singletons when it knew that they were no longer required, e.g. after -dispatching a command. diff --git a/docs/guide/infrastructure/exception-reporting.md b/docs/guide/infrastructure/exception-reporting.md index 982ade9f..4dd41312 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; +namespace CloudCreativity\Modules\Contracts\Application\Ports; use Throwable; @@ -67,9 +67,6 @@ interface ExceptionReporter { /** * Report the exception. - * - * @param Throwable $ex - * @return void */ public function report(Throwable $ex): void; } @@ -93,7 +90,9 @@ implementation looks like this: ```php namespace App\Modules\Shared\Infrastructure\Exceptions; -use CloudCreativity\Modules\Contracts\Application\Ports\ExceptionReporter;use Illuminate\Contracts\Debug\ExceptionHandler;use Throwable; +use CloudCreativity\Modules\Contracts\Application\Ports\ExceptionReporter; +use Illuminate\Contracts\Debug\ExceptionHandler; +use Throwable; final readonly class ExceptionReporterAdapter implements ExceptionReporter diff --git a/docs/guide/infrastructure/outbox.md b/docs/guide/infrastructure/outbox.md index e33ec21f..431a7dae 100644 --- a/docs/guide/infrastructure/outbox.md +++ b/docs/guide/infrastructure/outbox.md @@ -60,7 +60,7 @@ bus. Our recommended approach is to first place these events into an outbox. This means we need a driven port for the outbox: ```php -namespace App\Modules\EventManagement\Application\Ports\Driven\OutboundEvents; +namespace App\Modules\EventManagement\Application\Ports\OutboundEvents; use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; @@ -79,12 +79,12 @@ interface Outbox Domain event listeners would then use this instead of publishing the event themselves. For example: ```php -namespace App\Modules\EventManagement\Application\Internal\DomainEvents\Listeners; +namespace App\Modules\EventManagement\Application\Orchestration\Listeners; -use App\Modules\EventManagement\Application\Ports\Driven\OutboundEvents\Outbox; +use App\Modules\EventManagement\Application\Ports\OutboundEvents\Outbox; use App\Modules\EventManagement\Domain\Events\AttendeeTicketWasCancelled; +use App\Modules\EventManagement\Api\Output\V1\Events as IntegrationEvents; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\UuidFactory; -use VendorName\EventManagement\Shared\IntegrationEvents\V1 as IntegrationEvents; final readonly class PublishAttendeeTicketWasCancelled { @@ -124,7 +124,7 @@ If you are doing this, it is a good idea to make this _explicit_ in your code. I port `Queue` - as suggested by the [Queues chapter](./queues) - call it `Outbox` for clarity: ```php -namespace App\Modules\EventManagement\Application\Ports\Driven\Queue; +namespace App\Modules\EventManagement\Application\Ports\Queue; use CloudCreativity\Modules\Contracts\Application\Ports\Queue; diff --git a/docs/guide/infrastructure/persistence.md b/docs/guide/infrastructure/persistence.md index 9f5e9ee7..8647bd39 100644 --- a/docs/guide/infrastructure/persistence.md +++ b/docs/guide/infrastructure/persistence.md @@ -75,31 +75,30 @@ The first port would need to be used by our "cancel ticket" command to retrieve state back: ```php -namespace App\Modules\EventManagement\Application\Posts\Driven\Persistence\AttendeeRepository; +namespace App\Modules\EventManagement\Application\Ports\Persistence\AttendeeRepository; use App\Modules\EventManagement\Domain\Attendee; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; interface AttendeeRepository { - public function findOrFail(Identifier $attendeeId): Attendee; - public function update(Attendee $attendee): void; + public function findOrFail(Identifier $attendeeId): AttendeeState; + public function update(AttendeeUpdateState $state): void; } ``` The second port would be used by our "get all tickets" query: ```php -namespace App\Modules\EventManagement\Application\Posts\Driven\Persistence\ReadModels\V1\TicketModelRepository; +namespace App\Modules\EventManagement\Application\Ports\Persistence\ReadModels\V1\TicketModelRepository; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; -use VendorName\EventManagement\Shared\ReadModels\V1\TicketModel; interface TicketModelRepository { /** * @param Identifier $eventId - * @return list + * @return list */ public function getByEventId(Identifier $eventId): array; } @@ -121,15 +120,15 @@ To return to our example of an attendee aggregate root that contains ticket enti store and retrieve the ticket entities. This means we only need one port: ```php -namespace App\Modules\EventManagement\Application\Posts\Driven\Persistence\AttendeeRepository; +namespace App\Modules\EventManagement\Application\Ports\Persistence\AttendeeRepository; use App\Modules\EventManagement\Domain\Attendee; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; interface AttendeeRepository { - public function findOrFail(Identifier $attendeeId): Attendee; - public function update(Attendee $attendee): void; + public function findOrFail(Identifier $attendeeId): AttendeeState; + public function update(AttendeeUpdateState $attendee): void; } ``` @@ -151,7 +150,7 @@ final class MySQLAttendeeRepository implements AttendeeRepository { public function __construct(private PDO $pdo) {} - public function findOrFail(Identifier $attendeeId): Attendee + public function findOrFail(Identifier $attendeeId): AttendeeState { // fetch the attendee from the attendees table // fetch the tickets from the tickets table @@ -159,7 +158,7 @@ final class MySQLAttendeeRepository implements AttendeeRepository // create the attendee, injecting ticket entities } - public function update(Attendee $attendee): void + public function update(AttendeeUpdateState $attendee): void { // update the attendee in the attendees table // update the tickets in the tickets table @@ -206,10 +205,9 @@ but then map these models to our attendee aggregate root and ticket entities. Here's an illustrative example: ```php -use App\Modules\EventManagement\Domain\Attendee as AttendeeAggregate; -use App\Modules\EventManagement\Domain\Enums\TicketStatusEnum; -use App\Modules\EventManagement\Domain\Ticket as TicketEntity; -use App\Modules\EventManagement\Domain\ListOfTickets; +use App\Modules\EventManagement\Application\Ports\Persistence\AttendeeState; +use App\Modules\EventManagement\Application\Ports\Persistence\AttendeeUpdateState; +use App\Modules\EventManagement\Application\Ports\Persistence\TicketState; use App\Models\Attendee as AttendeeModel; use App\Models\Attendee as TicketModel; use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier; @@ -219,32 +217,32 @@ final class EloquentAttendeeRepository implements AttendeeRepository { private array $cache = []; - public function findOrFail(Identifier $attendeeId): AttendeeAggregate + public function findOrFail(Identifier $attendeeId): AttendeeState { $attendeeId = IntegerId::from($attendeeId); $model = AttendeeModel::with('tickets')->findOrFail($attendeeId->value); $this->cache[$attendeeId->value] = $model; - + $tickets = $model->tickets->map(function (TicketModel $ticket) { - return new TicketEntity( + return new TicketState( new IntegerId($ticket->getKey()), - TicketStatusEnum::from($ticket->status), + $ticket->status, ); }); - - return new AttendeeAggregate( + + return new AttendeeState( $attendeeId, - new ListOfTickets(...$tickets), + $tickets, ); } - public function update(Attendee $attendee): void + public function update(AttendeeUpdateState $attendee): void { $attendeeId = IntegerId::from($attendee->getId()); $model = $this->cache[$attendeeId->value] ?? null; - + assert($model instanceof AttendeeModel); - + // update the attendee model // update the ticket models } @@ -266,4 +264,4 @@ Eloquent model - for you to then immediately map that to an aggregate root. As your application scales, you might find that the performance of this approach is not sufficient. At that point, you can choose to implement a repository that directly uses the database row. The good thing hexagonal architecture is that you can do this without changing your domain model, logic or application layer. -::: \ No newline at end of file +::: diff --git a/docs/guide/infrastructure/publishing-events.md b/docs/guide/infrastructure/publishing-events.md index 83d85368..611bbeb8 100644 --- a/docs/guide/infrastructure/publishing-events.md +++ b/docs/guide/infrastructure/publishing-events.md @@ -14,7 +14,7 @@ the application layer and implemented in the infrastructure layer as an adapter. The following is an example port: ```php -namespace App\Modules\EventManagement\Application\Ports\Driven\OutboundEventBus; +namespace App\Modules\EventManagement\Application\Ports\OutboundEventBus; use CloudCreativity\Modules\Contracts\Application\Ports\OutboundEventPublisher; @@ -46,7 +46,7 @@ Define an adapter by extending this class: ```php namespace App\Modules\EventManagement\Infrastructure\OutboundEventBus; -use App\Modules\EventManagement\Application\Ports\Driven\OutboundEventBus\OutboundEventBus; +use App\Modules\EventManagement\Application\Ports\OutboundEventBus\OutboundEventBus; use CloudCreativity\Modules\Infrastructure\OutboundEventBus\ClosurePublisher; final class OutboundEventBusAdapter extends ClosurePublisher @@ -61,7 +61,14 @@ specific closures to specific events, and add middleware to the publisher. Here' ```php namespace App\Modules\EventManagement\Infrastructure\OutboundEventBus; -use App\Modules\EventManagement\Application\Ports\Driven\OutboundEventBus\OutboundEventBus;use App\Modules\EventManagement\Infrastructure\GooglePubSub\EventSerializer;use App\Modules\EventManagement\Infrastructure\GooglePubSub\SecureTopicFactory;use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent;use CloudCreativity\Modules\Infrastructure\OutboundEventBus\Middleware\LogOutboundEvent;use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer;use Psr\Log\LoggerInterface;use VendorName\EventManagement\Shared\IntegrationEvents\V1\AttendeeTicketWasCancelled; +use App\Modules\EventManagement\Application\Ports\OutboundEventBus\OutboundEventBus; +use App\Modules\EventManagement\Infrastructure\GooglePubSub\EventSerializer; +use App\Modules\EventManagement\Infrastructure\GooglePubSub\SecureTopicFactory; +use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; +use CloudCreativity\Modules\Infrastructure\OutboundEventBus\Middleware\LogOutboundEvent; +use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; +use Psr\Log\LoggerInterface; +use VendorName\EventManagement\Shared\IntegrationEvents\V1\AttendeeTicketWasCancelled; final readonly class OutboundEventBusAdapterProvider { @@ -122,22 +129,34 @@ Define an adapter by extending this class: ```php namespace App\Modules\EventManagement\Infrastructure\OutboundEventBus; -use App\Modules\EventManagement\Application\Ports\Driven\OutboundEventBus\OutboundEventBus; +use App\Modules\EventManagement\Application\Ports\OutboundEventBus\OutboundEventBus; use CloudCreativity\Modules\Infrastructure\OutboundEventBus\ComponentPublisher; +use CloudCreativity\Modules\Infrastructure\OutboundEventBus\DefaultPublisher; +use CloudCreativity\Modules\Infrastructure\OutboundEventBus\Publishes; +use CloudCreativity\Modules\Toolkit\Pipeline\Through; +use CloudCreativity\Modules\Infrastructure\OutboundEventBus\Middleware\LogOutboundEvent; +#[DefaultPublisher(MyDefaultPublisher::class)] +#[Publishes(FooIntegrationEvent::class, FoobarPublisher::class)] +#[Publishes(BarIntegrationEvent::class, BazbatPublisher::class)] +#[Through(LogOutboundEvent::class)] final class OutboundEventBusAdapter extends ComponentPublisher implements OutboundEventBus { } ``` -### Event Handlers +As with our other dispatcher classes, you use PHP attributes to customise the class. In this example, we define a +default publisher via the `DefaultPublisher` attribute, publishers for specific events via the `Publishes` attribute, +and middleware that runs for all events via the `Through` attribute. + +### Event Publishers -Event handlers are classes that implement a `publish()` method. For example, we could define a default handler as +Event publishers are classes that implement a `publish()` method. For example, we could define a default publisher as follows: ```php -namespace App\Modules\EventManagement\Infrastructure\OutboundEventBus\Publishers; +namespace App\Modules\EventManagement\Infrastructure\OutboundEventBus; final class DefaultPublisher { @@ -159,9 +178,9 @@ final class DefaultPublisher And then we could also define a handler for a specific event: ```php -namespace App\Modules\EventManagement\Infrastructure\OutboundEventBus\Publishers; +namespace App\Modules\EventManagement\Infrastructure\OutboundEventBus; -use VendorName\EventManagement\Shared\IntegrationEvents\V1\AttendeeTicketWasCancelled; +use App\Modules\EventManagement\Api\Output\V1\Events\AttendeeTicketWasCancelled; final class AttendeeTicketWasCancelledPublisher { @@ -182,69 +201,13 @@ final class AttendeeTicketWasCancelledPublisher ### Creating the Adapter -We can now create our adapter. This is injected with a handler container that knows how to construct each of your -handler classes. This container allows you to define a default handler to be used when no specific handler is bound to -an event. You can then bind specific handlers to specific events, and add middleware to the publisher. - -```php -namespace App\Modules\EventManagement\Infrastructure\OutboundEventBus; - -use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies; -use App\Modules\EventManagement\Application\Ports\Driven\OutboundEventBus\OutboundEventBus; -use App\Modules\EventManagement\Infrastructure\GooglePubSub\EventSerializer; -use App\Modules\EventManagement\Infrastructure\GooglePubSub\SecureTopicFactory; -use CloudCreativity\Modules\Infrastructure\OutboundEventBus\Middleware\LogOutboundEvent; -use CloudCreativity\Modules\Infrastructure\OutboundEventBus\PublisherHandlerContainer; -use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; -use Psr\Log\LoggerInterface; -use VendorName\EventManagement\Shared\IntegrationEvents\V1\AttendeeTicketWasCancelled; +Creating the adapter is easy - the `ComponentPublisher` class that was extended above only requires a PSR container. +This allows it to resolve any publishers and middleware via the container. -final readonly class OutboundEventBusAdapterProvider -{ - public function __construct( - private SecureTopicFactory $topicFactory, - private EventSerializer $serializer, - private Logger $logger, - ) { - } +For example: - public function getEventBus(): OutboundEventBus - { - $publisher = new OutboundEventBusAdapter( - handlers: $handlers = new PublisherHandlerContainer( - default: fn () => new Publishers\DefaultPublisher( - $this->topicFactory, - $this->serializer, - ), - ), - middleware: $middleware = new PipeContainer(), - ); - - /** Bind handlers for specific events (if needed) */ - $handlers->bind( - AttendeeTicketWasCancelled::class, - fn () => new Publishers\AttendeeTicketWasCancelledPublisher( - $this->topicFactory, - $this->serializer, - ), - ); - - /** Bind middleware factories */ - $middleware->bind( - LogOutboundEvent::class, - fn () => new LogOutboundEvent( - $this->logger, - ), - ); - - /** Attach middleware that runs for all events */ - $bus->through([ - LogOutboundEvent::class, - ]); - - return $publisher; - } -} +```php +$publisher = new OutboundEventBusAdapter($psrContainer); ``` ## Writing an Event Bus @@ -253,17 +216,14 @@ If you do not want to use either of these implementations, you can write your ow implement the following interface that was extended by the driven port: ```php -namespace CloudCreativity\Modules\Application\Ports\Driven\OutboundEventBus; +namespace CloudCreativity\Modules\Contracts\Application\Ports; use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; -interface EventPublisher +interface OutboundEventPublisher { /** * Publish an outbound integration event. - * - * @param IntegrationEvent $event - * @return void */ public function publish(IntegrationEvent $event): void; } @@ -287,22 +247,11 @@ Middleware is executed in the order it is added. Use our `LogOutboundEvent` middleware to log when an integration event is published. It takes a [PSR Logger](https://php-fig.org/psr/psr-3/). -```php -use CloudCreativity\Modules\Infrastructure\OutboundEventBus\Middleware\LogOutboundEvent; - -$middleware->bind( - LogOutboundEvent::class, - fn () => new LogOutboundEvent( - $this->dependencies->getLogger(), - ), -); -``` - The use of this middleware is identical to that described in the [Commands chapter.](../application/commands#logging) See those instructions for more information, such as configuring the log levels. Additionally, if you need to customise the context that is logged for an integration event then implement the -`ContextProvider` interface on your integration event message. See the example in the +`Contextual` interface on your integration event message. See the example in the [Commands chapter.](../application/commands#logging) ### Writing Middleware @@ -313,9 +262,11 @@ following signature: ```php namespace App\Modules\EventManagement\Application\Adapters\Middleware; -use Closure;use CloudCreativity\Modules\Contracts\Infrastructure\OutboundEventBus\OutboundEventMiddleware;use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; +use Closure; +use CloudCreativity\Modules\Contracts\Bus\Middleware\IntegrationEventMiddleware; +use CloudCreativity\Modules\Contracts\Messaging\IntegrationEvent; -final class MyMiddleware implements OutboundEventMiddleware +final class MyMiddleware implements IntegrationEventMiddleware { /** * Execute the middleware. @@ -340,7 +291,8 @@ final class MyMiddleware implements OutboundEventMiddleware :::tip If you're writing middleware that is only meant to be used for a specific integration event, do not use the -`OutboundEventMiddleware` interface. Instead, use the same signature but change the event type-hint to the event class +`IntegrationEventMiddleware` interface. Instead, use the same signature but change the event type-hint to the event +class your middleware is designed to be used with. ::: @@ -352,9 +304,10 @@ We provide a fake outbound event publisher that you can use in tests. This is th You can access any published events via the `$events` property: ```php +use App\Modules\EventManagement\Application\Ports\OutboundEventBus\OutboundEventBus; use CloudCreativity\Modules\Testing\FakeOutboundEventPublisher; -$publisher = new FakeOutboundEventPublisher(); +$publisher = new class () extends FakeOutboundEventPublisher implements OutboundEventBus {}; // do work that might publish an event diff --git a/docs/guide/infrastructure/queues.md b/docs/guide/infrastructure/queues.md index d1bcaa43..d6a37449 100644 --- a/docs/guide/infrastructure/queues.md +++ b/docs/guide/infrastructure/queues.md @@ -4,7 +4,8 @@ As described in the [Asynchronous Processing chapter](../application/asynchronou can allow commands to be queued via a command queuer driving port. Additionally, it may also choose to implement some internal processes as internal commands that are executed asynchronously. -To do either (or both!), you need to define a queue driven port. The adapter implementation then handles pushing commands onto a queue, and dispatching the command when it is pulled from the queue. +To do either (or both!), you need to define a queue driven port. The adapter implementation then handles pushing +commands onto a queue, and dispatching the command when it is pulled from the queue. We provide several queue adapters that you can use. These are designed to be simple to use and allow you to plug into any PHP queue implementation that you choose to use. This chapter describes these queue implementations. @@ -17,7 +18,7 @@ queue interface, our bounded context needs to expose its specific queue interfac We do this by defining an interface in our application's driven ports: ```php -namespace App\Modules\EventManagement\Application\Ports\Driven\Queue; +namespace App\Modules\EventManagement\Application\Ports\Queue; use CloudCreativity\Modules\Contracts\Application\Ports\Queue as Port; @@ -26,9 +27,11 @@ interface Queue extends Port } ``` -If you have a command queuer driving port, you will need to inject your queue adapter into the command queuer. See the [command queuer documentation](../application/commands.md#command-queuer) for examples. +If you have a command queuer driving port, you will need to inject your queue adapter into the command queuer. See +the [command queuer documentation](../application/commands.md#command-queuer) for examples. -This allows the presentation and delivery layer to asynchronously dispatch commands. When pulling commands from the queue, your queue adapter will need to dispatch the command to the command bus. +This allows the presentation and delivery layer to asynchronously dispatch commands. When pulling commands from the +queue, your queue adapter will need to dispatch the command to the command bus. We provide two concrete classes that allow you to push work onto a queue via your preferred PHP implementation. If neither of these work for you, you can instead write a queue that implements the above interface. @@ -39,12 +42,14 @@ The [asynchronous processing chapter](../application/asynchronous-processing) in commands. These are commands that are not exposed as use cases of your bounded context. Instead they are used to split long-running or complex work up into smaller write operations (commands) that are sequenced via a workflow. -If you have an internal command bus, you can provide a separate queue port for these commands. This segregates internal commands to a separate queue, which is advantageous to separate the concerns of commands that are use cases of your bounded context (driving ports) or internal to the application layer. +If you have an internal command bus, you can provide a separate queue port for these commands. This segregates internal +commands to a separate queue, which is advantageous to separate the concerns of commands that are use cases of your +bounded context (driving ports) or internal to the application layer. In this scenario, define another driven port: ```php -namespace App\Modules\EventManagement\Application\Ports\Driven\Queue; +namespace App\Modules\EventManagement\Application\Ports\Queue; use CloudCreativity\Modules\Contracts\Application\Ports\Queue as Port; @@ -65,7 +70,7 @@ Define a queue adapter by extending this class: ```php namespace App\Modules\EventManagement\Infrastructure\Queue; -use App\Modules\EventManagement\Application\Ports\Driven\Queue\Queue; +use App\Modules\EventManagement\Application\Ports\Queue\Queue; use CloudCreativity\Modules\Infrastructure\Queue\ClosureQueue; final class QueueAdapter extends ClosureQueue @@ -79,7 +84,10 @@ Then you can create the adapter by providing it with the default closure for que ```php namespace App\Modules\EventManagement\Infrastructure\Queue; -use App\Modules\EventManagement\Application\Ports\Driven\Queue\Queue;use CloudCreativity\Modules\Contracts\Messaging\Command;use CloudCreativity\Modules\Infrastructure\Queue\Middleware\LogPushedToQueue;use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; +use App\Modules\EventManagement\Application\Ports\Queue\Queue; +use CloudCreativity\Modules\Contracts\Messaging\Command; +use CloudCreativity\Modules\Infrastructure\Queue\Middleware\LogPushedToQueue; +use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; final class QueueAdapterProvider { @@ -145,73 +153,26 @@ Define a queue adapter by extending this class: ```php namespace App\Modules\EventManagement\Infrastructure\Queue; -use App\Modules\EventManagement\Application\Ports\Driven\Queue\Queue; +use App\Modules\EventManagement\Application\Ports\Queue\Queue; use CloudCreativity\Modules\Infrastructure\Queue\ComponentQueue; - +use CloudCreativity\Modules\Infrastructure\Queue\Middleware\LogPushedToQueue; +use CloudCreativity\Modules\Toolkit\Pipeline\Through; +use CloudCreativity\Modules\Infrastructure\Queue\DefaultEnqueuer; +use CloudCreativity\Modules\Infrastructure\Queue\Queues; + +#[DefaultEnqueuer(MyDefaultEnqueuer::class)] +#[Queues(SomeCommand::class, SomeCommandEnqueuer::class)] +#[Queues([SomeOtherCommand::class, YetAnotherCommand::class], SomeOtherCommandEnqueuer::class)] +#[Through(LogPushedToQueue::class)] final class QueueAdapter extends ComponentQueue implements Queue { } ``` -Then you can create the adapter by providing it with a default enqueuer for queuing commands. For example: - -```php -namespace App\Modules\EventManagement\Infrastructure\Queue; - -use App\Modules\EventManagement\Application\Ports\Driven\Queue\Queue; -use CloudCreativity\Modules\Infrastructure\Queue\Middleware\LogPushedToQueue; -use CloudCreativity\Modules\Infrastructure\Queue\EnqueuerContainer; -use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer; - -final class QueueAdapterProvider -{ - public function __construct( - private readonly LoggerInterface $logger, - ) { - } - - public function getQueue(): Queue - { - $queue = new QueueAdapter( - enqueuers: new EnqueuerContainer( - fn () => new DefaultEnqueuer(), - ), - ); - - $middleware->bind( - LogPushedToQueue::class, - fn () => new LogPushedToQueue($this->logger), - ); - - $queue->through([LogPushedToQueue::class]); - - return $adapter; - } -} -``` - -:::tip -As shown, the queue adapter can be configured with [queue middleware.](#middleware) -::: - -The closure provided to the adapter's constructor is the default enqueuer factory that will be used for all work that is -being queued. You can bind alternative enqueuers for specific commands as follows: - -```php -$queue = new QueueAdapter( - enqueuers: $enqueuers = new EnqueuerContainer( - fn () => new DefaultEnqueuer(), - ), -); - -$enqueuers->bind( - RecalculateSalesAtEventCommand::class, - fn () => new ReportingEnqueuer(), -); -``` +Notice that the default enqueuer, specific enqueuers and middleware can be registered via attributes on the class. -The enqueuer class can be implemented as you need. All it needs is a `push()` method that queues the given command. For +Each enqueuer class can be implemented as you need. All it needs is a `push()` method that queues the given command. For example: ```php @@ -242,7 +203,7 @@ If neither of these two queue adapters work for you, you can write your own queu implements the port interface that is extended in your application layer: ```php -namespace CloudCreativity\Modules\Application\Ports\Driven\Queue; +namespace CloudCreativity\Modules\Application\Ports\Queue; use CloudCreativity\Modules\Contracts\Messaging\Command; @@ -279,7 +240,13 @@ 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\CommandBus;use CloudCreativity\Modules\Contracts\Messaging\Command;use CloudCreativity\Modules\Toolkit\Result\FailedResultException;use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue; +use App\Modules\EventManagement\Application\Ports\Driving\CommandBus; +use CloudCreativity\Modules\Contracts\Messaging\Command; +use CloudCreativity\Modules\Toolkit\Result\FailedResultException; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; class DispatchCommandJob implements ShouldQueue { @@ -454,7 +421,9 @@ following signature: ```php namespace App\Modules\Shared\Infrastructure\Queue\Middleware; -use Closure;use CloudCreativity\Modules\Contracts\Infrastructure\Queue\QueueMiddleware;use CloudCreativity\Modules\Contracts\Messaging\Command; +use Closure; +use CloudCreativity\Modules\Contracts\Infrastructure\Queue\QueueMiddleware; +use CloudCreativity\Modules\Contracts\Messaging\Command; final class MyQueueMiddleware implements QueueMiddleware { @@ -492,7 +461,7 @@ We provide a fake queue that you can use in tests. This is the `CloudCreativity\ You can access any queued commands via the `$commands` property: ```php -use App\Modules\EventManagement\Application\Ports\Driven\Queue\Queue as Port; +use App\Modules\EventManagement\Application\Ports\Queue\Queue as Port; use CloudCreativity\Modules\Testing\FakeQueue; $queue = new class () extends FakeQueue implements Port {}; diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 126e4db3..bda32bdc 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -3,7 +3,6 @@ Install the package into your application using Composer: ```bash -composer config minimum-stability dev composer require cloudcreativity/ddd-modules:^6.0 ``` diff --git a/docs/guide/upgrade.md b/docs/guide/upgrade.md index abc9506c..4a931d55 100644 --- a/docs/guide/upgrade.md +++ b/docs/guide/upgrade.md @@ -1,11 +1,61 @@ # Upgrade Guide +## 5.x to 6.x + +Upgrade using Composer: + +```bash +composer require cloudcreativity/ddd-modules:^6.0 +``` + +This refactor finalises our approach to structing modules, and makes some final alterations to the namespacing of +classes provided by this package. + +### PSR Container Usage and PHP Attributes + +The main exciting change in this release is we've massively simplified wiring up dispatchers and infrastructure +components. All of the existing classes can now be injected with a PSR container, which is then used to resolve things +like middleware and handlers. Handlers and middleware are bound to the concrete implementation via PHP attributes, e.g. +`Through` for middleware. + +This has been implemented in a backwards-compatible way, so you do not need to immediately switch to using this new +approach. However, we recommend this approach going forward, and the documentation has been updated accordingly. + +### Bus Namespace + +We've moved bus contracts and implementations to the `Contracts\Bus` and `Bus` namespaces respectively. This is to make +it clearer that these are part of the bus implementation, not the application layer. While the application layer makes +heavy use of these buses, messaging via buses is not an approach that needs to be confined to the application layer. For +instance, you could have an infrastructure component that uses message. + +Moving these classes to their own `Bus` namespace makes this clearer. + +### Driven Ports + +All driven ports are now in the `Contracts\Application\Ports` namespace, i.e. no longer have a `Driven` sub-namespace. +This is because we've moved driving ports to an `Api` namespace - as shown throughout the updated docs. The only ports +that are defined in the application namespace are these driven ports; hence tidying up that namespace. + +### Event Buses + +Separate middleware interfaces for the inbound and event buses, like `InboundEventMiddleware`, `OutboundEventMiddleware` +and `EventBusMiddleware` have been removed. These are now consolidated in an `IntegrationEventMiddleware` interface. + +Also, some middleware `HandleInUnitOfWork` and `LogInboundEvent` have been removed. Instead you should use the existing +generic message middleware alternatives, e.g. `ExecuteInUnitOfWork` and `LogMessageDispatch`. + +This removes duplication and simplifies some of the middleware to work with any type of message. + +### Other Changes + +You might find other interfaces have moved - generally a search for the same interface name will result in you finding +the new location. + ## 4.x to 5.x Upgrade using Composer: ```bash -composer config minimum-stability composer require cloudcreativity/ddd-modules:^5.0 ``` diff --git a/package-lock.json b/package-lock.json index ffdceb3a..e39eabfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "ddd-modules", + "name": "php-ddd-modules", "lockfileVersion": 3, "requires": true, "packages": { @@ -153,7 +153,6 @@ "integrity": "sha512-KL1zWTzrlN4MSiaK1ea560iCA/UewMbS4ZsLQRPoDTWyrbDKVbztkPwwv764LAqgXk0fvkNZvJ3IelcK7DqhjQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.20.0", "@algolia/requester-browser-xhr": "5.20.0", @@ -1461,7 +1460,6 @@ "integrity": "sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-abtesting": "5.20.0", "@algolia/client-analytics": "5.20.0", @@ -1654,7 +1652,6 @@ "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.2.0" } @@ -2270,7 +2267,6 @@ "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -2373,7 +2369,6 @@ "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13",