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?
7 Answers 7
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.
-
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
andisset
in conjunction with anif
. Stick to theif
, or use a solution like mine below. \$\endgroup\$Mike– Mike2015年01月06日 21:39:16 +00:00Commented 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\$Quentin Pradet– Quentin Pradet2015年01月07日 07:57:03 +00:00Commented Jan 7, 2015 at 7:57
-
\$\begingroup\$ @MichaelJMulligan does my edit seem fair? \$\endgroup\$Quentin Pradet– Quentin Pradet2015年01月07日 08:03:02 +00:00Commented 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– Mike2015年01月07日 15:45:08 +00:00Commented Jan 7, 2015 at 15:45
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';
-
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\$David Harkness– David Harkness2012年07月15日 02:06:29 +00:00Commented 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– Brad2014年06月02日 14:59:16 +00:00Commented Jun 2, 2014 at 14:59 -
\$\begingroup\$ That's amazing, so clean and elegant :-) \$\endgroup\$Avishai– Avishai2016年09月28日 20:23:45 +00:00Commented Sep 28, 2016 at 20:23
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 anE_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
.
-
\$\begingroup\$ This is now integrated: php.net/manual/en/… \$\endgroup\$oliverpool– oliverpool2017年03月24日 10:05:47 +00:00Commented Mar 24, 2017 at 10:05
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.
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.
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';
}
-
\$\begingroup\$
!isset()
does not check if the value is falsey (like in the asker's script), only if the variable is undeclared ornull
.??=
does not check if the value is falsey, only if the variable is undeclared or isnull
. This answer is misleading/incorrect. \$\endgroup\$mickmackusa– mickmackusa2022年10月26日 21:59:47 +00:00Commented Oct 26, 2022 at 21:59
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 if
s 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).
-
\$\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\$Quentin Pradet– Quentin Pradet2015年01月07日 21:02:26 +00:00Commented Jan 7, 2015 at 21:02
-
\$\begingroup\$ It would, you are correct. I will think on how to make that not suck. \$\endgroup\$Mike– Mike2015年01月07日 22:12:27 +00:00Commented Jan 7, 2015 at 22:12
-
\$\begingroup\$ You could send an array of strings instead. Not sure this is worth thinking about, though! \$\endgroup\$Quentin Pradet– Quentin Pradet2015年01月08日 07:52:45 +00:00Commented 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– Mike2022年10月27日 17:32:25 +00:00Commented Oct 27, 2022 at 17:32
$newsItems[0]['image_url'] = $newsItems[0]['image_url'] ?? '/img/cat_placeholder.jpg';
php.net/manual/en/… \$\endgroup\$