Isset and IsNotEmpty operators have for sure been a hot topic for several years now and in my opinion rightly so. The non-DRY style of:
$my_array['my_long_boring_key'] = !empty($my_array['my_long_boring_key'])
? $my_array['my_long_boring_key'] : 'Default value';
$my_array['my_long_boring_key'] = isset($my_array['my_long_boring_key'])
? $my_array['my_long_boring_key'] : 'Default value';
is a true day-to-day hassle and addressing this annoyance would be a big win for
the PHP community as a whole. As PHP has two keywords isset
and empty
that
can check for a non existing variable without throwing errors I think there
should exist two assignment/ternary operators who mirror those.
I have been thinking [1] about the same problem for my meta language Snow and
also ended up using ??
as an isset operator.
So what should be solved before this new feature can be considered a succes? The solution should offer DRY solutions to the following patterns:
// Set one variable from another or a default value.
// 1a.
$a = isset($b) ? $b : 'Default value';
// 1b.
$a = !empty($b) ? $b : 'Default value';
// Set one variable from itself or a default value.
// 2a.
$a = isset($a) ? $a : 'Default value';
// 2b.
$a = !empty($a) ? $a : 'Default value';
// Find the first set value from a range of variables and statements.
// 3a.
$tmp = get_value();
if (isset($tmp)) $a = $tmp;
$tmp = $myarr['mykey'];
if (!isset($a) && isset($tmp)) $a = $tmp;
if (!isset($a)) $a = 'Default value';
// 3b.
$tmp = get_value();
if (!empty($tmp)) $a = $tmp;
$tmp = $myarr['mykey'];
if (!isset($a) && !empty($tmp)) $a = $tmp;
if (!isset($a)) $a = 'Default value';
I propose that two new operators ??
(IssetOperator) and ???
(NotEmptyOperator) are added. ??
mirrors isset
and ???
mirrors !empty
.
They are chainable ad nauseum but not with each other.
They would work like this:
// Set one variable from another or a default value.
// 1a.
$a = $b ?? 'Default value';
// 1b.
$a = $b ??? 'Default value';
// Set one variable from itself or a default value.
// 2a.
$a ??= 'Default value';
// 2b.
$a ???= 'Default value';
// Find the first set value from a range of variables and statements.
// 3a.
$a = get_value() ?? $myarr['mykey'] ?? 'Default value';
// 3b.
$a = get_value() ??? $myarr['mykey'] ??? 'Default value';
Old syntax:
$a = isset($b) ? $b : 42;
$a = !empty($b) ? $b : 42;
New syntax:
$a = $b ?? 42;
$a = $b ??? 42;
Old syntax:
$arr['key'] = isset($arr['key']) ? $arr['key'] : 42;
$arr['key'] = !empty($arr['key']) ? $arr['key'] : 42;
New syntax:
$arr['key'] ??= 42;
$arr['key'] ???= 42;
Old syntax:
// a)
$tmp = get_stuff('foo');
$a = isset($tmp) ? $tmp : 42;
// b)
$tmp = get_stuff('foo');
$a = !empty($tmp) ? $tmp : 42;
New syntax:
// a)
$a = get_stuff('foo') ?? 42;
// b)
$a = get_stuff('foo') ??? 42;
Old syntax [2]:
$a = false;
if (!empty($c) {
$a = $c;
} else {
$tmp = get_stuff();
$a = !empty($tmp) ? $tmp : false;
}
if ($a === false) {
$a = !empty($c) ? $c : 42;
}
New syntax:
$a = $c ??? get_stuff() ??? $b ??? 42;
$a = $d ?? $c ??? $b ?? 42; // `??` and `???` cannot be mixed.
The one thing preventing us PHP end users for implementing our own solution to
this problem is - as discussed many times before - that it is not possible to
check if a passed argument can be referenced and then reference it. This
proposal adds an AS_REFERENCE
bitmask that can be passed as the second
argument to func_get_arg
which then in turn throws an
ArgNotReferencableException
if the argument is not referenceable.
Below is an example of how firstset
(ifsetor) could be implemented.
function firstset() {
$num_args = func_num_args();
for ($i = 0; $i < $num_args; ++$i) {
try {
$arg = func_get_arg($i, AS_REFERENCE);
if (!isset($arg) {
$arg = null;
}
} catch (ArgNotReferencableException $e) {
$arg = func_get_arg($i);
}
if (isset($arg) {
return $arg;
}
}
return null;
}
This proposal adds four new function keywords: firstset
, firstfull
, setor
and fullor
. This can seem a lot but it is what is needed to solve the goal in
a 100% DRY way.
firstset
and firstfull
takes a number of variables and returns the first set
or not empty one. setor
and fullor
sets the first given argument by
refence to either itself if it is set or not empty or to the second argument
which is the default value.
I've made a userland implementation of them at
https://gist.github.com/934612 that works fine except that it does not shortcut
evaluation when it finds a match and that the use of the ar()
function is
neccesary.
They would work like this:
// Set one variable from another or a default value.
// 1a.
$a = firstset($b, 'Default value');
// 1b.
$a = firstfull($b, 'Default value');
// Set one variable from itself or a default value.
// 2a.
setor($a, 'Default value');
// 2b.
fullor($a, 'Default value');
// Find the first set value from a range of variables and statements.
// 3a.
$a = firstset(get_value(), $myarr['mykey'], 'Default value');
// 3b.
$a = firstfull(get_value(), $myarr['mykey'], 'Default value');
@Jordi, others
Some suggest adding an implicit isset
around the value being tested in the
ternary shortcut. This I think is less than optimal because:
- It would be a huge BC break, because a boolean test and a isset test differs greatly. [3]
- It would be a major WTF, because it would work different than the normal ternary operator that it is a shortcut of.
Jordi suggests adding an implicit !empty
around the value being tested in the
ternary shortcut. This would not break BC but it still wouldn't improve usecases
where the value tested for is allowed to be falsy ('', 0, "0", false, etc.):
function render_articles($rows, $settings) {
// Shows the last page per default. Zero based.
$settings['page_num'] = $settings['page_num'] ?: count($rows) - 1;
// ...
}
// Lets render_articles showing the first page.
echo render_articles ($rows, array('page_num' => 0));
The obvious bug here is that since 0 is considered falsy, render_articles will show the last page instead of the first. The correct version is back in non-dry mode:
$settings['page_num'] = isset($settings['page_num'])
? $settings['page_num']
: count($rows) - 1;
Im writing the same stuff three times. With the ??
operator it would be:
$settings['page_num'] ??= count($rows) - 1;
@Hannes, Ben
What's considered falsy in PHP is very dynamic. Depending on context the following can easily be considered falsy:
'', null, undefined, array(), false, 0
Therefore we need to be very precise when we are making decisions based on a
variable or statements value. This is where isset
and !empty
compliments
each other very well. Sometimes i want to use a default value when falsy means
"this variable is not defined". Other times I want to assign a default value
when falsy means "this variable is not an empty string".
Sometimes the behaviour we want will change while we are working on the code. Therefore I strongly believe that not having both operators would severely break the symmetry and force the end user to make tedious and non-dry refactorings just because he needs a function to - under certain conditions - return false or vice versa.
The main problem we are trying to solve is:
How can we in a nice way find the first defined and isset or not empty
variable out of a number of variables/values/statements
without getting errors regarding undefinedness?
The way we have done that up until now was using the ternary operator but I don't think the solution necessarily involves that!
I don't like this so much because it is trying to solve another problem:
How can we suppress errors when accessing arrays?
Example:
$arr['k3'] ?? : 'default'; // v3
I don't like this so much either because it is trying to solve another problem:
How can we suppress errors in the ternary operator?
Again we just want to find the first set or notempty value.
-
I really still think this is nice:
$a = $x ?? $y ?? foo() ?? 42; // First isset value. $a = $x ??? $y ??? foo() ??? 42; // First not empty value. // Changed this from ?! because of // parsing problems. $a ??= 42; // Set to 42 if not isset. $a ???= 42; // Set to 42 if empty.
-
Otherwise I'm much more in favor of the "filled()" or coalesce() versions. I've added a proposal 3 as my take on that.
I thought I would check out what normal use of isset/empty within ternary operators where. Therefore I made this little shell script:
#!/usr/bin/env sh
echo "Instances of isset:"
echo "###################\n"
grep --extended-regexp "isset\(.*\ \?\ " -r -h . | sed 's/^[ \t]*//' | nl
echo "\nCount:"
echo "##########################"
grep --extended-regexp "isset\(.*\ \?\ " -r . | wc -l
echo "\nInstances of empty:"
echo "###################\n"
grep --extended-regexp "empty\(.*\ \?\ " -r -h . | sed 's/^[ \t]*//' | nl
echo "\nCount:"
echo "##########################"
grep --extended-regexp "empty\(.*\ \?\ " -r . | wc -l
ran it on Symfony2, Zend-1.11.5, CakePHP 2.0.0-dev, Drupal 7 and Yii-1.1.7.r3135 and posted the results here: https://gist.github.com/934138 . Improvements welcome!
I think it gives a good overview of the problem we are trying to solve. The empty to isset ratio is 1:8 for Symfony, 1:5 for Zend, 1:2 for Cake, 1:2 for Drupal and 1:6 for Yii.
* [1]: http://code.google.com/p/php-snow/wiki/EmptyIssetOperators
* [2]: This could also be done by nesting ternary operators, but that gets
even more unreadable I think.
* [3]: http://php.net/manual/en/types.comparisons.php