How to import multiple macros? - symfony

I want to elegantly import multiple macros from a single place.
I created a file called "macros.twig" and have included it into my template:
{% include "_includes/macros" %}
Within that file, I hoped to import all my available macros like so:
{% import "_includes/macros/snippets" as snippets %}
{% import "_includes/macros/timestamp" as timestamp %}
{% import "_includes/macros/telephone" as telephone %}
{% import "_includes/macros/subscribe" as subscribe %}
{% import "_includes/macros/image" as image %}
{% import "_includes/macros/admin" as admin %}
This sort-of modular approach was suppose to make it easier to manage the macros I want to use globally; without cluttering the head of my main layout.
Currently, when I call a macro this way, I get a "Variable "subscribe" does not exist" error.
What's the preferred method of importing multiple macros at one time?
Thanks

macro tag in Twig is a kind of function you can use to avoid code repetition and is meant for a single template with {% import _self as macro %} or to be shared between some different templates for a group of controllers using the same view variables.
If you need to use a function globally in twig you better create a \Twig_SimpleFunction.
see http://twig.sensiolabs.org/doc/advanced.html#functions and http://symfony.com/doc/current/cookbook/templating/twig_extension.html
Edited based on comment
Anyway you could have something like this to autoload macro :
<?php
// src/AppBundle/Twig/MacroAutoloadExtension.php
namespace AppBundle\Twig;
class MacroAutoloadExtension extends \Twig_Extension
{
public function getFunctions()
{
return array(
// "*"" is used to get "template_macro" as $macro as third argument
new \Twig_SimpleFunction('macro_*', array($this, 'getMacro'), array(
'needs_environment' => true, // $env first argument will render the macro
'needs_context' => true, // $context second argument an array of view vars
'is_safe' => array('html'), // function returns escaped html
'is_variadic' => true, // and takes any number of arguments
))
);
}
public function getMacro(\Twig_Environment $env, array $context, $macro, array $vars = array())
{
list($name, $func) = explode('_', $macro);
$notInContext = 0; // helps generate unique context key
$varToContextKey = function ($var) use (&$context, $name, $func, &$notInContext) {
if (false !== $idx = array_search($var, $context, true)) {
return $idx;
}
// else the var does not belong to context
$key = '_'.$name.'_'.$func.'_'.++$notInContext;
$context[$key] = $var;
return $key;
};
$args = implode(', ', array_map($varToContextKey, $vars));
$twig = <<<EOT
{% import '_includes/macros/$name.twig' as $name %}
{{ $name.$func($args) }}
EOT;
try {
$html = $env->createTemplate($twig)->render($context);
} catch (\Twig_Error $e) {
$e->setTemplateFile(sprintf('_includes/macro/%s.twig', $name));
throw $e;
}
return $html;
}
public function getName()
{
return 'macro_autoload_extension';
}
}
Register the extension :
# app/config/sevices.yml
services:
...
app.macro_autoload_extension:
class: AppBundle\Twig\MacroAutoloadExtension
public: false
tags:
- { name: twig.extension }
Write some macros :
{# app/Resources/views/_includes/macros/list.twig #}
{% macro ol(array) %}
{% if array is iterable %}
<ol>
{% for item in array %}
<li>
{% if item is iterable %}
{% for sub_item in item %}{{ macro_list_ul(sub_item) }}{% endfor %}
{% else %}
{{ item }}
{% endif %}
</li>
{% endfor %}
</ol>
{% else %}
<ol><li>{{ array }}</li></ol>
{% endif %}
{% endmacro %}
{% macro ul(array) %}
{% if array is iterable %}
<ul>
{% for key, item in array %}
{{ key }}:
{% if item is iterable %}
{% for sub_item in item %}{{ macro_list_ul(sub_item) }}{% endfor %}
{% else %}{{ item }}{% endif %}
{% endfor %}
</ul>
{% else %}
<ul><li>{{ array }}</li></ul>
{% endif %}
{% endmacro %}
Then you can use everywhere in you views :
{{ macro_list_ol(['un', 'deux', 'trois']) }}
or:
{% set hash = { 'one': 1, 'two': 'deux', 'posts': posts } %}
{{ macro_list_ul(hash) }}
Bonus
Usually when you import macro in a template (one file) with _self or from another template, if you need a macro in a set tag, the macro is not available since set tag has a different scope than _self (even if it shares context) :
{# /app/Resources/views/includes/macro/outer.html.twig #}
{% macro function(args) %}
...
{% endmacro %}
plus
{# /app/Resources/views/Bundle/Controller/action.html.twig #}
{% macro inner_macro(arg1, arg2) %}
{# render something #}
{# cannot access context of this view, only args #}
{% endmacro %}
{% import _self as inner %}
{% import '/includes/macro/outer_macro.html.twig' as outer %} {# cannot access context either %}
...
{% set some_var %}
{# can access context but neither outer or inner #}
{{ inner.inner_macro('yes', 64) }} {# will not work #}
{# you need to do import _self as inner again %}
{# this is fix by both my answer and the one by #KalZekdor #}
{{ macro_outer_function(var_from_context) }} {# will work #}
{% endset %}
{{ some_var }}
You can even call macro from macro without using import.
Update
I created a gist

This was a bit difficult for me, as well, but I put together a nice bundle with an Event Listener that autoloads the macros.
namespace App\Common\UIExtensionBundle\Listeners;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Bundle\TwigBundle\TwigEngine;
class UIExtenderListener
{
private $macroNamespace = 'ui';
public function __construct(\Twig_Environment $oTwig, $aMacros)
{
$this->twig = $oTwig;
//Macros
$this->macros = $aMacros;
}
public function onKernelRequest(GetResponseEvent $oEvent)
{
$templates = [];
foreach ($this->macros as $macro => $template)
{
$templates[$macro] = $this->twig->loadTemplate($template);
}
$this->twig->addGlobal($this->macroNamespace, $templates);
}
}
The services.xml for the bundle:
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="uiext.macros" type="collection">
<parameter key="test">UIXBundle:ui:test.html.twig</parameter>
<parameter key="list">UIXBundle:ui:list.html.twig</parameter>
<parameter key="entity">UIXBundle:ui:entity.html.twig</parameter>
</parameter>
</parameters>
<services>
<service id="uiext.extender" class="App\Common\UIExtensionBundle\Listeners\UIExtenderListener" scope="container">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="1000" />
<argument type="service" id="twig"/>
<argument>%uiext.macros%</argument>
</service>
</services>
</container>
Here's the views\ui\list.html.twig file:
{% macro ol(arr) %}
<ol>
{% for item in arr %}
<li>{{ item }}</li>
{% endfor %}
</ol>
{% endmacro %}
{% macro ul(arr) %}
<ul>
{% for item in arr %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% endmacro %}
Then, from any twig template, just add {{ ui.list.ul(listArr) }}

Related

EasyAdmin 3.X - How to see related entities `toString` instead of the number of association in the list?

I have an entity Product with a ManyToMany relation to an entity Category
/**
* #ORM\ManyToMany(targetEntity="App\Domain\Category", inversedBy="stalls")
*/
private $categories;
//...
/**
* #return Collection|Category[]
*/
public function getCategories(): Collection
{
return $this->categories;
}
In the ProductCrudController class I have the following configureFields method:
public function configureFields(string $pageName): iterable
{
return [
Field::new('name'),
Field::new('description'),
AssociationField::new('categories'),
];
}
When creating/editing a Product everything works as expected in the relation, but in the list of products instead of showing the related categories I see the number of categories the product has. How can I change this behaviour?
In the following image the first product has 1 category and the second one in the list has 2 different categories. I would like the name of the categories to be shown here.
As a side note: Category class has a __toString method returning the name of the category.
EDIT:
The behaviour I am looking for is the same as the Tags column in the following image:
You can make a template for that like so:
// somewhere here templates/admin/field/category.html.twig
{% for category in field.value %}
{%- set url = ea_url()
.setController('Path\\To\\Your\\CategoryCrudController')
.setAction('detail')
.setEntityId(category.id)
-%}
<a href="{{ url }}">
{{ category.name }}{% if not loop.last %}, {% endif %}
</a>
{% else %}
<span class="badge badge-secondary">None</span>
{% endfor %}
And just add it to the field
// in ProductCrudController
AssociationField::new('categories')->setTemplatePath('admin/field/category.html.twig'),
You can format the value using the method formatValue like this :
->formatValue(function ($value, $entity) {
$str = $entity->getCategories()[0];
for ($i = 1; $i < $entity->getCategories()->count(); $i++) {
$str = $str . ", " . $entity->getCategories()[$i];
}
return $str;
})
I had the same issue on my detail page. So instead of a template, I change the field type depending on the pagename
if (Crud::PAGE_DETAIL === $pageName) {
$field = ArrayField::new('field')->setLabel('label');
} else {
$field = AssociationField::new('field')->setLabel('label');
}
I will do that way :
->formatValue(function ($value, $entity) {
return implode(",",$entity->getCategories()->toArray());
})
Building on top of the most upvoted answer, you could make the Twig snippet universal like this:
{% for member in field.value %}
{%- if field.customOption('crudControllerFqcn') is not empty -%}
{%- set url = ea_url()
.setController(field.customOption('crudControllerFqcn'))
.setAction('detail')
.setEntityId(member.id)
-%}
<a href="{{ url }}">
{{ member }}
</a>
{%- else -%}
{{ member }}
{%- endif -%}
{%- if not loop.last %}, {% endif -%}
{% else %}
<span class="badge badge-secondary">None</span>
{% endfor %}
This way you could set the link destination in your CRUD controller and use the template everywhere. When no CRUD controller is not set, you will still have the individual items enumerated, but not as links.
In my TeamCrudController, I'm doing this:
public function configureFields(string $pageName): \Generator
{
yield TextField::new('name')
->setDisabled()
;
$members = AssociationField::new('members')
->hideOnForm()
;
if (Crud::PAGE_DETAIL === $pageName) { // I want to see the number on INDEX, but the list on DETAIL
$members
->setCrudController(EmployeeCrudController::class)
->setTemplatePath('admin/field/expanded_association_field.html.twig')
;
}
yield $members;
}
Alternatively, if you would like to become the default behaviour, override the EasyAdminBundle template by placing the following content in templates/bundles/EasyAdminBundle/crud/field/association.html.twig:
{# #var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{# #var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
{# #var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
{% if 'toMany' == field.customOptions.get('associationType') %}
{% if ea.crud.currentPage == 'detail' %}
{% for member in field.value %}
{%- if field.customOption('crudControllerFqcn') is not empty -%}
{%- set url = ea_url()
.setController(field.customOption('crudControllerFqcn'))
.setAction('detail')
.setEntityId(member.id)
-%}
<a href="{{ url }}">
{{- member -}}
</a>
{%- else -%}
{{ member }}
{%- endif -%}
{%- if not loop.last %}, {% endif -%}
{% else %}
<span class="badge badge-secondary">None</span>
{% endfor %}
{% else %}
<span class="badge badge-secondary">{{ field.formattedValue }}</span>
{% endif %}
{% else %}
{% if field.customOptions.get('relatedUrl') is not null %}
{{ field.formattedValue }}
{% else %}
{{ field.formattedValue }}
{% endif %}
{% endif %}

How do you check if a form field has data in twig template that is rendering a controller

This is a follow up question to this one . What I want to know is how to check if the entity variable exist/is defined/not null. I thought I could do this:
{% if entity.orgId is defined %}
{{ render(controller(
'CompanyNameofBundle:OrgMember:test', {'orgid':entity.orgId})) }}
{% endif %}
But if entity.orgId is null I get an exception has been thrown during the rendering of a template ("The product does not exist").
Change your controller to return null instead of exception:
public function testAction($orgid = null) {
if (!$orgid) { return null; }
// Rest of code.
}
You have two options:
Don't call the render controller using the check
{% if entity.orgId is defined and entity.orgId is not null %}
Make the testAction in the OrgMemberController null-safe (check if the parameter orgid is null)
Try this:
{% if entity.orgId is defined %}
{% if entity.orgId is null %}
{# do something #}
{% else %}
{# do anythingelse #}
{% endif %}
{% endif %}

Toggle html validation globally

I've made a couple of twig extensions but I'm stumped on this one.
I have the following template logic that I want to make into an extension.
I need reuse this logic into many different forms instead of copying and pasting the following code everywhere:
{% if html5validation is not defined %}
{{ form_start(some_form) }}
{% else %}
{% if html5validation %}
{{ form_start(some_form) }}
{% else %}
{{ form_start
(
company, {'attr': {'novalidate': 'novalidate'}}
)
}}
{% endif %}
{% endif %}
With the above code from the controller I can do the following to turn the html5 validator on and off:
$this->render(..., array(html5validation => false));
I want put the template logic into the twig extension below...
I just don't know if it's possible to implement what I've done above in a twig extension.
class HTML5Validation extends \Twig_Extension
{
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('html5validation', array($this, 'setValidation')),
);
}
public function setValidation($boolean)
{
//Implement the same logic as the twig template.
}
public function getName()
{
return 'html5validator';
}
}
The short answer is no - you can't do this using a twig extension, it's not what they're meant for.
Looking at your template fragment I'd say you need to customise the form_start block. To do this see Symfony Form Theming and How to customise form rendering.
EDIT: This solution does not work if your customised code requires local twig variables - only global twig variables are available for form theming. You can define your own twig globals in config.yml or in a twig extension.
For example, to override form_start globally, you find the default definition of the form_start block in form_div_layout.html.twig, copy it into your own form theme file e.g. YourBundle/Form/fields.html.twig, modify it as required and and update the twig configuration to apply your form theme file. Something like this:
{# src/YourBundle/Form/fields.html.twig #}
{% extends 'form_div_layout.html.twig' %}
{% block form_start -%}
{% if html5validation is not defined %}
{{ parent() }}
{% else %}
{% if html5validation %}
{{ parent() }}
{% else %}
{{ parent
(
company, {'attr': {'novalidate': 'novalidate'}}
)
}}
{% endif %}
{% endif %}
{%- endblock form_start %}
Config:
# app/config/config.yml
twig:
form:
resources:
- 'YourBundle:Form:fields.html.twig'
I actually found a better way to do what I wanted.
As a plus it works globally instead of having to populate more fields into your controller!
In YourBundle/Resources/views/validation.toggle.html.twig
{% extends 'form_div_layout.html.twig' %}
{% block form_start -%}
{% if html5validation is defined and html5validation == false %}
{% set attr = attr|merge({'novalidate': 'novalidate'}) %}
{% endif %}
{{ parent() }}
{%- endblock form_start %}
Then if you want to turn off html5 validation across the whole website:
# app/config/config.yml
twig:
global:
html5validation: false
Or
Even better just use it in your dev_config.yml if you want validation on by default on production mode but the ability to toggle validation on and off for dev mode.
# app/config/dev_config.yml
twig:
global:
html5validation: false
resources:
- 'YourBundle::validation.toggle.html.twig'
Finally use it in your twig template normally:
{% form_theme your_form 'YourBundle::validation.toggle.html.twig' %}
form_start(your_form)
Reusable and non invasive, exactly like I wanted it. :)
I got the hint from:
https://github.com/symfony/symfony/issues/11409#issuecomment-49358377
In the absence of a more elegant solution, you can always put the twig fragment given in your question into a separate file and use twig include from your various forms. The included fragment has access to the variables from the surrounding context:
{# YourBundle/Resources/views/form_start.html.twig #}
{% if html5validation is not defined %}
{{ form_start(some_form) }}
{% else %}
{% if html5validation %}
{{ form_start(some_form) }}
{% else %}
{{ form_start
(
company, {'attr': {'novalidate': 'novalidate'}}
)
}}
{% endif %}
{% endif %}
Then in the twig file for the form:
{% include 'YourBundle::form_start.html.twig' %}
If you typically pass a 'form' variable into render() in your controller(s) then you can use that in your form_start fragment. Otherwise you can pass the appropriate form in as a variable:
{% include 'YourBundle::form_start.html.twig' with {'form': localForm} %}

Symfony2 and form theming/customization (required/help/errors)

Maybe I'm overlooking something, and hopefully this is done very easy.
I have a form and what I want in the end is the following result:
Fields which:
are mandatory/required
have an error currently
have help
should get an extra a-Tag after the label and an extra div, filled with the help and/or the error, if applicable.
What I got to work is, that required fields get the a-Tag by using this:
{% use 'form_div_layout.html.twig' with field_label as base_field_label %}
{% block field_label %}
{{ block('base_field_label') }}
{% if required %}
<span> </span>
{% endif %}
{% endblock %}
So, what I tried already were different versions of this:
{% use 'form_div_layout.html.twig' with field_label as base_field_label %}
{% block field_label %}
{{ block('base_field_label') }}
{% if required or help is defined %}
<span> </span>
{% endif %}
{% endblock %}
{% block field_row %}
{% spaceless %}
<div class="row">
{% if required or help is defined %}
<div>
{{ form_errors(form) }}
{{ help }}
</div>
{% endif %}
{{ form_label(form) }}
{{ form_widget(form, { 'attr': {'class': 'grid_4'} }) }}
</div>
{% endspaceless %}
{% endblock field_row %}
And I can't get this to work.
So my questions are:
Where do I get the help text from, which can also contain HTML? I tried this within the form builder without success - but at least with an exception:
$builder ->add('subject', 'text', array(
'label' => 'Subject',
'help' => 'Can be formatted content with <strong>HTML-Elements</strong>',
));
How can I tell that the current field has an error (to add a class to the row) and if so also display it? {{ form_errors(form) }} did not output anything, no matter where I place it within `field_row˚.
There is no help text, you have to create Form Extension for field and add it to default options.
Example in SF 2.1 Beta 1:
namespace Webility\Bundle\WebilityBundle\Form\Extension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class HelpFormTypeExtension extends AbstractTypeExtension
{
public function buildView(FormViewInterface $view, FormInterface $form, array $options){
$view->setVar('help', $options['help']);
}
public function getExtendedType(){
return 'field';
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'help' => null
));
}
}
And register it as a service:
<service id="webility.form.extension.help" class="Webility\Bundle\WebilityBundle\Form\Extension\HelpFormTypeExtension">
<tag name="form.type_extension" alias="field" />
</service>
For the errors question:
Do you have any errors to print? Check that in controller if validation fails:
echo '<pre>'; print_r( $form->getErrorsAsString() ); echo '</pre>'; exit;
To solve it as stated in my question Maciej Pyszyński's anwser was very helpful.
I solved it in this case in another way, which I also want to post here. According to the manual "Adding "help" messages" I build this:
Note This solution won't work together with the formbuilder and needs some tweaking in twig.
To get the help ''-tags (actually they are divs now) …
{% block field_label %}
{{ block('base_field_label') }}
{% if attr.class is defined and '_hint' == attr.class %}
<div>
<a><span class="help">Help Icon</span></a>
<div class="tooltip">
{% if help is defined %}
{{ help|raw }}
{% else %}
Somebody forgot to insert the help message
{% endif %}
</div>
</div>
{% endif %}
{% endblock %}
To get the right class on an error
{% block field_row %}
{% spaceless %}
<div class="row{% if form_errors(form) %} error{% endif %}">
{{ form_label(form) }}
{{ form_widget(form, { 'attr': {'class': 'grid_4'} }) }}
</div>
{% endspaceless %}
{% endblock field_row %}
And the call from the template
<div class="row{% if form_errors(form.url) %} _error{% endif %}">
{{ form_label(form.field, null, { 'attr': {'class': '_hint'}, 'help': 'Help text or variable containing it' }) }}
{{ form_widget(form.field, { 'attr': {'class': 'grid_4'} }) }}
</div>

Error message customization in file upload

Using the guide http://symfony.com/doc/current/cookbook/doctrine/file_uploads.html i'm trying to customize error message, but i've a problem: variable errors is not defined, as we don't validate an entity and we are not calling $this->get('validator')->validate($entity).
{% block field_errors %}
{% spaceless %}
{# errors is undefined here #}
{% endspaceless %}
{% endblock field_errors %}
This is the sample code:
public function uploadAction()
{
$document = new Document();
$form = $this->createFormBuilder($document)
->add('name')
->add('file')
->getForm()
;
if ($this->getRequest()->getMethod() === 'POST') {
$form->bindRequest($this->getRequest());
if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$em->persist($document);
$em->flush();
$this->redirect($this->generateUrl('...'));
}
}
// Variable 'errors' is not assigned
return array('form' => $form->createView());
}
Not sure I understand. If you are following the example then $document has validation rules on it which will be tested by $form->isValid(). {{ form_errors(form) }} should output any errors.
If it is just a template customization question then you need to test for the existence of errors before trying to process them:
{% block field_errors %}
{% spaceless %}
{% if errors|length > 0 %}
<span style="color:red">
{% for error in errors %}
{{ error.messageTemplate|trans(error.messageParameters, 'validators') }}<br />
{% endfor %}
{% endif %}
{% endspaceless %}
{% endblock field_errors %}

Resources