Events i Observers

Com utilitzar events del cicle de vida del model i observers a Eloquent per executar lògica automàtica.

Events del model#

Eloquent dispara events en moments clau del cicle de vida d'un model. Cada operació important (crear, actualitzar, eliminar) té un event "before" i un event "after": el "before" es dispara abans de l'operació a la base de dades, i l'"after" es dispara després que l'operació s'hagi completat amb èxit.

Els events disponibles són:

retrieved    → Quan un model es recupera de la BD
creating     → Abans de crear un registre nou
created      → Després de crear un registre nou
updating     → Abans d'actualitzar un registre existent
updated      → Després d'actualitzar un registre existent
saving       → Abans de crear O actualitzar
saved        → Després de crear O actualitzar
deleting     → Abans d'eliminar
deleted      → Després d'eliminar
restoring    → Abans de restaurar un soft delete
restored     → Després de restaurar un soft delete
replicating  → Quan es replica un model

La distinció entre creating/created i saving/saved és important: saving i saved es disparen tant quan crees com quan actualitzes un model. creating i created només quan és un registre nou. updating i updated només quan el registre ja existeix.

Registrar events al model#

La manera més senzilla de respondre a events és definir-los al mètode booted() del model:

class Article extends Model
{
    protected static function booted(): void
    {
        static::creating(function (Article $article) {
            $article->slug = Str::slug($article->title);
            $article->user_id = $article->user_id ?? auth()->id();
        });
 
        static::updating(function (Article $article) {
            if ($article->isDirty('title')) {
                $article->slug = Str::slug($article->title);
            }
        });
 
        static::deleting(function (Article $article) {
            // Eliminar fitxers associats
            if ($article->image) {
                Storage::delete($article->image);
            }
        });
    }
}

Els events "before" (creating, updating, saving, deleting) poden cancel·lar l'operació retornant false:

static::creating(function (Article $article) {
    if (str_contains($article->title, 'spam')) {
        return false; // Cancel·la la creació
    }
});

Exemple pràctic: generar slugs#

Un cas d'ús molt comú és generar automàticament el slug d'un article a partir del títol:

class Article extends Model
{
    protected static function booted(): void
    {
        static::saving(function (Article $article) {
            if ($article->isDirty('title')) {
                $slug = Str::slug($article->title);
 
                // Assegurar unicitat
                $count = Article::where('slug', 'like', "{$slug}%")
                    ->where('id', '!=', $article->id ?? 0)
                    ->count();
 
                $article->slug = $count > 0 ? "{$slug}-{$count}" : $slug;
            }
        });
    }
}

Observers#

Quan la lògica dels events es fa complexa o el model acumula massa closures al booted(), és millor extreure-la a un observer. Un observer és una classe dedicada que agrupa tots els handlers d'events d'un model en mètodes separats i amb nom.

Crear un observer#

php artisan make:observer ArticleObserver --model=Article

Això genera un fitxer a app/Observers/ArticleObserver.php:

namespace App\Observers;
 
use App\Models\Article;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
 
class ArticleObserver
{
    public function creating(Article $article): void
    {
        $article->slug = Str::slug($article->title);
        $article->user_id = $article->user_id ?? auth()->id();
    }
 
    public function updating(Article $article): void
    {
        if ($article->isDirty('title')) {
            $article->slug = Str::slug($article->title);
        }
    }
 
    public function saved(Article $article): void
    {
        if ($article->wasChanged('published') && $article->published) {
            // Notificar els subscriptors
            $article->author->notify(new ArticlePublished($article));
        }
    }
 
    public function deleted(Article $article): void
    {
        // Netejar fitxers associats
        if ($article->image) {
            Storage::delete($article->image);
        }
 
        // Netejar cache
        cache()->forget("article:{$article->slug}");
    }
 
    public function forceDeleted(Article $article): void
    {
        // Netejar tot quan s'elimina permanentment
        Storage::deleteDirectory("articles/{$article->id}");
    }
}

