Skip to content

Observer

The Observer Pattern is a behavioral design pattern where multiple objects (Observers) subscribe to a central object (Subject). Whenever that object's state changes, it sends notifications to all subscribed objects so they can react accordingly.


Structure

Role Example Responsibility
Subject WeatherStation Central object that notifies Observers on state change
Abstract Observer WeatherObserver Base class that type-checks the subject before delegating to concrete observers
Observer WebSubscriber, AppSubscriber, AlertSubscriber Receives updates from the WeatherStation

Steps

  1. Create Observer interface
  2. Create Abstract Observer
  3. Create Concrete Observers
  4. Create Concrete Subject
classDiagram
    class SplObserver {
        <<interface>>
        +update(SplSubject)
    }

    class SplSubject {
        <<interface>>
        +attach(SplObserver)
        +detach(SplObserver)
        +notify()
    }

    class WeatherObserver {
        <<abstract>>
        +update(SplSubject)
        #onWeatherChanged(WeatherStation)
    }

    class WebSubscriber {
        #onWeatherChanged(WeatherStation)
    }

    class AppSubscriber {
        #onWeatherChanged(WeatherStation)
    }

    class AlertSubscriber {
        -threshold: float
        -minPressure: float
        #onWeatherChanged(WeatherStation)
    }

    class WeatherStation {
        -observers: SplObjectStorage
        -temperature: float
        -humidity: float
        -pressure: float
        +attach(SplObserver)
        +detach(SplObserver)
        +notify()
        +recordMeasurement(float, float, float)
        +getTemperature() float
        +getHumidity() float
        +getPressure() float
    }

    WeatherObserver ..|> SplObserver
    WeatherStation ..|> SplSubject
    WebSubscriber --|> WeatherObserver
    AppSubscriber --|> WeatherObserver
    AlertSubscriber --|> WeatherObserver
    WeatherStation --> SplObserver : notifies

The subject (WeatherStation) maintains a list of observers (WebSubscriber, AppSubscriber, AlertSubscriber) and notifies them automatically whenever its state changes, without knowing anything about their implementations.


Real-World Example

For a more detailed real-world example, see the WeatherStation implementation below.

The WeatherObserver follows the Template Method design pattern. This avoids repeating validation logic and provides a strongly typed WeatherStation to concrete observers.

In PHP, we can use the SPL library that provides the SplSubject and SplObserver interfaces to implement the Observer design pattern.


WeatherStation

AppSubscriber.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\Observer\WeatherStation;

use SplSubject;
use SplObjectStorage;
use SplObserver;

class WeatherStation implements SplSubject
{
    /** @var SplObjectStorage<SplObserver, null> */
    private SplObjectStorage $observers;
    public function __construct(
        private float $temperature = 0.0,
        private float $humidity = 0.0,
        private float $pressure = 0.0
    ) {
        $this->observers = new SplObjectStorage();
    }

    public function attach(SplObserver $observer): void
    {
        //SplObjectStorage::detach() is deprecated since 8.5
        $this->observers->offsetSet($observer);
    }

    public function detach(SplObserver $observer): void
    {
        $this->observers->offsetUnset($observer);
    }

    public function notify(): void
    {
        /** @var SplObserver $observer */
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    /**
     * @param float $temperature
     * @param float $humidity
     * @param float $pressure
     * @return void
     */
    public function recordMeasurement(
        float $temperature,
        float $humidity,
        float $pressure
    ): void {
        $this->temperature = $temperature;
        $this->humidity = $humidity;
        $this->pressure = $pressure;

        $this->notify();
    }

    /**
     * @return float
     */
    public function getTemperature(): float
    {
        return $this->temperature;
    }
    /**
     * @return float
     */
    public function getHumidity(): float
    {
        return $this->humidity;
    }
    /**
     * @return float
     */
    public function getPressure(): float
    {
        return $this->pressure;
    }
}
AppSubscriber.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\Observer\WeatherStation\Observers;

use DesignPattern\Behavioural\Observer\WeatherStation\WeatherStation;
use SplSubject;
use SplObserver;

/**
 * Base observer using the Template Method pattern.
 *
 * Handles the SplObserver::update() logic and delegates the
 * actual reaction to onWeatherChanged().
 */
abstract class WeatherObserver implements SplObserver
{
    public function update(SplSubject $subject): void
    {
        if ($subject instanceof WeatherStation) {
            $this->onWeatherChanged($subject);
        }
    }

    abstract protected function onWeatherChanged(WeatherStation $station): void;
}
AppSubscriber.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\Observer\WeatherStation\Observers;

use DesignPattern\Behavioural\Observer\WeatherStation\WeatherStation;

class AppSubscriber extends WeatherObserver
{
    protected function onWeatherChanged(WeatherStation $station): void
    {
        printf(
            "[App] %.1f C  %.1f %%  %.1f \n",
            $station->getTemperature(),
            $station->getHumidity(),
            $station->getPressure(),
        );
    }
}
WebSubscriber.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\Observer\WeatherStation\Observers;

use DesignPattern\Behavioural\Observer\WeatherStation\WeatherStation;

class WebSubscriber extends WeatherObserver
{
    protected function onWeatherChanged(WeatherStation $station): void
    {
        printf("[Web] Temperature: %.1f C\n", $station->getTemperature());
        printf("[Web] Humidity: %.1f %%\n", $station->getHumidity());
        printf("[Web] Pressure: %.1f \n", $station->getPressure());
    }
}
AlertSubscriber.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\Observer\WeatherStation\Observers;

use DesignPattern\Behavioural\Observer\WeatherStation\WeatherStation;

class AlertSubscriber extends WeatherObserver
{
    public function __construct(
        private readonly float $threshold = 35.0,
        private readonly float $minPressure = 980.0,
    ) {
    }

