Advanced Laravel

Design Patterns: Observer & Decorator

18 min Lesson 24 of 40

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.

Key Components:
  • 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.

Use Cases: Adding features to objects at runtime, when subclassing would create an explosion of classes, middleware systems, logging/caching wrappers.

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.

Laravel Middleware: Laravel's middleware system is a perfect example of the Chain of Responsibility pattern.
// 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,
]);
Exercise 1: Create an Observer pattern for a blog post system:
  • Subject: BlogPost with publish() method
  • Observers: EmailSubscribers, SocialMediaPublisher, SearchIndexer, CacheClearer
  • Notify all observers when post is published
Exercise 2: Build a Decorator system for database queries:
  • Base component: Query that returns results
  • Decorators: CacheDecorator, LoggingDecorator, ProfilingDecorator, RetryDecorator
  • Allow chaining multiple decorators
Exercise 3: Implement Chain of Responsibility for request filtering:
  • Validators: AuthCheck, RateLimitCheck, InputSanitization, PermissionCheck
  • Each validator either passes or throws exception
  • Build a reusable validation chain
Pattern Relationships: Observer decouples subjects from observers. Decorator adds responsibilities without inheritance. Chain of Responsibility creates a processing pipeline. All three patterns increase flexibility and maintainability.