Cada mètode de l'observer correspon a un event del model. El mètode rep la instància del model com a paràmetre.

Registrar l'observer#

La manera recomanada de registrar un observer és amb l'atribut ObservedBy al model:

use App\Observers\ArticleObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
 
#[ObservedBy(ArticleObserver::class)]
class Article extends Model
{
    // ...
}

Alternativament, pots registrar-lo al AppServiceProvider:

use App\Models\Article;
use App\Observers\ArticleObserver;
 
public function boot(): void
{
    Article::observe(ArticleObserver::class);
}

L'atribut #[ObservedBy] és més pràctic perquè manté la relació entre el model i l'observer visible directament al fitxer del model.

Silenciar events#

De vegades necessites executar operacions sense disparar els events del model. Per exemple, quan fas una actualització massiva de dades i no vols que els observers s'executin per cada registre:

// Silenciar tots els events d'un model
Article::withoutEvents(function () {
    Article::where('published', false)
        ->where('created_at', '<', now()->subYear())
        ->update(['archived' => true]);
});
 
// Guardar un model sense events
$article->saveQuietly();
 
// Eliminar sense events
$article->deleteQuietly();
 
// Restaurar sense events (soft deletes)
$article->restoreQuietly();

Això és especialment útil en migracions de dades, scripts de manteniment o operacions en massa on els observers podrien causar efectes secundaris no desitjats o problemes de rendiment.

Events amb closures dispatched#

Per a lògica més complexa que necessita executar-se de manera asíncrona, pots disparar events de Laravel complets des dels events del model:

class Article extends Model
{
    protected $dispatchesEvents = [
        'created' => ArticleCreated::class,
        'updated' => ArticleUpdated::class,
        'deleted' => ArticleDeleted::class,
    ];
}

Després, crea un event i un listener:

// app/Events/ArticleCreated.php
class ArticleCreated
{
    public function __construct(
        public Article $article,
    ) {}
}
 
// app/Listeners/SendArticleNotification.php
class SendArticleNotification
{
    public function handle(ArticleCreated $event): void
    {
        $event->article->author->notify(
            new NewArticleNotification($event->article)
        );
    }
}

La diferència amb els observers és que els events de Laravel es poden processar en cues (queues), cosa que permet descarregar lògica pesada com l'enviament de notificacions o la generació de thumbnails a un procés en segon pla.

Exemple complet#

Un patró habitual és combinar un observer per a la lògica de dades amb events de Laravel per a la lògica de negoci:

// L'observer gestiona la integritat de les dades
class ArticleObserver
{
    public function creating(Article $article): void
    {
        $article->slug = Str::slug($article->title);
        $article->reading_time = $this->calculateReadingTime($article->body);
    }
 
    public function updating(Article $article): void
    {
        if ($article->isDirty('title')) {
            $article->slug = Str::slug($article->title);
        }
 
        if ($article->isDirty('body')) {
            $article->reading_time = $this->calculateReadingTime($article->body);
        }
    }
 
    private function calculateReadingTime(string $body): int
    {
        $words = str_word_count(strip_tags($body));
        return max(1, (int) ceil($words / 200));
    }
}
// Els events de Laravel gestionen la lògica de negoci asíncrona
class Article extends Model
{
    protected $dispatchesEvents = [
        'created' => ArticleCreated::class,
    ];
}
 
// El listener s'executa en una cua
class ProcessNewArticle implements ShouldQueue
{
    public function handle(ArticleCreated $event): void
    {
        // Generar thumbnail
        // Enviar notificacions als subscriptors
        // Actualitzar el sitemap
        // Publicar a xarxes socials
    }
}

Aquesta separació manté el model net: l'observer s'encarrega de la consistència de les dades (slug, temps de lectura), i els events/listeners gestionen les accions de negoci que poden ser asíncrones.