Observer di Laravel ti consente di creare applicazioni disaccoppiate e di facile manutenzione.
Elegante gestione degli eventi, flussi di lavoro automatizzati e architettura scalabile.
In questa guida puoi trovare esempi pratici che ti aiuteranno a padroneggiare il pattern Observer.
Introduzione
Il pattern Observer è uno dei pattern di progettazione comportamentale più potenti disponibili in Laravel, che consente di costruire applicazioni a basso accoppiamento e di facile manutenzione. Questo pattern permette agli observer di monitorare e reagire agli eventi che accadono ad altri oggetti (soggetti) senza creare strette dipendenze tra di essi.
Nell’ecosistema di Laravel, il pattern Observer si manifesta principalmente attraverso gli Eloquent Model Observer e il sistema Event/Listener, fornendo soluzioni eleganti a problematiche trasversali come la registrazione dei log, le notifiche, l’invalidazione della cache e la sincronizzazione dei dati.
Tempo di lettura: 7 minuti
Modello Observer
Il pattern Observer definisce una dipendenza uno-a-molti tra oggetti. Quando lo stato del soggetto cambia, tutti gli observer registrati vengono notificati e aggiornati automaticamente. Questo crea un meccanismo publish-subscribe in cui:
- Soggetto (osservabile) : l’oggetto osservato
- Observer: Oggetti che reagiscono ai cambiamenti del soggetto
- Meccanismo di notifica : il sistema che attiva gli aggiornamenti dell’observer
Perché utilizzare il pattern Observer?
Questo modello risolve diversi problemi critici nello sviluppo di applicazioni:
Separazione delle responsabilità : la logica di business rimane separata dagli effetti collaterali come notifiche, registrazione o analisi. Il modello utente non ha bisogno di conoscere i servizi di posta elettronica, i gestori di cache o i registri di controllo.
Principio aperto/chiuso : puoi aggiungere nuovi observer senza modificare il codice esistente. Hai bisogno di aggiungere notifiche Slack quando gli utenti si registrano? Crea un nuovo observer senza toccare la logica di registrazione.
Manutenibilità : i comportamenti correlati sono raggruppati in classi di observer anziché essere sparsi tra controller e modelli.
Riutilizzabilità : gli observer possono essere riutilizzati in diverse parti dell’applicazione o persino in progetti diversi.
Implementazione del pattern Observer in Laravel
Laravel offre due implementazioni principali del pattern Observer:
1. Observer dei modelli Eloquent
Gli observer dei modelli sono costantemente in ascolto di eventi scatenati dai modelli Eloquent come creating, created, updating, updated, deleting, deleted, saving, saved, restoring, restored, retrieved, replicating, e forceDeleted.
2. Sistema di event/listener
Il sistema di eventi di Laravel offre un’implementazione più ampia che consente di definire eventi personalizzati e più listener, offrendo maggiore flessibilità rispetto agli observer di un modello Eloquent.
Creazione e utilizzo di observer di modello Eloquent
Creazione di un observer
Con l’artisan possiamo creare un observer, nell’esempio seguente creiamo UserObserver operativo sul modello User:
php artisan make:observer UserObserver --model=UserQui di seguito la classe observer con i metodi (modificati rispetto il default) che verranno invocati al verificarsi di specifici eventi:
- creating: invocato prima che un nuovo modello venga salvato nel database;
- created: invocato dopo che un nuovo modello venga salvato nel database;
- updating: invocato prima che un modello venga modificato nel database;
- updated: invocato dopo che un modello venga aggiornato nel database;
- deleting: invocato prima che un modello venga cancellato nel database;
- deleted: invocato dopo che un modello venga aggiornato nel database;
- restored: invocato dopo che “soft-deleted data” vengono ripristinati;
- force deleted: invocato dopo che “soft-deleted data” vengono definitivamente cancellati;
<?php
namespace App\Observers;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class UserObserver
{
/**
* Gestione dell'evento "creating" sul modello User.
* Si verifica prima che un nuovo modello venga salvato nel database.
*/
public function creating(User $user): void
{
// Imposta i valori di default
if (empty($user->uuid)) {
$user->uuid = (string) Str::uuid();
}
// normalizza, portando le lettere al minuscolo, l'email
$user->email = strtolower($user->email);
}
/**
* Gestione dell'evento "created" sul modello User.
* Viene invocato dopo che un nuovo modello venga salvato nel database.
*/
public function created(User $user): void
{
// Invio della notifica di benvenuto
Mail::to($user->email)->send(new WelcomeEmail($user));
// Scrive nel Log la registrazione dell'utente
Log::info('Registrato il nuovo utente ', [
'user_id' => $user->id,
'email' => $user->email
]);
// Pulisce la cache
Cache::tags(['users'])->flush();
}
/**
* Gestione dell'evento "updating" sul modello User.
*/
public function updating(User $user): void
{
// Traccia quali record sono cambiati
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
}
/**
* Gestione dell'evento "updated" sul modello User.
*/
public function updated(User $user): void
{
// Invalida, cancella una specifica cache
Cache::forget("user.{$user->id}");
// Sincronizza dei cambiamenti con specifici servizi esterni
if ($user->wasChanged('email')) {
// mette in coda il job per aggiornare l'email nel CRM esterno
UpdateCRMEmail::dispatch($user);
}
}
/**
* Gestione dell'evento "deleting" sul modello User.
*/
public function deleting(User $user): void
{
// Previene la cancellazione se un utente ha attive le sottoscrizioni
if ($user->subscriptions()->active()->exists()) {
throw new \Exception('Non è possibile cancellare l\'utente perchè attive delle sottoscrizioni.');
}
}
/**
* Gestione dell'evento "deleted" sul modello User.
*/
public function deleted(User $user): void
{
// Pulisce i dati corrispondenti
$user->posts()->delete();
$user->comments()->delete();
// Log deletion
Log::warning('Utente eliminato', ['user_id' => $user->id]);
}
/**
* Gestione dell'evento "restored" sul modello User.
*/
public function restored(User $user): void
{
// Ripristina i soft-deleted data
$user->posts()->restore();
Log::info('Utente ripristinato', ['user_id' => $user->id]);
}
/**
* Gestione dell'evento "force deleted" sul modello User.
*/
public function forceDeleted(User $user): void
{
// Cancella definitivamente i record corrispondenti
Storage::deleteDirectory("users/{$user->id}");
Log::alert('Utente cancellato definitivamente ', ['user_id' => $user->id]);
}
}Registrazione di un observer
Gli observer devono essere registrati nel provider EventService: App\Providers\EventServiceProvider:
<?php
namespace App\Providers;
use App\Models\User;
use App\Models\Post;
use App\Models\Order;
use App\Observers\UserObserver;
use App\Observers\PostObserver;
use App\Observers\OrderObserver;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* Gli observer dell'applicazione.
*
* @var array
*/
protected $observers = [
User::class => [UserObserver::class],
Post::class => [PostObserver::class],
Order::class => [OrderObserver::class],
];
/**
* Registrazione degli eventi del progetto.
*/
public function boot(): void
{
//
}
}In alternativa, è possibile registrarsi secondo le modalità previste dal fornitore di servizi boot:
public function boot(): void
{
User::observe(UserObserver::class);
}Modelli di observer avanzati
Esecuzione dell’observer condizionale
Gli observer possono includere una logica condizionale per essere eseguiti solo in determinate circostanze:
public function created(User $user): void
{
// Invia l'email solo se sei in ambiente di produzione
if (app()->environment('production')) {
Mail::to($user->email)->send(new WelcomeEmail($user));
}
// Solo oer utenti specifici premium
if ($user->type === 'premium') {
$this->setupPremiumFeatures($user);
}
}
Interruzione dell’esecuzione dell’observer
Se restituisci false dai metodi creating, updating, deleting, o saving allora puoi interrompere l’operazione in corso, vietandone il completamento. Ad esempio nel creating possiamo annullare la creazione di un nuovo utente con i permessi di amministratore, mentre nella cancellazione possiamo prevenire la cancellazione di un utente amministratore:
public function creating(User $user): bool
{
// Validazione della regola di business
if ($this->emailAlreadyExists($user->email)) {
return false; // Previene la creazione dell'utente
}
return true;
}
public function deleting(User $user): bool
{
// Previene la cancellazione di utenti admin
if ($user->hasRole('admin')) {
Log::warning('Tentata cancellazione di utente admin ', ['user_id' => $user->id]);
return false;
}
return true;
}
Observer basati su code Queue
Per le operazioni che richiedono molto tempo, implementare l’interfaccia ShouldQueue:
<?php
namespace App\Observers;
use App\Models\Order;
use Illuminate\Contracts\Queue\ShouldQueue;
class OrderObserver implements ShouldQueue
{
/**
* Il nome della connessione della coda da utilizzare.
*/
public $connection = 'redis';
/**
* Il nome della coda a cui inviare i Jobs.
*/
public $queue = 'observers';
/**
* Gestisci l'evento Created
*/
public function created(Order $order): void
{
// Questo verrà eseguito in coda
$this->generateInvoicePDF($order);
$this->notifyWarehouse($order);
$this->updateInventory($order);
}
}
Observer con dipendenze
Inietta le dipendenze tramite il costruttore:
<?php
namespace App\Observers;
use App\Models\Post;
use App\Services\SearchIndexer;
use App\Services\CacheManager;
use Illuminate\Support\Facades\Log;
class PostObserver
{
public function __construct(
protected SearchIndexer $searchIndexer,
protected CacheManager $cacheManager
) {}
public function created(Post $post): void
{
$this->searchIndexer->index($post);
$this->cacheManager->invalidatePostCache();
}
public function updated(Post $post): void
{
$this->searchIndexer->update($post);
$this->cacheManager->invalidate("post.{$post->id}");
}
public function deleted(Post $post): void
{
$this->searchIndexer->remove($post->id);
$this->cacheManager->invalidate("post.{$post->id}");
}
}
Problemi nell’uso degli Observer e soluzioni
Vediamo ora alcuni problemi che potrebbero nascere dall’uso scorretto degli Observer
Caso 1: Cicli infiniti
Problema : gli observer che aggiornano i modelli possono attivarsi ricorsivamente.
// PERICOLOSO: Questa impostazione genera un loop infinito
public function updated(User $user): void
{
$user->update(['last_activity' => now()]); // Triggers updated again!
}Soluzioni :
// Soluzione 1: Utilizzare saveQuietly() per ignorare gli observer
public function updated(User $user): void
{
$user->saveQuietly(['last_activity' => now()]);
}
// Soluzione 2: Verificare se un attributo specifico è cambiato
public function updated(User $user): void
{
if (!$user->wasChanged('last_activity')) {
$user->saveQuietly(['last_activity' => now()]);
}
}
// Soluzione 3: Utilizzare withoutEvents() per operazioni in blocco
User::withoutEvents(function () use ($user) {
$user->update(['last_activity' => now()]);
});Caso 2: Ordine di esecuzione dell’observer
Problema : più observer sullo stesso modello possono avere dipendenze dall’ordine di esecuzione.
Soluzione : utilizzare un singolo observer con metodi ordinati oppure utilizzare le priorità degli eventi:
// Crea un observer coordinato
class UserObserver
{
public function created(User $user): void
{
// Passaggio 1: Operazioni critiche per prime
$this->assignDefaultRole($user);
// Passaggio 2: Genera dati derivati
$this->createUserProfile($user);
// Passaggio 3: Notifiche non critiche per ultime
$this->sendWelcomeEmail($user);
}
}Caso 3: Complicazioni nei test
Problema : Gli observer vengono eseguiti durante i test, causando effetti collaterali imprevisti.
Soluzione : Disabilitare gli observer nei test o utilizzare il mocking:
// Nel tuo test
use Illuminate\Support\Facades\Event;
public function test_user_creation(): void
{
// Disabilita tutti gli observer
Event::fake();
$user = User::factory()->create();
// Asserzioni senza effetti collaterali degli observer
$this->assertDatabaseHas('users', ['email' => $user->email]);
}
// Disabilita eventi specifici del modello
public function test_without_specific_events(): void
{
User::withoutEvents(function () {
$user = User::factory()->create();
// Test senza attivare gli observer
});
}
// Simula selettivamente gli eventi
public function test_with_some_events(): void
{
Event::fake([
UserCreated::class, // Solo questo evento viene simulato
]);
$user = User::factory()->create();
// Gli altri observer vengono comunque eseguiti
}Caso 4: Problemi di prestazioni
Problema : gli observer che vengono eseguiti in modo sincrono rallentano le richieste.
Soluzioni :
// Soluzione 1: Implementare ShouldQueue
class UserObserver implements ShouldQueue
{
public function created(User $user): void
{
// Operazioni pesanti spostate in coda
$this->processUserAnalytics($user);
$this->syncToExternalServices($user);
}
}
// Soluzione 2: Inviare job invece di eseguirli direttamente
public function created(User $user): void
{
// Le operazioni rapide avvengono immediatamente
Cache::forget('users.count');
// Operazioni pesanti inviate in coda
ProcessNewUser::dispatch($user);
SendWelcomeEmail::dispatch($user);
SyncToAnalytics::dispatch($user);
}
// Soluzione 3: Utilizzare le transazioni del database in modo efficiente
public function created(User $user): void
{
DB::transaction(function () use ($user) {
// Raggruppa le operazioni del database correlate
$user->profile()->create([...]);
$user->settings()->create([...]);
});
}Caso 5: Gestione delle operazioni di massa
Problema : Gli observer si attivano per ogni modello nelle operazioni in blocco, causando problemi di prestazioni.
Soluzioni :
// Soluzione 1: Disabilita gli eventi per le operazioni bulk
User::withoutEvents(function () {
User::where('status', 'pending')->update(['status' => 'active']);
});
// Soluzione 2: Utilizza query raw che bypassano Eloquent
DB::table('users')
->where('status', 'pending')
->update(['status' => 'active']);
// Soluzione 3: Gestisci le operazioni bulk nella classe
class UserObserver
{
protected static bool $bulkOperation = false;
public static function startBulkOperation(): void
{
static::$bulkOperation = true;
}
public static function endBulkOperation(): void
{
static::$bulkOperation = false;
// Esegui la pulizia dopo l'operazione bulk
Cache::tags(['users'])->flush();
}
public function updated(User $user): void
{
if (static::$bulkOperation) {
return; // Salta l'observer durante l'operazione bulk
}
// Logica di aggiornamento normale
Cache::forget("user.{$user->id}");
}
}
// Utilizzo
UserObserver::startBulkOperation();
User::where('active', true)->update(['last_checked' => now()]);
UserObserver::endBulkOperation();Caso 6: Annullamento delle transazioni
Problema : gli observer vengono eseguiti anche se la transazione viene annullata.
Soluzione : utilizzare transazioni di database con hook after commit:
public function created(User $user): void
{
// Attendi fino al commit della transazione
DB::afterCommit(function () use ($user) {
Mail::to($user->email)->send(new WelcomeEmail($user));
Cache::tags(['users'])->flush();
});
}
// Oppure in Laravel 10+, usa l'attributo AfterCommit
use Illuminate\Queue\Attributes\AfterCommit;
#[AfterCommit]
class UserObserver
{
public function created(User $user): void
{
// Attendi automaticamente il commit della transazione
Mail::to($user->email)->send(new WelcomeEmail($user));
}
}migliori pratiche
1. Mantenere gli observer snelli
Ciascun observer dovrà assumersi una singola responsabilità:
// BENE: CSemplifica gli Observer
class UserNotificationObserver { /* gestisci notifiche */ }
class UserCacheObserver { /* gestisce la cache */ }
class UserAuditObserver { /* gestisce i log */ }
// DA EVITARE: observer onnipotente che fa tutto
class UserObserver
{
public function created(User $user): void
{
// Troppe responsabilità
$this->sendEmail($user);
$this->clearCache($user);
$this->logAudit($user);
$this->syncCRM($user);
$this->updateAnalytics($user);
}
}2. Utilizzare i suggerimenti di tipo
Per chiarezza e supporto IDE, inserisci sempre suggerimenti di tipo per i parametri del modello:
public function created(User $user): void // GOOD
{
//
}
public function created($user) // AVOID
{
//
}3. Tipi di rendimento con leva finanziaria
Utilizzare i tipi di ritorno per rendere esplicito il comportamento di arresto:
public function creating(User $user): bool
{
if ($this->isDuplicate($user)) {
return false; // Explicit halt
}
return true;
}4. Documentare il comportamento dell’observer
Utilizza i docblock per spiegare i comportamenti non ovvi:
/**
* Gestisce l'evento "Created" dell'utente.
*
* Questo osservatore:
* - Invia l'email di benvenuto (in coda)
* - Crea le impostazioni utente predefinite
* - Invalida la cache del conteggio utenti
* - Sincronizza con il CRM (in coda, può riprovare fino a 3 volte)
*
* @param User $user
* @return void
*/
public function created ( User $user ): void
{
//
}
5. Gestire i fallimenti con eleganza
Non permettere che i guasti degli observer compromettano l’operazione principale:
public function created ( User $user ): void
{
try {
Mail :: to ( $user ->email)-> send ( new WelcomeEmail ( $user ));
} catch (\ Exception $e ) {
Log :: error ( 'Impossibile inviare l'email di benvenuto' , [
'user_id' => $user ->id,
'error' => $e -> getMessage (),
]);
// Continua l'esecuzione, non generare un'eccezione
}
// Le altre operazioni continuano anche se l'email fallisce
Cache :: tags ([ 'users' ])-> flush ();
}
6. Utilizzare i flag di funzionalità
Consenti di attivare/disattivare gli observer senza modifiche al codice:
public function created ( User $user ): void
{
if ( config ( 'features.user_welcome_email' )) {
Mail :: to ( $user ->email)-> send ( new WelcomeEmail ( $user ));
}
if ( config ( 'features.crm_sync' )) {
SyncToCRM:: dispatch ( $user );
}
}
Test degli observer
Test unitari
Testare i metodi di osservazione in modo isolato:
<?php
namespace Tests \ Unit \ Observers ;
use Tests \ TestCase ;
use App \ Models \ User ;
use App \ Observers \ UserObserver ;
use Illuminate \ Support \ Facades \ Mail ;
use App \ Mail \ WelcomeEmail ;
class UserObserverTest extends TestCase
{
public function test_sets_uuid_on_creating ( ): void
{
$observer = new UserObserver ();
$user = new User ([ 'email' => 'test@example.com' ]);
$observer -> creating ( $user );
$this -> assertNotNull ( $user ->uuid);
}
public function test_sends_welcome_email_on_created ( ): void
{
Mail :: fake ();
$observer = new UserObserver ();
$user = User :: factory ()-> create ();
$observer -> created ( $user );
Mail :: assertSent ( WelcomeEmail :: class , function ( $mail ) use ($ user ) {
return $ mail -> hasTo ($ user -> email );
});
}
public function test_prevents_deletion_with_active_subscriptions ( ): void
{
$user = User :: factory ()-> create ();
$user -> subscriptions ()-> create ([ 'status' => 'active' ]);
$observer =nuovo UserObserver ();
$this- > expectException ( \Exception :: class );
$observer- > deleting ( $user );
}
}
Test di integrazione
Observer di prova nel contesto delle operazioni del modello:
public function test_user_creation_triggers_observer ( ): void
{
Mail :: fake ();
Cache :: spy ();
$user = User :: create ([
'name' => 'John Doe' ,
'email' => 'john@example.com' ,
'password' => bcrypt ( 'password' ),
]);
// Assert observer side effects
$this -> assertNotNull ( $user -> fresh ()->uuid);
Mail :: assertSent ( WelcomeEmail :: class );
Cache :: shouldHaveReceived ( 'tags' )-> with ([ 'users' ]);
}
Test senza observer
Testare il comportamento del modello senza effetti collaterali dovuti all’observer:
public function test_user_attributes_without_observers ( ): void
{
Event :: fake ();
$user = User :: factory ()-> create ([
'email' => 'TEST@EXAMPLE.COM' ,
]);
// L'email non verrà convertita in minuscolo perché l'observer non è stato eseguito
$this -> assertEquals ( 'TEST@EXAMPLE.COM' , $user ->email);
}
Observer vs. Event/Listener
Comprendere quando utilizzare ciascun approccio:
Utilizzare gli observer quando:
- Stai reagendo agli eventi del ciclo di vita del modello Eloquent
- La logica è strettamente legata a un modello specifico
- Hai bisogno di una gestione degli eventi semplice e diretta
- Si desidera mantenere insieme i comportamenti del modello correlati
Utilizzare l’event/listener quando:
- Hai bisogno di eventi personalizzati che vadano oltre il ciclo di vita del modello.
- Più modelli dovrebbero attivare lo stesso listener
- È necessaria una logica complessa per la gestione degli eventi.
- Desideri la massima flessibilità e testabilità.
- Stai costruendo un sistema con molti componenti disaccoppiati
Esempio di confronto:
// Approccio Observer (strettamente accoppiato al modello User)
class UserObserver
{
public function created ( User $user ): void
{
event ( new UserRegistered ( $user ));
}
}
// Approccio Event/Listener (flessibile, riutilizzabile)
class UserRegistered
{
public function __construct ( public User $user ) {}
}
class SendWelcomeEmail
{
public function handle ( UserRegistered $event ): void
{
Mail :: to ( $event ->user)-> send ( new WelcomeEmail ( $event ->user));
}
}
class UpdateAnalytics
{
public function handle ( UserRegistered $event ): void
{
Analytics :: track ( 'user.registered' , $event ->user);
}
}
Uso dell’Observer per ottimizzare le prestazioni
1. Operazioni batch
Elabora più record in modo efficiente:
public function created(Order $order): void
{
// Invece di elaborare ogni ordine singolarmente
static $orders = [];
$orders[] = $order;
// Elabora gli ordini in batch, cioè a gruppi di 100
if (count($orders) >= 100) {
$this->processBatch($orders);
$orders = [];
}
}2. Prevenzione del caricamento differito (Lazy Loading)
Evitare query N+1 negli Observer:
public function created(Order $order): void
{
// EVITARE: Ciò causa N+1 se si elaborano più ordini
$customer = $order->customer; // Lazy loads
// OTTIMALE: Caricamento immediato nella query
// Order::with('customer')->create([...]);
}3. Rigenerazione della cache
Con l’Observer puoi rigenerare la cache nel modo seguente:
public function updated(Product $product): void
{
// Clear cache immediately
Cache::forget("product.{$product->id}");
// Warm cache in background
WarmProductCache::dispatch($product);
}Conclusione
Il pattern Observer in Laravel fornisce un potente meccanismo per la creazione di applicazioni disaccoppiate e di facile manutenzione.
Implementando correttamente gli observer, è possibile:
- Mantieni i tuoi modelli puliti e focalizzati
- Gestire con eleganza le problematiche trasversali
- Creare applicazioni scalabili e di facile manutenzione
- Reagire ai cambiamenti senza un accoppiamento stretto
Ricorda questi principi fondamentali:
- Mantenere gli Observer concentrati su singole responsabilità.
- Gestire gli errori con eleganza per evitare problemi a cascata
- Utilizzare le code per le operazioni che richiedono molto tempo.
- Eseguire test approfonditi per garantire che gli observer si comportino correttamente.
- Documentare il comportamento in modo che gli altri sviluppatori possano comprendere il flusso
- Monitorare le prestazioni e ottimizzarle secondo necessità.
- Evita i cicli infiniti utilizzando saveQuietly() o controllando gli attributi modificati
Il pattern Observer non è una soluzione miracolosa. Se implementati con attenzione, gli observer diventano uno strumento prezioso nel vostro kit di sviluppo Laravel.
