Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use SplObserver for Hook system?

Tags:

php

I code a class for Hook system. But this is outdated. I want to use splObserver to code it.

<?php
class Event
{
    private static $filters = [];
    private static $actions = [];

    public static function addAction($name, $callback, $priority = 10)
    {
        if (! isset(static::$actions[$name])) {
            static::$actions[$name] = [];
        }

        static::$actions[$name][] = [
            'priority' => (int)$priority,
            'callback' => $callback,
        ];
    }

    public function doAction($name, ...$args)
    {
        $actions = isset(static::$actions[$name]) ? static::$actions[$name] : false;

        if (! $actions) {
            return;
        }

        // sort actions by priority
        $sortArr = array_map(function ($action) {
            return $action['priority'];
        }, $actions);

        \array_multisort($sortArr, $actions);

        foreach ($actions as $action) {
            \call_user_func_array($action['callback'], $args);
        }
    }
}

Event::addAction('action1', function(){
   echo 'balabala1';
});
Event::addAction('action1', function(){
   echo 'balabala2';
});
Event::doAction('action1');

Output: balabala1 balabala2

It works good. I want to use SplObserver to re-code it and try to code but no idea.

like image 259
Km Van Avatar asked Mar 20 '26 04:03

Km Van


1 Answers

I don't really know whether this implementation could be useful in a real life application or not but, for the sake of answering your question, here we go...

Let's imagine we have a User class that we'd like to hook with our custom functions.

First, we create a reusable trait containing the Subject logic, capable of managing "event names" to whom we can hook our actions.

trait SubjectTrait {

    private $observers = [];

    // this is not a real __construct() (we will call it later)
    public function construct()
    {
        $this->observers["all"] = [];
    }

    private function initObserversGroup(string $name = "all")
    {
        if (!isset($this->observers[$name])) {
            $this->observers[$name] = [];
        }
    }

    private function getObservers(string $name = "all")
    {
        $this->initObserversGroup($name);
        $group = $this->observers[$name];
        $all = $this->observers["all"];

        return array_merge($group, $all);
    }

    public function attach(\SplObserver $observer, string $name = "all")
    {
        $this->initObserversGroup($name);
        $this->observers[$name][] = $observer;
    }

    public function detach(\SplObserver $observer, string $name = "all")
    {
        foreach ($this->getObservers($name) as $key => $o) {
            if ($o === $observer) {
                unset($this->observers[$name][$key]);
            }
        }
    }

    public function notify(string $name = "all", $data = null)
    {
        foreach ($this->getObservers($name) as $observer) {
            $observer->update($this, $name, $data);
        }
    }
}

Next, we use the trait in our SplSubject User class:

class User implements \SplSubject
{

    // It's necessary to alias construct() because it
    // would conflict with other methods.
    use SubjectTrait {
        SubjectTrait::construct as protected constructSubject;
    }

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

    public function create()
    {
        // User creation code...

        $this->notify("User:created");
    }

    public function update()
    {
        // User update code...

        $this->notify("User:updated");
    }

    public function delete()
    {
        // User deletion code...

        $this->notify("User:deleted");
    }
}

The last step is to implement a reusable SplObserver. This observer is able to bind himself to a Closure (anonymous function).

class MyObserver implements SplObserver
{
    protected $closure;

    public function __construct(Closure $closure)
    {
        $this->closure = $closure->bindTo($this, $this);
    }

    public function update(SplSubject $subject, $name = null, $data = null)
    {
        $closure = $this->closure;
        $closure($subject, $name, $data);
    }
}

Now, the test:

$user = new User;

// our custom functions (Closures)
$function1 = function(SplSubject $subject, $name, $data) {
    echo $name . ": function1\n"; // we could also use $data here
};

$function2 = function(SplSubject $subject, $name, $data) {
    echo $name . ": function2\n";
};

// subscribe the first function to all events
$user->attach(new MyObserver($function1), 'all');
// subscribe the second function to user creations only
$user->attach(new MyObserver($function2), 'User:created');

// run a couple of methods to see what happens
$user->create();
$user->update();

The output will be:

User:created: function2
User:created: function1
User:updated: function1

NOTE: we could use SplObjectStorage instead of an array, to store observers in the trait.

like image 101
Gustavo Avatar answered Mar 21 '26 21:03

Gustavo



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!