Search code examples
phpnull-coalescing-operator

Is this a bug in PHP null coalesce operator or expected behaviour?


I've stumbled about an if statement, using PHPs null coalesce operator, not behaving as "expected". The code in question looks something like this:

if ($foo['bar'] ?? false || $foo['baz'] ?? false) { /* ... */ }

changing it to

if (($foo['bar'] ?? false) || ($foo['baz'] ?? false)) { /* ... */ }

solves it.

I ran a quick test in my terminal:

root@docker:/application# php -v
PHP 7.2.11-2+ubuntu18.04.1+deb.sury.org+1 (cli) (built: Oct 15 2018 11:40:35) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.11-2+ubuntu18.04.1+deb.sury.org+1, Copyright (c) 1999-2018, by Zend Technologies
    with Xdebug v2.6.1, Copyright (c) 2002-2018, by Derick Rethans
root@docker:/application# php -a
Interactive mode enabled

php > $test = ['foo' => 'bar'];
php > var_dump($test['baz'] ?? null); // as expected
php shell code:1:
NULL
php > var_dump(($test['baz'] ?? null)); // as expected
php shell code:1:
NULL
php > var_dump($test['baz'] ?? null || $test['foobar'] ?? null); // as expected, but there's a Notice
PHP Notice:  Undefined index: foobar in php shell code on line 1
PHP Stack trace:
PHP   1. {main}() php shell code:0
php shell code:1:
bool(false)
php > var_dump(($test['baz'] ?? null) || ($test['foobar'] ?? null)); // as expected
php shell code:1:
bool(false)

Now, what I think happens in test no. 3, is that it get's executed as

$test['baz'] ?? (null || $test['foobar']) ?? null

so if the $test['baz'] evaluates to unset (which it obviously does), next null || $test['foobar'] get's executed, which leads to, $test['foobar'] throwing the notice.

My question: Is this the expected behaviour of the null coalesce operator in PHP? I kind of expected it to bind stronger than for example the || (or) operator.
On the other hand, in the RFC (https://wiki.php.net/rfc/isset_ternary), there's an explicit example:

var_dump(0 || 2 ?? 3 ? 4 : 5); // ((0 || 2) ?? 3) ? 4 : 5 => int(4)

which could indicate, the example above is correct behaviour.

What do you think? Should this get reported as bug? I know It's not a "proper" question, however since I wasn't able to find a bug report/discussion/thread about it, I thought there should be a resource documenting it.
If you/the mods don't agree, I'll remove the question again.


Solution

  • This is expected behavior due to operator precedence

    || has higher precedence than ??, so your original statement is treated as

    if ($foo['bar'] ?? (false || $foo['baz']) ?? false)