How to call function in twig in Symfony2? - php

In my model (Task) I have a function:
public function isTaskOverdue()
{
if ("now"|date('Y-m-d') > task.deadline|date('Y-m-d')){
return false;
} else{
return true;
}
}
In twig (edit) I want to display form:
{% extends 'base.html.twig' %}
{% block title %}app:Resources:Task:edit{% endblock %}
{% block body %}
{{ form(form) }}
{% endblock %}
I want to display form, if this function return true.
How can I call this function in twig?

Pass the task entity to twig and call method from object task :
{% if task.isTaskOverdue %}
{{ form(form) }}
{% endif %}

I think it should be your controller that receives the function result and display the form or not depending on it.
Also you can write your function like so :
public function isTaskOverdue()
{
return ("now"|date('Y-m-d') > task.deadline|date('Y-m-d'));
}

Pass the task entity to twig and do :
{% extends 'base.html.twig' %}
{% block title %}app:Resources:Task:edit{% endblock %}
{% block body %}
{% if "now"|date("Ymd") <= task.deadline|date("Ymd") %}
{{ form(form) }}
{% endif %}
{% endblock %}
But, caution :
If you just not display the form, there is a security issue, because if an attacker submit the form from an self rebuilded HTML page, your controller will receive the form data and apply it.
So I would do the check in the controller, and only create and pass the form to the twig template if the condition is true.
Then, in twig you can use :
{% if form is defined %}
{{ form(form) }}
{% endif %}

Related

Adding a global action in EasyAdmin, place my button underneath the table instead of above

I created a global Action :
public function export(
Admin $user,
StructureRepository $structureRepository,
ExportCsvEntity $exportCsvEntity,
string $entityClass
): HttpFoundationResponse {
$zipcodes = $structureRepository->getZipcodesByStructureFromAdmin($user->getId());
$exportCsvEntity->export(
$zipcodes,
$entityClass
);
return $exportCsvEntity->download($entityClass);
}
public function exportButton(): Action
{
return Action::new('export', 'admin.crud.user.field.activities_tracking.button.export')
->linkToCrudAction('export')
->displayAsLink()
->setCssClass('btn btn-primary')
->createAsGlobalAction()
;
}
Then in my Crud Controller I call it :
if ($this->getUser() instanceof Admin) {
$export = $this->exportAction->exportButton();
$actions->add(Crud::PAGE_INDEX, $export);
}
In the doc its written => Global actions are displayed above the listed entries.
But in my case the button is underneath the table
Have a look here
My template is extending '#!EasyAdmin/crud/index.html.twig'then I override the global_actions block :
{% block global_actions %}
{{ parent() }}
{% endblock global_actions %}
Now my button is above the table but also underneath :
Have a look here
What do I do wrong ?
You are correct when trying to do this by overriding the index template.
An easy way to do this considering how the template is organized is to modify the global_actions block by filtering actions you do not want to show above the list. For example by using a css class to not show a global action above the list.
{% block global_actions %}
<div class="global-actions">
{% for action in global_actions|filter(a => 'under-list' not in a.cssClass) %}
{{ include(action.templatePath, { action: action }, with_context = false) }}
{% endfor %}
</div>
{% endblock global_actions %}
And in your crud controller:
Action::new('customAction', $label, $icon)
->addCssClass('under-list')
->createAsGlobalAction()
->linkToCrudAction('customAction');
And you need to add your new list of actions under your list by overriding the main block.
{% block main %}
{{ parent() }}
{% block under_list_global_actions %}
<div class="under-list-global-actions">
{% for action in global_actions|filter(a => 'under-list' in a.cssClass) %}
{{ include(action.templatePath, { action: action }, with_context = false) }}
{% endfor %}
</div>
{% endblock under_list_global_actions %}
{% endblock main %}
And you should get your custom global action (with the css class under-list) under your list.

Symfony Form Collection Theming but feed some data

