Event Driven Development for Laravel 5

Hi, I'm Steven Maguire

  • I've been building software since 2004.
  • Contribute to open source.
  • Author for Pluralsight.com
    (Landing soon!).
  • Tweet from @stevenmaguire.

Don't panic; no new acronyms!

Event Driven Development

Considering the events before building

Events = Domain Events

"Captures the memory of something interesting which affects the domain" - Martin Fowler

http://martinfowler.com/eaaDev/DomainEvent.html

Such as

  • User registered
  • Plan changed
  • Payment processed
  • Tweet posted
  • Account flagged
  • User notified
  • Refund processed
  • Account deleted

Sad story!

Events tell the story

Event Anatomy

Subjects

User

Plan

Payment

Account

Tweet

Refund

Types

registered

changed

processed

posted

flagged

notified

deleted

Events

  • User registered
  • Plan changed
  • Payment processed
  • Tweet posted
  • Account flagged
  • User notified
  • Refund processed
  • Account deleted

Event Anatomy

Subjects

User

Plan

Payment

Account

Tweet

Refund

Singular, not plural

 

Typically models or other mutable objects

Event Anatomy

Types

registered

selected

processed

activated

posted

flagged

notified

deleted

Past tense, not present or future

 

When an event is observed, its already happened

Why?

Why?

  • Loosely coupled
  • Single responsibility
  • Flexible to requirements changes
  • Syncronous or asyncronous
    (we need more async!)
  • Expects the unexpected
  • Complements service-oriented architecture
    (SOA)

Why not?

  • Challenging
    • Education
    • Comfort/Scary
    • Legacy code
  • Not applicable

Classic example

E-commerce order flow

E-commerce order flow

It all starts innocently enough; "It's just a simple order process"

\\ App\Services\OrderManager@processOrder

$order = $this->createOrder($attributes);

$user = $this->getCurrentUser();

$user->addOrder($order);

$payment = $this->processPayment($order);

$user->sendReceipt($order);

E-commerce order flow

What if payment fails?

\\ App\Services\OrderManager@processOrder

$order = $this->createOrder($attributes);

$user = $this->getCurrentUser();

$user->addOrder($order);

$payment = $this->processPayment($order);

if ($payment) {

    $user->sendReceipt($order);    

} else {

    throw new FailedPaymentException();

}

E-commerce order flow

Marketing says new users get "welcome email"!

\\ App\Services\OrderManager@processOrder

$order = $this->createOrder($attributes);

$user = $this->getCurrentUser();

$user->addOrder($order);

$payment = $this->processPayment($order);

if ($payment) {

    $user->sendReceipt($order);   
    
    if ($user->isNew()) {
    
        $user->sendWelcome();
    
    }  

} else {

    throw new FailedPaymentException();

}

E-commerce order flow

Marketing says all customers get only one email!

\\ App\Services\OrderManager@processOrder

$order = $this->createOrder($attributes);

$user = $this->getCurrentUser();

$user->addOrder($order);

$payment = $this->processPayment($order);

if ($payment) {

    if ($user->isNew()) {
        
        $user->sendWelcomeReceipt($order);

    } else {
        
        $user->sendReceipt($order); 

    }  

} else {

    throw new FailedPaymentException();

}

Do you see where this is going?

One method to rule them all!

You saw it coming.

Because complexity, testing, and maintenance.

en.wikipedia.org/wiki/Cyclomatic_complexity

Change

Risk

Anti

Patterns

Bug Zone!

How?

Laravel 5!

Built-in support

  • Events
  • Channels
    (Syncronous & Queues)
  • Handlers
  • Listeners
    (Subscribers)

laravel.com/docs/5.0/events

The Plan

  • Establish scenario
  • Inventory events & actions
  • Create event objects
  • Create handler objects
  • Bind event subscribers to IoC container
  • Define event trigger placement
  • Demo event system!

Establish scenario

Let's clone a website!

 

I don't work for

If This Then That

I don't know exactly how it works

I am guessing for entertainment

This isn't actually anything like ifttt

Don't judge me; I'm doing this for you

Premise

Our application receives events from external applications and triggers other external actions, automagically.

Inventory
events & actions

Events

  • User registered
  • Plan changed
  • Payment processed
  • Tweet posted

Actions

  • Send welcome email
  • Assign default plan
  • Process payment
  • Send receipt email
  • Retweet @mentions

Create event objects

php artisan make:event UserWasRegistered

php artisan make:event PlanWasChanged

php artisan make:event PaymentWasProcessed

php artisan make:event TweetWasPosted

Create event objects

// App\Events\UserWasRegistered;
public function __construct(User $user)
{
    $this->user = $user;
}
// App\Events\PlanWasChanged;
public function __construct(Plan $plan)
{
    $this->plan = $plan;
}
// App\Events\PaymentWasProcessed;
public function __construct(Payment $payment)
{
    $this->payment = $payment;
}
// App\Events\TweetWasPosted;
public function __construct(Tweet $tweet)
{
    $this->tweet = $tweet;
}

Inject event subject(s)

Create event objects

<?php namespace App\Events;

use Carbon\Carbon;

abstract class Event {

    public $created_at;

    public function __construct()
    {
        $this->created_at = Carbon::now();
    }
}

Capturing time information in base class

