popcorn4dinner / commands
An implementation of the command pattern, designed to be used as application layer of an onion architecture based application
Requires
- php: >=7.1
- popcorn4dinner/string-format: ^0.1.0
- psr/log: ^1.0
- symfony/http-foundation: ^4.0
Requires (Dev)
- fzaninotto/faker: ^1.5.0
- phpunit/phpunit: ~7
This package is not auto-updated.
Last update: 2025-04-13 07:44:49 UTC
README
An opinionated implementation of the command pattern, designed to be used as application layer of an onion architecture based application.
Installation
composer require popcorn4dinner/commands
Usage
Each action in your application will be represented by 2 classes. The command and the handler: The command represents the input to your action. It’s a simple stateful object containing all data needed to perform the action.
class PlaceOrder implements CommandInterface { public $itemId; public $placedAt; public $price; }
The handler is responsible for the flow of performing the action. It makes sure all needed steps are executed in the right order.
class PlaceOrderHandler extends AbstractAuthenticatedCommandHandler { /** * @param CommandInterface $command * @return Order * @throws PolicyValidationException */ public function handle(CommandInterface $command, $user = null): Order { $order = $this->orderFactory->createOrderFor($command->orderItem, $command->placedAt, $user->id) if($this->orderValidator->isValid($order, OrderAction::create())){ $order = $this->orderRepository->create($order); $this->paymentGateway->schedulePaymentFor($order->price); $this->mailer->sendConfrmationEmailTo($user, $order); $this->mailer->sendIncomingOrderNotificationTo($CUSTOMER_SERVICE, $order); $this->crm->upload($order); } return $order; } }
Handler types
This library provides two types of handlers, one to be used within applications that have a user notion (AbstractAuthenticatedCommandHandler), and one for applications without (AbstractCommandHandler). Take a look at the example folder for more details.
Logging
If you instantiate your handlers with a logger implementing the Psr/log/LoggerInterface, every action performed in your application as well as possible failures will be logged automatically.
Populating Commands
This library comes with a small helper, that populates command objects for you, if you follow a simple convention.
- The properties in your command must have the same name as the parameter in your request
- The name of the request parameters are snake case, the command properties are camel case
Example:
Command property | request parameter |
---|---|
userId | user_id |
username | username |
dateOfBirth | date_of_birth |
Using this helper, can make your controllers wonderfully lean: It’s only responsibility is now to interface between your application and the web.
class UserController { [...] public function resetPassword( Request $request, RequestPasswordResetHandler $handler, \Twig_Environment $twig ) { if ($request->getMethod() === static::HTTP_GET) { return $twig->render('forgot-password.twig'); } else { $command = $this->commandPopulator->populate(new RequestPasswordReset(), $request); $user = $handler->handle($command); return $twig->render('login.twig', ['message' => 'You will receive an email with reset instructions shortly.']); } } }
A word on Onion Architecture
How I see it, Onion Architecture is essentially one thing: A simplified version of DDD that helps you to keep your easy to read, extend, maintain and reason about. This is a exelent blog post on Onion Architecture that is worth reading: http://jeffreypalermo.com/blog/the-onion-architecture-part-1/
Short summary
If you want your application to be an onion, you will essentially distinguish three layers: Infrastructure, Application and Domain. That being said, most of the literature describes a few more or calls them slightly differently. I’m trying to keep this simple, so we will go with the three above.
The golden rule: Dependencies are only allowed to go inverts. That means that the everything in the infrastructure is allowed to depend on the application or domain layer, but not the other way around. For this to work, it’s common practice to create interfaces inside your domain that determine the contract with the infrastructure you want to use. At the same time, it allows you to replace databases, filesystems and even frameworks on the fly, without the important part of your application to be effected. You can even think of scenarios, where, for testing, you want to use an in-memory database instead of running an sql server, or write emails to files instead of sending them. Following Onion Architecture, those things are no problem at all.
Imagine we were designing a micro service that allows placing orders in a online shop…
Infrastructure
The infrastructure layer of your onion contains things like the UI, Repositories to communicate with Databases, Filesystems, etc. If you are using an MVC frameworks, this is part of your infrastructure layer as well.
Application
Here is, where I use this library. The application layer is responsible for the flow of your application. It’s where you determine which steps have to be performed in which order. Take an incoming order as an example:
class PlaceOrderHandler extends AbstractAuthenticatedCommandHandler { /** * @param CommandInterface $command * @return Order * @throws PolicyValidationException */ public function handle(CommandInterface $command, $user = null): Order { $order = $this->orderFactory->createOrderFor($command->orderItem, $command->placedAt, $user->id) if($this->orderValidator->isValid($order, OrderAction::create())){ $order = $this->orderRepository->create($order); $this->paymentGateway->schedulePaymentFor($order->price); $this->mailer->sendConfrmationEmailTo($user, $order); $this->mailer->sendIncomingOrderNotificationTo(static::CUSTOMER_SERVICE, $order); $this->crm->upload($order); } return $order; } }
Domain
Your main domain model in this example would probably be an order. Part of your domain would also be all the knowledge about what an order has to look like and how to communicate with external services like payment gateways and APIs of your logistics partner In our example, you would probably find classes like there:
- Order
- OrderAction
- OrderValidator
- OrderRepositoryInterface
- MailerInterface
- PaymentGatewayInterface
- CrmInterface