Symfony 3.3
I have a form of my Voyage entity
Voyage entity has a Collection in it, named cities, collection of entity City.
And so do the form with the Collection named cities.
The user first use the form and create an instance of Voyage and add some cities to it, I managed to customise the prototype and render it via javascript when the user click "add city" button.
The form is rendered this way for the interesting part (cleaned version without html):
{% extends "#User/layout.html.twig" %}
{% form_theme form.cities '#Prototype/city.html.twig' %}
{% block content %}
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_row(form.cities) }}
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock content %}
The theme for the 'form.cities' :
{% block collection_widget %}
{% import '#Prototype/prototype.city.twig' as proto %}
{% spaceless %}
<div class="collection">
{% if prototype is defined %}
{% set attr = attr|merge({'data-prototype': proto.city(prototype)|escape }) %}
{% endif %}
<div {{ block('widget_container_attributes') }}></div>
<div id="container-cities">
{# Here I will add the cities via javascript when user add one #}
</div>
</div>
{% endspaceless %}
{% endblock collection_widget %}
The macro file used in this theme and imported as proto :
{% macro city(widget, id, name, zip) %}
{% spaceless %}
<div
class="added-city border-gray"
data-id="{{id|default('__id__')}}"
id="{{name|default('__name__')}}">
{{name|default('__city_name__')}} ({{zip|default('__zip__')}})
{{ form_errors(widget) }}
{{ form_widget(widget) }}
</div>
{% endspaceless %}
{% endmacro %}
My general problem : When the user wants to edit its instance of Voyage, it already has some cities in it. How can I render them ? How can I access the cities variable from within the theme.
My partial solution : I wanted to extract the 'container-cities' block from the theme file to the rendered html where the form is initialy rendered and where I can access the variables and do this :
{% import '#Prototype/prototype.city.twig' as proto %}
{% for city in form.cities %}
{{ proto.city(city, city.name) }}
{% endfor %}
But it give me this error :
Neither the property "name" nor one of the methods "name()", "getname()"/"isname()"/"hasname()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView".
(It doesn't fail on city.id probably because of some other field named id)
My question :
How can I access the cities within the theme and render them with my macro ?
Or
How to access to the cities items where I render the form, because the form.cities doesn't seem to be the actual City entity Collection, and get rid of that error ?
Thanks
I finally solved it, it was that simple :
I just used this in the form theme :
{% for city in form %}
{{ proto.city(city, city.vars.value.id, city.vars.value.name, city.vars.value.zip) }}
{% endfor %}

Find if twig array is empty of non-protected values

Given an a php array in a twig template:
object(Drupal\Core\Template\Attribute)#1208 (1) {
["storage":protected]=> array(0) { }
}
How do I check if there are no non-protected elements in the array? The idea is that I can only operate on non-protected values, so I can pretend the array is empty if only protected values are present.
So far, my check is as follows:
{% if attributes is defined and attributes is not empty %}
<div{{ attributes }}>
{{ content }}
</div>
{% else %}
{{ content }}
{% endif %}
In its current form, this displays <div>[Content]</div>. Instead, I'd like to see: [Content]
Any help?
If this is in Drupal 8, you can pass the attributes value through render to find out, like this:
{% if attributes|render %}
<div{{ attributes }}>
{{ content }}
</div>
{% else %}
{{ content }}
{% endif %}
Extend twig
<?php
$twig = new Twig_Environment($loader);
$twig->addFilter(new Twig_SimpleFilter('accessible_properties', 'get_object_vars'));
Use it inside template
{% set public_attributes = attributes is defined ? (attributes|accessible_properties) : [] %}
{% if public_attributes is not empty %}
...
{% else %}
...
{% endif %}

How can I use break or continue within for loop in Twig template?

I try to use a simple loop, in my real code this loop is more complex, and I need to break this iteration like:
{% for post in posts %}
{% if post.id == 10 %}
{# break #}
{% endif %}
<h2>{{ post.heading }}</h2>
{% endfor %}
How can I use behavior of break or continue of PHP control structures in Twig?
This can be nearly done by setting a new variable as a flag to break iterating:
{% set break = false %}
{% for post in posts if not break %}
<h2>{{ post.heading }}</h2>
{% if post.id == 10 %}
{% set break = true %}
{% endif %}
{% endfor %}
An uglier, but working example for continue:
{% set continue = false %}
{% for post in posts %}
{% if post.id == 10 %}
{% set continue = true %}
{% endif %}
{% if not continue %}
<h2>{{ post.heading }}</h2>
{% endif %}
{% if continue %}
{% set continue = false %}
{% endif %}
{% endfor %}
But there is no performance profit, only similar behaviour to the built-in break and continue statements like in flat PHP.
From docs TWIG 2.x docs:
Unlike in PHP, it's not possible to break or continue in a loop.
But still:
You can however filter the sequence during iteration which allows you to skip items.
Example 1 (for huge lists you can filter posts using slice, slice(start, length)):
{% for post in posts|slice(0,10) %}
<h2>{{ post.heading }}</h2>
{% endfor %}
Example 2 works TWIG 3.0 as well:
{% for post in posts if post.id < 10 %}
<h2>{{ post.heading }}</h2>
{% endfor %}
You can even use own TWIG filters for more complexed conditions, like:
{% for post in posts|onlySuperPosts %}
<h2>{{ post.heading }}</h2>
{% endfor %}
A way to be able to use {% break %} or {% continue %} is to write TokenParsers for them.
I did it for the {% break %} token in the code below. You can, without much modifications, do the same thing for the {% continue %}.
AppBundle\Twig\AppExtension.php:
namespace AppBundle\Twig;
class AppExtension extends \Twig_Extension
{
function getTokenParsers() {
return array(
new BreakToken(),
);
}
public function getName()
{
return 'app_extension';
}
}
AppBundle\Twig\BreakToken.php:
namespace AppBundle\Twig;
class BreakToken extends \Twig_TokenParser
{
public function parse(\Twig_Token $token)
{
$stream = $this->parser->getStream();
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
// Trick to check if we are currently in a loop.
$currentForLoop = 0;
for ($i = 1; true; $i++) {
try {
// if we look before the beginning of the stream
// the stream will throw a \Twig_Error_Syntax
$token = $stream->look(-$i);
} catch (\Twig_Error_Syntax $e) {
break;
}
if ($token->test(\Twig_Token::NAME_TYPE, 'for')) {
$currentForLoop++;
} else if ($token->test(\Twig_Token::NAME_TYPE, 'endfor')) {
$currentForLoop--;
}
}
if ($currentForLoop < 1) {
throw new \Twig_Error_Syntax(
'Break tag is only allowed in \'for\' loops.',
$stream->getCurrent()->getLine(),
$stream->getSourceContext()->getName()
);
}
return new BreakNode();
}
public function getTag()
{
return 'break';
}
}
AppBundle\Twig\BreakNode.php:
namespace AppBundle\Twig;
class BreakNode extends \Twig_Node
{
public function compile(\Twig_Compiler $compiler)
{
$compiler
->write("break;\n")
;
}
}
Then you can simply use {% break %} to get out of loops like this:
{% for post in posts %}
{% if post.id == 10 %}
{% break %}
{% endif %}
<h2>{{ post.heading }}</h2>
{% endfor %}
To go even further, you may write token parsers for {% continue X %} and {% break X %} (where X is an integer >= 1) to get out/continue multiple loops like in PHP.
From #NHG comment — works perfectly
{% for post in posts|slice(0,10) %}
I have found a good work-around for continue (love the break sample above).
Here I do not want to list "agency". In PHP I'd "continue" but in twig, I came up with alternative:
{% for basename, perms in permsByBasenames %}
{% if basename == 'agency' %}
{# do nothing #}
{% else %}
<a class="scrollLink" onclick='scrollToSpot("#{{ basename }}")'>{{ basename }}</a>
{% endif %}
{% endfor %}
OR I simply skip it if it doesn't meet my criteria:
{% for tr in time_reports %}
{% if not tr.isApproved %}
.....
{% endif %}
{% endfor %}

HTML in Symfony2 form labels instead of plain text

I am trying to implement something like this:
<div>
<input type="checkbox" name="checkbox" id="checkbox_id" />
<label for="checkbox_id">I agree to the Terms of Service</label>
</div>
The closest I've come to implement this is through:
<div>
{{ form_widget(form.agreeWithTos) }}
<label for="{{ form.agreeWithTos.vars.id }}">I agree to the Terms of Service</label>
</div>
Is there a better way? Having to specify {{ form.agreeWithTos.vars.id }} is inelegant. :)
Solved this problem using the following code in my form-theme:
{# ---- form-theme.html.twig #}
{% block checkbox_row %}
{% spaceless %}
<div>
{{ form_errors(form) }}
<label class="checkbox" for="{{ form.vars.id }}">
{{ form_widget(form) }}
{{ label|default(form_label(form)) | raw }}
</label>
</div>
{% endspaceless %}
{% endblock %}
in your Form-Template you can then use:
{% form_theme form '::form-theme.html.twig' %}
{{form_row(form.termsOfServiceAccepted, {
'label' : 'I have read and agree to the Terms and conditions'
})
}}
this way, the block from the form-theme would apply to any checkbox on the page. If you need to also use the default-theme, you can add a parameter to enable special-rendering:
{# ---- form-theme.html.twig #}
{% block checkbox_row %}
{% spaceless %}
{% if not useTosStyle %}
{{ parent() }}
{% else %}
{# ... special rendering ... #}
{% endif %}
{% endspaceless %}
{% endblock %}
which would be used like this:
{% form_theme form '::form-theme.html.twig' %}
{{form_row(form.termsOfServiceAccepted, {
'useTosStyle' : true,
'label' : 'I have read and agree to the Terms and conditions'
})
}}
Thanks to a recent commit to Symfony, you can use label_html from Symfony 5.1 onward:
{{ form_label(
form.privacy,
'I accept the privacy terms.',
{
'label_html': true,
},
) }}
I've been beating my head over this then had a eureka moment. The easiest way to do this–BY FAR–is to create a Twig extension.
Here's my Twig code:
{# twig example #}
{% block form_label %}
{% set label = parent() %}
{{ label|unescape|raw }}
{% endblock %}
and PHP:
<?php
new Twig_SimpleFilter('unescape', function($value) {
return html_entity_decode($value);
});
A couple notes:
This unescapes all previously escaped code. You should definitely re-escape afterwards as necessary.
This requires an extends tag for your target form theme in your own custom form theme which you can use in your subsequent forms. Put the twig code in a custom form theme and replace references to your other form theme/themes with this one.
Overall this is the fewest lines of code for the biggest and best outcome that I've been able to find. It's also ultra-portable and DRY: You can extend any form theme and the label will change without you changing the rest of your code.
Another very simple approach is to override the form theme directly in the template which renders the form. Using {% form_theme form _self %} it is as simple as this:
{% form_theme form _self %}
{% block form_label %}
{{ label | raw }}
{% endblock %}
{{ form_start(form) }}
See the corresponding section in the docs:
https://symfony.com/doc/current/form/form_customization.html#method-1-inside-the-same-template-as-the-form
The easiest way to customize the [...] block is to customize it directly in the template that's actually rendering the form.
By using the special {% form_theme form _self %} tag, Twig looks inside the same template for any overridden form blocks. [...]
The disadvantage of this method is that the customized form block can't be reused when rendering other forms in other templates. In other words, this method is most useful when making form customizations that are specific to a single form in your application. If you want to reuse a form customization across several (or all) forms in your application, read on to the next section.
Another approach is to use a simple Twig replacement:
{% set formHtml %}
{{ form(oForm) }}
{% endset %}
{{ formHtml|replace({'[link]': '', '[/link]': ''})|raw }}
In your form, you have something like this in order to make it work:
$formBuilder->add('conditions', CheckboxType::class, [
'label' => 'Yes, I agree with the [link]terms and conditions[/link].'
]
);
Of course, you may change [link] to anything else. Please note that you do not use HTML tags ;-)
I think you are looking for form theming. That way you are able to style each part of form, in an independent file, anyway you want and then just render it in "elegant" way, row by row with {{ form_row(form) }} or simply with {{ form_widget(form) }}. It's really up to you how you set it up.
Symfony 4.2
TWIG:
{% block main %}
....
{% form_theme form _self %}
...
{{ form_row(form.policy, {'label': 'security.newPassword.policy'|trans({"%policyLink%":policyLink, "%termsLink%":termsLink})}) }}
...
{% endblock %}
{% block checkbox_radio_label %}
<label{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}>
{{- widget|raw }} {{ label|unescape|raw }}
</label>
{% endblock checkbox_radio_label %}
PHP:
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class AppExtension extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('unescape', function ($value) {
return html_entity_decode($value);
}),
];
}
}
So form theming is pretty complicated. The easiest thing I've found is to just suppress the field's label ('label'=> false in Symfony form class) and then just add the html label in the twig html.
You could leverage form theming in another way: you could move the <label> tag outside the form_label() function.
Create a custom form theme, and for checkboxes only move the <label> tag outside the form_label function:
{% block checkbox_row %}
<label>{{ form_label(form) }}</label>
{{ form_errors(form) }}
{{ form_widget(form) }}
{% endblock checkbox_row %}
{% block checkbox_label %}
{{ label }}
{% endblock checkbox_label %}
Now, in your teplate, override the label of your checkbox, and thus effectively inject HTML into the label function:
{% form_theme form 'yourtheme.html.twig' _self %}
{% block _your_TOS_checkbox_label %}
I agree with terms and conditions
{% endblock _your_TOS_checkbox_label %}

Categories