command-oriented architecture
TRANSCRIPT
“CRUD doesn't express behaviour. Avoid setters, and use expressive, encapsulated operations instead.”
<?php
$order = new Order();
$order->setStatus('paid');
$order->setPaidAmount(120);
$order->setPaidCurrency('EUR');
$order->setCustomer($customer);
<?php
$order = new Order();
$money = new Money(120, new Currency('EUR'));
$order->pay($customer, $money);
class CommentsController extends Controller {
public function store($postId)
{
$post = Post::find($postId);
$comment = new Comment([
'message' => 'A new comment.',
'user_id' => Auth::user()->id
]);
$post->comments()->save($comment);
return redirect()
->route('posts.view, $post)
->withMessage('Your comment was successfully created');
}
}
class CommentsController extends Controller {
public function store($postId)
{
$post = Post::find($postId);
$comment = new Comment([
'message' => 'A new comment.',
'user_id' => Auth::user()->id
]);
$post->comments()->save($comment);
return redirect()
->route('posts.view, $post)
->withMessage('Your comment was successfully created');
}
}
class CommentsController extends Controller {
public function store($postId)
{
$post = Post::find($postId);
$comment = new Comment(['message' => 'A new comment.']);
$user = Auth::user();
$post->comment($user, $comment);
return redirect()
->route('posts.index', $post)
->withMessage('Your comment was successfully created');
}
}
class CommentsController extends Controller {
public function store($postId)
{
$post = Post::find($postId);
$comment = new Comment(['message' => 'A new comment.']);
$user = Auth::user();
$post->comment($user, $comment);
return redirect()
->route('posts.index', $post)
->withMessage('Your comment was successfully created');
}
}
class Post extends Model
{
// ...
public function comment(User $user, Comment $comment)
{
$comment->user_id = $user->id;
$this->comments()->save($comment);
}
// ...
}
class SendSMS {
public function fire($job, $data)
{
$twilio = new Twilio_SMS($apiKey);
$twilio->sendTextMessage(array(
'to' => $data['user']['phone_number'],
'message' => $data['message'],
));
$user = User::find($data['user']['id']);
$user->messages()->create([
'to' => $data['user']['phone_number'],
'message' => $data['message'],
]);
$job->delete();
}
}
class SendSMS {
public function fire($job, $data)
{
$twilio = new Twilio_SMS($apiKey);
$twilio->sendTextMessage(array(
'to' => $data['user']['phone_number'],
'message' => $data['message'],
));
$user = User::find($data['user']['id']);
$user->messages()->create([
'to' => $data['user']['phone_number'],
'message' => $data['message'],
]);
$job->delete();
}
}
class SendSMS {
public function fire($job, $data)
{
$twilio = new Twilio_SMS($apiKey);
$twilio->sendTextMessage(array(
'to' => $data['user']['phone_number'],
'message' => $data['message'],
));
$user = User::find($data['user']['id']);
$user->messages()->create([
'to' => $data['user']['phone_number'],
'message' => $data['message'],
]);
$job->delete();
}
}
class SendSMS {
function __construct(UserRepository $users, SmsCourierInterface $courier)
{
$this->users = $users;
$this->courier = $courier;
}
public function fire($job, $data)
{
$user = $this->users->find($data['user']['id']);
$user->sendSmsMessage($this->courier, $data['message']);
$job->delete();
}
}
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function sendSmsMessage(SmsCourierInterface $courier, $message)
{
$courier->sendMessage($this->phone_number, $message);
return $this->messages()->create([
'to' => $this->phone_number,
'message' => $message,
]);
}
}
class SmsTest extends PHPUnit_Framework_TestCase {
public function test_user_can_send_sms_message() {
$user = Mockery::mock('User[messages]');
$relation = Mockery::mock('StdClass');
$courier = Mockery::mock('SmsCourierInterface');
$user->shouldReceive('messages')->once()->andReturn($relation);
$relation->shouldReceive('create')->once()->with(array(
'to' => '555-555-5555',
'message' => 'Test',
));
$courier->shouldReceive('sendMessage')->once()->with(
'555-555-5555', 'Test'
);
$user->phone_number = '555-555-5555';
$user->sendSmsMessage($courier, 'Test');
}
}
class SmsTest extends PHPUnit_Framework_TestCase {
public function test_user_can_send_sms_message() {
$user = Mockery::mock('User[messages]');
$relation = Mockery::mock('StdClass');
$courier = Mockery::mock('SmsCourierInterface');
$user->shouldReceive('messages')->once()->andReturn($relation);
$relation->shouldReceive('create')->once()->with(array(
'to' => '555-555-5555',
'message' => 'Test',
));
$courier->shouldReceive('sendMessage')->once()->with(
'555-555-5555', 'Test'
);
$user->phone_number = '555-555-5555';
$user->sendSmsMessage($courier, 'Test');
}
}
class SmsTest extends PHPUnit_Framework_TestCase {
public function test_user_can_send_sms_message() {
$user = Mockery::mock('User[messages]');
$relation = Mockery::mock('StdClass');
$courier = Mockery::mock('SmsCourierInterface');
$user->shouldReceive('messages')->once()->andReturn($relation);
$relation->shouldReceive('create')->once()->with(array(
'to' => '555-555-5555',
'message' => 'Test',
));
$courier->shouldReceive('sendMessage')->once()->with(
'555-555-5555', 'Test'
);
$user->phone_number = '555-555-5555';
$user->sendSmsMessage($courier, 'Test');
}
}
class SmsTest extends PHPUnit_Framework_TestCase {
public function test_user_can_send_sms_message() {
$user = Mockery::mock('User[messages]');
$relation = Mockery::mock('StdClass');
$courier = Mockery::mock('SmsCourierInterface');
$user->shouldReceive('messages')->once()->andReturn($relation);
$relation->shouldReceive('create')->once()->with(array(
'to' => '555-555-5555',
'message' => 'Test',
));
$courier->shouldReceive('sendMessage')->once()->with(
'555-555-5555', 'Test'
);
$user->phone_number = '555-555-5555';
$user->sendSmsMessage($courier, 'Test');
}
}
class CommentsController extends Controller
{
public function store($postId)
{
$user = Auth::user();
$post = Post::find($postId);
$comment = new Comment(['message' => 'A new comment.']);
$post->comment($user, $comment);
return redirect()
->route('posts.index', $post)
->withMessage('Your comment was successfully created');
}
}
class CommentsController extends Controller
{
public function store($postId)
{
$user = Auth::user();
$post = Post::find($postId);
$comment = new Comment(['message' => 'A new comment.']);
$post->comment($user, $comment);
return redirect()
->route('posts.index', $post)
->withMessage('Your comment was successfully created');
}
}
class CommentsController extends Controller
{
public function store($postId)
{
$user = Auth::user();
$post = Post::find($postId);
$comment = new Comment(['message' => 'A new comment.']);
$post->comment($user, $comment);
return redirect()
->route('posts.index', $post)
->withMessage('Your comment was successfully created');
}
}
class CommentsController extends Controller
{
public function store($postId)
{
$user = Auth::user();
$message = Input::get('message');
$command = new LeaveCommentCommand($user, $postId, $message);
return redirect()
->route('posts.index', $post)
->withMessage('Your comment was successfully created');
}
}
class LeaveCommentCommand
{
public $user;
public $postId;
public $message;
public function __construct(User $user, $postId, $message)
{
$this->user = $user;
$this->postId = $postId;
$this->message = $message;
}
}
class CommentsController extends Controller
{
public function store($postId)
{
$user = Auth::user();
$message = Input::get('message');
$command = new LeaveCommentCommand($user, $postId, $message);
return redirect()
->route('posts.index', $post)
->withMessage('Your comment was successfully created');
}
}
use Illuminate\Foundation\Bus\DispatchesCommands;
class CommentsController extends Controller {
use DispatchesCommands;
public function store($postId) {
$user = Auth::user();
$message = Input::get('message');
$command = new LeaveCommentCommand($user, $postId, $message);
$this->dispatch($command);
return redirect()
->route('posts.index', $post)
->withMessage('Your comment was successfully created');
}
}
class LeaveCommentCommandHandler
{
public function handle(LeaveCommentCommand $command)
{
$post = Post::find($command->postId);
$comment = new Comment(['message' => $command->message]);
$post->comment($command->user, $comment);
}
}
class LeaveCommentCommandHandler {
private $mailer;
function __construct(UserMailer $mailer) {
$this->mailer = $mailer;
}
public function handle(LeaveCommentCommand $command) {
$post = Post::find($command->postId);
$comment = new Comment(['message' => $command->message]);
$post->comment($command->user, $comment);
$this->notifyPostCreator($post->creator, $post, $comment);
}
// ...
}
class LeaveCommentCommandHandler
{
// ...
private function notifyPostCreator(
User $creator, Post $post, Comment $comment)
{
$this->mailer->sendTo(
$creator->email,
sprintf("New comment on [%s]", $post->title),
sprintf("User @%s left a comment for you: \n%s",
$comment->user->username,
$comment->message)
);
}
}
use Illuminate\Contracts\Events\Dispatcher;
class LeaveCommentCommandHandler {
private $events;
function __construct(Dispatcher $events) {
$this->events = $events;
}
public function handle(LeaveCommentCommand $command) {
$post = Post::find($command->postId);
$comment = new Comment(['message' => $command->message]);
$post->comment($command->user, $comment);
$this->dispatchEvents($post->releaseEvents());
}
// ...
}
use Illuminate\Contracts\Events\Dispatcher;
class LeaveCommentCommandHandler {
// ...
private function dispatchEvents(array $events)
{
foreach ($events as $event)
$this->events->fire($event);
}
}
class Post extends Model
{
use EventGenerator;
public function comment(User $user, Comment $comment)
{
$comment->user_id = $user->id;
$this->comments()->save($comment);
$this->raise(new CommentWasLeft($post, $comment, $user));
}
}
trait EventGenerator
{
protected $domainEvents = [];
public function raise($event)
{
$this->domainEvents[] = $event;
}
public function releaseEvents()
{
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}
class CommentWasLeft
{
public $post;
public $user;
public $comment;
public function __construct(Post $post, User $user, Comment $comment)
{
$this->post = $post;
$this->user = $user;
$this->comment = $comment;
}
}
class NotifyPostOwnerAboutNewCommentHandler {
private $mailer;
function __construct(UserMailer $mailer) {
$this->mailer = $mailer;
}
public function handle(CommentWasLeft $event) {
$this->mailer->sendTo(
$event->post->creator->email,
sprintf("New comment on [%s]", $event->post->title),
sprintf("User @%s left a comment for you: \n%s",
$event->user->username, $event->comment->message)
);
}
}
class EventServiceProvider extends ServiceProvider
{
/**
* The event handler mappings for the application.
* @param array
*/
protected $listen = [
CommentWasLeft::class => [
NotifyPostOwnerAboutNewCommentHandler::class
]
];
}
Recap:
➔ Boundaries interacts through commands;
➔ Command is executed by its handler;
➔ Command handlers fires/triggers domain
events;
➔ Events are listened by event handlers/listeners.
$ tree app
app
├── Commands
├── Console
├── Events
├── Exceptions
├── Handlers
├── Http
├── Providers
├── Services
└── User.php
$ tree app/Commands
app/Commands
├── Command.php
└── LeaveCommentCommand.php
$ tree app/Handlers
app/Handlers
├── Commands
│ └── LeaveCommentCommandHandler.php
└── Events
└── NotifyPostOwnerAboutNewCommentHandler.php
$ tree app/Commands
app/Commands
├── CreateUserCommand.php
└── DeleteUserCommand.php
└── UpdateUserCommand.php
class DeactivateInventoryItemCommand
{
public $userId;
public $itemId;
public $comment;
public function __construct($userId, $itemId, $comment)
{
$this->userId = $userId;
$this->itemId = $itemId;
$this->comment = $comment;
}
}
use Illuminate\Contracts\Queue\ShouldBeQueued;
class DeactivateInventoryItemCommand implements ShouldBeQueued {
public $userId;
public $itemId;
public $comment;
public function __construct($userId, $itemId, $comment) {
$this->userId = $userId;
$this->itemId = $itemId;
$this->comment = $comment;
}
}
use Illuminate\Contracts\Queue\ShouldBeQueued;
class NotifyPostOwnerAboutNewCommentHandler implements ShouldBeQueued {
private $mailer;
function __construct(UserMailer $mailer) {
$this->mailer = $mailer;
}
public function handle(CommentWasLeft $event) {
$this->mailer->sendTo(
$event->post->creator->email,
sprintf("New comment on [%s]", $event->post->title),
sprintf("User @%s left a comment for you: \n%s",
$event->user->username, $event->comment->message)
);
}
}
Resources
➔ Command Bus by Shawn Mccool
➔ Dev Discussions - The Command Bus
➔ Screaming Archirecture by Uncle Bob
➔ The Clean Archirecture by Uncle Bob
➔ Laravel: From Apprentice to Artisan
Resources
➔ Commands and Domain Events
(Laracasts)
➔ Task-based UIs
➔ CRUD is an antipattern by Mathias
Verraes