Vote Details for "PHP_Callback" by cellog

» Details
» Comment
I have been thinking about this proposal quite a bit and have finally come to my own conclusions about it.

Obviously, it will not be accepted as a new package in its current state, so the question is only how to reformulate the idea so that it will be a useful package.

From my end, the extensive wrapping around calling the callback must go. There is no good reason to do this:

<?php
require_once 'PHP/Callback.php';
$a = new PHP_Callback(array('hello', 'there'));
$a->addParameter(1);
$a->addParameter(2);
$a->addParameter(3);
$a->execute();
?>

instead of this:

<?php
call_user_func(array('hello', 'there'), 1, 2, 3);
?>

As I said in my offlist email to you, the primary issue I have experienced when coding with callbacks is improper validation. This means two things:

1) forgetting to do an is_callable() check
2) forgetting to ensure that the callback actually accepts the arguments I want it to accept

The primary mechanisms in place for ensuring #2 in PHP 5 are reflection, interfaces, and abstract classes.

At a certain point, validating this aspect of a callback can become quite complex, i.e.:

<?php
if (!is_callable($callback)) return;
if (is_array($callback)) {
if (is_string($callback[0])) {
// callback is array('class', 'method')
$a = new ReflectionClass($callback[0]);
if ($a->implementsInterface('someInterface')) {
// check to see if this callback implements a
// specific method's signature
if ($callback[1] != 'someMethod') return;
} else {
// more loosely, make sure this callback
// accepts 3 parameters
$m = $a->getMethod($callback[1]);
if ($m->getNumberOfParameters() < 3 || $m->getNumberOfRequiredParameters() > 3) return;
}
} else {
// callback is array($object, 'method')
if ($callback[0] instanceof someInterface) {
// check to see if this callback implements a
// specific method's signature
if ($callback[1] != 'someMethod') return;
} else {
// more loosely, make sure this callback
// accepts 3 parameters
$a = new ReflectionClass($callback[0]);
$m = $a->getMethod($callback[1]);
if ($m->getNumberOfParameters() < 3 || $m->getNumberOfRequiredParameters() > 3) return;
}
}
// if we reach here we are certain that this is the interface we expect
} else {
// callback is 'function'
// make sure this callback function
// accepts 3 parameters
$a = new ReflectionFunction($callback);
if ($a->getNumberOfParameters() < 3 || $a->getNumberOfRequiredParameters() > 3) return;
}
?>

*this* is the kind of thing that may be useful to abstract - a callback validator that can accept as its arguments:

- an interface that must be validated (require an array() callback)
- the number of parameters that must be accepted

With this kind of functionality, one can then use it inside a method

<?php
require_once 'PHP/CallbackValidator.php';
...
function whatever($a)
{
if (!PHP_CallbackValidator::validate($a, PHP_CallbackValidator::INTERFACE, 'interfaceName', 'method')) {
throw new Whatever_Exception('what evER');
}
}
?>

or inside a wrapper PHP_Callback-style object for type hinting:

<?php
require_once 'PHP/CallbackValidator.php';
class MY_CustomCallback
{
private $_callback;
function __construct($callback)
{
if (!PHP_CallbackValidator::validate($callback, PHP_CallbackValidator::ARGS, 3)) {
throw new MY_Exception('callback must accept 3 arguments');
}
$this->_callback = $callback;
}

function execute($a, $b, $c)
{
call_user_func($this->_callback, $a, $b, $c);
}
}
?>

This PHP_CallbackValidator package need only be a 1-static method class, but it would still be useful and impose a minimal performance penalty for the functionality it would provide. The flexibility of being able to use it as a one-off validation inside a method or function or to create a customized PHP_Callback that can be used for type-hinting is far better in my opinion. I hope that with these ideas, you can find a way to glean the most useful part of your proposal into a package that all PEAR and all PHP developers will want to use.