Events

https://laravel.com/docs/5.6/events

One of the most powerful way of coding: Event-Driven Software

Natively Laravel is using events just like the auth middleware does, see https://laravel.com/docs/5.6/authentication#events

Table des matières

1. Introduction to events

By using events, the programmer will extend without any limitation the possibilities of its application.

By creating a new user, writing a record in the database, publishing a file, ..., he can fire an event “New user created”, “Sales record stored in DB”, “File xxx published” and let any other piece of code, unknown at design time to answer to that event and “do things”.

The core of the program doesn’t need to know how many pieces of code will handle the event and doesn’t need to know what will be done.

But, the core can also interact with the listeners: fire the event and wait that listeners have done their job then receive f.i. a result like a GO/NOGO (can the user be created?).

Using events will also make the core smaller and more manageable. This will reduce the risk of regression since only listeners needs to be updated (in case of just adding a feature (i.e. a listener)).

2. Listening events

https://laravel.com/api/5.6/Illuminate/Auth/Events/Login.html

2.1. Listeners

Laravel use a Provider for knowing who is listening events.

Check the file /app/Providers/EventServiceProvider.php:

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        'App\Events\Event' => ['App\Listeners\EventListener'],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();
    }
}

For adding a listener, we just need to add our listener in $listen so Laravel can manage who is listening which event.

We’ll need to extend the $listen array and provide to information’s:

  1. The namespace of the listened event. If we want to listen to the Login event, the name is Illuminate\Auth\Events\Login (defined in /vendor/laravel/framework/src/Illuminate/Auth/Events/Login.php)
  2. The namespace of the listener. Since the default location is folder /app/Listeners the listener can be /app/Listeners/MyListeningClass.

For instance:

protected $listen = [
    'App\Events\Event' => ['App\Listeners\EventListener'],
    'Illuminate\Auth\Events\Login' => ['App\Listeners\LoginSuccess']
];

2.2. A few examples

2.2.1. Queries

Each time a query is fired, an event is triggered with the SQL statement of the query just ran against the database.

We can catch this for, f.i., displaying the list of queries during our optimization process:

2.2.1.1. Spy the list of fired queries

A very fast way is to add this function in our /app/routes.php file. Just add this bloc:

if (env('APP_DEBUG', false)) {
    DB::listen(function ($query) {
        echo '<pre style="background-color:yellow;' .
        'font-size:x-small;">' .
        'Query fired ' .
        '"' . $query->sql . '" ' .
        '<small>(' . __FILE__ . ' - ' . __LINE__ . ')</small>' .
        '</pre>';
    });
}

This will output the list of queries when APP_DEBUG is set.

Listen queries

2.2.2. Listening Login event

2.2.2.1. Why ?

Once someone is making a login, we can:

2.2.2.2. Attach a listener

Edit /app/Providers/EventServiceProvider.php and update the $listen property:

protected $listen = [
    'App\Events\Event' => ['App\Listeners\EventListener'],
    'Illuminate\Auth\Events\Login' => ['App\Listeners\LoginSuccess']
];

We inform Laravel that, when the event Illuminate\Auth\Events\Login is fired, he should call App\Listeners\LoginSuccess i.e. file /app/Listeners/LoginSuccess.php.

The example here above adds a listener for the login event and the code is located in folder /app/Listeners/LoginSuccess.php.

2.2.2.3. Create the listener

To create such file, there is an Artisan command:

php artisan make:listener LoginSuccess

Tip: we can immediately specify the name of the event to observe

php artisan make:listener LoginSuccess --event=Illuminate\Auth\Events\Login

The /app/Listeners/LoginSuccess.php file will be generated and will contains:

<?php

namespace App\Listeners;

class LoginSuccess
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
    }

    /**
     * Handle the event.
     *
     * @param  object $event
     * @return void
     */
    public function handle($event)
    {
    }
}

To do something when an event is raised, we just need to add our code to the handle() function. Here, since our event is attached to the Login feature, we can better typecast the parameter; not just $event but it’s a login:

use Illuminate\Auth\Events\Login;

public function handle(Login $login)
{
    Session::flash('message', 'Hi ' . $login->user->name . ', nice to see you again');
}

The idea is just to retrieve the username and store in the message Session variable a Hi Christophe, nice to see you again sentence (or anything else).

2.2.2.4. Display our message

When the login has been made, Laravel will, by default, display the home view i.e. /resources/views/home.blade.php so, if we want to display the message, we’ll add this directive in the file:

@if(Session::has('message'))
  <p class="alert alert-success">{{ Session::get('message') }}</p>
@endif
2.2.2.5. Test Login events

Just go to your http://127.0.0.1:8000/login page.

login

Fill in your credentials and you should see

message

2.2.2.6. Code improvement

The code that is proposed here above has a problem: if we’ve a second, a third, ... listener where a message is stored in the message Session, the last fired listener will crush what the others have done.

The first listener will have:

Session::flash('message', 'Hi ' . $login->user->name . ', nice to see you again');

And a second:

