Twig
Twig copied to clipboard
Add ??? Empty Coalesce Operator
Empty Coalesce adds the ??? operator to Twig that will return the first thing that is defined, not null, and not empty. This is particularly useful when you're dealing with a number of fallback/default values that may or may not exist, and may or may not be empty.
The ??? Empty Coalescing operator is similar to the ?? null coalesce operator, but also ignores empty strings (""), 0, 0.0, null, false, and empty arrays ([]) as well.
Because this is an Empty Coalesce Operator, it functions identically to the PHP empty() function in terms of return values.
Example:
{% set bar = null %}
{% set foo = '' %}
{% set baz = [] %}
{% set foobar = woof ??? bar ??? foo ??? baz ??? 'bark' %}
{{ foobar }}
This will output:
bark
...because:
woofis undefinedbarisnullfoois an empty stringbazis an empty array
There is precedence for this operator in languages such as Swift:
https://medium.com/@JanLeMann/providing-meaningful-default-values-for-empty-or-absence-optionals-via-nil-or-empty-coalescing-379abd22ae77
What is the difference with the |default filter we have.
Well, one of them is that it relies on PHP's empty, meaning that '0' is empty while |default does not consider that. But is there any other difference ?
btw, your implementation also ignores '0', 0 and false, in addition to what you documented as being ignored.
Thanks for looking @stof !
What is the difference with the |default filter we have.
It's the same reason why the ?? null coalescing operator exist in Twig right now, despite default being available. It is far more convenient and much more clear what is going on to have:
{% set foobar = woof ??? bar ??? foo ??? baz ??? 'bark' %}
...using the default filter. It can be done, but it is quite ugly and verbose.
I'm a big fan of this idea — It comes up frequently enough in my Twig templates (where I'm comparing a chain of values and I want to use/output the first non-empty one) that I've created my own stock extension for it, and it's a default add to all my projects.
I think ??? as an operator could be confusing vis-a-vis ??. And also, the visual difference between ?? and ??? is not super-scannable. Perhaps _? for this.
My only reservation about this in-practice is the case of "0" being empty.... but, one can't really do anything about it; I think ultimately it's reasonable to be consistent with PHP's empty() and expect devs to learn that behavior.
It's definitely a non-standard op, but I think the utility of it is worth adding the sugar to Twig even though no similar operator exists upstream in PHP.
+1 to this PR - i am using @khalwat's null-coalescing operator as a plugin on a site and it is really nice to have in any context where your Twig data contains, for instance, a lot of empty arrays.
Why not use ?: to provide this behavior?
I think ??? as an operator could be confusing vis-a-vis ??. And also, the visual difference between ?? and ??? is not super-scannable. Perhaps _? for this.
_? is horrible to type; I think ??? is easier to type, and clearer in that it's ?? plus more. The fact that others have independently come up with the same ??? operator for this functionality makes me feel even better about it.
My only reservation about this in-practice is the case of "0" being empty.... but, one can't really do anything about it; I think ultimately it's reasonable to be consistent with PHP's empty() and expect devs to learn that behavior.
It actually uses empty() under the hood, so yes, it'd be consistent with empty()'s behavior, which I think makes sense.
@nicolas-grekas — The primary benefit is that ?: will throw an error if it encounters an undefined operand. We'd really like this to behave like the null-coalescing operator, which treats undefined things as null.
(Also, it needs to be right-associative... I think ?: is, but I don't remember offhand.)
Why not use ?: to provide this behavior?
@nicolas-grekas what would the Twig code for this be using the ?: operator for this construct:
{% set foobar = woof ??? bar ??? foo ??? baz ??? 'bark' %}
In PHP ?: just determines truthiness, and would throw an error if a variable was undefined, unlike ???
and would throw an error if a variable was undefined
twig is a different language, we can remove this behavior if we want to.
twig is a different language, we can remove this behavior if we want to.
That's definitely true, but I'd rather define a new operator for new functionality than hoist new functionality on an operator for which there's already a defined behavior.
Many people who write in Twig also write in PHP, and it might be very confusing if ?: operated differently in Twig than it does in PHP... at least to me.
twig is a different language, we can remove this behavior if we want to.
that would require changing the existing operator, and that makes mistake detection harder in case you make typos in your variable names. I would vote for keeping the existing behavior of the operator.
Another vote to not modify existing operator behaviors! (And a vote for ???)
Welp, looks like the ??= operator is being added in PHP 7.4: https://twitter.com/nikita_ppv/status/1087662379037528064
It's not the same thing, but it does show some precedence for a three character operator... so I hope ??? can make it into Twig!
btw, your implementation also ignores '0', 0 and false, in addition to what you documented as being ignored.
If we’re calling this the “Empty Coalesce Operator”, I would expect that any value PHP considers “empty” would also be considered empty here, so '0' and 0 should in fact invoke the following operator argument. Safe to assume that if PHP ever adds the same operator, they would go with the same behavior as empty() as well.
Yeah that's the idea @brandonkelly -- it's called the Empty Coalesce Operator because the behavior is what we'd expect from chained/nested empty() calls.
https://github.com/twigphp/Twig/pull/2787/files#diff-b445caeb1cc1391b4cc1966bd1b1f76cR28
After compilation, wouldn't the current implementation eventually evaluate both the left-hand side and the right-hand side twice at runtime? I mean it's possible that func1 or func2 in func1(arg1) ??? func2(arg2) be executed twice? In PHP, both L and R in L ?? R would be only evaluated once.
If there is a static variable (such as a static counter to log how many time the func has been executed) in func1 or func2, a counterintuitive behavior would happen?
@jfcherng suggestions on how to mitigate the behavior you've mentioned?
@khalwat Sorry I cannot really answer that. But that is what I saw when I was following the ??= RFC. Initially, L ??= R was implemented as a simple syntax sugar for L = L ?? R, but soon problems came out.
For example, consider the expression $a[print 'X'] ??= $b. A simple desugaring into $a[print 'X'] = $a[print 'X'] ?? $b will result in 'X' being printed twice. However, this is not how all other existing compound assignment operators behave: They will print X only once, as the LHS is only evaluated once. I assume that ??= would behave the same way.
$compiler
->raw('(('.self::class.'::empty(')
->subcompile($this->getNode('left'))
->raw(') ? null : ')
->subcompile($this->getNode('left'))
->raw(') ?? ('.self::class.'::empty(')
->subcompile($this->getNode('right'))
->raw(') ? null : ')
->subcompile($this->getNode('right'))
->raw('))')
;
With this, I am assuming L ??? R will be de-sugared into something like
(empty(L) ? null : L) ?? (empty(R) ? null : R)
The worst case is evaluating both L and R twice when executing the compiled template.
Maybe worth a note in the docs if this cannot be resolved?
How about using ??: instead? From what I can tell, this new operator combines the effects of the existing ?? and ?: operators in PHP.
@dharkness ?: can’t be used in succession like ?? can though, e.g. foo ?? bar ?? baz
@brandonkelly I can only speak for PHP, but both ?? and ?: can be used in a series.
> $y = null;
> echo $x ?? $y ?? 'foo';
foo
> echo 0 ?: '' ?: 'foo';
foo
Your example is easy to do with operators ?? and ?:.
{% set bar = null %}
{% set foo = '' %}
{% set baz = [] %}
{% set foobar = woof ?? null ?: bar ?: foo ?: baz ?: 'bark' %}
{{ foobar }}
@GromNaN that presupposes that you know the state of the variables. The point is that any of them could be undefined, null, or empty, and you don't know ahead of time.
I did't imply this feature was not useful, just trying to help using the existing features.
So <variable> ??? <default> would be a shortcut for (<variable> ?? null) ?: <default>.
If the existence of any variable is unknown, the example can be:
{% set foobar = (woof ?? null) ?: (bar ?? null) ?: (foo ?? null) ?: (baz ?? null) ?: 'bark' %}
@GromNaN I don’t think anyone here is unaware of existing syntax options. You could have made the same point about ?? … that <variable> ?? <default> it is the same thing as doing (<variable> is defined and <variable> is not same as(null) ? <variable> : <default>. The point is to simplify, to make templates more readable & maintainable.
Excepted that with ?? the <variable> have to be repeated several times. Which is very verbose when <variable> contains a complex expression.
What's the status of this? Thought I'd bump it up since it seems it got abandoned without any particular reason.
Did the team decide this isn't desirable? the comment voting from the community seems to suggest otherwise. Is it that the PR needs more work? as far as I can tell there isn't any specific request of what it needs.
So I guess the question is
- Are the maintainers open to this?
- If not, why, given that the community wants it and every objection so far has been answered?
- If so, is there something that needs to be done on the PR to get it merged?
Currently, the PR is borked. I'd be happy to attempt to redo it if there is interest from the maintainers.
I want to add my +1 on that. And thanks for the work on that.
{% set theme = entry is defined and entry.theme != "" ? entry.theme : defaults.theme != "" ? defaults.theme : "dark" %}
vs.
{% set theme = entry.theme ??? defaults.theme ??? "dark" %}
The readability is just a whole other level.