38
\$\begingroup\$

This is the pattern I've been using for "if the variable is empty, set a default value", and for variables like $muffin, it has all seemed well and good. But in the following real example, this pattern gives me a super long line which smells a bit to me. Is there a cleaner way to do this?

$newsItems[0]['image_url']= $newsItems[0]['image_url']=='' ? '/img/cat_placeholder.jpg' : $newsItems[0]['image_url'];

Maybe:

$newsItems[0]['image_url']= $newsItems[0]['image_url']=='' ? 
    '/img/cat_placeholder.jpg' : 
    $newsItems[0]['image_url'];   //this doesn't look much better to me?
\$\endgroup\$
2

7 Answers 7

35
\$\begingroup\$

reader from the future! PHP 7.0 introduced ?? which is a null coalescing operator, just use that: https://www.php.net/manual/en/migration70.new-features.php#migration70.new-features.null-coalesce-op.


Before dealing with your question, I'd like to point out that you probably want to use empty() instead of comparing with an empty string and that if you really want to compare to an empty string, use the === operator.

Anyway, default initialization is a real problem in many languages. C# even has a ?? operator to solve this kind of issue! So, let's try to make it better to make your code better step by step.

Ternary operator no more

The one-liner is too long, and using the ternary operator on multiple lines is a bad idea: that's what if-statement are for:

if (empty($newsItems[0]['image_url'])) {
    $newsItems[0]['image_url'] = '/img/cat_placeholder.jpg';
}

This is more readable for two reasons : it's easier to understand the syntax, but more importantly, it's easier to see that you simply want a default value when there's nothing. We don't need the 'else' part which is confusing with the ternary operator.

Note: as mentioned by Simon Scarfe and corrected by mseancole, PHP also has some special ternary syntax to do this:

$newsItems[0]['image_url'] = $newsItems[0]['image_url'] ?: 'img/cat_placeholder.jpg';

Factorize it!

If you're doing this only once or twice, then all is good, but otherwise you'd want to factorize it into a function, since you don't repeat yourself, right? The simple way is:

function default_value($var, $default) {
    return empty($var) ? $default : $var;
}

$newsItems[0]['image_url'] = default_value($newsItems[0]['image_url'], '/img/cat_placeholder.jpg');

(The ternary operator does make sense here since variable names are short and both branches of the condition are useful.)

However, we're looking up $newsItems[0]['image_url'] when calling default_value, and this is possibly not defined, and will raise an error/warning. If that's a concern (it should), stick to the first version, or look at this other answer that gives a more robust solution at the expense of storing PHP code as a a string and thus cannot be checked syntactically.

Still too long

If we don't care about the warning/error, can we do better? Yes we can! We're writing the variable name twice, but passing by reference can help us here:

function default_value(&$var, $default) {
    if (empty($var)) {
        $var = $default;
    }
}

default_value($newsItems[0]['image_url'], '/img/cat_placeholder.jpg');

It's much shorter, and feels more declarative: you can look at a bunch of default_value calls and see what the default values are instantly. But we still have the same warning/error issue.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ The second two options presented here will raise an error and also (because of this) impact performance. I shouldn't have to mention the performance bit, though, as the fact that an error (Warning, in this case, undefined index) should simply be enough. Even if reporting/logging is turned off, the fact that the interpreter notices at all incurs more performance impact than empty and isset in conjunction with an if. Stick to the if, or use a solution like mine below. \$\endgroup\$
    – Mike
    Jan 6, 2015 at 21:39
  • \$\begingroup\$ Thank you, upvoted. Of course, the performance does not matter here, unless it is proven that this code is part of a bottleneck. \$\endgroup\$ Jan 7, 2015 at 7:57
  • \$\begingroup\$ @MichaelJMulligan does my edit seem fair? \$\endgroup\$ Jan 7, 2015 at 8:03
  • \$\begingroup\$ @QuentinPradet, Fair. As for the syntactic check, can you elaborate on my answer? I'd be interested. \$\endgroup\$
    – Mike
    Jan 7, 2015 at 15:45
18
\$\begingroup\$

From here:

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.

So could you write?:

$newsItems[0]['image_url'] = $newsItems[0]['image_url'] ?: '/img/cat_placeholder.jpg';
\$\endgroup\$
3
  • 3
    \$\begingroup\$ This is one of my favorite things to write in PHP. It feels elegant and naughty at the same time! :) I normally only use it when assigning to another variable or return from a function, though. \$\endgroup\$ Jul 15, 2012 at 2:06
  • 7
    \$\begingroup\$ Note that if your variable is undefined, PHP will throw notices about it. This, unfortunately, is no replacement for $var = isset($var) ? $var : 'default value'; \$\endgroup\$
    – Brad
    Jun 2, 2014 at 14:59
  • \$\begingroup\$ That's amazing, so clean and elegant :-) \$\endgroup\$
    – Avishai
    Sep 28, 2016 at 20:23
16
\$\begingroup\$

Lo and behold, the power of PHP 7's null coalesce operator! It is also called the isset ternary operator, for obvious reasons.

Here's a passage from php.net's wiki page about it

Proposal

The coalesce, or ??, operator is added, which returns the result of its first operand if it exists and is not NULL, or else its second operand. This means the $_GET['mykey'] ?? "" is completely safe and will not raise an E_NOTICE.

