Using Laravel Observers and Events to Create History Logs - ⋮IWConnect

Using Laravel Observers and Events to Create History Logs

Introduction

Laravel is a PHP framework that comes with a really nice, elegant syntax. It is followed by a lot of interesting and useful features, one of which is the observer pattern. Laravel observer can be used to create history logs. In our article, we are going to talk exactly about that – how to use Laravel observer to create history logs. Before we dive into how we can use Laravel observers to create history logs, we will explain some of the terms that will be used further in the article.

As we generally know, an observer is someone who observes things/behavior. In Laravel, the Observer pattern is a really good way to hook into some current behavior of the system. It is a behavioral design pattern that identifies common communication patterns among objects and realizes these patterns. This type of pattern increases communication flexibility. What is even more important they create a dependent relationship between objects, thus providing information to all dependents whenever an object state changes.

As you can see from the picture above, both Subjects and Observer are objects, the difference is that the Subject is the Object being watched by other Objects, and the Objects that are watching the Subject’s state are called Observers. There are a lot of observers which observe a subject in this pattern and they are mainly focused on any changes that happen within the subject.

Observers and event in Laravel

Laravel Eloquent models have several events, allowing you to hook into the following points in a model’s lifecycle:

  • retrieved
  • creating
  • created
  • updating
  • updated
  • saving
  • saved
  • deleting
  • deleted
  • restoring
  • restored

