Tips al Usar Twig

Twig es una libreria de templates para php, que es muy utilizada en la actualidad, el framework Symfony lo utiliza por defecto para la parte de las vistas, y yo personalmente lo uso en todos los proyectos php que construyo.

El siguiente post es una recomendación personal y una serie de consejos que podemos seguir al utilizar esta libreria, y pretendo explicar más o menos lo que generalmente hago a la hora de trabajar las vistas y algunos diseños con twig.

Lo principal es usar todo el potencial de twig, ya que este no es solo extender templates, twig ofrece otras caracteristicas como macros y embed, que son super potentes y nos pueden ayudar muchisimo a mejorar nuestro rendimiento al desarrollar aplicaciones.

A continuación mostraré algunas de las cosas que he implementado para reutilizar diseños y facilitar el manejo de algunas partes de mis vistas twig:

Macros

Las macros nos permiten definir “funciones” twig en las mismas vistas, y son utiles para crear bloques de código que son muy utilizados en la aplicación.

Un caso donde los he venido usando es al crear links como botones e inputs de tipo submit, button o reset, ya que uso muy a menudo el bootstrap de twitter, acostumbro definir un archivo de macros, para estos botones, ejemplo:

Tenemos el siguiente archivo twig (views/macros/buttons.html.twig):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
{# Los "-" guiones que se ven entre las definicones de etiquetas {%- y -%} son para quitar espacios en blanco y minimizar el código generado #}

{# Macros para botones de formulario #}
{%- macro button(text, type, icon = null) -%}
<button type="button" class="btn btn-{{ type|default('default') }}">
    {% if icon %}<i class="glyphicon glyphicon-{{ icon }}"></i> {% endif %}{{ text }}
</button>
{%- endmacro -%}

{%- macro submit(text = 'Guardar', icon = 'save') -%}
<button type="submit" class="btn btn-{{ type|default('default') }}">
    {% if icon %}<i class="glyphicon glyphicon-{{ icon }}"></i> {% endif %}{{ text }}
</button>
{%- endmacro -%}

{# Macros para links con estilos de boton #}
{%- macro link(url, text, type, icon = null, class, htmlAttributes) -%}
<a href="{{ url }}" class="btn btn-{{ type|default('default') }} {{ class }}" {{ htmlAttributes }}>
    {% if icon %}<i class="glyphicon glyphicon-{{ icon }}"></i> {% endif %}{{ text }}
</a>
{%- endmacro -%}

{%- macro primaryLink(url, text, icon = null, class, htmlAttributes) -%}
{% import _self as links %}
{{ links.link(url, text, 'primary', icon, class, htmlAttributes) }}
{%- endmacro -%}

{%- macro successLink(url, text, icon = null, class, htmlAttributes) -%}
{% import _self as links %}
{{ links.link(url, text, 'success', icon, class, htmlAttributes) }}
{%- endmacro -%}

{%- macro infoLink(url, text, icon = null, class, htmlAttributes) -%}
{% import _self as links %}
{{ links.link(url, text, 'info', icon, class, htmlAttributes) }}
{%- endmacro -%}

{%- macro warningLink(url, text, icon = null, class, htmlAttributes) -%}
{% import _self as links %}
{{ links.link(url, text, 'warning', icon, class, htmlAttributes) }}
{%- endmacro -%}

{%- macro dangerLink(url, text, icon = null, class, htmlAttributes) -%}
{% import _self as links %}
{{ links.link(url, text, 'danger', icon, class, htmlAttributes) }}
{%- endmacro -%}

{# Macros para botones de operaciones CRUD #}
{%- macro back(url, text = 'Atras', icon = 'undo') -%}
{% import _self as links %}
{{ links.link(url, text, 'default', icon) }}
{%- endmacro -%}

{%- macro edit(url, text = 'Editar', icon = 'edit', size = 'xs') -%}
{% import _self as links %}
{{ links.link(url, text, 'link btn-'~size, icon) }}
{%- endmacro -%}

{%- macro add(url, text, icon = 'plus', size = null) -%}
{% import _self as links %}
{{ links.link(url, text|default('Nuevo'), 'primary btn-'~size, icon) }}
{%- endmacro -%}

{%- macro save(text) -%}
{% import _self as btn %}
{{ btn.submit(text|default('Guardar')) }}
{%- endmacro -%}

Ahora los utilizamos en nuestras otras vistas:

1
2
3
4
5
6
7
8
9
10
{% import 'macros/buttons.html.twig' as btn %}

{{ btn.link('/url', 'Home') }}

{{ btn.submit() }}
{{ btn.back() }}

{{ btn.button('Texto del boton') }}

{{ btn.primaryLink('/url', 'Agregar Producto', 'tags') }}

La ventaja de usar las macros en ves de escribir el código directamente en cada vista, es que codificamos menos, y si el día de mañana queremos cambiar por ejemplo el color del botón de submit, solo lo modificamos una vez en nuestra macro, y todas las demás vistas tendran el cambio sin hacer nada.

Ahora veremos un ejemplo del uso de la etiqueta embed de twig:

Embed

Esta etiqueta permite redefinir los bloques de un template incluido en nuestra vista, es decir, es una combinacion de las funcionalidades ofrecidas por la función include y el tag extends.

Un caso donde he utilizado esta etiqueta es a la hora de usar el panel del bootstrap de twitter; la idea es definir un template que tenga el código html necesario para el panel y crear los bloques que serán luego redefinidos en las vistas que quieran incorporar el panel.

Acá el código que en principio he implementado (views/panel.html.twig):

1
2
3
4
5
6
7
8
9
<div class="panel panel-{{ type|default('default') }}">

    <div class="panel-heading">{% block header %}{% endblock %}</div>

    <div class="panel-body">{% block body %}{% endblock %}</div>

    <div class="panel-footer">{% block footer %}{% endblock %}</div>

</div>

Ahora en cualquier vista twig podemos embeber el template que contiene el panel, y llenar los bloques con información:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{% extends 'base.html.twig' %}

{% block page_title %}Page title{% endblock %}

{% block content %}

    <p>Page content!!!</p>

    {% embed 'panel.html.twig' %}
        {# este import debe ir dentro del embed para que esté disponible en los bloques #}
        {% import 'macros/buttons.html.twig' as btn %}

        {% block header %}Título del Panel{% endblock %}

        {% block body %}Contenido del Panel{% endblock %}

        {% block footer %}
            {{ btn.submit() }} {{ btn.back() }}
        {% endblock %}

    {% endembed %}

    Una vista puede tener tantos embed como se quiera.

    {% embed 'panel.html.twig' %}
        {# este embed no tiene footer #}

        {% block header %}Título del Panel{% endblock %}

        {% block body %}Contenido del Panel{% endblock %}

        {# No definimos el bloque footer, ya que no lo necesitamos en este caso #}

    {% endembed %}

{% endblock %}

Como ven, esta etiqueta nos da un gran potencial a la hora de escribir código repetitivo, sin embargo este template en particular aun tiene un problema, y es que si no definimos algún bloque (ya sea porque no lo necesitamos), igual se genera el div sin contenido y se verá en el diseño gracias a que contiene un padding.

Para solventar esto, el panel que he implementado es uno donde todos los bloques (header, body y footer) son opcionales:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{# views/panel.html.twig #}
{% set _header  =  block('header') %}
{% set _body    =  block('body')   %}
{% set _footer  =  block('footer') %}

<div class="panel panel-{{ type|default('default') }}">

    {# 
        La idea acá es que ningun bloque sea obligatorio
        Si alguno está vacio o no definido, 
        simplemente no se cumple la condición y no se imprime el div
     #}

    {% if _header is not empty %}<div class="panel-heading">{{ _header|raw }}</div>{% endif %}

    {% if _body is not empty %}<div class="panel-body">{{ _body|raw }}</div>{% endif %}

    {% if _footer is not empty %}<div class="panel-footer">{{ _footer|raw }}</div>{% endif %}

</div>

La ventaja de usar embed, es que si por alguna razón debemos cambiar o ajustar el código html para el panel, añadiendo divs, clases, etc, con hacerlo en la vista panel.html.twig el cambio se verá reflejado en todas las vistas que hayan embebido el panel.


Esta es una pequeña muestra de lo que podemos hacer para mejorar el rendimiento y mantenimiento al programar nuestras vistas, queda de tu parte idear otros posibles usos para las macros y la etiqueta embed.

Otros tips al usar Twig sin un Framework

Cuando eres tú el encargado de crear la instancia de twig, y definir los directorios de las vistas, es recomendable usar los Namespaces del Loader, ya que esto ayuda a mantener organizados los archivos de las vistas.

Una posible organización puede ser:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
views
  |
  |------_shared
  |         |
  |         |----templates
  |         |       |----base.twig
  |         |----partials
  |         |       |----menu.twig
  |         |       |----panel.twig
  |         |----macros
  |         |       |----buttons.twig
  |
  |------home.twig
  |------edit-user.twig
  |------user
          |----list.twig
          |----create.twig
          |----edit.twig

Para lograrlo, nuestro loader deberá estar más o menos así:

1
2
3
4
5
6
7
8
<?php

$loader = new \Twig_Loader_Filesystem();

$loader->addPath('/path_to_views');
$loader->addPath('/path_to_views/_shared/templates', 'templates');
$loader->addPath('path_to_views/_views/_shared/partials', 'partials');
$loader->addPath('path_to_views/_views/_shared/macros', 'macros');

Con esto conseguimos tener varios directorios a los que hacer referencia a la hora de incluir o extender templates, por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% extends '@templates/base.twig' %}

{% import '@macros/buttons.twig' as btn %}

{% block content %}

    {% embed '@partials/panel.twig' %}

        {% block header %}Título del Panel{% endblock %}

        {% block body %}Contenido del Panel{% endblock %}
    {% endembed %}

{% endblock %}

Esto nos da la ventaja de tener los templates más organizados y nos da la flexibilidad de que si luego cambiamos el directorio de los templates, partials o macros, mientras mantengan el mismo namespace no se dañaran nuestras vistas.