Observer Pattern

13.03.2020 | PHP

Um ein Veröffentlichungs-/Abonnementverhalten für ein Objekt zu implementieren, werden die angefügten "Beobachter" benachrichtigt, wenn ein "Subject"-Objekt seinen Status ändert.

Es wird verwendet, um die Menge der gekoppelten Objekte zu verkürzen und verwendet stattdessen lose Kopplung.

Gutes Beispiel sind Logger, die bei speziellen Events getriggert werden sollen.

<?php

namespace Hli\dev\ObserverPattern\Example;

// allowed Events
enum Event{
case Init;
case Created;
case Notified;
case Deleted;
case Updated;
case All;
}

/**
 * The UserRepository represents a Subject. Various objects are interested in
 * tracking its internal state, whether it's adding a new user or removing one.
 */
class UserRepository implements \SplSubject
{
    /**
     * @var array The list of users.
     */
    private $users = [];

    /**
     * @var array
     */
    private $observers = [];

    public function __construct()
    {
        // A special event group for observers that want to listen to all events.
        $this->observers[Event::All->name] = [];
    }

    private function initEventGroup(Event $event = Event::All): void
    {
        if (!isset($this->observers[$event->name])) {
            $this->observers[$event->name] = [];
        }
    }

    private function getEventObservers(Event $event = Event::All): array
    {
        $this->initEventGroup($event);
        $group = $this->observers[$event->name];
        $all = $this->observers[Event::All->name];

        return array_merge($group, $all);
    }

    public function attach(\SplObserver $observer, Event $event = Event::All): void
    {
        $this->initEventGroup($event);

        $this->observers[$event->name][] = $observer;
    }

    public function detach(\SplObserver $observer, Event $event = Event::All): void
    {
        foreach ($this->getEventObservers($event) as $key => $s) {
            if ($s === $observer) {
                unset($this->observers[$event][$key]);
            }
        }
    }

    public function notify(Event $event = Event::All, $data = null): void
    {
        echo __METHOD__  . "  Broadcasting the $event->name event.\n";
        foreach ($this->getEventObservers($event) as $observer) {
            $observer->update($this, $event, $data);
        }
    }

    // Here are the methods representing the business logic of the class.

    public function initialize($filename): void
    {
        //$filename wird fuer dieses Beispiel nicht benötigt
        echo __METHOD__  . "  Loading user records from a file.\n";
        $this->notify(Event::Init, $filename);
    }

    public function createUser(array $data): User
    {
        echo __METHOD__  . "  Creating a user.\n";

        $user = new User();
        $user->update($data);

        $id = bin2hex(openssl_random_pseudo_bytes(16));
        $user->update(["id" => $id]);
        $this->users[$id] = $user;

        $this->notify(Event::Created, $user);

        return $user;
    }

    public function updateUser(User $user, array $data): User
    {
        echo __METHOD__  . "  Updating a user.\n";

        $id = $user->attributes["id"];
        if (!isset($this->users[$id])) {
            return null;
        }

        $user = $this->users[$id];
        $user->update($data);

        $this->notify(Event::Updated, $user);

        return $user;
    }

    public function deleteUser(User $user): void
    {
        echo __METHOD__  . "  Deleting a user.\n";

        $id = $user->attributes["id"];
        if (!isset($this->users[$id])) {
            return;
        }

        unset($this->users[$id]);

        $this->notify(Event::Deleted, $user);
    }
}

/**
 * Let's keep the User class trivial since it's not the focus of our example.
 */
class User
{
    public $attributes = [];

    public function update($data): void
    {
        $this->attributes = array_merge($this->attributes, $data);
    }
}

/**
 * This Concrete Component logs any events it's subscribed to.
 */
class Logger implements \SplObserver
{
    private $filename;

    public function __construct($filename)
    {
        $this->filename = $filename;
        if (file_exists($this->filename)) {
            unlink($this->filename); // deleted if exists, every run creates a fresh log.txt, will never exceed 3 rows for this example
        }
    }

    public function update(\SplSubject $repository, Event $event = null, $data = null): void
    {
        $entry = date("Y-m-d H:i:s") . ": Event::$event->name with data '" . json_encode($data) . "'\n";
        file_put_contents($this->filename, $entry, FILE_APPEND);

        echo __METHOD__." has written Event::$event->name entry to the log.\n";
        return;
    }
}

/**
 * This Component sends notification about new user creation to admin email. The client
 * is responsible for attaching this component to a proper user creation event.
 */
class OnboardingNotification implements \SplObserver
{
    private $adminEmail;

    public function __construct($adminEmail)
    {
        $this->adminEmail = $adminEmail;
    }

    public function update(\SplSubject $repository, Event $event = null, $data = null): void
    {
        // mail($this->adminEmail, "Onboarding required",  "We have a new user. Here's his info: " .json_encode($data));
        echo __METHOD__." $event->name notification has been emailed to $this->adminEmail. New User: ". json_encode($data)."\n";
        return ;
    }
}

/**
 * The client code.
 */

$repository = new UserRepository();
$repository->attach(new Logger(__DIR__ . "/log.txt"), Event::All);
// special Observer for user created events only
$repository->attach(new OnboardingNotification("1@example.com"), Event::Created);

$repository->initialize(__DIR__ . "/users.csv"); // for this example this file is not required 

// creating new user
$user = $repository->createUser([
    "name" => "John Smith",
    "email" => "john99@example.com",
]);

$repository->deleteUser($user);

Modifiziertes Beispiel von refactoring.guru

Analyse

Entwurf

Development

Launch