Skip to Content
Course content

78: The Observer Pattern in PHP

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen in almost every mid-sized project I've inherited: a class that started simple but slowly mutated into a "God Object." It usually happens when you have a core business action that needs to trigger five other unrelated things across your system.

Take a look at this Order class. It looks fine at first glance, but it's actually a maintenance nightmare waiting to happen.

class Order {
    private $status = 'pending';
    private $emailService;
    private $inventoryManager;
    private $shippingDepartment;

    public function __construct($emailService, $inventoryManager, $shippingDepartment) {
        $this->emailService = $emailService;
        $this->inventoryManager = $inventoryManager;
        $this->shippingDepartment = $shippingDepartment;
    }

    public function shipOrder() {
        $this->status = 'shipped';
        
        // Triggering side effects
        $this->emailService->sendOrderConfirmation();
        $this->inventoryManager->reduceStock();
        $this->shippingDepartment->generateLabel();
        
        echo "Order has been shipped!";
    }
}

The Tight Coupling Trap

The "bug" here isn't a syntax error; it's a design flaw. The Order class knows way too much. It knows that when an order ships, the email service needs to run, the inventory needs to drop, and the shipping label needs to be printed.

What happens next week when the marketing team tells you they also want to trigger a "Customer Loyalty Point" update whenever an order ships? You have to go back into the Order class, inject a LoyaltyService into the constructor, and add another line to the shipOrder method. You're violating the Open/Closed Principle: the class is open for modification (which is dangerous) rather than open for extension.

Decoupling with SplSubject and SplObserver

PHP provides built-in interfaces for the Observer pattern: SplSubject and SplObserver. Instead of the Order class calling specific services, it simply announces, "Hey, I've changed!" and lets any registered "observers" decide how to react.

Here is how we refactor that mess into something professional.

class Order implements SplSubject {
    private $observers = [];
    public $status = 'pending';

    public function attach(SplObserver $observer): void {
        $this->observers[] = $observer;
    }

    public function detach(SplObserver $observer): void {
        $this->observers = array_filter($this->observers, fn($o) => $o !== $observer);
    }

    public function notify(): void {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function shipOrder() {
        $this->status = 'shipped';
        echo "Order status updated to shipped. Notifying observers...\n";
        $this->notify();
    }
}

class EmailNotifier implements SplObserver {
    public function update(SplSubject $subject): void {
        if ($subject->status === 'shipped') {
            echo "EmailNotifier: Sending shipment confirmation email.\n";
        }
    }
}

class InventoryNotifier implements SplObserver {
    public function update(SplSubject $subject): void {
        if ($subject->status === 'shipped') {
            echo "InventoryNotifier: Reducing stock levels.\n";
        }
    }
}

// Usage
$order = new Order();
$order->attach(new EmailNotifier());
$order->attach(new InventoryNotifier());

$order->shipOrder();

Why this actually solves the problem

Notice that the Order class no longer knows that EmailNotifier or InventoryNotifier even exist. It only knows that it has a list of SplObserver objects.

If the marketing team comes back and wants those loyalty points, you don't touch the Order class at all. You just create a new LoyaltyNotifier class that implements SplObserver and attach it at runtime. I love this approach because it isolates your business logic from your side effects. If the email service crashes or needs to be replaced with a different provider, your core Order logic remains untouched and untested.

One quick tip: in a real-world production app, you'd likely use a Dependency Injection container or an Event Dispatcher (like the one in Symfony) to handle the "attaching" part, but under the hood, it's still this exact Observer pattern.




📋 Practical Task

Build a User Account Security Monitor

Your task is to implement the Observer pattern to monitor security events on a user account.

Requirements:

  • Create a UserAccount class that implements SplSubject. It should have a method changePassword($newPassword).
  • Create a SecurityLogObserver that implements SplObserver. When notified, it should print: "Security Log: Password changed for user [username]."
  • Create an EmailAlertObserver that implements SplObserver. When notified, it should print: "Email Alert: A password change was detected. If this wasn't you, contact support."
  • Instantiate the account, attach both observers, and trigger a password change to verify that both systems react without the UserAccount class knowing the details of the logging or emailing systems.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.