This is what it looks like

$survey_answers = $_POST['survey_questions'] ?? null;

It can even be chained. It'll use the first value that exists and isn't null, consider the following:

$a = 11;
$b = null;
$c = 'test';

$d = $a ?? $b ?? $c;

print $d;

This would result in 11, however if $a = null; it would print test.

\$\endgroup\$
1
5
\$\begingroup\$

The extract() function can also help.

It doesn't work for individual array keys like you have here, but it could be useful in other circumstances.

extract(array('foo'=>'bar'), EXTR_SKIP);

This will assign bar to $foo as a default value only if it's not already set. If $foo is already set it will skip it. You can include multiple variables in the array.

\$\endgroup\$
4
\$\begingroup\$

Doing it OOP style

If you are doing OOP you could also reconsider if using a plain array is the way to go. I guess image_url is not the only attribute of your news items, so convert this array structure to an object. Furthermore, $newsItems may get changed to an array of NewsItem instances.

/img/cat_placeholder.jpg could be the default value of the attribute image_url in your new class NewsItem. Either your client code decides whether to set a new value (e.g. based on being !empty()) or your NewsItem class has a setter method for the attribute image_url' that automatically sets the default value if it gets an empty string.

\$\endgroup\$
1
\$\begingroup\$

PHP 7.4 adds ??=, the null coalescing assignment operator, which is exactly what you need.

$newsItems[0]['image_url'] ??= '/img/cat_placeholder.jpg';

This is functionally equivalent to the following, or to your examples:

if (!isset($newsItems[0]['image_url'])) {
    $newsItems[0]['image_url'] = '/img/cat_placeholder.jpg';
    }
\$\endgroup\$
1
  • \$\begingroup\$ !isset() does not check if the value is falsey (like in the asker's script), only if the variable is undeclared or null. ??= does not check if the value is falsey, only if the variable is undeclared or is null. This answer is misleading/incorrect. \$\endgroup\$ Oct 26, 2022 at 21:59
1
\$\begingroup\$

Warning

Do not do either of these two things:

$array['index'] = $array['index'] ?: $default;

function default_value(&$var, $default) {
    if (empty($var)) {
        $var = $default;
    }
}

default_value($array['index'], $default);

Both will raise errors due to an undefined index (both first attempt getting the value at the index first) if the index is not set. This is a small thing, but even if you have error reporting AND logging turned off, the error is raised, is built, and propagates through the stack before it is checked if there is anywhere to report the error. And thus, simply doing something that CAN raise an error, incurs a performance hit.

Keep this in mind, because this goes for all Error Types (Notices, Warnings, etc), and in large apps can be a big deal. To avoid this you must use a function that does not raise an error on a missing index, like isset($array['index']) or empty($array['index']), and not use the shortened ternary form.

Try a function like:

function apply_default(Array &$array, $path, $default) {
    // may need to be modified for your use-case
    preg_match_all("/\[['\"]*([^'\"]+)['\"]*\]/",$path,$matches);
    if(count($matches[1]) > 0) {
        $destinaion =& $array;
        foreach ($matches[1] as $key) {
            if (empty($destinaion[$key]) ) {
                $destinaion[$key] = array();
            }
            $destinaion =& $destinaion[$key];
        }
        if(empty($destinaion)) {
            $destinaion = $default;
            return TRUE;
        }
    }
    return FALSE;
}

$was_applied = apply_default($array,
    "['with a really long name']['and multiple']['indexes']",
    $default_value);

This doesn't raise errors, and will create the path if it does not exist. It also benefits from consolidating the logic to set defaults in one place, rather than ifs everywhere.

As mentioned elsewhere, the other option is to:

if(empty($array['key'])) {
    $array['key'] = $default;
}

Which is just as good, and probably more performant.

However, all that said, ideally this should only be handled in the consuming service. If the value is empty, the consumer (whether a view or another client [like javascript]) should be allowed to make this choice on it's own. As, in actuality, the semantic data is empty for the model. How this is handled should be up to the consumer it's self, ie, if this is going to a template/view, the conditional should be handled during display. If it is sent as JSON, the client should be able to make the choice.

You are asking about styles, that is probably the most important. The best style is, if you are providing data to a client, don't fake it (but document the possible empty response).

\$\endgroup\$
4
  • \$\begingroup\$ Great answer, and nobody had seen this before. So you asked about the syntax errors. Say you write "['with a really long name'][and multiple']". If this wasn't in a PHP string, but actually interpreted by PHP as PHP code, you would get a meaningful error, but in your function the regex would simply fail, I guess? \$\endgroup\$ Jan 7, 2015 at 21:02
  • \$\begingroup\$ It would, you are correct. I will think on how to make that not suck. \$\endgroup\$
    – Mike
    Jan 7, 2015 at 22:12
  • \$\begingroup\$ You could send an array of strings instead. Not sure this is worth thinking about, though! \$\endgroup\$ Jan 8, 2015 at 7:52
  • \$\begingroup\$ @mickmackusa - very good point! I kind of blindly include those flags in most all of my regular expressions just because of the way I am usually using them. That said, you are correct that they have NO impact on the expression at hand. Fixed. \$\endgroup\$
    – Mike
    Oct 27, 2022 at 17:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.