Session::flash('message', 'Don\'t forget to buy some milk ?');

So the message will be Don’t forget to buy some milk ? and no more Hi Christophe.

Writing event-driven-software implies that you never know how many listeners you’ve.

Refactored, we’ll have:

$arr = Session::get('message');
$arr[] = [
    'type' => 'success',
    'message' => 'Hi ' . $login->user->name . ', nice to see you again'
];
Session::flash('message', $arr);
$arr = Session::get('message');
$arr[] = [
    'type' => 'warning',
    'message' => 'Don\'t forget to buy some milk ?'
];
Session::flash('message', $arr);

And our display:

@section('content')

<?php

  if (Session::has('message')) {
    $alert = '<div class="alert alert-%s alert-dismissible ' .
      'fade show" role="alert">%s' .
      '<button type="button" class="close" data-dismiss="alert" ' .
      'aria-label="Close"><span aria-hidden="true">&times;</span> ' .
      '</button></div>';

    foreach (Session::get('message') as $msg) {
        echo sprintf($alert, $msg['type'], $msg['message']);
    }
  }

?>

[...]

@endsection

The output will then display as many information we’ve in our Session:

3. Create own events

3.1. Four steps

We’ll need to make four steps:

  1. Create an event
  2. Create a listener
  3. Reference the listener
  4. Raise the event

For instance, we’ll create a event TodoCreated when a new Todo (like Bring some chocolates for your nice colleagues) has been submitted. We’ll call this event in the controller, after the store method so our listener will receive the inserted record and then be able to work with it.

3.2. Create a new event

With artisan:

php artisan make:event TodoCreated

(where TodoCreated is the name of the event)

This will process a new file /app/Events/TodoCreate.php. Artisan will use a template file with a lot of lines of code but we can make a few cleanings. The minimal code will be:

<?php

namespace App\Events;

use App\Todo;

class TodoCreated
{
    public $todo;

    public function __construct(Todo $todo)
    {
        $this->todo = $todo;
    }
}

In the constructor of the event, we specify our model.

So, our event is called App\Events\TodoCreated i.e. the namespace App\Events followed by the class name TodoCreated.

Code that will listen our event will receive a Todo record (our Model referenced by our use App\Todo; sentence).

3.3. Create a listener

With the help of artisan:

php artisan make:listener Created --event=App\Todo\TodoCreated

We’ve called the listener Created just because we can’t use the same name of the event.

Since we know the name of the event to listen, we can immediatley specify it on the prompt.

This will process a new file /app/Listeners/Created.php, we can replace the automatic content with:

<?php

namespace App\Listeners;

use App\Events\TodoCreated;

class Created
{
    public function handle(TodoCreated $event)
    {
        echo '<h1>A todo has been created right now!</h1>' .
            '<pre>' . print_r($event, true) . '</pre>';
        die();
    }
}

So, when the App\Events\TodoCreated will be raised, we’ll handle() the event and, here, we’ll just echo a sentence and dump the content of the record and stop any further process.

In real world, we’ll probably make things like sending a notification to someone (the guy who should take action f.i.) or post the todo on a social network or ...

3.4. Reference the listener

Add the listener in $listen in /app/Providers/EventServiceProvider.php

protected $listen = [
    ...
    'App\Events\TodoCreated' => [
        'App\Listeners\Created',
    ],
];

Our event is called App\Events\TodoCreated and our listener App\Listeners\Created so just add that sentence in the $listen list.

3.5. Raise the event

The event has been defined (step 1), the listener has been added (step 2) and is listening (step 3), so, last thing is trigger the event (step 4).

Edit the controller where a new Todo is created. This is probably done in file /app/Http/Controllers/TodoController.php and add two lines in the file: first add a reference to the event and call it.

use App\Events\TodoCreated;

public function store(TodoRequest $request)
{
    $todo = $this->todoRepository->store($request->all());

    event(new TodoCreated($todo));

    return redirect()->route('todos.show', ['id' => $todo->id])->withOk('Todo has been successfully created');
}

3.6. Test TodoCreated

Go to http://127.0.0.1:8000/todos/create (i.e. the submission form) and type a new Todo:

Create a new todo

Our listener (/app/Listeners/Created.php) will then dump the record and stop:

A todo has been created

We can see that we’ve well received a model and, also, the full inserted record. This because the event was triggered after the store() in the controller.

The controller was:

use App\Events\TodoCreated;

public function store(TodoRequest $request)
{
    $todo = $this->todoRepository->store($request->all());

    event(new TodoCreated($todo));

    return redirect()->route('todos.show', ['id' => $todo->id])->withOk('Todo has been successfully created');
}

A real world example can be:

$bContinue = event(new onBeforeStoreTodo($todo));

if ($bContinue)
{
    $todo = $this->todoRepository->store($request->all());
    event(new onAfterSaveTodo($todo));
}

Start a onBefore event, listeners will make checks (can the user be able to ..., does the third party is ok, ...) and if yes, store the record and call a onAfterSave so new listeners can send notification, log things, ...