    protected function onWeatherChanged(WeatherStation $station): void
    {
        if ($station->getTemperature() > $this->threshold) {
            printf(
                "[Alert] High temperature: %.1f C (threshold: %.1f C)\n",
                $station->getTemperature(),
                $this->threshold,
            );
        }

        if ($station->getPressure() < $this->minPressure) {
            printf(
                "[Alert] Low pressure: %.1f (threshold: %.1f)\n",
                $station->getPressure(),
                $this->minPressure,
            );
        }
    }
}

Tests

WeatherStationTest.php
<?php

declare(strict_types=1);

namespace Tests\Behavioural\Observer\WeatherStation;

use DesignPattern\Behavioural\Observer\WeatherStation\Observers\AppSubscriber;
use DesignPattern\Behavioural\Observer\WeatherStation\WeatherStation;
use DesignPattern\Behavioural\Observer\WeatherStation\Observers\WeatherObserver;
use DesignPattern\Behavioural\Observer\WeatherStation\Observers\WebSubscriber;
use DesignPattern\Behavioural\Observer\WeatherStation\Observers\AlertSubscriber;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(WeatherStation::class)]
class WeatherStationTest extends TestCase
{
    private WeatherStation $station;

    protected function setUp(): void
    {
        $this->station = new WeatherStation();
    }

    /**
     * @return WeatherObserver&object{callCount: int, lastStation: ?WeatherStation}
     */
    private function makeSpyObserver(): WeatherObserver
    {
        return new class extends WeatherObserver {
            public int $callCount = 0;
            public ?WeatherStation $lastStation = null;
            protected function onWeatherChanged(WeatherStation $station): void
            {
                $this->callCount++;
                $this->lastStation = $station;
            }
        };
    }

    public function testUpdatePassesTheCorrectStation(): void
    {
        $observer = $this->makeSpyObserver();

        $this->station->attach($observer);
        $this->station->recordMeasurement(20.0, 50.0, 1010.0);

        self::assertSame($this->station, $observer->lastStation);
    }

    public function testMultipleObserversAreAllNotified(): void
    {
        $first  = $this->makeSpyObserver();
        $second = $this->makeSpyObserver();
        $third  = $this->makeSpyObserver();

        $this->station->attach($first);
        $this->station->attach($second);
        $this->station->attach($third);

        $this->station->recordMeasurement(22.0, 65.0, 1015.0);

        self::assertSame(1, $first->callCount);
        self::assertSame(1, $second->callCount);
        self::assertSame(1, $third->callCount);
    }

    public function testNotifyOncePerRecordMeasurement(): void
    {
        $observer = $this->makeSpyObserver();

        $this->station->attach($observer);

        $this->station->recordMeasurement(20.0, 50.0, 1010.0);
        $this->station->recordMeasurement(21.0, 55.0, 1011.0);
        $this->station->recordMeasurement(22.0, 60.0, 1012.0);

        self::assertSame(3, $observer->callCount);
    }

    public function testMultipleRecordMeasurement(): void
    {
        $observer = self::createMock(WeatherObserver::class);
        $observer->expects(self::exactly(3))->method('update');

        $this->station->attach($observer);

        $this->station->recordMeasurement(20.0, 50.0, 1010.0);
        $this->station->recordMeasurement(21.0, 55.0, 1011.0);
        $this->station->recordMeasurement(22.0, 60.0, 1012.0);
    }

    public function testAppAndWebObservers(): void
    {
        $this->station->attach(new AppSubscriber());
        $this->station->attach(new WebSubscriber());

        $this->station->recordMeasurement(22.5, 60.0, 1013.0);

        $this->expectOutputString(
            "[App] 22.5 C  60.0 %  1013.0 \n" .
            "[Web] Temperature: 22.5 C\n" .
            "[Web] Humidity: 60.0 %\n"  .
            "[Web] Pressure: 1013.0 \n"
        );
    }

    public function testDetachedOneObserver(): void
    {
        $appObserver = new AppSubscriber();

        $this->station->attach($appObserver);
        $this->station->attach(new WebSubscriber());
        $this->station->detach($appObserver);

        $this->station->recordMeasurement(22.5, 60.0, 1013.0);

        $this->expectOutputString(
            "[Web] Temperature: 22.5 C\n" .
            "[Web] Humidity: 60.0 %\n"  .
            "[Web] Pressure: 1013.0 \n"
        );
    }

    public function testAlertObserverOnHighTemperature(): void
    {
        $this->station->attach(new AlertSubscriber(threshold: 35.0, minPressure: 980.0));
        $this->station->recordMeasurement(36.0, 50.0, 1010.0);

        $this->expectOutputString("[Alert] High temperature: 36.0 C (threshold: 35.0 C)\n");
    }

    public function testAlertObserverNoOutput(): void
    {
        $this->station->attach(new AlertSubscriber(threshold: 35.0, minPressure: 980.0));
        $this->station->recordMeasurement(20.0, 50.0, 1010.0);

        $this->expectOutputString('');
    }
}