facebook youtube pinterest twitter reddit whatsapp instagram

PHP Pluggable and Modular System - Part 1 (Abstract View) [Observable and Mediator Pattern]

If there is one thing that is brutally confusing to me ever since I started coding, it would be a way to not only implement a Modular system but a way to make it extensible (adding and removing plugins, yes, visually).

For example, ClassicPress/Wordpress or Drupal are extensible by the way of Hooks, Actions, and Filters.

Most if not all of 'em use a form of Mediator Pattern but some people might describe the pattern as Observable (spoiler alert: WordPress is not an observer pattern, you'll learn more below), it is not exactly the same thing but there are similarities.

Understanding these patterns can be really powerful and knowing when to use one over the other might be helpful, so, before we go into any implementation, let's look at the differences:

Observable Pattern

The observable pattern is as simple as making multiple objects notifiable when a change occurs to a single object (e.g, when a change occurs to that specific object, all other objects that depend on it would be notified, simple), so, this is a one-to-many-dependency between objects.

PHP Standard Library provides interfaces around the observable pattern. We have the SplSubject and SplObserver:

SplSubject Interface:

interface SplSubject  {

        /**
         * Attach an SplObserve
         * @param SplObserver $observer
         * @return void 
         */
        public function attach (SplObserver $observer);

        /**
         * Detach an observer
         * @param SplObserver $observe
         * @return void
         */
        public function detach (SplObserver $observer);

        /**
         * Notify an observer
         * @return void 
         */
        public function notify ();

}

SplObserver Interface

interface SplObserver  {

        /**
         * Receive update from subject
         * @param SplSubject $subject
         * @return void 
         */
        public function update (SplSubject $subject);

}

In the Interfaces, we have four methods which are:

  • attach() - It provides a way to attach or register an observer(subscriber or listeners) with a subject (publisher)
  • detach() - as you might have guessed, is used to detach a listener or an observer from a publisher (the subject they are listening to).
  • notify() - This notifies all subscribed listeners or observers that something has changed which then calls the update() on each observer.

To keep things simple, I have created the following:

<?php
namespace App\Library;

use SplObserver;
use SplSubject;

class EventOne implements \SplSubject
{
    private array $listeners = [];
    protected string $currentPrice = '$100';


    /**
     * @inheritDoc
     */
    public function attach(SplObserver $observer)
    {
        // TODO: Implement attach() method.
         $this->listeners[] = $observer;
    }

    /**
     * @inheritDoc
     */
    public function detach(SplObserver $observer)
    {
        // TODO: Implement detach() method.
        foreach($this->listeners as $key => $val) {
            if ($val == $observer) {
                unset($this->listeners[$key]);
            }
        }
    }

    /**
     * @inheritDoc
     */
    public function notify()
    {
        // TODO: Implement notify() method.
        foreach ($this->listeners as $observer) {
            $observer->update($this);
        }
    }

    function updateCurrentPrice($newPrice) {
        $this->currentPrice = $newPrice;
    }

    function getCurrentPrice() {
        return $this->currentPrice;
    }
}

class ListenerOne implements SplObserver {
    public function update(SplSubject $subject) {
        dump("I have the current price list, which is {$subject->getCurrentPrice()}");
    }
}

class ListenerTwo implements SplObserver {
    public function update(SplSubject $subject) {
        dump("I also have the current price list, which is {$subject->getCurrentPrice()}");
    }
}

class ListenerThree implements SplObserver {
    public function update(SplSubject $subject) {
        dump("Haha, me also have the price ;) {$subject->getCurrentPrice()}");
    }
}

We can use it like so:

$subject = new EventOne();
$subject->attach(new ListenerOne);
$subject->attach(new ListenerTwo);
$subject->attach(new ListenerThree);
$subject->notify();

// Ouput
"I have the current price list, which is $100"
"I also have the current price list, which is $100"
"Haha, me also have the price ;) $100"

As you can see, I have three listener or observers that is attached to a single subject, I like to think of the subject as the Event, this makes it is easier to understand.

If you have experienced using Event in JS, then you can quickly grab this, for example, the click event would listen for a click, and if a click occurs, it notifies the listener or listeners listening to that event. It is as simple as that).

