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=ArticleAixò 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.