// App\Events\UserWasRegistered;
public function __construct(User $user)
{
    parent::__construct();
    $this->user = $user;
}

Create handler objects

php artisan handler:event SendWelcomeEmail --event=UserWasRegistered

php artisan handler:event AssignDefaultPlan --event=UserWasRegistered

php artisan handler:event ProcessPayment --event=PlanWasChanged

php artisan handler:event SendReceiptEmail --event=PaymentWasProcessed

php artisan handler:event RetweetMention --event=TweetWasPosted

Create handler objects

// App\Handlers\Events\SendWelcomeEmail
public function __construct(
    Illuminate\Contracts\Mail\Mail $mail)
{
    $this->mail = $mail
}

public function handle(UserWasRegistered $event)
{
    $user = $event->user;

    $this->mail->send('email.view', 
        ['name' => $user->name], 
        function($message) use ($user)
        {
            $message->to($user->email, $user->name)
                ->subject('Welcome!');
        }
    );
}

Inject required services

Use service to handle event

Bind event subscribers to IoC container

// App\Providers\EventServiceProvider

protected $listen = [
    'App\Events\UserWasRegistered' => [
        'App\Handlers\Events\SendWelcome',
        'App\Handlers\Events\AssignDefaultPlan',
    ],
    'App\Events\PlanWasChanged' => [
        'App\Handlers\Events\ProcessPayment',
    ],
    'App\Events\PaymentWasProcessed' => [
        'App\Handlers\Events\SendReceiptEmail',
    ],
    'App\Events\TweetWasPosted' => [
        'App\Handlers\Events\RetweetMention',
    ],
];

Start with Provider!

php artisan event:generate

Define event trigger placement

  • Models
  • Command Handlers
  • Services
  • Controllers

Define event trigger in Model

// App\User

use App\Events\UserWasCreated;
use Illuminate\Support\Facades\Event;

class User extends Eloquent
{
    //
    
    public static function createUser($attributes = []) 
    {
    
        $user = static::create($attributes);

        if ($user) {
            $result = Event::fire(new UserWasCreated($user));
        }

        return $user;
    }

    //
}

Define event trigger in Command Handler

// App\Handlers\Commands\CreateUser

use App\User;
use App\Events\UserWasCreated;
use Illuminate\Support\Facades\Event;
// using statements for base Command and SelfHandling contract

class CreateUser extends Command implements SelfHandling {
{

    protected $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }
           
    public function handle()
    {
        // Handle the logic to create the $user...

        $result = Event::fire(new UserWasCreated($user));
    }
}

Define event trigger in Service

// App\Services\Registrar

use App\User;
use App\Events\UserWasCreated;
use Illuminate\Support\Facades\Event;
use Illuminate\Contracts\Auth\Registrar as RegistrarContract;

class Registrar implements RegistrarContract 
{
    public function create(array $data)
    {
        $user = User::create([
	    'name' => $data['name'],
	    'email' => $data['email'],
	    'password' => bcrypt($data['password']),
	]);

        if ($user) {
            Event::fire(new UserWasCreated($user));
        }

        return $user;
    }
}

Define event trigger in Controller

// App\Http\Controllers\UserController

use App\User;
use App\Events\UserWasCreated;
use Illuminate\Support\Facades\Event;

class UserController extends Controller
{
    public function postRegisterUser(Request $request)
    {
        $user = new User($request->input());
        
        if ($user) {
            $result = Event::fire(new UserWasRegistered($user));
        }
        
        return $user;
    }
}

Demo

github.com/stevenmaguire/ifttt-demo

(bit.ly/laravel-events-demo)

But wait, there's more

Event Subscribers

Sprinkle in for style

// App\Handlers\Events
class UserEventHandler {
    public function onUserRegistration($event)
    {
        //
    }
    
    public function subscribe($events)
    {
        $events->listen('App\Events\UserWasRegistered', 
            'App\Handlers\Events\UserEventHandler@onUserRegistration'
        );
    }
}
Event::subscribe('App\Handlers\Events\UserEventHandler');

Include in boot() method of Service Provider

Event Closure Listeners

If that tickles your fancy

Event::listen('App\Events\UserWasRegistered', function($event)
{
    // Handle the event...
});

Include in boot() method in Service Provider

Event::listen('App\Events\UserWasRegistered', function($event)
{
    // Handle the event...

    return false;
});

Stopping the propagation of an Event

Queued Event Handlers

Queues FTW

php artisan handler:event SendWelcomeEmail \
    --event=UserWasRegistered --queued

--queued flag generates handler with queue support

// App\Handlers\Events\SendWelcomeEmail
public function handle(UserWasRegistered $event)
{
    if (true)
    {
        $this->release(30);
    }
}

laravel.com/docs/5.0/queues

Global method or Facade

use Illuminate\Support\Facades\Event;

//

Event::fire(new UserWasCreated($user));
event(new UserWasCreated($user));

==

Questions?

Thank you!

@stevenmaguire
on twitter

stevenmaguire@gmail.com
on electronic mail

stevenmaguire
on github

Event Driven Development for Laravel 5

By Steven Maguire

Event Driven Development for Laravel 5

Deck originally created for a presentation to a gathering of the Chicago Laravel Meetup group - bit.ly/laravel-events

  • 5,863