Observer Pattern in Laravel With Observers and Events

Updated
featured-image.png

💡 I included the link to the example repository at the conclusion of this tutorial.

The observer pattern is a cool design pattern. Imagine you have a big box of LEGOs, and you like building things with LEGOs. One day, you decide to build a cool spaceship. You also have some friends who enjoy watching you build.

Now, every time you add a new item to your spaceship, your friends want to know about it. Instead of notifying each friend individually every time you add a work, you create a plan: you’ll shout “New item added!” every time you do it.

Your friends are waiting and listening. When they hear you shout, they turn around and see what new part you added. This way, they stay updated without you having to notify them individually.

In computer terms, you (the builder) are called the “subject” and your friends are called the “observer”. The Observer Pattern is a way for your friends (observers) to keep track of what you are building (subjects) just by listening to your screams (notifications). This makes it easier for everyone to stay informed about changes. This pattern is particularly useful in applications where multiple components need to stay updated about state changes.

Observer Pattern in Laravel

Laravel makes it easy for us to implement the observer pattern. They offer us two features, namely Laravel Observers and Laravel Events.

Laravel Observers

So what is the difference between the two? Laravel Observers are basically model related event handlers, they only occur in Eloquent Models (creating a record, updating a record, deleting, etc).

Laravel Events

Laravel Events are general, do not have to be tied to a particular model (although they can be), apply to the entire application. You can fire events using Laravel Events when a user logs in, clicks a button, etc. This is different from Laravel Observers which can only be used in Eloquent Models.

Examples

Let’s create an example implementation for each of the two.

Laravel Observers Example

Let’s create an example implementation of Laravel Observers. Let’s say we want to create an online marketplace. We will handle the purchase of goods, every time a user buys something, the stock of that item will decrease. This is a great use case for Laravel Observer. Sounds like a lot of fun? Let’s begin!

Create the Migrations

Before continuing, you may want to set up your Laravel application with a .env configuration. Prepare the database you will use in this tutorial before continuing.

First, let’s prepare the tables needed for our demo. We will create users, products and purchases tables.

Now, make sure we have a users table first. Actually I would just use the users table migration that comes from the Laravel installation as is. But I’ll still include the migration code here.

// database\migrations\xxxxx_create_users_table.php
public function up(): void
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->unique();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password');
        $table->rememberToken();
        $table->timestamps();
    });

    Schema::create('password_reset_tokens', function (Blueprint $table) {
        $table->string('email')->primary();
        $table->string('token');
        $table->timestamp('created_at')->nullable();
    });

    Schema::create('sessions', function (Blueprint $table) {
        $table->string('id')->primary();
        $table->foreignId('user_id')->nullable()->index();
        $table->string('ip_address', 45)->nullable();
        $table->text('user_agent')->nullable();
        $table->longText('payload');
        $table->integer('last_activity')->index();
    });
}

Also create a migration for the products table. Here I will only create name, price, and stock. Later we will reduce this stock every time a user buys it.

php artisan make:model Product -m
// database\migrations\xxxxx_create_products_table.php
public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->decimal('price', 8, 2);
        $table->integer('stock');
        $table->timestamps();
    });
}

Read also:

We will also create a purchases table where we will create an observer for the table. Every time we write to the purchases table, there should be a decrease in the product purchased.

php artisan make:model Purchase -m
// database\migrations\xxxxx_create_purchases_table.php
public function up(): void
{
    Schema::create('purchases', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('user_id');
        $table->unsignedBigInteger('product_id');
        $table->integer('quantity');
        $table->decimal('total_price', 8, 2);
        $table->timestamps();
    });
}

Create the Seeders

We need to create seeders for the user and product tables so that we can have dummy data to write to the purchases table.

Create the seeder with the following command:

php artisan make:seeder UserSeeder

And write the following code to the seeder:

<?php
// database\seeders\UserSeeder.php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        User::create([
            'name' => 'Jepri',
            'email' => 'jepri@test.com',
            'password' => 'password',
        ]);
    }
}

Before running this seeder, make sure you have written the properties written here inside the $fillable property in the User model like so:

// app\Models\User.php
protected $fillable = [
    'name',
    'email',
    'password',
];

Now let’s create the seeder for the products table.

Create it with the following command:

php artisan make:seeder UserSeeder

And write the following code to the seeder:

<?php
// database\seeders\ProductSeeder.php

namespace Database\Seeders;

use App\Models\Product;
use Illuminate\Database\Seeder;

class ProductSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Product::create([
            'name' => 'The Amazing Book',
            'price' => 25,
            'stock' => 100,
        ]);
    }
}

You also need to write the properties written here inside the $fillable property in the Product model like so:

// app\Models\Product.php
protected $fillable = [
    'name',
    'email',
    'password',
];

Finally, call the seeder inside DatabaseSeeder so we can run just one command to execute the seeder.

