The correct way of doing delegates or callbacks in PHP
Asked Answered
S

3

12

I need to implement the following pattern in php:

class EventSubscriber
{
    private $userCode;
    public function __construct(&$userCode) { $this->userCode = &$userCode; }
    public function Subscribe($eventHandler) { $userCode[] = $eventHandler; }
}

class Event
{
    private $subscriber;
    private $userCode = array();

    public function __construct()
    {
        $this->subscriber = new Subscriber($this->userCode)
    }

    public function Subscriber() { return $this->subscriber; }

    public function Fire()
    {
        foreach ($this->userCode as $eventHandler)
        {
            /* Here i need to execute $eventHandler  */
        }
    }
}

class Button
{
    private $eventClick;

    public function __construct() { $this->eventClick = new Event(); }

    public function EventClick() { return $this->eventClick->Subscriber(); }

    public function Render()
    {
        if (/* Button was clicked */) $this->eventClick->Fire();

        return '<input type="button" />';
    }
}

class Page
{
    private $button;

    // THIS IS PRIVATE CLASS MEMBER !!!
    private function ButtonClickedHandler($sender, $eventArgs)
    {
        echo "button was clicked";
    }

    public function __construct()
    {
        $this->button = new Button();
        $this->button->EventClick()->Subscribe(array($this, 'ButtonClickedHandler'));
    }

    ...

}    

what is the correct way to do so.

P.S.

I was using call_user_func for that purpose and believe it or not it was able to call private class members, but after few weeks of development i've found that it stopped working. Was it a bug in my code or was it some something else that made me think that 'call_user_func' is able call private class functions, I don't know, but now I'm looking for a simple, fast and elegant method of safely calling one's private class member from other class. I'm looking to closures right now, but have problems with '$this' inside closure...

Strage answered 3/1, 2011 at 11:54 Comment(4)
$this is not supported in Closures (as mentioned in the manual) and private members are private, because they the one, who declared them as private, dont want them to be called from any object from a different class. This is the definition of "private member" ;)Vibrate
Yes, private members are private because they are private :) But imagine you're telling your button 'Hey button, execute my private code in "ButtonClickedHandler" when you will be clicked'. In this case I don't see why the button should answer 'Oh, I don't want to execute your code, because it is private, make it public, then I will...' But either I don't see a reason why should I make my private code - public... Hope you get the pointStrage
PHP isn't .Net. Unlike peanut butter and chocolate. PHP won't taste better with these things shoehorned into it. Also, anyone that comes along after you will probably toss out your code and rebuild it all a sane way.Pelvis
Agree that php is not .net, but there is nothing bad about having an OOP wrapper for php, nobody forced to use it. I just can't stand spaghettis that php encourages developers to do.Strage
S
5

Anyway, if someone's interested, I've found the only possible solution via ReflectionMethod. Using this method with Php 5.3.2 gives performance penalty and is 2.3 times slower than calling class member directly, and only 1.3 times slower than call_user_func method. So in my case it is absolutely acceptable. Here's the code if someone interested:

class EventArgs {

}

class EventEraser {

    private $eventIndex;
    private $eventErased;
    private $eventHandlers;

    public function __construct($eventIndex, array &$eventHandlers) {
        $this->eventIndex = $eventIndex;
        $this->eventHandlers = &$eventHandlers;
    }

    public function RemoveEventHandler() {
        if (!$this->eventErased) {
            unset($this->eventHandlers[$this->eventIndex]);

            $this->eventErased = true;
        }
    }

}

class EventSubscriber {

    private $eventIndex;
    private $eventHandlers;

    public function __construct(array &$eventHandlers) {
        $this->eventIndex = 0;
        $this->eventHandlers = &$eventHandlers;
    }

    public function AddEventHandler(EventHandler $eventHandler) {
        $this->eventHandlers[$this->eventIndex++] = $eventHandler;
    }

    public function AddRemovableEventHandler(EventHandler $eventHandler) {
        $this->eventHandlers[$this->eventIndex] = $eventHandler;

        $result = new EventEraser($this->eventIndex++, $this->eventHandlers);

        return $result;
    }

}

class EventHandler {

    private $owner;
    private $method;

    public function __construct($owner, $methodName) {
        $this->owner = $owner;
        $this->method = new \ReflectionMethod($owner, $methodName);
        $this->method->setAccessible(true);
    }

    public function Invoke($sender, $eventArgs) {
        $this->method->invoke($this->owner, $sender, $eventArgs);
    }

}

class Event {

    private $unlocked = true;
    private $eventReceiver;
    private $eventHandlers;
    private $recursionAllowed = true;

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

    public function GetUnlocked() {
        return $this->unlocked;
    }

    public function SetUnlocked($value) {
        $this->unlocked = $value;
    }

    public function FireEventHandlers($sender, $eventArgs) {
        if ($this->unlocked) {
            //защита от рекурсии
            if ($this->recursionAllowed) {
                $this->recursionAllowed = false;

                foreach ($this->eventHandlers as $eventHandler) {
                    $eventHandler->Invoke($sender, $eventArgs);
                }

                $this->recursionAllowed = true;
            }
        }
    }

    public function Subscriber() {
        if ($this->eventReceiver == null) {
            $this->eventReceiver = new EventSubscriber($this->eventHandlers);
        }

        return $this->eventReceiver;
    }

}
Strage answered 3/1, 2011 at 22:3 Comment(0)
S
5

Callbacks in PHP aren't like callbacks in most other languages. Typical languages represent callbacks as pointers, whereas PHP represents them as strings. There's no "magic" between the string or array() syntax and the call. call_user_func(array($obj, 'str')) is syntactically the same as $obj->str(). If str is private, the call will fail.

You should simply make your event handler public. This has valid semantic meaning, i.e., "intended to be called from outside my class."

This implementation choice has other interesting side effects, for example:

class Food {
    static function getCallback() {
        return 'self::func';
    }

    static function func() {}

    static function go() {
        call_user_func(self::getCallback());  // Calls the intended function
    }
}

class Barf {
    static function go() {
        call_user_func(Food::getCallback());  // 'self' is interpreted as 'Barf', so:
    }                                         // Error -- no function 'func' in 'Barf'
}
Southward answered 3/1, 2011 at 22:18 Comment(1)
Thank you zildjohn01, actually I was using this approach before, the interesting thing how php handles the context, because call_user_func seems to be working with private class methods inside the class it self. But stops working outside. Regarding making my eventHandlers public, I think this is pure evil in terms of OOP, and I was looking for a way to call private class members outside class it self, and this is possible in PHP with the help of ReflectionMethod, look at post below. Anyway thanks for bothering to answer.Strage
G
0

As time passes, there are new ways of achieving this. Currently PSR-14 is drafted to handle this use case.

So you might find any of these interesting: https://packagist.org/?query=psr-14

Geometrician answered 17/1, 2019 at 10:24 Comment(1)
Whooping 8 years to emerge for such a simple concept, I don't know how did everyone lived without it for all this time, proper callbacks feel so natural, like breathingStrage

© 2022 - 2024 — McMap. All rights reserved.