Slide 1

Slide 1 text

Command-Oriented Architecture Maceió DEV Meetup #6

Slide 2

Slide 2 text

who am I? ➔ Tony Messias ~ @tony0x01 ➔ Building web stuff since ~2010

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

before we start... ➔ CRUD thinking ➔ MVC ➔ Commands/Events ➔ Clean Architecture

Slide 5

Slide 5 text

“CRUD is an antipattern” (Mathias Verraes)

Slide 6

Slide 6 text

“CRUD doesn't express behaviour. Avoid setters, and use expressive, encapsulated operations instead.”

Slide 7

Slide 7 text

setStatus('paid'); $order->setPaidAmount(120); $order->setPaidCurrency('EUR'); $order->setCustomer($customer);

Slide 8

Slide 8 text

pay($customer, $money);

Slide 9

Slide 9 text

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'); } }

Slide 10

Slide 10 text

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'); } }

Slide 11

Slide 11 text

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'); } }

Slide 12

Slide 12 text

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'); } }

Slide 13

Slide 13 text

class Post extends Model { // ... public function comment(User $user, Comment $comment) { $comment->user_id = $user->id; $this->comments()->save($comment); } // ... }

Slide 14

Slide 14 text

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(); } }

Slide 15

Slide 15 text

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(); } }

Slide 16

Slide 16 text

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(); } }

Slide 17

Slide 17 text

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(); } }

Slide 18

Slide 18 text

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, ]); } }

Slide 19

Slide 19 text

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'); } }

Slide 20

Slide 20 text

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'); } }

Slide 21

Slide 21 text

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'); } }

Slide 22

Slide 22 text

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'); } }

Slide 23

Slide 23 text

be careful with MVC

Slide 24

Slide 24 text

your framework is not your architecture

Slide 25

Slide 25 text

$ tree rails/app rails/app ├── assets ├── controllers ├── helpers ├── mailers ├── models └── views

Slide 26

Slide 26 text

“this is a rails app”

Slide 27

Slide 27 text

Screaming Architecture

Slide 28

Slide 28 text

ok, but what does it have to do with Commands?

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

they are basically DTOs, with cool names

Slide 31

Slide 31 text

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'); } }

Slide 32

Slide 32 text

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'); } }

Slide 33

Slide 33 text

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'); } }

Slide 34

Slide 34 text

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'); } }

Slide 35

Slide 35 text

class LeaveCommentCommand { public $user; public $postId; public $message; public function __construct(User $user, $postId, $message) { $this->user = $user; $this->postId = $postId; $this->message = $message; } }

Slide 36

Slide 36 text

how do I execute them?

Slide 37

Slide 37 text

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'); } }

Slide 38

Slide 38 text

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'); } }

Slide 39

Slide 39 text

what does dispatch do?

Slide 40

Slide 40 text

finds a handler for our command

Slide 41

Slide 41 text

one Command can be executed by one and only one Handler

Slide 42

Slide 42 text

LeaveCommentCommand LeaveCommentCommandHandler

Slide 43

Slide 43 text

class LeaveCommentCommandHandler { public function handle(LeaveCommentCommand $command) { $post = Post::find($command->postId); $comment = new Comment(['message' => $command->message]); $post->comment($command->user, $comment); } }

Slide 44

Slide 44 text

what if I want to notify the post creator about that new comment?

Slide 45

Slide 45 text

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); } // ... }

Slide 46

Slide 46 text

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) ); } }

Slide 47

Slide 47 text

works, but we can do better...

Slide 48

Slide 48 text

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()); } // ... }

Slide 49

Slide 49 text

use Illuminate\Contracts\Events\Dispatcher; class LeaveCommentCommandHandler { // ... private function dispatchEvents(array $events) { foreach ($events as $event) $this->events->fire($event); } }

Slide 50

Slide 50 text

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)); } }

Slide 51

Slide 51 text

trait EventGenerator { protected $domainEvents = []; public function raise($event) { $this->domainEvents[] = $event; } public function releaseEvents() { $events = $this->domainEvents; $this->domainEvents = []; return $events; } }

Slide 52

Slide 52 text

events are also just DTOs

Slide 53

Slide 53 text

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; } }

Slide 54

Slide 54 text

but they can (and most of the time they do) have lots of listeners/handlers

Slide 55

Slide 55 text

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) ); } }

Slide 56

Slide 56 text

class EventServiceProvider extends ServiceProvider { /** * The event handler mappings for the application. * @param array */ protected $listen = [ CommentWasLeft::class => [ NotifyPostOwnerAboutNewCommentHandler::class ] ]; }

Slide 57

Slide 57 text

Recap: ➔ Boundaries interacts through commands; ➔ Command is executed by its handler; ➔ Command handlers fires/triggers domain events; ➔ Events are listened by event handlers/listeners.

Slide 58

Slide 58 text

$ tree app app ├── Commands ├── Console ├── Events ├── Exceptions ├── Handlers ├── Http ├── Providers ├── Services └── User.php

Slide 59

Slide 59 text

$ tree app/Commands app/Commands ├── Command.php └── LeaveCommentCommand.php $ tree app/Handlers app/Handlers ├── Commands │ └── LeaveCommentCommandHandler.php └── Events └── NotifyPostOwnerAboutNewCommentHandler.php

Slide 60

Slide 60 text

avoid CRUD thinking

Slide 61

Slide 61 text

$ tree app/Commands app/Commands ├── CreateUserCommand.php └── DeleteUserCommand.php └── UpdateUserCommand.php

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

class DeactivateInventoryItemCommand { public $userId; public $itemId; public $comment; public function __construct($userId, $itemId, $comment) { $this->userId = $userId; $this->itemId = $itemId; $this->comment = $comment; } }

Slide 65

Slide 65 text

you can easily use queues to speed up your requests.

Slide 66

Slide 66 text

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; } }

Slide 67

Slide 67 text

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) ); } }

Slide 68

Slide 68 text

questions?

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Resources ➔ Commands and Domain Events (Laracasts) ➔ Task-based UIs ➔ CRUD is an antipattern by Mathias Verraes