A lot of shiny things around the Laravel world that I would like to talk about, so I chose the new CommandBus that Laravel 5 brings by default. To start I’d like to say that it’s pretty damn cool.
Command-Oriented Architecture
I’m not gonna explain in detail this approach, but I’m going to give a brief introduction on the topic and link a few more pieces of content at the bottom.
Basically, we should describe our application in commands to make our intentions explicit. Let’s say you have a subscription system on your app, you’d have a “SubscribeUserCommand” class, or something similar, that maps to its own handler, in this case, “SubscribeUserCommandHandler”. By doing so you actually decouple your application from the transport layer (HTTP, CLI, queue job, event handler, etc..). It means that you can dispatch this command from a Controller or a console command (CLI) with no trouble.
Laravel way
We used to implement this approach using some packages (see laracasts/commander), which are actually pretty neat and work like a charm. However, Laravel 5 brings its own CommandBus with a plus: it can handle commands (AND EVENTS!!!) in the background (queues).
A typical command looks like this:
app/Commands/SubscribeUserCommand.php
<?php
amespace App\Commands;
use App\Subscriptions\MembershipType;
class SubscribeUserCommand extends Command
{
public $userId;
public $membershipType;
public function __construct($userId, MembershipType $membershipType)
{
$this->userId = $userId;
$this->membershipType = $membershipType;
}
}
Then you should have a handler like this:
app/Handlers/Commands/SubscribeUserCommandHandler.php
<?php
namespace App\Handlers\Commands;
use App\Commands\SubscribeUserCommand;
use Illuminate\Contracts\Events\Dispatcher;
use App\Payment\PaymentInterface;
class SubscribeUserCommandHandler
{
private $userRepository;
private $events;
private $payment;
public function __construct(
UserRepository $userRepository,
Dispatcher $events,
PaymentInterface $payment
) {
$this->userRepository = $userRepository;
$this->events = $events;
$this->payment = $payment;
}
public function handle(SubscribeUserCommand $command)
{
$user = $this->userRepository->find($command->userId);
$user->subscribe($command->membershipType, $this->payment);
$this->dispatchEvents($user->releaseEvents());
}
/**
* @param array $events
* @return void
*/
private function dispatchEvents(array $events)
{
foreach ($events as $event)
$this->events->fire($event);
}
}
}
Which you can dispatch, let’s say, from your controller like so:
app/Http/Controllers/SubscriptionsController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Contracts\Auth\Guard;
use App\Commands\SubscribeUserCommand;
class SubscriptionControllers extends Controller
{
private $auth;
public function __construct(Guard $auth)
{
$this->middleware('auth');
$this->auth = $auth;
}
public function subscribe(SubscribeUserRequest $request)
{
$command = new SubscribeUserCommand(
$this->auth->user()->id,
MembershipType::build($request->get("membership_type"))
);
$this->dispatch($command);
return redirect()->route("home");
}
}
The dispatch method is inherited from the Controller class (which uses the IlluminateFoundationBusDispatchesCommands
) and it maps commands to handlers. Cool stuff. This example works synchronously. If you need to handle the command in background (queue jobs) you just have to implement the IlluminateContractsQueueShouldBeQueued
interface on your command like so:
app/Commands/SubscribeUserCommand.php
<?php
namespace App\Commands;
use App\Subscriptions\MembershipType;
use Illuminate\Contracts\Queue\ShouldBeQueued;
class SubscribeUserCommand extends Command implements ShouldBeQueued
{
public $userId;
public $membershipType;
public function __construct(
$userId,
MembershipType $membershipType
) {
$this->userId = $userId;
$this->membershipType = $membershipType;
}
}
That is it! Well, actually you have to setup the queue config on config/queue.php
, but I’m making a point here.
Handling Events in background
As I said, it is also possible to handle events in the background, let’s see an example. Let’s assume your User you have a subscribe named constructor on your model that builds the user instance and saves it (Eloquent/ActiveRecord). Your model should look like:
app/User.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\Events\UserSubscribedEvent;
use App\Subscriptions\MembershipType;
use App\Payment\PaymentInterface;
class User extends Model
{
// ...
public function subscribe(
MembershipType $membershipType,
PaymentInterface $payment
) {
$payment->purchaseSubscription($this, $membershipType);
$this->subscription()->create($membershipType->toArray());
$this->raise(new UserSubscribedEvent(
$this->id,
$membershipType
));
return $this;
}
// ...
}
Your event class is just a DTO and looks like this:
app/Events/UserSubscribedEvent.php
<?php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
use App\Subscriptions\MembershipType;
class UserSubscribedEvent extends Event
{
use SerializesModels;
public $userId;
public $membershipType;
public function __construct(
$userId,
MembershipType $membershipType
) {
$this->userId = $userId;
$this->membershipType = $membershipType;
}
}
Then you have a handler like so:
app/Handlers/Events/UserSubscribedEventHandler;
<?php
namespace App\Handlers\Events;
use App\Events\UserSubscribedEvent;
use App\Mailers\UserMailer;
class UserSubscribedEventHandler
{
public function __construct(
UserMailer $mailer,
UserRepository $userRepository
) {
$this->mailer = $mailer;
$this->userRepository = $userRepository;
}
public function handle(UserSubscribedEvent $event)
{
$user = $this->userRepository->find($event->userId);
$this->mailer->sendTo($user, $this->buildMessage(
$event->membershipType
));
}
// ... the buildMessage should be private or protected
}
To register your handler just go to app/Providers/EventServiceProvider.php
and add your listener to the $listen
property, like so:
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event handler mappings for the application.
*
* @var array
*/
protected $listen = [
AppEventsUserSubscribedEvent::class => [
AppHandlersEventsUserSubscribedEventHandler::class
]
];
}
This action is executed synchronously, it means that your user is waiting for the event handler to act before being redirected to the application.
To handle the event in background you just have to implement the same IlluminateContractsQueueShouldBeQueued
interface on your event handler class, like so:
app/Handlers/Events/UserSubscribedEventHandler;
<?php
namespace App\Handlers\Events;
use App\Events\UserSubscribedEvent;
use App\Mailers\UserMailer;
use Illuminate\Contracts\Queue\ShouldBeQueued;
class UserSubscribedEventHandler implements ShouldBeQueued
{
public function __construct(
UserMailer $mailer,
UserRepository $userRepository
) {
$this->mailer = $mailer;
$this->userRepository = $userRepository;
}
public function handle(UserSubscribedEvent $event)
{
$user = $this->userRepository->find($event->userId);
$this->mailer->sendTo($user, $this->buildMessage(
$event->membershipType
));
}
// ... the buildMessage should be private or protected
}
Oh, by the way, you just give the event class to the event dispatcher, like so (using the Facade):
<?php
// somewhere in your application
use App\Events\UserSubscribedEvent;
Event::fire(new UserSubscribedEvent($userId, $membershipType));
Conclusion
That is it. To sum up, I like to think that Commands can change state, while Events just react to these state changes and if an Event handler has to change anything it MUST do it through Commands.
This command bus looks pretty cool. Fun fact: you can have multiple event listeners/handlers where some of them executes synchronously and others execute asynchronously. I loved it, to make it work before we had to have an event listener that add a job to the queue and then handle the event on the job handler. Now it’s pretty damn simple.
Useful resources
- DevDiscussions – The Command Bus
- Laracast about the Laravel 5 Command bus
- Laracast series about Commands and Domain Events
- Task-based UI
- Command Bus by Shawn McCool
- CRUD is an antipattern
Member discussion