// database\seeders\DatabaseSeeder.php
public function run(): void
{
    $this->call([
        UserSeeder::class,
        ProductSeeder::class,
    ]);
}

Read also:

Once the migration and seeder are ready, run the following command to run the migration and seeder at once:

php artisan migrate --seed

Create the Purchase Function

To illustrate purchasing an item, create a PurchaseController and store function within it.

php artisan make:controller PurchaseController

Write the store function inside the controller.

// app\Http\Controllers\PurchaseController.php

use App\Models\Product;
use App\Models\Purchase;

public function store(Request $request)
{
    // Suppose we have a user with id 1 who wants to buy
    // 2 product quantities with id 1
    $userId = 1;
    $quantity = 2;
    $productId = 1;
    $product = Product::find($productId);

    // write the transaction to the purchases table
    $purchase = Purchase::create([
        'user_id' => $userId,
        'product_id' => $productId,
        'quantity' => $quantity,
        'total_price' => $product->price * $quantity,
    ]);

    return response()->json([
        'purchase' => $purchase->load('product'),
    ], 201);
}

Create the Observer

Now here’s the interesting part. We will create an observer who listen to each purchase record creation. So run the following command to create an observer in Laravel:

php artisan make:observer PurchaseObserver

You will notice that there is a new folder inside the app folder namely Observers. Write the following code to the PurchaseObserver:

<?php
// app\Observers\PurchaseObserver.php

namespace App\Observers;

use App\Models\Purchase;

class PurchaseObserver
{
    /**
     * Handle the Purchase "created" event.
     */
    public function created(Purchase $purchase): void
    {
        // Notify the user about the purchase here

        // Notify the seller about the purchase here

        // You may also want to use Laravel Database Transactions to handle these
        // https://fajarwz.com/blog/laravel-database-transaction-for-data-consistency/

        // Update the product stock
        $product = $purchase->product;
        $product->stock -= $purchase->quantity;
        $product->save();
    }

    /**
     * Handle the Purchase "updated" event.
     */
    public function updated(Purchase $purchase): void
    {
        //
    }

    /**
     * Handle the Purchase "deleted" event.
     */
    public function deleted(Purchase $purchase): void
    {
        //
    }

    /**
     * Handle the Purchase "restored" event.
     */
    public function restored(Purchase $purchase): void
    {
        //
    }

    /**
     * Handle the Purchase "force deleted" event.
     */
    public function forceDeleted(Purchase $purchase): void
    {
        //
    }
}

Very easy! Now register the observer by placing it using the ObservedBy attribute on the appropriate model:

// app\Models\Purchase.php

use App\Observers\PurchaseObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[ObservedBy([PurchaseObserver::class])]
class Purchase extends Model
{
    // ...
}

Create the Route

Don’t forget to create the purchase route. Here I will just use the GET method for the route so that I can just call it in the browser.

// routes\web.php

use App\Http\Controllers\PurchaseController;

Route::get('purchases', [PurchaseController::class, 'store']);

Test the Action

Finally, everything is ready! We can run the Laravel application and try visiting /purchases. The first time we visited the route, the product stock should have decreased to 98, because we tried to buy 2 quantities of the product.

purchases.png

Laravel Events Example

Now we will try how to use Laravel Events and Listeners. Let’s say we create some kind of blog site that contains content and creators. We want to encourage authors/creators to create engaging content, and to do this, we will notify content creators whenever visitors to their content/page reach a certain threshold. This should be a lot of fun! Keep following me and I’ll walk you through this example.

Create the Show Page Function

I’m still using the same project. Now I will create a new controlller to handle the page visits called PageController.

php artisan make:controller PageController

Inside this controller, let’s create a function called show to serve the content/page and check whether the page has reached the visitor threshold.

<?php
// app\Http\Controllers\PageController.php

namespace App\Http\Controllers;

use App\Events\VisitorsThresholdReached;
use App\Models\User;
use Illuminate\Http\Request;

class PageController extends Controller
{
    public function show(Request $request)
    {
        // Suppose a page reaches 1000 visitors
        // this data can be collected from the page table.
        // You may also want to run this check elsewhere,
        // probably in some kind of middleware
        $visitorCount = 1000;
        $creator = User::first();

        if ($visitorCount >= 1000) {
            VisitorsThresholdReached::dispatch($visitorCount, $creator);
        }

        return 'my page';
    }
}

Read also:

Create the “Visitor Threshold Reached” Event

Next we will create the event. Let’s create it with the following command:

php artisan make:event VisitorThresholdReached

Notice that Laravel creates a new folder called Events inside the app folder. This is where Laravel will read events by default. You can check whether your event is registered or not using the following command:

php artisan event:list

Write the following code inside the event so that it can serve the variables collected from the controller to the listeners.

<?php
// app\Events\VisitorsThresholdReached.php

namespace App\Events;