In our case, all the listener would get anything that occurs in EventOne, here is where it gets interesting, I can update the EventOne currentPrice property, and all of the listeners would be notified:

$subject = new EventOne();
$subject->attach(new ListenerOne);
$subject->attach(new ListenerTwo);
$subject->attach(new ListenerThree);
$subject->updateCurrentPrice('$5000'); // <--- here
$subject->notify();

// Output:
"I have the current price list, which is $5000"
"I also have the current price list, which is $5000"
"Haha, me also have the price ;) $5000"

I can also detach (remove an object that is observing) a listener:

$subject = new EventOne();
$subject->attach(new ListenerOne);
$subject->attach(new ListenerTwo);
$subject->attach(new ListenerThree);
$subject->updateCurrentPrice('$5000');
$subject->detach(new ListenerThree); // <----- here
$subject->notify();

//Output:
"I have the current price list, which is $5000"
"I also have the current price list, which is $5000"

Now, think about creating multiple subjects(multiple events) that do several things, e.g you can have an event that notifies certain listeners when a post is published, you can have another one that notifies certain observers or listeners when a post is deleted.

Most of the time though, this event would pass along the data to the listeners which the listener can then perform more processing on the data.

While we have a somewhat loose coupling between the subject and the observer, there are a couple of things to note when using the observable pattern (whether this is a drawback depends on what you are using it for):

  • There is no bi-directional communication, the publisher or the subject simply notifies the listener of changes and that's it. A radio station doesn't care about who is listening but as long as you are tuned into that specific radio frequency, you'll be able to listen to whatever is playing(changes).
  • Unintuitive when used for an event-driven system as each listener would need to bind to the object they want to listen to, a better way to circumvent this is using the...

Mediator Pattern

By using the Mediator Pattern, we can control how the observers are notified, that is you would have a central manager or object that controls what listeners would be attached to a particular event or events, the good thing about this approach is you no longer need to implement the notify functionality in the subject no more, it would be handled by the mediator.

The reason why I love the Mediator pattern is the fact that it ceases direct binding between the observable (the subject) and the observers (the listeners).

It controls the flow of how they communicate, this way, the components depend only on a single mediator class instead of being bound to several things.

For example, when using the Observable Pattern, and you have an EventOne object that you want several listeners to listen to, the listener would have to register to that object.

If you have an EventTwo object that you want a listener to listen to, the listeners or the observers would also have to register to that specific object, you can see how this can cause a mess down the line, the remedy to this is simply having something that controls the flow.

This is why I said above that WordPress isn't an Observable Pattern, it is a procedural version of the Mediator Pattern (yeah, anything achievable in OO can also be done in procedural and vice versa, it's a matter of does it makes sense to do it in one or the other).

In WordPress, add_action is used to subscribe to an event, for example, if you hook into the "activated_plugin" event then whatever function you defined would be fired when a plugin has been activated:

add_action('activated_plugin', 'after_plugin_activated', 10, 2);
function after_plugin_activated($plugin, $network_wide) {
   // do some processing....
}

Somewhere in the WordPress core, there is a do_action call for the "activated_plugin" event that accepts a callback function that would fire all the listeners listening to such event, if this was an Observable pattern, then the listener would directly bind to the "activated_plugin" hook, so, you can see this is an example of the Mediator Pattern since there is a central manager controlling the flow.

However, WordPress uses the global $wp_filter variable to store all of its events or hooks or whatever, which is really messy, this is an example of God Object, it does way too much and knows too much.

I have already implemented the event system in my project before realizing this is what WordPress was also doing, messy or no messy, it works well for WordPress, the approach I followed not only works well for my project but it's also maintainable.

We can do better, so, here is a simple implementation of the Mediator Pattern:

class Mediator {

    protected array $events = [];

    /**
     * @param $eventName
     * @param $callback
     * @return Mediator
     */
    public function attachListener($eventName, $callback): Mediator
    {
        $this->events[$eventName][] = $callback;
        return $this;
    }