Events exist to ease the code execution whenever a specific model class is saved or updated in a database. Each event receives the instance of the model through its constructor. Imagine that you have an application that is growing fast, and you have to listen to most of the above events in your model. Listening to a lot of events within the model can make the model very big, making it even messier. By using model observers, this event can be grouped into a single class. The method names in the observer class will reflect the even that you are listening to. (Reference: https://www.larashout.com/how-to-use-laravel-model-observers).

Creating the history log

Let’s imagine that we have a Client’s table and the Client has a lot of clients. They are created in the system in the normal way using the application GUI and one client can have a lot of information associated with him/her. Let’s suppose that among all other information, the client has also a home address and phone number, and after several months the client changes the home address and/or the phone number.

Once this happens, we edit the client’s details in our application and the new values for the home address and/or the phone number are updated in the database. This is the standard behavior of one system. But what will happen if after some time we would like to have a history of all the changes made in the client profile? How can we implement the client history log?

This is where we use Laravel observers/events. We will go step by step and explain how we can use them to create the history log. The first thing that we need to do is create and register the observer. In the Client Observer, we register the events on which the observer will listen/observe. In our case, this is the updating method. This means that the updating method will fire on each update on the Client. Considering the behavior of the Observer that we’ve written, we could use the observer to create the Client History Log. This is the ClientObserver:

<?php
namespace App\Observers;

use App\Events\CreateEntityEvent;
use App\Events\HistoryCorrectionEvent;
use App\Models\Client;
use App\Observers\Traits\ObserverTrait;

class ClientObserver
{
    use ObserverTrait;

    /**
     * updating - handle the client "updating" event
     *
     * @param \App\Models\Client $client
     */
    public function updating(Client $client)
    {
        if ($this->appropriateURL($client)) {
            // Prevent duplicates
            if (!isset(request()->all()['logInitiated'])) {
                // Set the log initiated
                request()->merge(['logInitiated' => 'initiated']);
                event(new HistoryCorrectionEvent($client));
            }
        }
    }
}

The signature of the updating method is with only one input parameter ($client which is an instance of Client). The $client holds old information about the client which is about to be updated, and with this information, we are firing the History Correction Event. The History Correction Listener is listening on the History Correction Event, and the whole logic for creating the log is inside the History Correction Listener.

The observer can be registered in the boot method of one of the service providers. In our scenario, we register the observer in the EventServiceProvider (please take a look at the below code). In the below code we register the Client Observer in the boot method (Client::observe(ClientObserver::class)).

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

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

But how will we know the information that has been changed? How will we know that the client’s phone number has been changed?

As stated previously, we already have the old information about the client, so we would only need the newly entered information about the client. The newly entered information can be easily fetched from the incoming request:

$request = request()->all();

Thus, when we have the new and the old information of the Client, we can easily compare the information, and we will know what has been changed. The changes can be inserted in a different history log table. This is how we are using the Observers to create History Log.

This is all fine, but this Observer will be fired every time when we are updating the client information. In cases when we have a big project, we can update the client from several places, but what should we do if we would like to keep track of client changes when the client has been updated from one place?

Let’s suppose that we have the following URL: https://observerusage.com/client/1/edit. This is the URL that the users can use to update the client. Some of the users visit the URL and update the client with an id equal to 1 (standard system behavior). The function in the above code snipped, appropriateURL($client) allows the history log to be created, only if the request is coming from the update URL: https://observerusage.com/client/1/edit.

If a mistake happens in the code, and the client is being updated two times instead of one, the log will be created twice. In order to prevent this, we will insert in the request the “logInitiated” parameter. The first time when the update is triggered the logInitiated will be set, and with the below code we will prevent multiple insertions of the history log.

<?php
namespace App\Observers;

use App\Events\CreateEntityEvent;
use App\Events\HistoryCorrectionEvent;
use App\Models\Client;
use App\Observers\Traits\ObserverTrait;

class ClientObserver
{
    use ObserverTrait;

    /**
     * updating - handle the client "updating" event
     *
     * @param \App\Models\Client $client
     */
    public function updating(Client $client)
    {
        if ($this->appropriateURL($client)) {
            // Prevent duplicates
            if (!isset(request()->all()['logInitiated'])) {
                // Set the log initiated
                request()->merge(['logInitiated' => 'initiated']);
                event(new HistoryCorrectionEvent($client));
            }
        }
    }
}

So, when the client is updated the HistoryCorrectionEvent is fired, and the HistoryCorrectionListener listens to this event.

In the HistoryCorrentionEvent we can initialize some of the things. In our case this is how the Event is written:

<?php
namespace App\Events;

use App\Http\Repositories\ClientContractRepository;
use Carbon\Carbon;
use Illuminate\Queue\SerializesModels;

class HistoryCorrectionEvent
{
    use SerializesModels;

    /** 
     * Create a new event instance.
     *
     * @param  \App\Order  $order
     * @return void
     */
    public function __construct($changedModel, $invoiceHistoryModel = null)
    {
        $this->setModelTableName($changedModel);
        $this->setTheEvents($changedModel, $invoiceHistoryModel);
    }

    /**
     * setTheEvents - Setting the request, model, inastance type, history model,
     *     - and correction filds
     *
     * @param Model $changedModel - instance of touched model
     */
    public function setTheEvents($changedModel, $invoiceHistoryModel)
    {
        // Check if correction is allowed on this url
        if (in_array(correctionLogUrl(), config('correctionLog.allowedUrl')[$this->modelTableName])) {
            $this->setRequestParams();
            $this->setModel($changedModel);
            $this->setInstanceType($changedModel);
            $this->setHistoryModels();
            $this->setHistoryFillables();
            $this->setCorrectionFields($changedModel);

            if ($invoiceHistoryModel != null) {
                $this->setinvoiceHistoryModel($invoiceHistoryModel);

                $this->setOriginalContract();
            }
        }
}

As you can see, we are just setting some of the things needed for the Listener like setting the model table, the requested parameters, and some other things, while the Listener is handling all the logic, creating the proper logs (saving the logs in the database or in the filesystem depending requirement) is handled in the Listener.

<?php
namespace App\Listeners\Log;

use App\Events\HistoryCorrectionEvent;

class HistoryCorrectionListener
{
   /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        // Silence is golden
    }

    /**
     * Handle the event.
     *
     * @param $event
     * @return void
     */
    public function handle(HistoryCorrectionEvent $historyCorEvent)
    {
        try {
            $url = $this->checkCurrentUrl($historyCorEvent);

            if ($url == true) {
                $this->initiateTheHistoryLog($historyCorEvent);
            }
        } catch (\Exception $e) {
            HandleExeption::handle("correctionLogError", $e, false, false);
        }
   }
}

initiateTheHistoryLog method is called, and this is the function where the log is actually saved in a database/filesystem. Usually, it would be better if this function is written outside of the Listener, in some helper or Utility class. In this way, you will keep your code clean and easy to maintain.

Conclusion

Using observers is a good way to create a history log implementation. In this way, we can hook into some of the database methods, and we can use the observers to create history logs for some of the needed methods. Additionally, by using the appropiateUrl and the logInitiated we can make additional preventions, and we can be sure that the history logs are always accurate and no duplicates are created.

For more information, or if you have any questions feel free to contact us.


Stefan Sidorovski
Stefan Sidorovski

Lead Technical Consultant