How to show font awesome icon in symfony form select - php

I Want to show all font awesome icon in select option with Symfony Form Builder.
I add the select field with :
$choices = $this->getFontAwesome();
$form->add( $key, ChoiceType::class, array('label' => 'Texte', 'choices' => $choices, 'attr' => array('class' => "fa" ) ) );
My Function getFontAwesome();
public function getFontAwesome(){
$webroot = $this->get('kernel')->getRootDir() . '/../web';
$pattern = '/\.(fa-(?:\w+(?:-)?)+):before\s+{\s*content:\s*"\\\\(.+)";\s+}/';
$subject = file_get_contents( $webroot . '/assets/vendor/font-awesome/css/font-awesome.css');
preg_match_all($pattern, $subject, $matches, PREG_SET_ORDER);
foreach($matches as $match) {
$icons[$match[1]] = '&#x' . $match[2] . ';' ;
}
return $icons ;
}
But in the select field, don't see the icon:
Field show the code and not the icon
How i can do ?
I Try htmlspecialschars and others ( htmlentities, .. ) but don't work.

If you aren't using any js plugins like Select2 or Bootstrap-select, then you have http://jsfiddle.net/NyL7d/ this possibility, but we need work a bit to reach it.
First, to say that using <i class="fa fa-heart"></i> as label isn't a choice, because the <option> element can't have any child elements, but only text. (see related issue)
For reusability let's build a form type named "IconChoiceType" as child of "ChoiceType":
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class IconChoiceType extends AbstractType
{
/**
* Cache for multiple icon fields or sub-requests.
*
* #var array
*/
private $choices;
private $kernelRootDir;
public function __construct($kernelRootDir)
{
$this->kernelRootDir = $kernelRootDir;
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
// Pass this flag is necessary to render the label as raw.
// See below the twig field template for more details.
$view->vars['raw_label'] = true;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'attr' => [
// It's the key of the solution and can be done in many ways.
// Now, the rendered <select> element will have a new font.
'style' => "font-family: 'FontAwesome';"
],
'choices' => $this->getFontAwesomeIconChoices(),
]);
}
public function getParent()
{
return ChoiceType::class;
}
protected function getFontAwesomeIconChoices()
{
if (null !== $this->choices) {
// don't to load again for optimal performance.
// useful for multi-icon fields and sub-requests.
return $this->choices;
}
// BTW we could configure the path to the "font-awesome.css".
$fontAwesome = file_get_contents($this->kernelRootDir.'/../web/assets/vendor/font-awesome/css/font-awesome.css');
// this regular expression only works with uncompressed version (not works with "font-awesome.min.css")
$pattern = '/\.(fa-(?:\w+(?:-)?)+):before\s+{\s*content:\s*"\\\\(.+)";\s+}/';
if (preg_match_all($pattern, $fontAwesome, $matches, PREG_SET_ORDER)) {
foreach ($matches as list(, $class, $code)) {
// this may vary depending on the version of Symfony,
// if the class name is displayed instead of the icon then swap the key/value
$this->choices['&#x'.$code.';'] = $class;
}
}
return $this->choices;
}
}
and their respective service to register:
# app/config/service.yml
services:
app.form.icon_choice_type:
class: AppBundle\Form\Type\ChoiceIconType
# Symfony has already a container parameter to the kernel root directory.
arguments: ['%kernel.root_dir%']
tags:
- { name: form.type }
well, so far there is no result different from yours.
<select id="form_icon" name="form[icon]" style="font-family: 'FontAwesome';">
<option value="fa-glass"></option>
<option value="fa-music"></option>
...
</select>
Where is the problem now? The <select> font family is ready, but the icons they aren't showing, why?
By default, in Symfony the Twig environment escapes all values that are rendered using htmlspecialchars (more details), so we need overwrite this behavior for this form type only. For that, we create a fields.html.twig template in app/Resources/views/form directory and copy this code inside:
{# app/Resources/views/form/fields.html.twig #}
{#
here isn't need to create the expected `icon_choice_widget` like shown
the documentation, because this looks equal to `choice_widget` from
`ChoiceType`, only we need overwrite the block that renders the label.
#}
{%- block choice_widget_options -%}
{% for group_label, choice in options %}
{%- if choice is iterable -%}
<optgroup label="{{ choice_translation_domain is same as(false) ? group_label : group_label|trans({}, choice_translation_domain) }}">
{% set options = choice %}
{{- block('choice_widget_options') -}}
</optgroup>
{%- else -%}
{# this line has been overwritten, see {{- block('choice_option_label') -}} to end #}
<option value="{{ choice.value }}"{% if choice.attr %} {% set attr = choice.attr %}{{ block('attributes') }}{% endif %}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{- block('choice_option_label') -}}</option>
{%- endif -%}
{% endfor %}
{%- endblock choice_widget_options -%}
{%- block choice_option_label -%}
{# this block has been called from choice_widget_options block #}
{%- if raw_label|default(false) -%}
{# the label is rendered as raw when IconChoiceType is used #}
{{ choice_translation_domain is same as(false) ? choice.label|raw : choice.label|trans({}, choice_translation_domain)|raw }}
{%- else -%}
{{ choice_translation_domain is same as(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}
{%- endif -%}
{%- endblock -%}
Note that {{ choice.label|raw }} raw filter displays the raw text stored (it prevents from being escaped) into label, in this case the icon font content.
finally, you need to register the form theme like describe the documentation:
# app/config/config.yml
{# ... #}
twig:
form_themes:
- 'form/fields.html.twig'
Conclusion:
$form->add('icon', IconChoiceType::class);

I know this is quite an old post and #yceruto's answer is correct andd has its place, but I think it's too complicated for something as simple as putting an icon on a form. Therefore, for new visitors, I offer my own version.
in formBilder
public function buildForm(FormBuilderInterface $builder, array $options)
{
// prepare an array with icons
$icons = [
'',
'',
];
// decode our icons
$icons = array_flip(array_map('html_entity_decode',$icons));
// add field to formBuilder
$builder->add('icon', ChoiceType::class, [
'choices' => $icons,
'mapped' => false,
]);
}
include FontAwesome library in you page and add css style
select { font-family: 'FontAwesome', serif }

Related

Adding a CSS class to drupal_set_message

I'm trying to add a CSS class to a specific message in Drupal that is output upon success when subscribing to a mailchimp list, here's the code for for submission function:
public function submitForm(array &$form, FormStateInterface $form_state) {
global $base_url;
$list_details = mailchimp_get_lists($this->signup->mc_lists);
$subscribe_lists = array();
// Filter out blank fields so we don't erase values on the Mailchimp side.
$mergevars = array_filter($form_state->getValue('mergevars'));
$email = $mergevars['EMAIL'];
$mailchimp_lists = $form_state->getValue('mailchimp_lists');
// If we only have one list we won't have checkbox values to investigate.
if (count(array_filter($this->signup->mc_lists)) == 1) {
$subscribe_lists[0] = array(
'subscribe' => reset($this->signup->mc_lists),
'interest_groups' => isset($mailchimp_lists['interest_groups']) ? $mailchimp_lists['interest_groups'] : NULL,
);
}
else {
// We can look at the checkbox values now.
foreach ($mailchimp_lists as $list) {
if ($list['subscribe']) {
$subscribe_lists[] = $list;
}
}
}
$successes = array();
// Loop through the selected lists and try to subscribe.
foreach ($subscribe_lists as $list_choices) {
$list_id = $list_choices['subscribe'];
$interests = isset($list_choices['interest_groups']) ? $list_choices['interest_groups'] : array();
if (isset($this->signup->settings['safe_interest_groups']) && $this->signup->settings['safe_interest_groups']) {
$current_status = mailchimp_get_memberinfo($list_id, $email);
if (isset($current_status->interests)) {
$current_interests = array();
foreach ($current_status->interests as $id => $selected) {
if ($selected) {
$current_interests[$id] = $id;
}
}
$interests[] = $current_interests;
}
}
$result = mailchimp_subscribe($list_id, $email, $mergevars, $interests, $this->signup->settings['doublein']);
if (empty($result)) {
drupal_set_message(t('There was a problem with your newsletter signup to %list.', array(
'%list' => $list_details[$list_id]->name,
)), 'warning');
}
else {
$successes[] = $list_details[$list_id]->name;
}
}
if (count($successes) && strlen($this->signup->settings['confirmation_message'])) {
drupal_set_message($this->signup->settings['confirmation_message'], 'status');
}
$destination = $this->signup->settings['destination'];
if (empty($destination)) {
$destination_url = Url::fromRoute('<current>');
}
else {
$destination_url = Url::fromUri($base_url . '/' . $this->signup->settings['destination']);
}
$form_state->setRedirectUrl($destination_url);
}
I'm specifically interested in altering this portion:
if (count($successes) && strlen($this->signup->settings['confirmation_message'])) {
drupal_set_message($this->signup->settings['confirmation_message'], 'status');
}
I would like to add a class that is output only for this confirmation message, and not for all of them. I've tried a couple things:
According to some related Q&A, I've tried editing the 'status' portion above to add a class there: 'status conf' or 'status, conf', neither of these work, the only accepted values are 'status', 'warning', and 'error', other values are not translated.
I've also tried this:
if (count($successes) && strlen($this->signup->settings['confirmation_message'])) {
drupal_set_message('' . $this->signup->settings['confirmation_message'] . '', 'status');
This option doesn't add the markup and just outputs it as a string:
"<div class="conf">Our confirmation message</div>"
Any suggestions?
A twig template is used to output the message html.
Why the documentation suggests there are only 3 options for the 'type' parameter, I don't know, but it is wrong. The status messages are just like any other themable (is that a word?) output.
Adding your own class, eg. drupal_set_message('Our confirmation message', 'conf'); does work, except the class (when the classy theme template is used) will be messages--conf.
In the case of the 'classy' theme, the template for messages is located at "core/themes/classy/templates/misc/status-messages.html.twig" and it looks like this:
{#
/**
* #file
* Theme override for status messages.
*
* Displays status, error, and warning messages, grouped by type.
*
* An invisible heading identifies the messages for assistive technology.
* Sighted users see a colored box. See http://www.w3.org/TR/WCAG-TECHS/H69.html
* for info.
*
* Add an ARIA label to the contentinfo area so that assistive technology
* user agents will better describe this landmark.
*
* Available variables:
* - message_list: List of messages to be displayed, grouped by type.
* - status_headings: List of all status types.
* - attributes: HTML attributes for the element, including:
* - class: HTML classes.
*/
#}
{% block messages %}
{% for type, messages in message_list %}
{%
set classes = [
'messages',
'messages--' ~ type,
]
%}
<div role="contentinfo" aria-label="{{ status_headings[type] }}"{{ attributes.addClass(classes)|without('role', 'aria-label') }}>
{% if type == 'error' %}
<div role="alert">
{% endif %}
{% if status_headings[type] %}
<h2 class="visually-hidden">{{ status_headings[type] }}</h2>
{% endif %}
{% if messages|length > 1 %}
<ul class="messages__list">
{% for message in messages %}
<li class="messages__item">{{ message }}</li>
{% endfor %}
</ul>
{% else %}
{{ messages|first }}
{% endif %}
{% if type == 'error' %}
</div>
{% endif %}
</div>
{# Remove type specific classes. #}
{% set attributes = attributes.removeClass(classes) %}
{% endfor %}
{% endblock messages %}
To override it, just add your own 'status-messages.html.twig' to your theme (MY_THEME/templates/misc/status-messages.html.twig) and alter as needed.

twig: rendering child templates first, then passing to parent template

I am new to Twig and need to check whether the way I use it in my MVC is the 'correct' way. I have a feeling that it isn't;
I want to have a controller for each region in my site and have each controller render their own twig template. I read about including twig templates inside twig templates such as:
main.twig
{% include 'header.twig' %}
{% include 'menu.twig' %}
{% include 'content.twig' %}
{% include 'footer.twig' %}
The problem with this is that I cannot run a separate controller for each region before the template is included. I would have to pass the variables for all regions as once to main.twig and I don't like to do that.
So I now do something like the following:
$regions=[];
//...preprocessing menu items here in a controller...
$template=$twig->loadTemplate('regions/menu.twig');
$regions['menu'] = $template->render(array(
'home' => 'Go to Home',
'contact' => 'Contact page'
));
//...other regions...
$template=$twig->loadTemplate('main.twig');
echo $template->render([
'regions'=>$regions
]);
And regions inside main.twig are then printed using the raw value: {{regions.menu|raw}}
This way I have full control over the data that is passed to each template which is what I want. However I have the feeling that I am now not using Twig the way it is supposed to, because I am saving rendered html in variables and then rendering it again.
If what I am trying to achieve is possible in a better way, please let me know.
I'm thinking it's causing a lot of overhead as you always will need to copy/paste the regions whenever you want to create a new page/controller. Idealy would be to use a main template with the includes and let your views extend from the base one.
base.twig.html
<!DOCTYPE html>
<html>
<head>
<title>{{ page.title | default('') }}</title>
<link rel="stylesheet" type="text/css" href="default.css" />
{% block css %}
{% endblock %}
</head>
<body>
{% block nav %}
<nav id="main">
{% for link in main.links %}
{{ link.title }}
{% endfor %}
</nav>
{% endblock %}
<div id="content">
{% block content %}
{% endblock %}
</div>
{% block javascript %}
{% endblock %}
</body>
</html>
{% extends "base.twig.html" %}
{% block content %}
<h1>{{ title }}</h1>
{% endblock %}
If you want to have a controller for each region you could create a helper class which calls all the controllers you need a returning an multi-dimensional array defined by the class name of the region.
This way your variables will never collide as you can access them by e.g. main.title / menu.title / title
(code is just pseudo-code, did not test/run it, just to give you an idea)
<?php
$regions = (new \Project\Regions\Container())->addRegion('Main')
->addRegion('Menu');
echo $twig->render('child.html', array_merge($regions->getParameters(), [
'title' => 'Hello World',
]);
class Container {
private $regions = [];
public function __construct($regions = []) {
$this->regions = $regions;
}
public function setRegions($regions = []) {
$this->regions = $regions;
return $this;
}
public function addRegion($region) {
if (!in_array($region, $this->regions)) $this->regions[] = $region;
return $this;
}
public function getParameters() {
$data = [];
foreach($this->regions as $region) {
$class = '\Project\Regions\\'.$region;
if (!class_exists($class)) continue;
$data[strtolower($region)] = (new $class())->getParameters();
}
return $data;
}
}
<?php
namespace Project\Regions;
abstract class Region {
public function getParameters() {
return [];
}
}
<?php
namespace Project\Regions;
class Page extends Region {
public function getParamters() {
return [
'title' => 'foo',
];
}
}
<?php
namespace Project\Regions;
class Menu extends Region {
return [
'title' => 'bar',
];
}

Adding a custom action to Sonata Admin

I have created a custom action that renders a small form at the bottom of my show template for orders. The form is a basic checkbox and a select field to with tow buttons. It works perfectly but the rendering is not right.
I know the way I render the show template is not 100% correct, because when it renders, the left hand side menu doesn't work anymore.
Here is my custom controller with action;
namespace Qi\Bss\FrontendBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Qi\Bss\FrontendBundle\Crud\Crud;
use Qi\Bss\BaseBundle\Entity\Business\PmodOrder;
use Symfony\Component\HttpFoundation\RedirectResponse;
class PmodOrderController extends Controller
{
/**
* #Route("/{id}/approve", name = "order_approve")
* #Security("is_granted('IS_AUTHENTICATED_FULLY')")
* #Method({"GET", "POST"})
*/
public function approveAction(Request $request, $id){
$em = $this->getDoctrine()->getManager();
$order = $em->getRepository('QiBssBaseBundle:PmodOrder')->find($id);
$approveForm = $this->createFormBuilder($order)
->add('requireApproval', 'checkbox', array('label' => 'Require second Approval', 'required' => false, 'mapped' => false))
->add('secondApprover', 'choice', array('choices' => Crud::enumStatus(), 'label' => 'User', 'required' => false))
->getForm();
$approveForm->handleRequest($request);
if ($approveForm->isSubmitted() && $approveForm->isValid()) {
$secondApproval = $request->request->get('form');
$approval = $approveForm->getData();
if (isset($secondApproval['requireApproval'])) {
$approval->setStatus(PmodOrder::STATUS_PARTLY_APPROVED);
$em->persist($approval);
$em->flush();
return new RedirectResponse($this->container->get('router')->generate('admin_bss_base_business_pmodorder_show', array('id' => $order->getId())));
} else {
$approval->setSecondApprover(NULL);
$approval->setStatus(PmodOrder::STATUS_APPROVED);
$em->persist($approval);
$em->flush();
return new RedirectResponse($this->container->get('router')->generate('admin_bss_base_business_pmodorder_show', array('id' => $order->getId())));
}
}
return $this->render('QiBssFrontendBundle:PmodOrder:order_approve.html.twig', array(
'order' => $order,
'form' => $approveForm->createView(),
));
}
}
What bothers me is the fact that I'm actually suppose to extend from Sonata's CRUDController. And when I do that I get an error;
An exception has been thrown during the rendering of a template
("There is no _sonata_admin defined for the controller
Path\To\Controller\PmodOrderController and the current
route ``")
And I am also aware that I'm actually suppose to use a return like return new RedirectResponse($this->admin->generateUrl('show'));
At this point I don't know what to do anymore. If somebody can please guide me how to extend correctly from CRUDController in my scenario, it would be really appreciated
Here an example, I don't know if it's the best solution but I hope that can help you :
1- Create a custom CRUDcontroller :
# CustomCRUDcontroller.php :
class CustomCRUDDController extends Controller
{
/**
* Show action.
*
* #param int|string|null $id
* #param Request $request
*
* #return Response
*
* #throws NotFoundHttpException If the object does not exist
* #throws AccessDeniedException If access is not granted
*/
public function showAction($id = null)
{
$request = $this->getRequest();
// DO YOUR LOGIC IN THE METHOD, for example :
if(isset($request->get('yourFormParam'))){
$this->doTheJob();
}
$id = $request->get($this->admin->getIdParameter());
$object = $this->admin->getObject($id);
if (!$object) {
throw $this->createNotFoundException(sprintf('unable to find the object with id : %s', $id));
}
$this->admin->checkAccess('show', $object);
$preResponse = $this->preShow($request, $object);
if ($preResponse !== null) {
return $preResponse;
}
$this->admin->setSubject($object);
return $this->render($this->admin->getTemplate('show'), array(
'action' => 'show',
'object' => $object,
'elements' => $this->admin->getShow(),
), null);
}
}
2- Register it in admin.yml :
# admin.yml :
x.admin.x:
class: Namespace\YourAdminClass
arguments: [~, Namespace\Entity, Namespace:CustomCRUD]
tags:
- {name: sonata.admin, manager_type: orm, group: X, label: X}
3- Create your own custom_show.html.twig (just a copy and paste of the original template base_show.html.twig located in the sonata-admin folder), here you can display extra elements to the view :
# custom_show.html.twig :
{% extends base_template %}
{% import 'SonataAdminBundle:CRUD:base_show_macro.html.twig' as show_helper %}
{% block actions %}
{% include 'SonataAdminBundle:CRUD:action_buttons.html.twig' %}
{% endblock %}
{% block tab_menu %}
{{ knp_menu_render(admin.sidemenu(action), {
'currentClass' : 'active',
'template': sonata_admin.adminPool.getTemplate('tab_menu_template')
}, 'twig') }}
{% endblock %}
{% block show %}
<div class="sonata-ba-view">
{{ sonata_block_render_event('sonata.admin.show.top', { 'admin': admin, 'object': object }) }}
{% set has_tab = (admin.showtabs|length == 1 and admin.showtabs|keys[0] != 'default') or admin.showtabs|length > 1 %}
{% if has_tab %}
<div class="nav-tabs-custom">
<ul class="nav nav-tabs" role="tablist">
{% for name, show_tab in admin.showtabs %}
<li{% if loop.first %} class="active"{% endif %}>
<a href="#tab_{{ admin.uniqid }}_{{ loop.index }}" data-toggle="tab">
<i class="fa fa-exclamation-circle has-errors hide"></i>
{{ admin.trans(name, {}, show_tab.translation_domain) }}
</a>
</li>
{% endfor %}
</ul>
<div class="tab-content">
{% for code, show_tab in admin.showtabs %}
<div
class="tab-pane fade{% if loop.first %} in active{% endif %}"
id="tab_{{ admin.uniqid }}_{{ loop.index }}"
>
<div class="box-body container-fluid">
<div class="sonata-ba-collapsed-fields">
{% if show_tab.description != false %}
<p>{{ show_tab.description|raw }}</p>
{% endif %}
{{ show_helper.render_groups(admin, object, elements, show_tab.groups, has_tab) }}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% elseif admin.showtabs is iterable %}
{{ show_helper.render_groups(admin, object, elements, admin.showtabs.default.groups, has_tab) }}
{% endif %}
</div>
{{ sonata_block_render_event('sonata.admin.show.bottom', { 'admin': admin, 'object': object }) }}
{% endblock %}
4- Then indicate to your adminController to display your custom_show template when the current route is "show" (instead of the default template base_show.html.twig) :
# YourEntityAdminController.php :
class YourEntityAdminController extends Controller
{
// allows you to chose your custom showAction template :
public function getTemplate($name){
if ( $name == "show" )
return 'YourBundle:Admin:custom_show.html.twig' ;
return parent::getTemplate($name);
}
}

How to wrap HTML code without including new files in Twig?

I have divs with CSS that represent boxes, they wrap html code.
<div class="box indent">
<div class="padding">
my code here
</div>
</div>
I created a "layoutbundle" where every HTML wrapper (such as boxes, tabs, grids, and so on) is put inside separate twig files. In such a way, views on other bundles can be implemented with other layouts.
But I get tired of includes. Every small html wrapper requires an include, and I wonder if there is a simpler way to wrap HTML code.
Let's have an example with a simple box. Actually, I created several files :
A box.html.twig file that contain the box and include the content :
<div class="box indent">
<div class="padding">
{% include content %}
</div>
</div>
Several box-content.html.twig files, containing content of my boxes.
And finally, I create a box in a view by doing :
{%
include 'AcmeDemoBundle:layout:box.html.twig'
with {
'content': 'ReusableBundle:feature:xxx.html.twig'
}
%}
Is there a way to create wrappers such as :
a) I declare once a new wrapper :
{% wrapperheader "box" %}
<div class="box indent">
<div class="padding">
{% endwrapperheader %}
{% wrapperfooter "box" %}
</div>
</div>
{% endwrapperfooter %}
b) And then in my pages, I use :
{% wrapper "box" %}
{# here my content #}
{% endwrapper %}
I think I'll need to add new tag extensions in Twig, but first I want to know if something similar is natively possible.
The block method
This method was proposed by Sebastiaan Stok on GitHub.
This idea uses the block function. It writes the given block contents, and can be called several times.
Wrappers file :
{# src/Fuz/LayoutBundle/Resources/views/Default/wrappers.html.twig #}
{% block box_head %}
<div class="box indent">
<div class="padding">
{% enblock %}
{% block box_foot %}
</div>
</div>
{% enblock %}
Feature page :
{{ block('box_head') }}
Some content
{{ block('box_foot') }}
The wrap extension with macros
This idea was proposed by Charles on GitHub.
First you declare a macro in a macro.html.twig file.
{% macro box(content) %}
<div class="box indent">
<div class="padding">
{{ content | raw }}
</div>
</div>
{% endmacro %}
Amd then, instead of calling {{ macros.box('my content') }} (see the doc you develop a {% wrap %} tag that will handle the macro call, with what's between [% wrap %} and {% endwrap %} as parameter.
This extension was easy to develop. I thought it could be hard to access macros, but in fact, they are stored in the context as objects, and calls can be compiled easily.
Just some changes : we will use the following syntax :
{# to access a macro from an object #}
{% wrap macro_object macro_name %}
my content here
{% endwrap %}
{# to access a macro declared in the same file #}
{% wrap macro_name %}
macro
{% endwrap %}
In the following code, don't forget to change namespaces if you want to get it work!
First, add the extension in your services.yml:
parameters:
fuz_tools.twig.wrap_extension.class: Fuz\ToolsBundle\Twig\Extension\WrapExtension
services:
fuz_tools.twig.wrap_extension:
class: '%fuz_tools.twig.wrap_extension.class%'
tags:
- { name: twig.extension }
Inside your bundle, create a Twig directory.
Add the extension, it will return the new TokenParser (in english: it will declare the new tag).
Twig/Extension/WrapExtension.php:
<?php
// src/Fuz/ToolsBundle/Twig/Extension/WrapExtension.php
namespace Fuz\ToolsBundle\Twig\Extension;
use Fuz\ToolsBundle\Twig\TokenParser\WrapHeaderTokenParser;
use Fuz\ToolsBundle\Twig\TokenParser\WrapFooterTokenParser;
use Fuz\ToolsBundle\Twig\TokenParser\WrapTokenParser;
class WrapExtension extends \Twig_Extension
{
public function getTokenParsers()
{
return array (
new WrapTokenParser(),
);
}
public function getName()
{
return 'wrap';
}
}
Then add the TokenParser itself, it will be met when the parser will find a {% wrap %} tag. This TokenParser will check if the tag is called correctly (for our example, it has 2 parameters), store those parameters and get the content between {% wrap %} and {% endwrap %}`.
Twig/TokenParser/WrapTokenParser.php:
<?php
// src/Fuz/ToolsBundle/Twig/TokenParser/WrapTokenParser.php
namespace Fuz\ToolsBundle\Twig\TokenParser;
use Fuz\ToolsBundle\Twig\Node\WrapNode;
class WrapTokenParser extends \Twig_TokenParser
{
public function parse(\Twig_Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$object = null;
$name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue();
if ($stream->test(\Twig_Token::BLOCK_END_TYPE))
{
if (!$this->parser->hasMacro($name))
{
throw new \Twig_Error_Syntax("The macro '$name' does not exist", $lineno);
}
}
else
{
$object = $name;
$name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue();
}
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array ($this, 'decideWrapEnd'), true);
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
return new WrapNode($object, $name, $body, $token->getLine(), $this->getTag());
}
public function decideWrapEnd(\Twig_Token $token)
{
return $token->test('endwrap');
}
public function getTag()
{
return 'wrap';
}
}
Next, we need a compiler (a Node in the twig dialect), it will generate the PHP code associated with our {% wrap %} tag.
This tag is an alias of {{ macro_object.box(content) }}, so I wrote that line in a template and watched the resulting code in the resulting generated php file (stored in your app/cache/dev/twig directory). I got :
echo $this->getAttribute($this->getContext($context, "(macro object name)"), "(name)", array("(body)"), "method");
So my compiler became :
Twig/Node/WrapNode.php:
<?php
// src/Fuz/ToolsBundle/Twig/Node/WrapNode.php
namespace Fuz\ToolsBundle\Twig\Node;
class WrapNode extends \Twig_Node
{
public function __construct($object, $name, $body, $lineno = 0, $tag = null)
{
parent::__construct(array ('body' => $body), array ('object' => $object, 'name' => $name), $lineno, $tag);
}
public function compile(\Twig_Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->write('ob_start();');
$compiler
->addDebugInfo($this)
->subcompile($this->getNode('body'));
if (is_null($this->getAttribute('object')))
{
$compiler
->write(sprintf('echo $this->get%s(ob_get_clean());', $this->getAttribute('name')) . "\n");
}
else
{
$compiler
->write('echo $this->getAttribute($this->getContext($context, ')
->repr($this->getAttribute('object'))
->raw('), ')
->repr($this->getAttribute('name'))
->raw(', array(ob_get_clean()), "method");')
->raw("\n");
}
}
}
Note : to know how work the subparsing / subcompiling, I read the spaceless extension source code.
That's all! We get an alias that let us use macros with a large body. To try it:
macros.html.twig:
{% macro box(content) %}
<div class="box indent">
<div class="padding">
{{ content | raw }} {# Don't forget the raw filter! #}
</div>
</div>
{% endmacro %}
some layout.html.twig:
{% import "FuzLayoutBundle:Default:macros.html.twig" as macros %}
{% wrap macros box %}
test
{% endwrap %}
{% macro test(content) %}
some {{ content | raw }} in the same file
{% endmacro %}
{% wrap test %}
macro
{% endwrap %}
Outputs:
<div class="box indent">
<div class="padding">
test
</div>
</div>
some macro in the same file
The wrapperheader, wrapperfooter, wrapper extension
This method is the one I tell you about in my question. You can read / implement it if you want to train yourself with token parsers, but functionnaly, that's less nice than the previous method.
In a wrapper.html.twig file, you declare all wrappers :
{% wrapperheader box %}
<div class="box">
{% endwrapper %}
{% wrapperfooter box %}
</div>
{% endwrapperfooter %}
In your features twig files, you use your wrappers :
{% wrapper box %}
This is my content
{% endwrapper %}
The following extension has 3 issues :
There is no way to store data (such as context variables) in the Twig Environnement. So when you define a {% wrapperheader NAME %}, you have basically no clean way to check if a header for NAME is already defined (in this extension, I use static properties).
When you include a twig file, it is parsed at runtime, not immediately (I mean, the included twig template is parsed while the generated file is executed, and not when the include tag is parsed). So that's not possible to know if a wrapper exists on a previousely included file when you parse the {% wrapper NAME %} tag. If your wrapper does not exist, this extension just displays what's between {% wrapper %} and {% endwrapper %} without any notice.
The idea of this extension is : when the parser meet a wrapperheader and wrapperfooter tag, the compiler store the content of the tag somewhere for a later use with the wrapper tag. But the twig context is passed to {% include %} as a copy, not by reference. So that's not possible to store the {% wrapperheader %} and {% wrapperfooter %} information inside that context, for an usage at upper level (in files that include files). I needed to use a global context too.
Here is the code, take care to change your namespaces.
First we need to create an extension that will add new token parsers to Twig.
Inside services.yml of a bundle, add the following lines to activate the extension :
parameters:
fuz_tools.twig.wrapper_extension.class: Fuz\ToolsBundle\Twig\Extension\WrapperExtension
services:
fuz_tools.twig.wrapper_extension:
class: '%fuz_tools.twig.wrapper_extension.class%'
tags:
- { name: twig.extension }
Inside your bundle, create a Twig directory.
Create the following Twig\Extension\WrapperExtension.php file :
<?php
// src/Fuz/ToolsBundle/Twig/Extension/WrapperExtension.php
namespace Fuz\ToolsBundle\Twig\Extension;
use Fuz\ToolsBundle\Twig\TokenParser\WrapperHeaderTokenParser;
use Fuz\ToolsBundle\Twig\TokenParser\WrapperFooterTokenParser;
use Fuz\ToolsBundle\Twig\TokenParser\WrapperTokenParser;
class WrapperExtension extends \Twig_Extension
{
public function getTokenParsers()
{
return array(
new WrapperHeaderTokenParser(),
new WrapperFooterTokenParser(),
new WrapperTokenParser(),
);
}
public function getName()
{
return 'wrapper';
}
}
Now we need to add the token parsers : our syntax is {% wrapper NAME %} ... {% endwrapper %} and the same with wrapperheader and wrapperfooter. So those token parsers are used to declare the tags, to retrive the wrapper's NAME, and to retrieve the body (what's between wrapper and endwrapper`).
The token parser for wrapper: Twig\TokenParser\WrapperTokenParser.php:
<?php
// src/Fuz/ToolsBundle/Twig/TokenParser/WrapperTokenParser.php
namespace Fuz\ToolsBundle\Twig\TokenParser;
use Fuz\ToolsBundle\Twig\Node\WrapperNode;
class WrapperTokenParser extends \Twig_TokenParser
{
public function parse(\Twig_Token $token)
{
$stream = $this->parser->getStream();
$name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue();
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideWrapperEnd'), true);
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
return new WrapperNode($name, $body, $token->getLine(), $this->getTag());
}
public function decideWrapperEnd(\Twig_Token $token)
{
return $token->test('endwrapper');
}
public function getTag()
{
return 'wrapper';
}
}
The token parser for wrapperheader: Twig\TokenParser\WrapperHeaderTokenParser.php:
<?php
// src/Fuz/ToolsBundle/Twig/TokenParser/WrapperHeaderTokenParser.php
namespace Fuz\ToolsBundle\Twig\TokenParser;
use Fuz\ToolsBundle\Twig\Node\WrapperHeaderNode;
class WrapperHeaderTokenParser extends \Twig_TokenParser
{
static public $wrappers = array ();
public function parse(\Twig_Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue();
if (in_array($name, self::$wrappers))
{
throw new \Twig_Error_Syntax("The wrapper '$name''s header has already been defined.", $lineno);
}
self::$wrappers[] = $name;
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideWrapperHeaderEnd'), true);
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
return new WrapperHeaderNode($name, $body, $token->getLine(), $this->getTag());
}
public function decideWrapperHeaderEnd(\Twig_Token $token)
{
return $token->test('endwrapperheader');
}
public function getTag()
{
return 'wrapperheader';
}
}
The token parser for wrapperfooter: Twig\TokenParser\WrapperFooterTokenParser.php:
<?php
// src/Fuz/ToolsBundle/Twig/TokenParser/WrapperFooterTokenParser.php
namespace Fuz\ToolsBundle\Twig\TokenParser;
use Fuz\ToolsBundle\Twig\Node\WrapperFooterNode;
class WrapperFooterTokenParser extends \Twig_TokenParser
{
static public $wrappers = array ();
public function parse(\Twig_Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue();
if (in_array($name, self::$wrappers))
{
throw new \Twig_Error_Syntax("The wrapper '$name''s footer has already been defined.", $lineno);
}
self::$wrappers[] = $name;
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideWrapperFooterEnd'), true);
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
return new WrapperFooterNode($name, $body, $token->getLine(), $this->getTag());
}
public function decideWrapperFooterEnd(\Twig_Token $token)
{
return $token->test('endwrapperfooter');
}
public function getTag()
{
return 'wrapperfooter';
}
}
The token parsers retrieve all the required information, we now need to compile those information into PHP. This PHP code will be generated by the twig engine inside a Twig_Template implementation (you can find generated classes in your cache directory). It generates code in a method, and the context of included files is not available (because the context array is not given by reference). In such a way, this is not possible to access what's inside the included file without a global context. That's why here, I use static attributes... That's not nice at all but I don't know how to avoid them (if you have ideas, please let me know! :)).
Compiler for the wrapper tag : Twig\Nodes\WrapperNode.php
<?php
// src/Fuz/ToolsBundle/Twig/Node/WrapperNode.php
namespace Fuz\ToolsBundle\Twig\Node;
class WrapperNode extends \Twig_Node
{
public function __construct($name, $body, $lineno = 0, $tag = null)
{
parent::__construct(array ('body' => $body), array ('name' => $name), $lineno, $tag);
}
public function compile(\Twig_Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->write('if (isset(\\')
->raw(__NAMESPACE__)
->raw('\WrapperHeaderNode::$headers[')
->repr($this->getAttribute('name'))
->raw('])) {')
->raw("\n")
->indent()
->write('echo \\')
->raw(__NAMESPACE__)
->raw('\WrapperHeaderNode::$headers[')
->repr($this->getAttribute('name'))
->raw('];')
->raw("\n")
->outdent()
->write('}')
->raw("\n");
$compiler
->addDebugInfo($this)
->subcompile($this->getNode('body'));
$compiler
->addDebugInfo($this)
->write('if (isset(\\')
->raw(__NAMESPACE__)
->raw('\WrapperFooterNode::$footers[')
->repr($this->getAttribute('name'))
->raw('])) {')
->raw("\n")
->indent()
->write('echo \\')
->raw(__NAMESPACE__)
->raw('\WrapperFooterNode::$footers[')
->repr($this->getAttribute('name'))
->raw('];')
->raw("\n")
->outdent()
->write('}')
->raw("\n");
}
}
Compiler for the wrapperheader tag : Twig\Nodes\WrapperHeaderNode.php
<?php
// src/Fuz/ToolsBundle/Twig/Node/WrapperHeaderNode.php
namespace Fuz\ToolsBundle\Twig\Node;
/**
* #author alain tiemblo
*/
class WrapperHeaderNode extends \Twig_Node
{
static public $headers = array();
public function __construct($name, $body, $lineno = 0, $tag = null)
{
parent::__construct(array ('body' => $body), array ('name' => $name), $lineno, $tag);
}
public function compile(\Twig_Compiler $compiler)
{
$compiler
->write("ob_start();")
->raw("\n")
->subcompile($this->getNode('body'))
->write(__CLASS__)
->raw('::$headers[')
->repr($this->getAttribute('name'))
->raw('] = ob_get_clean();')
->raw("\n");
}
}
Compiler for the wrapperfooter tag : Twig\Nodes\WrapperFooterNode.php
<?php
// src/Fuz/ToolsBundle/Twig/Node/WrapperFooterNode.php
namespace Fuz\ToolsBundle\Twig\Node;
class WrapperFooterNode extends \Twig_Node
{
static public $footers = array();
public function __construct($name, $body, $lineno = 0, $tag = null)
{
parent::__construct(array ('body' => $body), array ('name' => $name), $lineno, $tag);
}
public function compile(\Twig_Compiler $compiler)
{
$compiler
->write("ob_start();")
->raw("\n")
->subcompile($this->getNode('body'))
->write(__CLASS__)
->raw('::$footers[')
->repr($this->getAttribute('name'))
->raw('] = ob_get_clean();')
->raw("\n");
}
}
The implementation is ok now. Let's try it!
Create a view named wrappers.html.twig :
{# src/Fuz/LayoutBundle/Resources/views/Default/wrappers.html.twig #}
{% wrapperheader demo %}
HEAD
{% endwrapperheader %}
{% wrapperfooter demo %}
FOOT
{% endwrapperfooter %}
Create a view named what you want.html.twig :
{# src/Fuz/HomeBundle/Resources/views/Default/index.html.twig #}
{% include 'FuzLayoutBundle:Default:wrappers.html.twig' %}
{% wrapper demo %}
O YEAH
{% endwrapper %}
This shows up :
HEAD O YEAH FOOT
There's a fairly straight forward method with Twig variables and macros.
<div class="box indent">
<div class="padding">
my code here
</div>
</div>
Create a macro:
{% macro box(content) %}
<div class="box indent">
<div class="padding">
{{ content }}
</div>
</div>
{% endmacro %}
And call it like this:
{% set content %}
my code here
{% endset %}
{{ _self.box(content) }}
Not particularly elegant but less mountains of code!

symfony every block with spaceless

How i can wrap every block code with spaceless to crop whitespaces from my twig/html
for example now i have:
{% block content %}
<div class="box clearfix clearall">
<div class="ct colcontainer">
<div class="col-1">
<div class="chars">
<table class="layout data-char">
<thead>
blabla
{% endblock %}
And when symfony try to render it, i want that symfony saw
{% block content %}
{% spaceless %}
<div class="box clearfix clearall">
<div class="ct colcontainer">
<div class="col-1">
<div class="chars">
<table class="layout data-char">
<thead>
blabla
{% endspaceless %}
{% endblock %}
Define a custom Twig tag (the copy-and-paste way)
You can define a custom Twig tag spacelessblock which combines block and spaceless. Then you can use {% spacelessblock xyz %}…{% endspacelessblock %} in your templates. Here is how you do it the quick and dirty (copy and paste) way.
A new Twig node
First, define a class Twig_Node_SpacelessBlock (e.g. in the Extension directory of your bundle):
class Twig_Node_SpacelessBlock extends \Twig_Node_Block
{
public function __construct($name, Twig_NodeInterface $body, $lineno, $tag = null)
{
parent::__construct(array('body' => $body), array('name' => $name), $lineno, $tag);
}
public function compile(Twig_Compiler $compiler)
{
// top part of Block.compile
$compiler
->addDebugInfo($this)
->write(sprintf("public function block_%s(\$context, array \$blocks = array())\n", $this->getAttribute('name')), "{\n")
->indent()
;
// the content of the body is treated like in Spaceless.compile
$compiler
->write("ob_start();\n")
->subcompile($this->getNode('body'))
->write("echo trim(preg_replace('/>\s+</', '><', ob_get_clean()));\n")
;
// bottom part of Block.compile
$compiler
->outdent()
->write("}\n\n")
;
}
}
A new Twig token parser
Our new Twig node needs to be built somewhere whenever Twig finds a {% spacelessblock xyz %} in a template. For that, we need a token parser which we call Twig_TokenParser_SpacelessBlock. We basically copy and paste Twig_TokenParser_Block:
class Twig_TokenParser_SpacelessBlock extends \Twig_TokenParser
{
public function parse(Twig_Token $token)
{
// …
$this->parser->setBlock($name, $block = new Twig_Node_SpacelessBlock($name, new Twig_Node(array()), $lineno));
// …
}
public function decideBlockEnd(Twig_Token $token)
{
return $token->test('endspacelessblock');
}
public function getTag()
{
return 'spacelessblock';
}
}
Tell Twig about it
In your extension class:
class Extension extends \Twig_Extension
{
public function getTokenParsers()
{
return array(
new Twig_TokenParser_SpacelessBlock(),
);
}
}
Tell Symfony about it
If not already done, add the following to your your services.yml:
services:
# …
my.extension:
class: Acme\MyBundle\Extension\Extension
tags:
- { name: twig.extension }
Better alternatives
Preprocessor
A better way would be to use a preprocessor to simply replace
{% spacelessblock xyz %}
…
{% endspacelessblock %}
by
{% block xyz %}{% spaceless %}
…
{% endspaceless %}{% endblock %}
which reuses all the code that already has been written in the Twig project, including possible changes.

Categories