Search code examples
phpphp-7.4

Convert array of Callables into one Callable


How to create a callback function that has multiple callback functions from an array:

$fn = function() { echo '1';};
$fn2 = function() { echo '2';};

$array = [
    $fn,
    $fn2
];

$callback = ... $array; // Calls first $fn then $fn2.

Bigger context:

I am using some library where some class has a callback function as a property, this refers to a function that can be executed before the actual operation.

public function before(callable $fn)
{
    $this->before = $fn;

    return $this;
}

By default, for my work, I fill it with a certain function, so you can't add another one.

Due to the fact that the class has $this->before and few key methods privately created, I am not able to overwrite by my own classes and I unfortunately it is a third-party library and I can't make changes to it

I came up with the idea of overriding the class and the main method that is used to set this callback so that my class will have an array, and at the point of adding the callback function before calling the parent, I will create one callback function from all the functions added to the array.

/**
 * @var callable[]
 */
private array $beforeCallbacks = [];

public function before(callable $fn): ChildrenClass
{
    $this->beforeCallbacks[] = $fn;
    
    foreach ($this->beforeCallbacks as $callback) {
        if (!isset($newCallback)) {
            $newCallback = $callback;
        }
        $newCallback .= $callback;  // As you can guess, it doesn't work:C
    }
    return parent::before($newCallback); 
}

Any suggestions?

I wonder if that's even possible. And what if I wanted to inject a parameter into each function, is there any way to handle this?


Solution

  • One option is to wrap your callbacks in a structure that can handle calling multiple and in the order you want. The version below uses __invoke but you could do whatever callable syntax for PHP that you want.

    class MultipleCaller {
        private $callbacks = [];
        public function addCallback(callable $fn) {
            $this->callbacks[] = $fn;
        }
        
        public function __invoke() {
            foreach($this->callbacks as $callback) {
                $callback();
            }
        }
    }
    
    $mc = new MultipleCaller();
    $mc->addCallback(static function () { echo 1, PHP_EOL; } );
    $mc->addCallback(static function () { echo 2, PHP_EOL; } );
    
    $mc();
    

    edit

    Yes, arguments can be passed. One option is to use ... to pass things through

    class MultipleCaller {
        private $callbacks = [];
        public function addCallback(callable $fn) {
            $this->callbacks[] = $fn;
        }
        
        public function __invoke(...$args) {
            foreach($this->callbacks as $callback) {
                $callback(...$args);
            }
        }
    }
    
    $mc = new MultipleCaller();
    $mc->addCallback(static function (...$args) { echo 'Function 1', PHP_EOL, var_dump($args), PHP_EOL; } );
    $mc->addCallback(static function (...$args) { echo 'Function 2', PHP_EOL, var_dump($args), PHP_EOL; } );
    
    function doWork(callable $fn, ...$args) {
        $fn(...$args);
    }
    
    doWork($mc, 'alpha', 'beta');
    

    Demo: https://3v4l.org/TGdJq

    func_get_args could also be used in a similar fashion

    edit 2

    The magic __invoke can be skipped, too, if you'd rather have a more explicit method to call. You could then use [$mc, 'invoke'] or the more modern $mc->invoke(...) syntax.

    <?php
    
    class MultipleCaller {
        private $callbacks = [];
        public function addCallback(callable $fn) {
            $this->callbacks[] = $fn;
        }
        
        public function invoke(...$args) {
            foreach($this->callbacks as $callback) {
                $callback(...$args);
            }
        }
    }
    
    $mc = new MultipleCaller();
    $mc->addCallback(static function (...$args) { echo 'Function 1', PHP_EOL, var_dump($args), PHP_EOL; } );
    $mc->addCallback(static function (...$args) { echo 'Function 2', PHP_EOL, var_dump($args), PHP_EOL; } );
    
    function doWork(callable $fn, ...$args) {
        $fn(...$args);
    }
    
    doWork([$mc, 'invoke'], 'alpha', 'beta');
    doWork($mc->invoke(...), 'alpha', 'beta');
    

    Demo: https://3v4l.org/Zd1De#v8.2.2