10.10 Observer Pattern
Resumo
Observer Pattern — objetos se inscrevem em events e recebem notificação automática.
Componentes: Subject (publicador), Observer (assinante), Event (evento).
Importante: Laravel: Event + Listeners, Model Observers no Eloquent. ShouldQueue para listeners assíncronos.
Conteúdo
- O que é
- Como funciona
- Laravel Events e Listeners
- Quando usar
- Exemplo prático
- Na entrevista
- Exercícios práticos
O que é
O que é: Observer Pattern — objetos se inscrevem em events. Quando o event acontece, todos os assinantes recebem a notificação.
Componentes:
- Subject (publicador)
- Observer (assinante)
- Event (evento)
Como funciona
Observer básico:
// Subject (publicador)
interface Subject
{
public function attach(Observer $observer): void;
public function detach(Observer $observer): void;
public function notify(): void;
}
// Observer (assinante)
interface Observer
{
public function update(Subject $subject): void;
}
// Implementação do Subject
class Order implements Subject
{
private array $observers = [];
private string $status;
public function attach(Observer $observer): void
{
$this->observers[] = $observer;
}
public function detach(Observer $observer): void
{
$key = array_search($observer, $this->observers, true);
if ($key !== false) {
unset($this->observers[$key]);
}
}
public function notify(): void
{
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function setStatus(string $status): void
{
$this->status = $status;
$this->notify(); // Notifica todos os assinantes
}
public function getStatus(): string
{
return $this->status;
}
}
// Implementação do Observer
class EmailNotificationObserver implements Observer
{
public function update(Subject $subject): void
{
if ($subject instanceof Order) {
echo "Email enviado: status do pedido mudou para {$subject->getStatus()}\n";
}
}
}
class SmsNotificationObserver implements Observer
{
public function update(Subject $subject): void
{
if ($subject instanceof Order) {
echo "SMS enviado: status do pedido mudou para {$subject->getStatus()}\n";
}
}
}
// Uso
$order = new Order();
$order->attach(new EmailNotificationObserver());
$order->attach(new SmsNotificationObserver());
$order->setStatus('paid'); // Os dois observers recebem a notificaçãoLaravel Events e Listeners
Event:
// app/Events/OrderCreated.php
class OrderCreated
{
use Dispatchable, SerializesModels;
public function __construct(
public Order $order
) {}
}Listeners:
// app/Listeners/SendOrderConfirmation.php
class SendOrderConfirmation
{
public function handle(OrderCreated $event): void
{
Mail::to($event->order->user->email)
->send(new OrderConfirmationMail($event->order));
}
}
// app/Listeners/UpdateInventory.php
class UpdateInventory
{
public function handle(OrderCreated $event): void
{
foreach ($event->order->items as $item) {
$item->product->decrement('stock', $item->quantity);
}
}
}
// app/Listeners/SendAdminNotification.php
class SendAdminNotification
{
public function handle(OrderCreated $event): void
{
Notification::send(
User::where('role', 'admin')->get(),
new NewOrderNotification($event->order)
);
}
}EventServiceProvider:
// app/Providers/EventServiceProvider.php
protected $listen = [
OrderCreated::class => [
SendOrderConfirmation::class,
UpdateInventory::class,
SendAdminNotification::class,
],
];Dispatch do event:
class OrderController extends Controller
{
public function store(Request $request)
{
$order = Order::create($request->validated());
// Dispatch do event — todos os listeners são chamados
event(new OrderCreated($order));
return response()->json($order, 201);
}
}Quando usar
Observer para:
- Notificações
- Log
- Atualizar dados relacionados
- Operações assíncronas
NÃO use para:
- Dependência direta entre objetos
Exemplo prático
Model Observers:
// app/Observers/UserObserver.php
class UserObserver
{
public function creating(User $user): void
{
// Antes de criar
$user->uuid = Str::uuid();
}
public function created(User $user): void
{
// Depois de criar
event(new UserRegistered($user));
}
public function updating(User $user): void
{
// Antes de atualizar
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
}
public function updated(User $user): void
{
// Depois de atualizar
Cache::forget("user.{$user->id}");
}
public function deleted(User $user): void
{
// Depois de deletar
$user->posts()->delete();
$user->comments()->delete();
}
}
// app/Providers/EventServiceProvider.php
public function boot(): void
{
User::observe(UserObserver::class);
}Queue Listeners (assíncrono):
// app/Listeners/SendOrderConfirmation.php
class SendOrderConfirmation implements ShouldQueue
{
use InteractsWithQueue;
public $queue = 'emails';
public $tries = 3;
public $backoff = [10, 30, 60];
public function handle(OrderCreated $event): void
{
Mail::to($event->order->user->email)
->send(new OrderConfirmationMail($event->order));
}
public function failed(OrderCreated $event, Throwable $exception): void
{
Log::error("Falha ao enviar confirmação do pedido", [
'order_id' => $event->order->id,
'error' => $exception->getMessage(),
]);
}
}Event Subscribers (vários events):
// app/Listeners/UserEventSubscriber.php
class UserEventSubscriber
{
public function handleUserLogin(UserLoggedIn $event): void
{
$event->user->update(['last_login_at' => now()]);
}
public function handleUserLogout(UserLoggedOut $event): void
{
Cache::forget("user.session.{$event->user->id}");
}
public function subscribe(Dispatcher $events): array
{
return [
UserLoggedIn::class => 'handleUserLogin',
UserLoggedOut::class => 'handleUserLogout',
];
}
}
// EventServiceProvider
protected $subscribe = [
UserEventSubscriber::class,
];Conditional Listeners:
class SendInvoiceEmail implements ShouldQueue
{
public function handle(OrderCreated $event): void
{
Mail::to($event->order->user->email)
->send(new InvoiceEmail($event->order));
}
// Condição: só enfileira pedidos pagos
public function shouldQueue(OrderCreated $event): bool
{
return $event->order->status === 'paid';
}
}Na entrevista
"Observer Pattern é inscrição em events. O Subject avisa os Observers. No Laravel: Event + Listeners. Dispatch com
event(). Listeners entram no EventServiceProvider. Model Observers cobrem events do Eloquent (creating, created, updating, deleted). ShouldQueue para listener assíncrono. Event Subscribers quando um listener cobre vários events. Prós: baixo acoplamento, fácil de estender. Uso: notificação, log, limpar cache, atualizar dado relacionado."
Exercícios práticos
Exercício 1: Crie um Model Observer para Product
Implemente um ProductObserver que cria o slug sozinho, limpa o cache e grava log das mudanças.
Solução
// app/Observers/ProductObserver.php
namespace App\Observers;
use App\Models\Product;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class ProductObserver
{
public function creating(Product $product): void
{
// Cria o slug antes de persistir
if (empty($product->slug)) {
$product->slug = Str::slug($product->name);
}
// Valores padrão
if (!isset($product->is_active)) {
$product->is_active = true;
}
Log::info('Criando produto', ['name' => $product->name]);
}
public function created(Product $product): void
{
// Depois de criar, limpa o cache
Cache::forget('products.all');
Cache::forget('products.featured');
Log::info('Produto criado', [
'id' => $product->id,
'name' => $product->name
]);
// Dispara o event
event(new \App\Events\ProductCreated($product));
}
public function updating(Product $product): void
{
// Se o nome mudou, atualiza o slug
if ($product->isDirty('name')) {
$product->slug = Str::slug($product->name);
}
// Se desativou, zera o stock
if ($product->isDirty('is_active') && !$product->is_active) {
$product->stock = 0;
}
Log::info('Atualizando produto', [
'id' => $product->id,
'changes' => $product->getDirty()
]);
}
public function updated(Product $product): void
{
// Limpa o cache desse produto
Cache::forget("product.{$product->id}");
Cache::forget('products.all');
// Se o preço mudou, notifica os assinantes
if ($product->wasChanged('price')) {
event(new \App\Events\ProductPriceChanged($product));
}
Log::info('Produto atualizado', ['id' => $product->id]);
}
public function deleted(Product $product): void
{
// Apaga dados relacionados
$product->reviews()->delete();
$product->images()->delete();
// Limpa o cache
Cache::forget("product.{$product->id}");
Cache::forget('products.all');
Log::warning('Produto excluído', [
'id' => $product->id,
'name' => $product->name
]);
}
public function restored(Product $product): void
{
// Ao restaurar do soft delete
Cache::forget('products.all');
Log::info('Produto restaurado', ['id' => $product->id]);
}
public function forceDeleted(Product $product): void
{
// Na exclusão definitiva
// Apaga os arquivos de imagem
if ($product->image_url) {
Storage::delete($product->image_url);
}
Log::warning('Produto excluído em definitivo', ['id' => $product->id]);
}
}
// app/Providers/EventServiceProvider.php
namespace App\Providers;
use App\Models\Product;
use App\Observers\ProductObserver;
class EventServiceProvider extends ServiceProvider
{
public function boot(): void
{
Product::observe(ProductObserver::class);
}
}Exercício 2: Crie um Event com vários Listeners
Implemente o Event UserRegistered com 3 Listeners: envio de email, criação de perfil, bônus de boas-vindas.
Solução
// app/Events/UserRegistered.php
namespace App\Events;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
use Dispatchable, SerializesModels;
public function __construct(
public User $user
) {}
}
// app/Listeners/SendWelcomeEmail.php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Mail\WelcomeEmail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail implements ShouldQueue
{
use InteractsWithQueue;
public $queue = 'emails';
public $tries = 3;
public $backoff = [10, 30, 60];
public function handle(UserRegistered $event): void
{
Mail::to($event->user->email)
->send(new WelcomeEmail($event->user));
}
public function failed(UserRegistered $event, \Throwable $exception): void
{
Log::error('Falha ao enviar email de boas-vindas', [
'user_id' => $event->user->id,
'error' => $exception->getMessage()
]);
}
}
// app/Listeners/CreateUserProfile.php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Models\UserProfile;
class CreateUserProfile
{
public function handle(UserRegistered $event): void
{
UserProfile::create([
'user_id' => $event->user->id,
'bio' => '',
'avatar' => 'default-avatar.png',
'theme' => 'light',
'language' => 'pt-BR',
'notifications_enabled' => true,
]);
}
}
// app/Listeners/GiveWelcomeBonus.php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Models\UserBonus;
class GiveWelcomeBonus
{
public function handle(UserRegistered $event): void
{
UserBonus::create([
'user_id' => $event->user->id,
'amount' => 100,
'type' => 'welcome',
'description' => 'Bônus de boas-vindas',
'expires_at' => now()->addDays(30),
]);
}
}
// app/Providers/EventServiceProvider.php
protected $listen = [
UserRegistered::class => [
SendWelcomeEmail::class,
CreateUserProfile::class,
GiveWelcomeBonus::class,
],
];
// Uso no Controller
class RegisterController extends Controller
{
public function register(Request $request)
{
$user = User::create($request->validated());
// Dispatch do event — todos os listeners são chamados
event(new UserRegistered($user));
return redirect()->route('dashboard')
->with('success', 'Bem-vindo! Confira seu email.');
}
}Exercício 3: Implemente um Event Subscriber
Crie um UserActivitySubscriber que escuta vários events do usuário.
Solução
// Events
namespace App\Events;
class UserLoggedIn
{
public function __construct(public User $user) {}
}
class UserLoggedOut
{
public function __construct(public User $user) {}
}
class UserProfileUpdated
{
public function __construct(public User $user) {}
}
class UserPasswordChanged
{
public function __construct(public User $user) {}
}
// app/Listeners/UserActivitySubscriber.php
namespace App\Listeners;
use App\Events\UserLoggedIn;
use App\Events\UserLoggedOut;
use App\Events\UserProfileUpdated;
use App\Events\UserPasswordChanged;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class UserActivitySubscriber
{
public function handleUserLogin(UserLoggedIn $event): void
{
// Atualiza last_login_at
$event->user->update([
'last_login_at' => now(),
'last_login_ip' => request()->ip(),
]);
// Log
Log::info('Usuário fez login', [
'user_id' => $event->user->id,
'ip' => request()->ip(),
]);
// Guarda no cache
Cache::put(
"user.session.{$event->user->id}",
true,
now()->addHours(2)
);
}
public function handleUserLogout(UserLoggedOut $event): void
{
// Tira a sessão do cache
Cache::forget("user.session.{$event->user->id}");
Log::info('Usuário fez logout', [
'user_id' => $event->user->id,
]);
}
public function handleProfileUpdate(UserProfileUpdated $event): void
{
// Limpa o cache do perfil
Cache::forget("user.profile.{$event->user->id}");
Log::info('Perfil atualizado', [
'user_id' => $event->user->id,
'changes' => $event->user->getChanges(),
]);
// Se o email mudou, precisa verificar de novo
if ($event->user->wasChanged('email')) {
$event->user->update(['email_verified_at' => null]);
event(new \App\Events\EmailVerificationRequired($event->user));
}
}
public function handlePasswordChange(UserPasswordChanged $event): void
{
// Avisa o usuário da troca de senha
Mail::to($event->user->email)
->send(new \App\Mail\PasswordChangedMail($event->user));
Log::warning('Senha alterada', [
'user_id' => $event->user->id,
'ip' => request()->ip(),
]);
// Invalida as outras sessões
$event->user->sessions()
->where('id', '!=', session()->getId())
->delete();
}
// Registro dos events
public function subscribe(Dispatcher $events): array
{
return [
UserLoggedIn::class => 'handleUserLogin',
UserLoggedOut::class => 'handleUserLogout',
UserProfileUpdated::class => 'handleProfileUpdate',
UserPasswordChanged::class => 'handlePasswordChange',
];
}
}
// app/Providers/EventServiceProvider.php
protected $subscribe = [
UserActivitySubscriber::class,
];
// Outra forma de registrar no subscribe()
public function subscribe(Dispatcher $events): void
{
$events->listen(
UserLoggedIn::class,
[UserActivitySubscriber::class, 'handleUserLogin']
);
$events->listen(
UserLoggedOut::class,
[UserActivitySubscriber::class, 'handleUserLogout']
);
$events->listen(
UserProfileUpdated::class,
[UserActivitySubscriber::class, 'handleProfileUpdate']
);
$events->listen(
UserPasswordChanged::class,
[UserActivitySubscriber::class, 'handlePasswordChange']
);
}Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate