Today I decided to give php 5.3 a run on my laptop, in particular I was interested to test a closure for the case below.
First of all I am running tests on a fancy Linux Mint where there's no official 5.3 package for php so I took the 'compile way' following the clear instructions from Brandon (thanks!). It has been a while since I compiled php and I must say that debian pre-packaged libraries has made the task quite simple and panicless.
I was reading syntax I miss in PHP by Stas where I found something I've been struggling with too (point 4, `named parameters call`):
4. foo(”a” => 1, “c” => 2, “d” => 4)
Named parameters call, which allows you to specify parameters in arbitrary order by name. Would allow to build nice APIs which could accept wider ranges of parameters without resorting to using arrays. That’d also imply call_user_func_array() would accept named arguments.
The problem here might be what to do with unknown arguments – i.e. ones that the function did not expect. I guess the function could just ignore them.
I'm not going to suggest a real solution, just testing on closures and the ternary short cut "?:". The array solution probably opens other problems with Reflection and unit testing, I haven't tried yet.
As you know code grows and refactoring requires time, if you can plan your application you won't end with functions with ten parameters or more, but sometimes you just inherit code from previous developers... For backward compatibily you just add new parameters at the end of old functions, probably with a default value not to break old calls.
A function like:
function query ($sql, &$db)
{
// do query
}
could become:
function query ($sql, $db, $cache=true, $retrieve_from_cache = true,
$skip_security_checks = false, $kill_on_error = false,
$testing_mode = false, $wtf = '')
{
// do a lot of things, schedule refactoring asap
}
That's a worse case scenario, the function is far from the single responsibility principle just to say something, but let's go on. Suppose you are writing a new library or a new class and would like to have functions/methods where the number of parameters is free, where the order of the parameters is not important and where you can give a name to each parameter when calling them.
For instance:
function wrap_htmlentities ($string, $quote_style = ENT_COMPAT, $charset,
$double_encode = true)
{
return htmlentities($string, $quote_style, $charset, $double_encode);
}
$encoded = wrap_htmlentities($user_input, ENT_COMPAT, 'ISO-8859-1', false);
Do you always remember that charset is the third parameter, and what about the forth ? (well Netbeans and PDT might help...)
Wouldn't it be nice to write:
$encoded = wrap_htmlentities(array('string' => $user_input, 'double_encode' => false));
// or
$encoded = wrap_htmlentities(array(
'string' => $user_input,
'double_encode' => false
));
I like to know the meaning of parameters when I read the code, without jumping here and there on files and documentation. Not to mention php inconsistency on parameters order, but that's another point (do you know a way to redefine php functions without recompiling it ?).
The function call might look weird, but so far php doesn't have a short array syntax like other languages, e.g. ['string' => "bold text", 'double_encode' => false] and I was interested in testing php 5.3 so consider it just an example.
So, how could we implement the defaults ? What about closures and ternary short cut ?
There it is:
function set_defaults (&$params, $defaults)
{
$set = function($value, $key) use (&$params)
{
$params[$key] = (isset($params[$key]))?$params[$key]:$value;
};
array_walk($defaults, $set);
}
function wrap_htmlentities ($p)
{
set_defaults($p, array(
'quote_style' => ENT_COMPAT,
'charset' => 'ISO-8859-1',
'double_encode' => true
);
return htmlentities($p['string'], $p['quote_style'], $p['charset'],
$p['double_encode']);
}
$encoded = wrap_htmlentities(array(
'string' => $user_input,
'double_encode' => false
));
There is no way to achieve a default definition for missing values in a array, so I thought about a `set_defaults` function called just before anything else. It's a generic function that can be called by any other functions built in this way.
Basically, `wrap_htmlentities` has one mandatory parameter, an associative array with values. Associative array keys allow to give a name to parameters. The array itself allows to forget the order.
Indeed these are equivalent:
$encoded = wrap_htmlentities(array( 'string' => $user_input, 'charset' => 'UTF-8', 'double_encode' => false )); $encoded = wrap_htmlentities(array( 'charset' => 'UTF-8', 'string' => $user_input, 'double_encode' => false )); $encoded = wrap_htmlentities(array( 'double_encode' => false, 'charset' => 'UTF-8', 'string' => $user_input ));
Now to the closure, the php 5.3 feature I was looking to. Closures are anonymous functions that bind a local variable with `use`. In the code the closure is:
$set = function($value, $key) use (&$params) {
...
};
`$set` is "anonymous", a function without a name. `$set` value is a function. And the bound variable is `$params`.
In this case `$params` is passed by reference because we need to modify the array to set the defaults.
The `set_defaults` function takes `$params` and run `$set` on all parameters using `array_walk`.
The `$set` function checks if a value is not set and takes the default value from `$defaults` if needed.
I've not run benchmarks on this approach, it surely add some overhead to function calls, time and memory usage I guess.
This is how the library would look:
class twitter
{
function connect ($p)
{
self::set_defaults($p, array(
....
);
// do ...
}
function retrieve_friends_list ($p)
{
self::set_defaults($p, array(
....
);
// do ...
}
function retrieve_my_tweets ($p)
{
self::set_defaults($p, array(
....
);
// do ...
}
function tweet_something ($p)
{
set_defaults($p, array(
....
);
// do ...
}
function change_password ($p)
{
self::set_defaults($p, array(
....
);
// do ...
}
function set_defaults (&$params, $defaults)
{
$set = function($value, $key) use (&$params) {
$params[$key] = (isset($params[$key]))?$params[$key]:$value;
};
array_walk($defaults, $set);
}
}
The function definition is always the same, we care only about defaults.
Documentation is missing and we should add it of course. If you need reflection probably this approach will give you more problems than it solves, because it's not possible to describe array keys on "@param array" (or is it?)
Speaking about the ternary shortcut without the middle part...
I just figured that I couldn't use it in this case. When I first wrote the closure function it was coded like this:
$set = function($value, $key) use (&$params)
{
$params[$key] = $params[$key]?:$value;
};
It works fine on most cases, except with 0, '0', '' and false. I tried also using isset() but that gives other results. Indeed php manual says:
Since PHP 5.3, it is possible to leave out the middle part of the ternary operator. Expression expr1 ?: expr3 returns expr1 if expr1 evaluates to TRUE, and expr3 otherwise.
It would have been nice if "?:" would return expr1 if expr1 is defined (not null).
Some examples:
$params = array(); $key = 'key'; $value = 'default value'; // with isset() $params[$key] = 'A'; $params[$key] = (isset($params[$key]))?$params[$key]:$value; var_dump($params[$key]); // OK --> string(1) "A" $params[$key] = '0'; $params[$key] = (isset($params[$key]))?$params[$key]:$value; var_dump($params[$key]); // OK --> string(1) "0" $params[$key] = 0; $params[$key] = (isset($params[$key]))?$params[$key]:$value; var_dump($params[$key]); // OK --> int(0) $params[$key] = false; $params[$key] = (isset($params[$key]))?$params[$key]:$value; var_dump($params[$key]); // OK --> bool(false) // with ternary shortcut $params[$key] = 'A'; $params[$key] = $params[$key]?:$value; var_dump($params[$key]); // OK --> string(1) "A" $params[$key] = '0'; $params[$key] = $params[$key]?:$value; var_dump($params[$key]); // ERR --> string(13) "default value" $params[$key] = 0; $params[$key] = $params[$key]?:$value; var_dump($params[$key]); // ERR --> string(13) "default value" $params[$key] = false; $params[$key] = $params[$key]?:$value; var_dump($params[$key]); // ERR --> string(13) "default value" // with ternary shortcut using isset() $params[$key] = 'A'; $params[$key] = isset($params[$key])?:$value; var_dump($params[$key]); // ERR --> bool(true) $params[$key] = '0'; $params[$key] = isset($params[$key])?:$value; var_dump($params[$key]); // ERR --> bool(true) $params[$key] = 0; $params[$key] = isset($params[$key])?:$value; var_dump($params[$key]); // ERR --> bool(true) $params[$key] = false; $params[$key] = isset($params[$key])?:$value; var_dump($params[$key]); // ERR --> bool(true) unset($params[$key]); $params[$key] = isset($params[$key])?:$value; var_dump($params[$key]); // OK --> string(13) "default value"
I hoped the shortcut was meant for an undefined value, instead the third parameter is used when the first "does not evaluate to true".
So the following does not work as I was expecting:
$_GET['page'] = $_GET['page'] ?: 1;
See:
unset($_GET['page']); $_GET['page'] = $_GET['page'] ?: 1; var_dump($_GET['page'] ); // OK --> int(1) $_GET['page'] = 2; $_GET['page'] = $_GET['page'] ?: 1; var_dump($_GET['page'] ); // OK --> int(2) $_GET['page'] = 0; $_GET['page'] = $_GET['page'] ?: 1; var_dump($_GET['page'] ); // ERR --> int(1) $_GET['page'] = '0'; $_GET['page'] = $_GET['page'] ?: 1; var_dump($_GET['page'] ); // ERR --> int(1) $_GET['page'] = ''; $_GET['page'] = $_GET['page'] ?: 1; var_dump($_GET['page'] ); // AMBIGUOUS in the context --> int(1) $_GET['page'] = false; $_GET['page'] = $_GET['page'] ?: 1; var_dump($_GET['page'] ); // AMBIGUOUS in the context --> int(1)
So far I don't see big use cases for the new form without the second parameter, probably only when working explicitly with booleans, but not with undefined or numeric values. It seems to me a tiny tool to use in particular situation that could make debugging harder.
Instead closures and anonymous functions will have a great impact on future code, just think how much they are used in languages like javascript. Some php framework like Recess is already planning to use them.
It's a shame that Ubuntu won't include php 5.3 until 2010 due to the suhosin patch. If you do not want to compile, you can also see here. Enjoy!