The array-traversal functions like array_map() and so on that take a callback function and an array and does something to the one with the other are nice, but they have a drawback in that there is no straightforward way to supply additional arguments to the callback. A lot of the time the only sensible approach is go back to using a loop.
array_map(), for example, lets you supply as many arguments as you like to the callback, but you have to pass arrays to array_map. Let's say you have an array of strings and you want to remove the first and last three characters from each of them. substr() is the obvious function of choice, but that only works on one string and we have a whole array. So what do we do?
The most obvious idea is to forgo the idea of using a traversal function:
foreach($array as &$string)
{
$string = substr($string, 3, -3);
}
Or maybe temporary arrays containing the other arguments duplicated the appropriate number of times (yuck):
$array = array_map('substr', $array,
array_fill(0,count($array), 3),
array_fill(0, count($array), -3));
Or a specialised callback function:
function substr_chop_ends($string)
{
return substr($string, 3, -3);
}
$array = array_map('substr_chop_ends', $array);
Perhaps using create_function:
$array = array_map(create_function('$string','return substr($string, 3, -3);'), $array);
But we're losing flexibility one way or the other with those last two: our callback function really can only trim the end characters off. If we want it to do something else we have to write another function.
Well, there is another approach (prepare to vom):
$GLOBALS['substrCallback_start'] = 3;
$GLOBALS['substrCallback_length'] = -3;
function substrCallback($string)
{
return strtr($string, $GLOBALS['substrCallback_start'], $GLOBALS['substrCallback_length']);
}
$array = array_map('substrCallback', $array);
But we won't go there.
The previous two approaches seem like the most practical, but what is needed is
a way to generate them as needed instead of having to hand craft every one.
$array = array_map(create_function('$string','return substr($string, '.$start.', '.$length.');'), $array);
But of course we might want to do this with other functions, with different sets of arguments; we might not even want our array to be the first argument. There still seems to be more hand-carving going on than necessary.
Enough with the introduction. Here's the code being shared.
$bindings = array();
function bind($f, $bound_args)
{
$GLOBALS["bindings"][$ref = count($GLOBALS["bindings"])] = array($f, $bound_args);
return create_function('', '$args = func_get_args();
list($f, $bound_args) = $GLOBALS["bindings"]['.$ref.'];
$args = array_combine(array_slice(array_diff_key(range(0,' . (max(array_keys($bound_args))-1) . '+count($args)),$bound_args),0,count($args)),$args) + $bound_args;
ksort($args);
return call_user_func_array($f, $args);'
);
}
The bind() function takes two arguments, the name of a callback function, and an array of values that are to be bound to the function's parameters, effectively pre-filling them in advance. In the ongoing example:
$chop_ends = bind('substr', array(1=>3, 2=>-3));
$array = array_map($chop_ends, $array);
The format of the $bound_args argument is not really that complicated. As you can see, it's an array. The indices of the array match up with the callback function's parameters (counting from 0), and the values as you might expect are the corresponding arguments. So "substr($string, $start, $length)", when bound with array(1=>3, 2=>-3), becomes "substr($string, 3, -3)".
The code works by basically doing just that. It uses a global array $GLOBALS['bindings'] (ugly, I know, but its contents need to be globally accessible even inside functions - an object-oriented version might be smarter but, apart from namespace niceness, not significantly different: the functions contained therein are still by definition global).
Because the values of $bound_args aren't necessarily serialisable (let alone representable as source code), they can't be embedded directly into the callback under construction (the number one most irritating thing about PHP's handling of runtime-generated functions). What's more, the function names generated by create_function aren't directly embeddable into the code of new created functions without a lot of arsing about with chr(0) and whatnot (no, wait, that's the most irritating thing).
So $GLOBALS['bindings'] stores both of these items (the name of the callback having its arguments bound and the bound arguments themselves) in such a way that they'll be accessible to the callback being constructed.
Then the callback is constructed. All that hairy long line setting $args does is merges $bound_args with any other arguments that might be passed to the callback when it is actually called in such a way that the bound arguments go where they should go and the passed arguments fill in the remaining spaces.
For example, consider the line
$blag = bind('wibble', array(4=>'foo', 2=>'bar'));
With that assignment, $blag becomes callable. Let's do that.
$blag('a','b','c','d','e');
What does that result in? Well, the function $blag takes the name of the callback ('wibble') and its bound arguments (array(4=>'foo', 2=>'bar')), merges with latter with the other arguments that have been passed (array('a','b','c','d','e')) and applies the callback to them. Result:
wibble('a','b','bar','c','foo','d','e');
The thing to take away from that example is how the bound arguments and the passed arguments were merged. Note that "bar" is the second argument and "foo" the fourth (and "a" the zeroth).
Like I said, the whole point of the global $bindings array is that it is not necessary to use only callbacks and arguments that can be written down in PHP source code. The callback could be the name of a created function, or it could be an array that represents a class or object method; and the bound arguments could be pretty much anything that could be passed to a function (including objects and resources).
The global $bindings array is also its strongest weakness: anything that goes in that array stays in that array: it will sit around in there until the end of the script. So of course will the callback that each entry in the array is associated with.
Oh, one last thing. note that bind() itself only stores the bound arguments: they're used only when the created function is called. This matters for volatile things like objects and resources that may have had their state changed in the meantime.
And dribble off into some weedy examples...
function t1($a, $b, $c)
{
return $a+$b+$c;
}
$t3 = bind(bind('t1', array(3)), array(2));
echo $t3(42),"\t",$t3(1),PHP_EOL;
class Foo
{
static function bar($a,$b,$c)
{
return $a+$b+$c;
}
}
$t3 = bind(array('Foo','bar'), array(3));
$t3 = bind($t3, array(2));
echo $t3(42),PHP_EOL;
$t3 = bind(array('Foo','bar'), array(3,2));
echo $t3(42),PHP_EOL;
class Wibble
{
private $spink = 3;
function Womble($a,$b,$c)
{
return $a+$b+$c;
}
function screech()
{
$t3 = bind(array($this,'Womble'), array($this->spink, 2));
echo $t3(42);
}
}
$wibble = new Wibble;
$wibble->screech();
echo PHP_EOL;
Oh, and this entire post is obsolete as of PHP 5.3. So if you're using that version, reading this was a waste of your time🙂 You'd write:
function chop_ends($start, $end)
{
return function($word)use($start, $end)
{
return substr($word, $start, $end);
};
}
$array = array_map(chop_ends(3, -3), $array);
[Edit much much later:] PHP 7.4 introduced arrow functions. Those automatically close over variables from the outer context that are explicitly used in the function body.
$start = 3;
$end = -3;
$array = array_map(fn($word) => substr($word, $start, $end), $array);