use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class VisitorsThresholdReached
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $visitorCount;
    public User $creator;

    /**
     * Create a new event instance.
     */
    public function __construct($visitorCount, User $creator)
    {
        $this->visitorCount = $visitorCount;
        $this->creator = $creator;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            //
        ];
    }
}

Create the Listeners

We will notify the creator via Email and SMS, each of which is processed by a different listener. So we will create two listeners that listen from the same event.

We will create the listener for the email first.

php artisan make:listener VisitorThresholdEmailListener  --event VisitorsThresholdReached

It will create a listener that automatically imports the proper event class and type-hint the event in the handle method. You can omit the --event though.

Write the following code inside the listener:

<?php
// app\Listeners\VisitorThresholdEmailListener.php

namespace App\Listeners;

use App\Events\VisitorsThresholdReached;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class VisitorThresholdEmailListener
{
    /**
     * Create the event listener.
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     */
    public function handle(VisitorsThresholdReached $event): void
    {
        $visitorCount = $event->visitorCount;
        $creator = $event->creator;

        $message = "Hi $creator->name, your content has reached $visitorCount visitors! Keep up the good work.";

        // send the email notification
        // https://fajarwz.com/blog/easily-send-emails-in-laravel-with-brevo/
        \Mail::raw($message, function($msg) use ($creator) {
            $msg->to($creator->email)->subject('Trending page!');
        });
    }
}

There is no need to register the listener to the event, it will be automatically triggered by the event type-hinted in the handle method.

I’m using Laravel 11 so this implementation will work if I use the default mailer, which is log. Here are my .env values ​​regarding email configuration:

MAIL_MAILER=log
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

You can also try it using Mailtrap. All you need to do to integrate it is just update some .env values, for example something like:

MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=yourusername
MAIL_PASSWORD=yourpassword

That’s all for the listener for the email notification.

Now let’s create the listener for the SMS notification. You can just copy-paste from the listener for the email notification or you can run the following command:

php artisan make:listener VisitorThresholdSmsListener  --event VisitorsThresholdReached

Write the following code to illustrate an SMS notification:

<?php
// app\Listeners\VisitorThresholdSmsListener.php

namespace App\Listeners;

use App\Events\VisitorsThresholdReached;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class VisitorThresholdSmsListener
{
    /**
     * Create the event listener.
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     */
    public function handle(VisitorsThresholdReached $event): void
    {
        $visitorCount = $event->visitorCount;
        $creator = $event->creator;

        $message = "Hi $creator->name, your content has reached $visitorCount visitors! Keep up the good work.";

        // send the sms notification
        // https://fajarwz.com/blog/sending-sms-in-laravel-using-twilio-a-step-by-step-tutorial/
        info($message);
    }
}

That’s all for our listeners.

Add the Show Page Route

Don’t forget to add the route for the show page action:

// routes\web.php
use App\Http\Controllers\PageController;

Route::get('trending-page', [PageController::class, 'show']);

Read also:

Try the Events Demo

When you visit /trending-page you will only see a page with the text “my page”, but in your laravel log located at storage\logs\laravel.log, you will see a log illustrating email and SMS notifications.

events-log.png

Queued Event Listeners

Sometimes listeners perform slow tasks such as sending emails or making HTTP requests. This is where the queued listeners can be beneficial. You can queue listeners with just simple code. To specify that the listener should be queued, implement ShouldQueue in the listener class.

use Illuminate\Contracts\Queue\ShouldQueue;

// add this "implements"
class VisitorThresholdEmailListener implements ShouldQueue
{
    // 
}

I will use the default queue implementation in Laravel 11 which is database. You should also make sure to set up the queue driver in the .env.

QUEUE_CONNECTION=database

After that, let’s run the queue worker with the following command:

php artisan queue:work

Now let’s try visiting /trending-page again. When you previously tried using Mailtrap you will notice that now the page loads faster than before, that is because now the email sending uses a queue rather than being sent immediately before loading the page.

email-mailtrap.png

Conclusion

We have explored the fun and powerful implementation of the Observer Pattern in Laravel using Observers and Events. We’ve also learned how to integrate this pattern with email notifications and queuing. This knowledge equips you to create more robust and real-world features that benefit from this implementation. By using Observers and Events, you can decouple your application components, making your code more modular, maintainable, and scalable. Enjoy building your applications with this new, fun approach!

💻 The repository for this example can be found at fajarwz/blog-laravel-observers-events.

Fajarwz's photo Fajar Windhu Zulfikar

I'm a full-stack web developer who loves to share my software engineering journey and build software solutions to help businesses succeed.

Email me
Ads
  • Full-Stack Laravel: Forum Web App (Complete Guide 2024)
  • Join Coderfren Discord Server: Share knowledge, build projects & level up your skills.

Share

Support

Trakteer

Subscribe

Sign up for my email newsletter and never miss a beat in the world of web development. Stay up-to-date on the latest trends, techniques, and tools. Don't miss out on valuable insights. Subscribe now!

Comments

comments powered by Disqus