Design Patterns: Observer & Decorator
Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Laravel's event system is built on this pattern.
- Subject: The object being observed (maintains list of observers)
- Observer: Objects that want to be notified of changes
- Concrete Subject: Stores state and notifies observers when state changes
- Concrete Observer: Implements update method to react to changes
Implementing Observer Pattern
// Stock price monitoring system
namespace App\Observers;
interface Subject
{
public function attach(Observer $observer): void;
public function detach(Observer $observer): void;
public function notify(): void;
}
interface Observer
{
public function update(Subject $subject): void;
public function getName(): string;
}
class Stock implements Subject
{
private array $observers = [];
private string $symbol;
private float $price;
public function __construct(string $symbol, float $initialPrice)
{
$this->symbol = $symbol;
$this->price = $initialPrice;
}
public function attach(Observer $observer): void
{
$name = $observer->getName();
if (!isset($this->observers[$name])) {
$this->observers[$name] = $observer;
echo "Attached observer: {$name}\n";
}
}
public function detach(Observer $observer): void
{
$name = $observer->getName();
if (isset($this->observers[$name])) {
unset($this->observers[$name]);
echo "Detached observer: {$name}\n";
}
}
public function notify(): void
{
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function setPrice(float $price): void
{
echo "Stock {$this->symbol}: Price changed from {$this->price} to {$price}\n";
$this->price = $price;
$this->notify();
}
public function getPrice(): float
{
return $this->price;
}
public function getSymbol(): string
{
return $this->symbol;
}
}
// Concrete Observers
class EmailAlert implements Observer
{
private string $email;
private float $threshold;
public function __construct(string $email, float $threshold)
{
$this->email = $email;
$this->threshold = $threshold;
}
public function update(Subject $subject): void
{
if ($subject instanceof Stock) {
if ($subject->getPrice() >= $this->threshold) {
$this->sendEmail($subject);
}
}
}
private function sendEmail(Stock $stock): void
{
echo "EMAIL to {$this->email}: {$stock->getSymbol()} reached {$stock->getPrice()}\n";
}
public function getName(): string
{
return "EmailAlert-{$this->email}";
}
}
class SmsAlert implements Observer
{
private string $phoneNumber;
private float $threshold;
public function __construct(string $phoneNumber, float $threshold)
{
$this->phoneNumber = $phoneNumber;
$this->threshold = $threshold;
}
public function update(Subject $subject): void
{
if ($subject instanceof Stock) {
if ($subject->getPrice() >= $this->threshold) {
$this->sendSms($subject);
}
}
}
private function sendSms(Stock $stock): void
{
echo "SMS to {$this->phoneNumber}: {$stock->getSymbol()} is at {$stock->getPrice()}\n";
}
public function getName(): string
{
return "SmsAlert-{$this->phoneNumber}";
}
}
class DatabaseLogger implements Observer
{
public function update(Subject $subject): void
{
if ($subject instanceof Stock) {
$this->logToDatabase($subject);
}
}
private function logToDatabase(Stock $stock): void
{
echo "DB LOG: {$stock->getSymbol()} price updated to {$stock->getPrice()}\n";
// StockPrice::create([...])
}
public function getName(): string
{
return 'DatabaseLogger';
}
}
// Usage
$tesla = new Stock('TSLA', 200.0);
$emailAlert = new EmailAlert('investor@example.com', 250.0);
$smsAlert = new SmsAlert('+1234567890', 220.0);
$logger = new DatabaseLogger();
$tesla->attach($emailAlert);
$tesla->attach($smsAlert);
$tesla->attach($logger);
$tesla->setPrice(210.0); // Only logger notified
$tesla->setPrice(225.0); // Logger and SMS notified
$tesla->setPrice(255.0); // All three notified
Laravel Event System (Built-in Observer)
Laravel's event system is an elegant implementation of the Observer pattern.
// Event class
namespace App\Events;
use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped
{
use Dispatchable, SerializesModels;
public function __construct(public Order $order) {}
}
// Multiple Listeners (Observers)
namespace App\Listeners;
class SendShipmentNotification
{
public function handle(OrderShipped $event): void
{
$order = $event->order;
Mail::to($order->customer->email)
->send(new OrderShippedMail($order));
}
}
class UpdateInventory
{
public function handle(OrderShipped $event): void
{
foreach ($event->order->items as $item) {
$item->product->decrement('stock', $item->quantity);
}
}
}
class LogShipment
{
public function handle(OrderShipped $event): void
{
Log::info('Order shipped', [
'order_id' => $event->order->id,
'shipped_at' => now(),
]);
}
}
class NotifyWarehouse
{
public function handle(OrderShipped $event): void
{
// Notify warehouse system via API
Http::post('https://warehouse.example.com/api/notify', [
'order_id' => $event->order->id,
]);
}
}
// Register in EventServiceProvider
protected $listen = [
OrderShipped::class => [
SendShipmentNotification::class,
UpdateInventory::class,
LogShipment::class,
NotifyWarehouse::class,
],
];
// Dispatch event (notify all observers)
OrderShipped::dispatch($order);
Decorator Pattern
The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Problem: Text Formatting
You need to apply various formatting options (bold, italic, underline, color) to text, and users can combine them in any way.
// Component interface
interface TextComponent
{
public function render(): string;
}
// Concrete component
class PlainText implements TextComponent
{
public function __construct(private string $text) {}
public function render(): string
{
return $this->text;
}
}
// Base decorator
abstract class TextDecorator implements TextComponent
{
public function __construct(protected TextComponent $component) {}
public function render(): string
{
return $this->component->render();
}
}
// Concrete decorators
class BoldDecorator extends TextDecorator
{
public function render(): string
{
return '<strong>' . parent::render() . '</strong>';
}
}
class ItalicDecorator extends TextDecorator
{
public function render(): string
{
return '<em>' . parent::render() . '</em>';
}
}
class UnderlineDecorator extends TextDecorator
{
public function render(): string
{
return '<u>' . parent::render() . '</u>';
}
}
class ColorDecorator extends TextDecorator
{
public function __construct(TextComponent $component, private string $color) {
parent::__construct($component);
}
public function render(): string
{
return '<span style="color: ' . $this->color . '">'
. parent::render()
. '</span>';
}
}
class UppercaseDecorator extends TextDecorator
{
public function render(): string
{
return strtoupper(parent::render());
}
}
// Usage - Chain decorators dynamically
$text = new PlainText('Hello World');
// Just bold
$bold = new BoldDecorator($text);
echo $bold->render(); // <strong>Hello World</strong>
// Bold + Italic
$boldItalic = new ItalicDecorator(new BoldDecorator($text));
echo $boldItalic->render(); // <em><strong>Hello World</strong></em>
// Bold + Italic + Underline + Red Color
$formatted = new ColorDecorator(
new UnderlineDecorator(
new ItalicDecorator(
new BoldDecorator($text)
)
),
'red'
);
echo $formatted->render();
Decorator Pattern: API Response
// API Response decorator system
namespace App\Http\Decorators;
interface ApiResponse
{
public function toArray(): array;
}
class BaseResponse implements ApiResponse
{
public function __construct(private array $data) {}
public function toArray(): array
{
return $this->data;
}
}
abstract class ResponseDecorator implements ApiResponse
{
public function __construct(protected ApiResponse $response) {}
public function toArray(): array
{
return $this->response->toArray();
}
}
class TimestampDecorator extends ResponseDecorator
{
public function toArray(): array
{
$data = parent::toArray();
$data['timestamp'] = now()->toIso8601String();
return $data;
}
}
class PaginationDecorator extends ResponseDecorator
{
public function __construct(
ApiResponse $response,
private int $currentPage,
private int $perPage,
private int $total
) {
parent::__construct($response);
}
public function toArray(): array
{
$data = parent::toArray();
$data['pagination'] = [
'current_page' => $this->currentPage,
'per_page' => $this->perPage,
'total' => $this->total,
'last_page' => ceil($this->total / $this->perPage),
];
return $data;
}
}
class MetadataDecorator extends ResponseDecorator
{
public function __construct(
ApiResponse $response,
private array $metadata
) {
parent::__construct($response);
}
public function toArray(): array
{
$data = parent::toArray();
$data['meta'] = $this->metadata;
return $data;
}
}
class EncryptionDecorator extends ResponseDecorator
{
public function toArray(): array
{
$data = parent::toArray();
$data['encrypted'] = encrypt(json_encode($data['data']));
unset($data['data']);
return $data;
}
}
class CompressionDecorator extends ResponseDecorator
{
public function toArray(): array
{
$data = parent::toArray();
$data['compressed'] = base64_encode(gzcompress(json_encode($data['data'])));
$data['compression'] = 'gzip';
unset($data['data']);
return $data;
}
}
// Controller usage
class UserController extends Controller
{
public function index(Request $request)
{
$users = User::paginate(15);
// Base response
$response = new BaseResponse([
'data' => $users->items(),
]);
// Add decorators based on requirements
$response = new TimestampDecorator($response);
if ($request->has('page')) {
$response = new PaginationDecorator(
$response,
$users->currentPage(),
$users->perPage(),
$users->total()
);
}
if ($request->has('include_meta')) {
$response = new MetadataDecorator($response, [
'version' => '1.0',
'endpoint' => $request->path(),
]);
}
return response()->json($response->toArray());
}
}
Chain of Responsibility Pattern
Chain of Responsibility passes a request along a chain of handlers. Each handler decides either to process the request or pass it to the next handler.
// Payment validation chain
namespace App\Payment\Validators;
abstract class PaymentValidator
{
private ?PaymentValidator $nextValidator = null;
public function setNext(PaymentValidator $validator): PaymentValidator
{
$this->nextValidator = $validator;
return $validator;
}
public function validate(array $paymentData): bool
{
if (!$this->check($paymentData)) {
return false;
}
if ($this->nextValidator) {
return $this->nextValidator->validate($paymentData);
}
return true;
}
abstract protected function check(array $paymentData): bool;
}
class AmountValidator extends PaymentValidator
{
protected function check(array $paymentData): bool
{
if (!isset($paymentData['amount']) || $paymentData['amount'] <= 0) {
throw new \InvalidArgumentException('Invalid amount');
}
return true;
}
}
class CardValidator extends PaymentValidator
{
protected function check(array $paymentData): bool
{
if (!isset($paymentData['card_number'])) {
throw new \InvalidArgumentException('Card number required');
}
// Luhn algorithm check
$cardNumber = str_replace(' ', '', $paymentData['card_number']);
if (strlen($cardNumber) < 13 || strlen($cardNumber) > 19) {
throw new \InvalidArgumentException('Invalid card number length');
}
return true;
}
}
class CvvValidator extends PaymentValidator
{
protected function check(array $paymentData): bool
{
if (!isset($paymentData['cvv'])) {
throw new \InvalidArgumentException('CVV required');
}
if (!preg_match('/^[0-9]{3,4}$/', $paymentData['cvv'])) {
throw new \InvalidArgumentException('Invalid CVV format');
}
return true;
}
}
class ExpiryValidator extends PaymentValidator
{
protected function check(array $paymentData): bool
{
if (!isset($paymentData['expiry'])) {
throw new \InvalidArgumentException('Expiry date required');
}
[$month, $year] = explode('/', $paymentData['expiry']);
$expiryDate = \Carbon\Carbon::createFromDate($year, $month, 1)->endOfMonth();
if ($expiryDate->isPast()) {
throw new \InvalidArgumentException('Card has expired');
}
return true;
}
}
class FraudCheckValidator extends PaymentValidator
{
protected function check(array $paymentData): bool
{
// Check against fraud detection service
$isFraudulent = false; // Fraud detection logic
if ($isFraudulent) {
throw new \RuntimeException('Payment flagged as fraudulent');
}
return true;
}
}
// Build validation chain
class PaymentValidationService
{
public function validate(array $paymentData): bool
{
$amountValidator = new AmountValidator();
$cardValidator = new CardValidator();
$cvvValidator = new CvvValidator();
$expiryValidator = new ExpiryValidator();
$fraudValidator = new FraudCheckValidator();
// Chain validators
$amountValidator
->setNext($cardValidator)
->setNext($cvvValidator)
->setNext($expiryValidator)
->setNext($fraudValidator);
// Start validation chain
return $amountValidator->validate($paymentData);
}
}
Laravel Pipeline Pattern
Laravel's Pipeline is an elegant implementation of Chain of Responsibility, commonly used for middleware.
use Illuminate\Pipeline\Pipeline;
class ImageProcessor
{
public function process(string $imagePath, array $operations): string
{
return app(Pipeline::class)
->send($imagePath)
->through($operations)
->then(fn($image) => $image);
}
}
// Operations (pipe stages)
class ResizeImage
{
public function handle(string $image, \Closure $next)
{
// Resize logic
$resized = $this->resize($image, 800, 600);
return $next($resized);
}
}
class AddWatermark
{
public function handle(string $image, \Closure $next)
{
// Watermark logic
$watermarked = $this->addWatermark($image);
return $next($watermarked);
}
}
class CompressImage
{
public function handle(string $image, \Closure $next)
{
// Compression logic
$compressed = $this->compress($image, 85);
return $next($compressed);
}
}
// Usage
$processor = new ImageProcessor();
$result = $processor->process('image.jpg', [
ResizeImage::class,
AddWatermark::class,
CompressImage::class,
]);
- Subject: BlogPost with publish() method
- Observers: EmailSubscribers, SocialMediaPublisher, SearchIndexer, CacheClearer
- Notify all observers when post is published
- Base component: Query that returns results
- Decorators: CacheDecorator, LoggingDecorator, ProfilingDecorator, RetryDecorator
- Allow chaining multiple decorators
- Validators: AuthCheck, RateLimitCheck, InputSanitization, PermissionCheck
- Each validator either passes or throws exception
- Build a reusable validation chain