Skip to content

Instantly share code, notes, and snippets.

@runekaagaard
Created April 8, 2011 12:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save runekaagaard/909711 to your computer and use it in GitHub Desktop.
Save runekaagaard/909711 to your computer and use it in GitHub Desktop.

Intro

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.

Goal

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';

Proposal 1 : Adding two new operators

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:

Example 0 : Solving of goal patterns

// 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';

Example 1 : Ternary shortcut

Old syntax:

$a = isset($b) ? $b : 42;
$a = !empty($b) ? $b : 42;

New syntax:

$a = $b ?? 42;
$a = $b ??? 42;

Example 2 : Direct assignment

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;

Example 3 : Works with statements too

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;

Example 4 : Chaining

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;

Example 5 : Illegal syntax

$a = $d ?? $c ??? $b ?? 42; // `??` and `???` cannot be mixed.

Proposal 2 : Making a userland implementation possible

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;
}

Proposal 3: firstset, firstfull, setor, fullor

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:

Example 0 : Solving of goal patterns

// 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');

Discussion

About changing the beviour of the ternary shortcut

@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:

  1. It would be a huge BC break, because a boolean test and a isset test differs greatly. [3]
  2. 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;

Why we need both an Isset and IsNotEmpty operator

@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 problem

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!

About the $_GET[?'var'] syntax

I don't like this so much because it is trying to solve another problem:

How can we suppress errors when accessing arrays?

About the "Checked ternary operator syntax"

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.

The solution

  1. 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.

  2. Otherwise I'm much more in favor of the "filled()" or coalesce() versions. I've added a proposal 3 as my take on that.

Data collection

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.

References

* [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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment