Symfony: tutorial 6: las vistas, cómo se trabajan las plantillas con Twig

Symfony tutorial templating

Recordando mientras que nos actualizamos con Symfony, traigo aquí otro code-kata o howto, esta vez, sobre plantillas. El sistema de plantillas de Symfony Flex, Symfony 4, desde hace tiempo que se basa en Twig. Es una de las mejoras cosas que le han pasado al mundo del PHP con respecto al desarrollo del frontend. Twig viene a añadir una sintaxis nueva: una serie de instrucciones a las plantillas: extends, block, for, if, etcétera..

Por un lado, Twig simplifica mucho el código fuente, queda mucho más limpio. Si eres un frontender, o un desarrollador web, te hará la vida mucho más fácil cuando te pongas a escribir códigos fuentes. Por otro lado, te ayuda a reutilizar las plantillas, no teniendo nunca que repetir el mismo código en plantillas distintas. Si estás repitiendo un código en una plantilla Twig, es que hay que refactorizar y reutilizar.

De todas formas seguimos teniendo el sistema de toda la vida con PHP en las plantillas. Dependerá de cómo le pongamos a cada fichero de plantilla la extensión, que así se renderizará la plantilla.

Un poco de teoría

Para ponernos en situación, es necesario ser conscientes de que Symfony es un framework de desarrollo ágil de aplicaciones web basado en el patrón de arquitectura de software MVC (Modelo-Vista-Controlador). Los modelos son la capa de abstracción de los datos, de la información que procesa la aplicación web. Los controladores, como ya hemos visto, son la capa que dirige qué hay que ejecutar, lleva el control de la ejecución de las acciones. Y las vistas son la capa de la interfaz visual con el usuario.

Es decir, parafraseando y resumiendo al absurdo, las vistas son la capa de la aplicación web que recibe los datos de los controladores y los pinta en el navegador web. Hay más casos de uso de las vistas, pero para empezar quedémonos con que son las plantillas que reciben los datos que los modelos le han pasado al controlador, y el controlador a la vista que corresponda.

Creando un proyecto de pruebas

Ejecutando los siguientes comandos creamos el projecto:

composer create-project symfony/skeleton symfony-tutorial-vistas
cd symfony-tutorial-vistas/
composer require --dev maker server
composer require twig annotation asset

Esperamos un poco a que se instalen todas las librerías, y si todo ha ido bien ya tenemos el proyecto listo para empezar. El siguiente paso es arrancar el servidor de pruebas que viene incluido en Symfony así:

php bin/console server:start

..y accedemos a http://localhost:8000/ para comprobar que todo va bien. El siguiente paso es crear un controlador y plantilla inicial para comenzar a trabajar las vistas. Para esto hacemos lo siguiente:

php bin/console make:controller

..y le ponemos de nombre MainController, por ejemplo. Esto nos creará los siguientes ficheros:

created: src/Controller/MainController.php
created: templates/main/index.html.twig

Con esto ya empezamos..

Plantilla base, layouts, herencia, bloques

Para ver todo esto es más fácil ver el código que explicarlo. La idea es que con Twig dejamos de escribir dos veces lo mismo, reutilizando las plantillas tantas veces como necesitemos, y el código fuente quedará muy limpio. Así, editamos el controlador para hacer la home y tres páginas, este es el fichero src/Controller/MainController.php:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class MainController extends AbstractController
{
    /**
     * @Route("/", name="home")
     */
    public function index()
    {
        $arrayMenu = array(
            'page1', 'page2', 'page3',
        );

        return $this->render('main/index.html.twig', [
            'arrayMenu' => $arrayMenu,
        ]);
    }

    /**
     * @Route("/page1", name="page1")
     */
    public function page1()
    {
        $arrayMenu = array(
            'page1', 'page2', 'page3',
        );
  
        return $this->render('main/page1.html.twig', [
            'arrayMenu' => $arrayMenu,
        ]);
    }

    /**
     * @Route("/page2", name="page2")
     */
    public function page2()
    {
        $arrayMenu = array(
            'page1', 'page2', 'page3',
        );

        return $this->render('main/page2.html.twig', [
            'arrayMenu' => $arrayMenu,
        ]);
    }

    /**
     * @Route("/page3", name="page3")
     */
    public function page3()
    {
        $arrayMenu = array(
            'page1', 'page2', 'page3',
        );

        return $this->render('main/page3.html.twig', [
            'arrayMenu' => $arrayMenu,
        ]);
    }
}

Fíjate que he puesto el array de elementos de menú $arrayMenu repetido en cada controlador. Lo he puesto mientras que construía porque necesitaba los elementos del menú pasarlos a las plantillas. Cuando vas construyendo es habitual llegar a esta situción. Luego lo mejoramos, y vemos de paso una cosa muy útil, que hará que sólo tengas que generar el $arrayMenu una vez en un sólo sitio, y reutilizarlo en todos los sitios que necesites (hablo de la renderización de Twig).

Plantilla base

Este fichero lo debes haber autogenerado al principio del post, es el templates/base.html.twig, que editado queda así:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
    {% block title %}¡Hola!{% endblock %}
</title>
{% block stylesheets %}
    <style>
    body {
        margin: 0;
        padding: 0;
    }
    #header-div {
        background-color: #aaa;
    }
    </style>
{% endblock %}
</head>
<body style="">
    {# ESTO INCLUYE LA CABECERA #}
    {{ include('menuheader.html.twig') }}
    <div style="border: 1px solid black; padding: 5px; margin: 5px;">
        {# ESTO INCLUYE EL CONTENIDO DE LA PÁGINA EN ESTA CAJA #}
        {% block body %}{% endblock %}
    </div>
    {# ESTO INCLUYE EL PIE DE PÁGINA #}
    {{ include('menufooter.html.twig') }}

    {% block javascripts %}{% endblock %}
</body>
</html>

Fíjate que incluimos 2 plantillas más, la de cabecera templates/menuheader.html.twig que es:

<div style="border: 1px solid black; padding: 5px; margin: 5px; text-align: center;" id="header-div">
    <h1><a href="{{ path('home') }}">Symfony: templating</a></h1>
    <p>Esto es la zona para la cabecera.</p>
    {% if arrayMenu is defined %}
        {% for arrayMenuItem in arrayMenu %}
            <a href="{{ path(arrayMenuItem) }}">{{ arrayMenuItem }}</a>
        {% endfor %}
    {% endif %}
</div>

..y la de pié de página, que es templates/menufooter.html.twig y queda así:

<div style="border: 1px solid black; padding: 5px; margin: 5px; text-align: center;">
    <p>Esto es una zona para el pie de página.</p>
    <p>Tutoriales sobre Symfony Flex, Symfony 4, jnjsite.com.</p>
</div>

Plantilla de home

Este fichero templates/main/index.html.twig también lo has autogenerado cuando has usado el comando make:controller al principio, entonces editado queda así:

{# LA HERENCIA DE PLANTILLAS SE HACE CON EXTEND #}
{% extends 'base.html.twig' %}

{% block title %}¡Hola mundo!{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    <style>
        body {
            background-color: #808080;
        }
    </style>
{% endblock %}

{% block javascripts %}{% endblock %}

{% block body %}
    <h2>Página principal</h2>
    <p>Contenido del body.</p>
    <p>Esto es un fichero de estilo que se puede abrir haciendo click aquí: <a href="{{ asset('estilo.css') }}">estilo.css</a></p>
    <p>Si quisiéramos usar una ruta absoluta podemos poner: {{ absolute_url(asset('estilo.css')) }}</p>
{% endblock %}

¡IMPORTANTE! Fíjate en al instrucción {{ parent() }}, que lo que hace es no machacar lo que haya en la plantilla base.html.twig en el bloque stylesheets. Esto lo que hace es añadir a lo haya, en vez de sustituir lo que haya en el bloque. Si quieres probar su funcionamiento prueba de quitar esa línea y mira los resultados..

Plantillas para las tres páginas

Aquí la página 1 de pruebas, este fichero no estaba, tienes que ponerlo en templates/main/page1.html.twig así:

{# LA HERENCIA DE PLANTILLAS SE HACE CON EXTEND #}
{% extends 'base.html.twig' %}

{% block title %}¡Esto es una página 1!{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    <style>
    body {
       background-color: #188;
    }
    </style>
{% endblock %}

{% block javascripts %}{% endblock %}

{% block body %}
    <h2>Página de pruebas 1</h2>
    <p>Contenido del body.</p>
{% endblock %}

Ahora la página 2 de pruebas, en el fichero templates/main/page2.html.twig, que es casi igual:

{# LA HERENCIA DE PLANTILLAS SE HACE CON EXTEND #}
{% extends 'base.html.twig' %}

{% block title %}¡Esto es una página 2!{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    <style>
    body {
        background-color: #828;
    }
    </style>
{% endblock %}

{% block javascripts %}{% endblock %}

{% block body %}
    <h2>Página de pruebas 2</h2>
    <p>Contenido del body.</p>
{% endblock %}

Y el tercer fichero de plantilla para la página 3, es templates/main/page3.html.twig, y queda así:

{# LA HERENCIA DE PLANTILLAS SE HACE CON EXTEND #}
{% extends 'base.html.twig' %}

{% block title %}¡Esto es una página 3!{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    <style>
    body {
        background-color: #883;
    }
    </style>
{% endblock %}

{% block javascripts %}{% endblock %}

{% block body %}
    <h2>Página de pruebas 3</h2>
    <p>Contenido del body.</p>
{% endblock %}

Ahora vamos con las aclaraciones..

Cómo funciona la herencia, el extend

Si te fijas arriba en cada plantilla de la home, y las páginas 1, 2 y 3. Tenemos la instrucción extends. En pocas palabras, esto lo que hace es poner los bloques que defines en la plantilla home/pagina1/pagina2/pagina3 en la plantilla que extiende, que es la base.html.twig.

Fíjate que simplemente se definen los bloques, y la plantilla base.html.twig se reutiliza una y otra vez.

Cómo no volver a escribir el mismo código, usando el include

Para esto, cuando empiezas a escribir lo mismo una y otra vez en varias plantillas, lo que hay que hacer es ponerlo en una plantilla e incluirlo en todas. Esto se hace con la instrucción include que podemos ver en la plantilla base. Es decir, siempre vemos la misma cabecera y pie de página en cada página, con lo que tenemos un fichero de cabecera templates/menuheader.html.twig y el fichero de pié de página templates/menufooter.html.twig.

También al separarlo en ficheros e incluirlo en la plantilla base mejor que mejor. Así el desarrollador que vea que tiene ficheros con el nombre menuheader.html.twig y menufooter.html.twig va también a tener claro lo que hay ¿verdad?

Resumiendo, tenemos que estamos reutilizando en la 4 secciones la misma plantilla base, que incluye los ficheros de cabecera y pie de página. Vamos ahora con otra mejora.. No se si nos hemos fijado que en el controlador estamos generando el menú una y otra vez. Es decir, en cada función del controlador generamos una y otra vez la variable $arrayMenu, vamos a mejorar esto generando esta variable una sóla vez. Así de paso vemos también el renderizado de bloque con Twig…

Terminando, renderizando un bloque completo que carga datos dinámicamente

Esto es muy útil, por ejemplo aquí tenemos la carga del menú de la cabecera. Vamos a ver cómo crear este $arrayMenu una sola vez y a reutilizarlo. Es simple, en vez de incluir la plantilla de cabecera, lo que hacemos es renderizar la cabecera, creando en el controlador este renderizado de la cabecera, cargando aquí el array de items de menú sólo una vez. Es más fácil leer el código que explicarlo con texto.

Aquí el controlador modificado para esto, que queda simplificado y la gestión de los items de menú también simplificada sólo estando en un sitio. De nuevo, reutilizando código sin escribir una y otra vez lo mismo. Marco en negrita la función que renderizará el menú de cabecera:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class MainController extends AbstractController
{
    /**
     * @Route("/", name="home")
     */
    public function index()
    {
        return $this->render('main/index.html.twig');
    }

    /**
     * @Route("/page1", name="page1")
     */
    public function page1()
    {
        return $this->render('main/page1.html.twig');
    }

    /**
     * @Route("/page2", name="page2")
     */
    public function page2()
    {
        return $this->render('main/page2.html.twig');
    }

    /**
     * @Route("/page3", name="page3")
     */
    public function page3()
    {
        return $this->render('main/page3.html.twig');
    }

    /**
     * @Route("/render-header", name="renderHeader")
     */
    public function renderHeader()
    {
        $arrayMenu = array(
            'page1', 'page2', 'page3',
        );

        return $this->render('menuheader.html.twig', [
            'arrayMenu' => $arrayMenu,
        ]);
    }
}

Si le diéramos un par de vueltas lo que podríamos hacer es incluso cargar los elementos de menú desde la base de datos. Esta vez así sólo en una función de renderización.

Aquí la plantilla base modificada, marco en negrita lo cambiado:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
    {% block title %}¡Hola!{% endblock %}
</title>
{% block stylesheets %}
    <style>
    body {
        margin: 0;
        padding: 0;
    }
    #header-div {
        background-color: #aaa;
    }
    </style>
{% endblock %}
</head>
<body style="">
    {# ESTO INCLUYE LA CABECERA #}
    {#{ include('menuheader.html.twig') }#}
    {{ render(url('renderHeader')) }}
    <div style="border: 1px solid black; padding: 5px; margin: 5px;">
        {# ESTO INCLUYE EL CONTENIDO DE LA PÁGINA EN ESTA CAJA #}
        {% block body %}{% endblock %}
    </div>
    {# ESTO INCLUYE EL PIE DE PÁGINA #}
    {{ include('menufooter.html.twig') }}

    {% block javascripts %}{% endblock %}
</body>
</html>

Esto es todo, este es el punto de partida para ir creando un buen sistema de plantillas. Me he extendido mucho en este post, pero aún quedan muchos detalles sobre el sistema de plantillas Twig. Es decir, Twig es una gran herramienta, no se puede abarcar todo en un sólo post y te invito a que sigas investigando.

Cómo se debería de ver en el navegador

Esto son las pantallas en local cómo deberían de verse en tu navegador en http://localhost:8000/ si pruebas este CODE-KATA. La home:

Symfony templating home

La página 1:

Symfony templating page1

La página 2:

Symfony templating page2

La página 3:

Symfony templating page3

Compartir..

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

14 − 7 =