    /**
     * @param string $eventName
     * @return Mediator
     */
    public function removeListener(string $eventName): Mediator
    {

        if (array_key_exists($eventName, $this->events)) {
            unset($this->events[$eventName]);
        }

        return $this;
    }

    /**
     * @param string $event
     * @return iterable
     */
    public function getListenersForEvent(string $event): iterable
    {
        if (array_key_exists($event, $this->events)) {
            return $this->events[$event];
        }

        return [];
    }

    /**
     * @param string $event
     * @return string
     */
    public function dispatch(string $event): string
    {

        foreach ($this->getListenersForEvent($event) as $listener) {
            $listener($event);
        }
        return $event;
    }
}

Here is the way it works:

  • The attachListener accepts two arguments, the eventname and the callback you want to call when the event is dispatched(fired)
  • The removeListener simply removes an event with all listeners listening to the event
  • getListenersForEvent gets all listeners of a particular event
  • dispatch simply fires all listeners listening to the event

Usage is as follows:

$Mediator = (new Mediator())
    ->attachListener('callPolice', function() { dump("Person One Calls Police"); })
    ->attachListener('callPolice', function() { dump("Person Two Calls Police"); })
    ->attachListener('callPolice', function() { dump("Person Three Calls Police"); })

    ->attachListener('publishPost', function() { dump("I'll Be Called, When a Post is Published"); })
    ->attachListener('publishPost', function() { dump("I'll Also Be Called When a Post is Published"); })
    ->attachListener('publishPost', function() { dump("Call me, When a Post is Published"); });

$Mediator->dispatch('callPolice'); // Output: "Person One Calls Police"
"Person Two Calls Police"
"Person Three Calls Police"

As you can see, I am attaching the listeners to the event with a Closure (anonymous function), you might be surprised why the "publishPost" event didn't get triggered even when we attached it alongside other events, this is because I was only dispatching the "callPolice" event, so, to dispatch the "publishPost" event, you need to actually call it. The Mediator controls the flow between the two parties, to remove events with its associated listeners is as simple as doing:

$Mediator->removeListener('callPolice');

I can even remove multiple events doing:

$Mediator->removeListener('callPolice')->removeListener('publishPost');

This is possible because I am returning the current object instance the method is being called on, this is also why I could attach multiple listeners in a go.

That aside, the issue with the Mediator Pattern is that it often knows a lot, which over time can become hard to maintain as your list of events and listeners grows (remember, we don't want it to be a God object), a solution to this is separating your code into modules e.g you can have Post, Menu and Widget Module.

Each module would have its own event and listeners. You would still be using the same Mediator, but it should only know about the context of the Module it's currently working on.

In the next part, I'll go over the implementation of the actual system in PHP using the Mediator Pattern:

Footnotes:

Related Post(s)

  • Laravel - Edit and Delete Data (+ User Authentication)

    In this guide, you'll learn how to edit and delete post data in Laravel, before proceeding you should read the previous guides as this guide would be the continuation, here are the previous guides:

  • Guide To Laravel - Model and Database Migrations

    I don't know if you have read my guide on Creating a Tiny PHP MVC Framework From Scratch where we create a tiny MVC framework in the hope of understanding the concepts of how major frameworks imple

  • Building Dependency Injection and The Container from Scratch in PHP

    In this guide, you'll learn about dependency injection and a way to build a simple DIC (Dependency Injection Container) in PHP using PSR-11 from scratch. First, What is an Ordinary Dependency? This

  • Creating a Tiny PHP MVC Framework From Scratch

    In this guide, we would go over creating a tiny PHP MVC Framework, this would sharpen your knowledge on how major frameworks (e.g Codeigniter or Laravel) works in general. I believe if you can unders

  • PHP Pluggable and Modular System – Part 2 (Implementation) [Event Dispatcher]

    In the first series of this guide, we discussed the theoretical aspect of building a pluggable system in PHP, I wrote a bit of code in that guide plus a couple of stuff you should avoid, you can lear

  • Best Way To Implement a Non-Breaking Friendly URL In PHP or Laravel or Any Language

    I was working on the link structure of my new Laravel app, and out of the blue I said: "What would happen if a user changes the slug of a post?" First Attempt - 301 Redirection The first solution I t