Check if a variable is defined AND truthy at once in Twig - twig

In PHP I can do
<?php if ($someVar): ?>
This checks if a variable exists and if its value is different from a zero-like value like 0, null, '', and so on.
Is there any way to do so in Twig or do I need to write my own filter for that? At the moment, I must do
{% if someVar is defined and someVar %}
which is a pain when it comes to more complex templates.

There are (at least) two ways of doing this without extending Twig. A third option is to extend Twig by creating e.g. a Twig function. I would probably choose the first way (using the default filter).
By using the default filter
As #The_Unknown pointed out, you can also use the default filter:
{% if someVar|default(null) %}
You can omit passing the default value to the filter, and even omit the parentheses. Then the value will default to an empty string (which is falsey). I.e. these two are equal and valid:
{% if someVar|default() %}
{% if someVar|default %}
Whichever style you choose (default to null, omit the value or omit the parens), stick to it. Be consistent.
See TwigFiddle for a demonstration that truthy values evaluate to true and falsey values evaluate to false (based on the table below).
By setting strict_variables to false
By setting the environment variable strict_variables to false, you can skip the if someVar is defined part and do just {% if someVar %}. As described in Twig's documentation:
strict_variables boolean
If set to false, Twig will silently ignore invalid variables
(variables and or attributes/methods that do not exist) and replace
them with a null value. When set to true, Twig throws an exception
instead (default to false).
Set the variable to false when creating a Twig_Environment instance:
$twig = new Twig_Environment($loader, ['strict_variables' => false]);
If someVar is undefined, then {% if someVar %} is obviously false. The if tag's documentation page describes the edge case rules for defined variables:
The rules to determine if an expression is true or false are the same
as in PHP; here are the edge cases rules:
Value Boolean evaluation
empty string false
numeric zero false
whitespace-only string true
empty array false
null false
non-empty array true
object true
See TwigFiddle for a demonstration (strict_variables is set to false behind the "More options..." link in the header).
By extending Twig
(Disclaimer: I wrote this approach before #The_Unknown pointed out that the default filter can also be used.)
If the idea of setting strict_variables to false is too general, you can also extend Twig. I'd argue that it's better to set strict_variables to true to avoid accidental errors caused by e.g. typos in variable names, so this approach might be better.
I don't think that you can create a filter to do this, as an undefined variable would still throw an exception. You might be able to create a custom tag, test or extension (see Extending Twig for ways to extend Twig); I'm going to create a custom function as it's probably the simplest approach.
$twig->addFunction(new Twig_Function('istruthy', function($context, $var) {
return array_key_exists($var, $context) && $context[$var];
}, ['needs_context' => true]));
The ['needs_context' => true] part is essential here, as then you will have access to $context, which contains the variables present in the current context. (You can e.g. put var_dump($context) above the return statement to see it yourself.)
If you want istruthy to support checking multiple variables at once, you can do this:
$twig->addFunction(new Twig_Function('istruthy', function($context, ...$vars) {
foreach ($vars as $var) {
if (!array_key_exists($var, $context) || !$context[$var]) {
return false;
}
}
return true;
}, ['needs_context' => true]));
Then in Twig you can do:
{% if istruthy('foo') %} ... {% endif %}
{% if istruthy('foo') or istruthy('bar') %} ... {% endif %}
{# These two are the same: #}
{% if istruthy('foo') and istruthy('bar') and istruthy('baz') %} ... {% endif %}
{% if istruthy('foo', 'bar', 'baz') %} ... {% endif %}
{# Ternary operator can also be used: #}
{{ istruthy('foo') ? 'yep' : 'nope' }}
You might want to check in the istruthy function whether the arguments are strings or something else and then act accordingly. array_key_exists expects the first argument to be either a string or an integer.

The following works well:
{% if someVar ?? false %}

Related

Using "if any():" in Jinja2?

In Jinja2 I'm looking for a way to check if at least one of a list of variables has a value. Basically in python I would do:
if any([item['genre'], item['type'], item['color']]):
However, in Jinja the following isn't valid:
{% if any([item['genre'], item['type'], item['color']]) %}
# some part of the Jinja template
{% endif %}
Is there a way to have the same "any()" check in Jinja2?
For background: the full piece of code that I currently try to add (but isn't valid):
{% if any([item['genre'], item['type'], item['color']]) %}
<ItemProperties>
<ItemProperty key="genre">{{ item['genre'] }}</ItemProperty>
<ItemProperty key="type">{{ item['type'] }}</ItemProperty>
<ItemProperty key="color">{{ item['color'] }}</ItemProperty>
</ItemProperties>
{% endif %}
There is no direct equivalent of the any() function in Jinja2 templates.
For 3 hard-coded elements, I'd just use boolean logic or:
{% if item['genre'] or item['type'] or item['color'] %}
Otherwise, you can use the select() filter without an argument (followed by first() to force iteration). Because select() is itself a generator, using first() on select() makes this short-circuit the way any() would:
{% if (item['genre'], item['type'], item['color'])|select|first %}
Without an argument, select() returns any objects from the input sequence that are true, and first() ensures it iterates no more than needed to find one such element.
The last option is to register a custom filter to just add any() to Jinja yourself; I'd also add all() in that case. You can register both functions directly, since neither takes options:
environment.filters['any'] = any
environment.filters['all'] = all
at which point you can then use
{% if (item['genre'], item['type'], item['color'])|any %}

Displaying a link in a Twig temple

It seems easy, but I encounter a weird behavior.
In a twig file :
{% set my_html = 'Hello world' %}
{{- true is not same as(false) ? (true is same as(false) ? ('1'~my_html)|raw : ('2'~my_html)|raw) -}}
The printed part is ('2'~my_html)|raw and it works fine : I see a real link.
Output is :
2Hello world
Now, it works only because I apply the raw filter to ('1'~my_html)... Try this :
{% set my_html = 'Hello world' %}
{{- true is not same as(false) ? (true is same as(false) ? ('1'~my_html) : ('2'~my_html)|raw) -}}
And it will display : 2Hello world
I don't understand why I need to apply a filter on something else to get the expected result ? Is it a bug ?
This is a documented behavior of the raw filter. I quote the note from that page:
Be careful when using the raw filter inside expressions:
{% autoescape %}
{% set hello = '<strong>Hello</strong>' %}
{% set hola = '<strong>Hola</strong>' %}
{{ false ? '<strong>Hola</strong>' : hello|raw }}
does not render the same as
{{ false ? hola : hello|raw }}
but renders the same as
{{ (false ? hola : hello)|raw }} {% endautoescape %}
The first ternary statement is not escaped: hello is marked as being
safe and Twig does not escape static values (see escape). In the
second ternary statement, even if hello is marked as safe, hola
remains unsafe and so is the whole expression. The third ternary
statement is marked as safe and the result is not escaped.
And a comment on a github issue clarifies that the concatenation operator marks your string as unsafe. So in your case
{% set my_html = '<' %}
{# ('1'~my_html) is not safe, so the whole expression is not #}
{{ false
? ('1'~my_html)
: ('2'~my_html)|raw
}}
includes two strings: a safe one, ('2'~my_html)|raw) and an unsafe one, ('1'~my_html) (because it does not apply the raw filter), so as the note from raw documentation says, the whole expression stays unsafe and autoescaping is applied. But in the other case when both strings are marked safe, the whole expression becomes safe and the autoesaping is not applied:
{% set my_html = '<' %}
{# now both strings are safe, so is the whole expression #}
{{ false
? ('1'~my_html)|raw
: ('2'~my_html)|raw
}}
This is not a bug but, due to the fact the default settings of twig will autoescape variables.
You can read more about it in the documentation.

Variables concatenation

After about an year of using Smarty i wanted to try Twig.
I am facing a problem concatenating a string and a variable to build dynamically the titles of the page when i switch the language.
In Smarty, the controller passes to the template the variables:
$title_it and $title_en
For the title of the page i do <title>{$title_{$lang}}</title> (where $lang is a global variable) and i can switch the values of the variables when i change the language.
I am not able to replicate this behaviour in Twig.
I tried the following methods without having success:
{{ title_ ~ {{ lang }} }} (I think Twig sees the variable "title_" doesn't exists.
'title_'~{{ lang }} (This prints 'title_it' and not it's content)
Is there a way to keep this logic and continuing to use this approach or do i have to handle the titles of the pages differenly?
Thanks a lot,
Manuel
The _context variable holds all variables in the current context, so you can do:
{{ _context['title_' ~ lang]|default }}
This is basically the same as using the attribute function:
{{ attribute(_context, 'title_' ~ lang)|default }}
I would personally use the former as it's more concise and in my opinion clearer.
The default filter is needed when the environment option strict_variables is set to true (the default value is false, but I prefer to set it to true to avoid accidental problems caused by e.g. typos), otherwise you'll get a Twig_Error_Runtime exception if the variable doesn't exist. For example, if you have variables title_en and title_it but try to output the variable title_de (which doesn't exist), you get that exception with the message Key "title_de" for array with keys "title_en, title_it, lang" does not exist.
A more verbose way to check the existence of a variable is to use the defined test:
{% if _context['title_' ~ lang] is defined %} ... {% endif %}
With the default filter you can also provide a default value:
{{ _context['title_' ~ lang]|default('Default title') }}
If you omit the default value (i.e. you do |default instead of |default('some value')), the default value will be an empty string.
See TwigFiddle
I think here the solutions which can fix your problems.
Controller code:
return $this->render('myTwig.html.twig', array(
'lang'=>'en',
'title_en'=>'English Title',
'title_it'=>'Italian Title'
));
Twig Code:
{% set myVar='title_'~lang %}
{{ attribute(_context, myVar) }}
This will display "English Title" on your page.
Here _context variable is a magic variable in twig where you can find all parameters which you passed from your controller to this twig.
attribute(_context,myVar)
it displays the value from passed parameters key to value.
Hope this will solve your problems.
Thanks
I would change the controller to pre-calculate the language dependent value for title before passing it to the twig template. If you are unable (unwilling) to do that, then ...
<title>{% if lang == 'it' %}{{ title_it }}{% else %}{{ title_en }}{% endif %}</title>

Twig: wrap variable in array if not already one, use empty array if missing

I have a variable modifier passed to a few twig templates. I want to be able to not pass it at all, to pass a single string, or to pass an array of strings. I then want to be able to assume it is always an array of strings (possibly empty) in my template code.
At the moment I have at the start of the twig templates
{% set modifier = modifier | default([]) %}
{% if modifier is not iterable %}
{% set modifier = [modifier] %}
{% endif %}
This does what I want, but is there an easier way? It's a lot of code to do something very simple.
You could one-line it with a filter (can't seem to be able to mimic the default filter though)
<?php
$filter = new Twig_SimpleFilter('wrap_array', function ($value) {
return is_array($value) ? $value : [ $value, ];
});
$twig = new Twig_Environment($loader);
$twig->addFilter($filter);
And use it in your template :
{% set foo = foo|default({})|wrap_array %}

Twig ternary not applying filter if false

When using Twig's ternary operator, everything goes as expected EXCEPT the |raw filter is not being applied when the condition is false:
{{ thing.description|length > 255 ? thing.description|striptags|slice(0,255) ~ '...' : thing.description|raw }}
When true, it IS applying the |striptags|slice(0,255) filters.
The |raw filter does get applied when coded like this (and false):
{% if thing.description|length > 255 %}
{{ thing.description|striptags|slice(0,255) ~ '...' }}
{% else %}
{{ thing.description|raw }}
{% endif %}
For the life of me I can't figure out why |raw isn't applied when using ternary syntax.
In fact |raw do nothing. Really. Look at its code here.
When you do operation, value is marked as unsafe. |raw mark it as safe. If you try to print unsafe value with {{ ... }}, it will be escaped. It's how Twig is designed.
Let's analyse that simple code: {{ (a|raw) ~ (b|raw) }}
a is marked as safe.
b is marked as safe.
a and b are glued together. It's marked as unsafe, because ~ is a operation.
Value a ~ b is printed ESCAPED. Wow.
You must use {{ (a ~ b)|raw }}.
In your problem: first you calculate thing.description|raw and then it's passed to ternary. Results are marked as unsafe, because ternary is operation. You must use |raw after all operations, so {% if %} is the only solution.

Resources