Continuamos con la serie sobre Symfony, esta vez vamos a tocar el tema de los templates. Con el metalenguaje Twig y atendiendo al estándar de tres niveles propuesto por Symfony, vamos a implementar un sistema de plantillas que nos permita utilizar un framework cliente HTML (responsive), Bootstrap, como salida de nuestras aplicaciones. Quizás sería más sencillo si el HTML fuese maquetado con funciones PHP nativas, pero sea como sea, es cierto que este concepto aleja la representación de la información de la lógica de negocio, poniéndolos en dos capas totalmente separadas. Además a día de hoy los sistemas de plantillas como smarty o twig hacen uso de estrategias que minimizan el impacto de esta capa sobre los proyectos, como compilar las plantillas a PHP nativo de manera transparente cada vez que se encuentra un cambio en una de ellas. La idea es crear un Layout, una plantilla global con la estructura básica de la página y con la inicialización del framework cliente, en ella inyectaremos los diferentes apartados que iremos creando para nuestra web.
Instalación del Layout mediante Composer.
Lo primero es implementar una librería HTML que nos proporcione un layout básico para el backend, un conjunto de plantillas que conformen el marco de la pagina (menús, cabecera, estilos, etc…) donde insertar nuestros interfaces para la gestión. Vamos a escoger un framework cliente (HTML, CSS, JS) que nos garantice unos componentes básicos para trabajar más rápido y unos estilos homogéneos entre todos los elementos, también buscamos que tenga la capacidad de adaptarse a la mayoría de los dispositivo desde los que se pueda acceder. Bootstrap lleva ya tiempo funcionando, se basa en JQuery y tiene algunas plantillas básicas que servirán perfectamente para este cometido. NO voy a entrar en nada de Bootstrap, es un framework sencillo, y voy a copiar el maquetado de los ejemplos que ya provee la plantilla. El maquetado y declaración de los componentes se puede realizar con clases CSS y etiquetas HTML, mediante estilos se pueden generar cuadriculas en los marcos que conforman la pagina y el motor JS se encargará de moverlas, redimensionarlas o hacer que cambie su comportamiento en atención al navegador del cliente.
Escogemos una plantilla ya hecha, de los ejemplos de la página startBootstrap, el layout SB Admin 2. Este se compone de tres grandes bloques, el Titulo/menú superior, el menú lateral de navegación y un bloque central vacío para nuestros componentes. Además es 100% responsive.
Para integrarlo en nuestro proyecto vamos a utilizar Composer, Bootstrap gestiona sus dependencias a través de herramientas JS similares a Composer, pero no compatibles. Por suerte el proyecto está disponible en Github con las dependencias ya integradas. Así que, en el archivo composer.json vamos a generar un repositorio tipo Git para poder instalar o updatear el paquete de manera estándar. Un repositorio de composer almacena la información básica de un paquete, el nombre, las versiones disponibles y el origen de los fuentes (Zip, sistema de versionado git, SVN, etc…).
En el primer nivel del esquema composer.json, después de cerrar la entrada de autoload o description, podemos incluir el elemento repositories.
1 | "repositories": [ |
Tampoco me voy a parar demasiado, los índices son bastante descriptivos, podéis ampliar la información haciendo click en la palabra repositorio.
Posteriormente definimos la dependencia para utilizar el repositorio, añadimos esta linea al final del apartado require del composer.json
1 | "BlackrockDigital/startbootstrap-sb-admin-2": "^3.0" |
Antes de instalar el SB Admin nos queda un último problema por solucionar, el template se almacena en la carpeta privada vendor, de manera que los navegadores no tienen acceso a ella. Si no queremos generar el plugin descrito en los siguientes párrafos, y si estamos desarrollando en Linux, se pueden hacer unos simples enlaces simbólicos en la carpeta /web a las librerías de CSS y JS. Alojadas en las carpetas dist y vendor, dentro de /vendor/BlackrockDigital/startbootstrap-sb-admin-2. Si estamos en Windows habrá que copiar estos contenidos al directorio público.
Para evitar cualquier diferencia y automatizar futuras instalaciones, he decidido crear un plugin de post-instalacion/post-update que copia estas estructuras al apartado público. Si queremos actualizar las librerías será tan sencillo como indizar las nuevas versiones dentro de repositories en el composer.json, y ejecutar composer update
para recibir los fuentes en el lugar adecuado.
Dentro de la carpeta src/AppBundle creamos un nuevo directorio al que llamamos Install y dentro de esta creamos el fuente PostInstallScript.php .
1 |
|
La clase es bastante simple, las dos funciones principales postPackageUpdate() y postPackageInstall() serán llamadas por Composer tras hacer el update o instalación de cualquier librería. Estas llaman a checkPackage() para que utilizando la API del packageEvent recibido, compruebe el nombre del paquete de la operación actual para lanzar la copia de archivos si es el layout. El package se obtiene de maneras diferentes en función de la operación, de ahí la necesidad del método getPackage(). La función recurse_copy la saqué de los comentarios escritos en php.net para la función copy() y parece que funciona decentemente.
Hará falta actualizar el fichero .gitignore del raíz para no incluir en los commits la carpeta vendor del apartado público, modificamos /vendor/ a **/vendor/**.
Para terminar con esto necesitamos informar a Composer que tras cada instalación/update debe ejecutar los métodos de esta clase. Así que nuevamente modificamos el composer.json para añadir al apartado Scripts, ya declarado por Symfony, la ejecución de nuestro plugin tras cada operación de package.
1 | "scripts": { |
Desde el raíz ejecutamos el comando composer update
y si hemos hecho todo correctamente, deberían copiarse los fuentes automáticamente al directorio público web/vendor. Es importante tener Composer actualizado mediante composer self-update
, no recuerdo bien que versión tenía en Ubuntu pero la API era significativamente diferente a la actual.
Se pueden ver algunas maquetas en la carpeta vendor/BlackrockDigital/startbootstrap-sb-admin-2/pages, de ahí sacaremos la mayoría del código que vamos a usar para crear la base del proyecto. Si accedemos a esta carpeta y hacemos click sobre alguna de ellas, debería abrirse correctamente en el navegador.
Twig, base layout
Symfony utiliza un metalenguaje para las plantillas, Twig, con el que podremos implementar lógica dentro de la estructura HTML. Twig soporta herencia y nos aporta funciones para crear bucles o asignaciones, diferentes herramientas simplificadas propias de un lenguaje de programación. Está pensado para que un maquetador pueda diseñar un sistema que utilizando los datos recibidos del Controller conforme la plantilla HTML. Por ejemplo los menús, que podrán ser diferentes en atención al rol del usuario, o los contenidos, que según sus atributos podrán ser representados de una u otra manera.
Según el estándar que definen muchos frameworks, Symfony utiliza un layout base (un archivo HTML y Twig con las secciones de titulo, menús y cuerpo), en el que se inyectan las distintas plantillas específicas. De esta manera se evita repetir la lógica o maqueta de todos los elementos comunes a todas las pantallas del sistema.
Echando un ojo al archivo /app/Resources/views/base.html.twig podemos ver la mínima expresión de un layout.
1 |
|
Es HTML y algunas etiquetas Twig, en este caso etiquetas de definición de bloques. Son la modularidad del layout, cada bloque será reemplazado, mediante herencia o inclusión, por el contenido procesado de otros templates. Así una vez que tengamos conformada la plantilla base, sólo nos tendremos que preocupar por generar las plantillas de las funcionalidades a implementar, despreocupándonos de tener que repetir para cada caso una la maquetación de las funcionalidades comunes y el infierno que representaría tener que cambiar el comportamiento de estas.
Las partes desarrolladas en Twig se identifican fácilmente porque van encerradas entre llaves, hay tres tipos de etiquetas funcionales que se pueden identificar por las llaves y un segundo carácter de apertura y cierre.
- {{ ... }} Pinta en el template el contenido de la variable entre las llaves o el resultado de una expresión, como podría ser una suma.
- {% ... %} Es una etiqueta de acción, controla la lógica del template, dentro irán [elementos de control de flujo](http://twig.sensiolabs.org/doc/tags/index.html), como bucles o condicionales y/o [funciones](http://twig.sensiolabs.org/doc/functions/index.html).
- {# ... #} Comentarios, lo que ca entre las llaves con almohadillas se convertirá en un comentario que no se renderizará al HTML resultante.
Otra característica de Twig son los filtros, modificadores de los contenidos a representar. Se implementan insertando un pipe tras el valor/variable e indicando a continuación el nombre del filtro. Así {{ title|upper }} hará que el texto almacenado en la variable TITLE se renderice en mayúsculas.
Twig, extensión por herencia o inclusión
Como ya hemos mencionado el resultado que se mostrara al cliente se compondrá de pequeñas piezas ensambladas para formar un conjunto. La estrategia que debemos seguir es localizar todas las partes que se van a repetir, para generar templates que podamos reutilizar en cualquier otro momento. Hay dos caminos a seguir, por herencia o por inclusión.
Por inclusión, con la función include indicamos el nombre de un template que deseamos incrustar en el punto exacto de la plantilla padre, donde se declara. Para algunos casos es práctico, pero en muchos sentidos va a limitar la reusabilidad del código, ya que se está definiendo al mismo tiempo la dependencia con esta sub-plantilla.
El sistema de herencia, en el que generamos una base como la que acabamos de ver, poblada con bloques y heredada por todos los templates atómicos que vayamos a utilizar. En estos es donde el contenido concreto reemplazará las declaraciones de bloque de los ancestros. Cada vez que un template hereda de otro debe indicar cual es el bloque que va a reemplazar. Si algún bloque queda sin reemplazar se utilizara el contenido escrito entre las etiquetas de la definición como valor por defecto. Este contenido por defecto puede ser lógica Twig, así podremos, en caso necesario, reemplazar y modificar el comportamiento estándar de una manera limpia, heredándolo y sobreescribiendo con una plantilla todavía más específica.
Este sería el contenido de la plantilla base original de Symfony que vamos a sobreescribir.
1 |
|
Si queremos crear una plantilla simple que extienda los contenidos del BODY, para el apartado blog, crearíamos el archivo app/Resources/views/blog/list.html.twig con el siguiente contenido (No es necesario ya que vamos a reescribir estas plantillas).
En la primera línea, es obligatorio incluir la clausula extends.
1 | {% extends 'base.html.twig' %} |
El estándar respetado por la mayoría de los Bundles (paquetes) de Symfony es una estructura de plantillas en tres niveles. Siguiendo la propia documentación de Symfony nosotros podremos hacer lo mismo, una plantilla base para la aplicación con un bloque Body y por cada funcionalidad que dotemos, blog, usuarios, etc… generamos una base y dentro de esta base es donde se inyectaran las plantillas finales, sobre el bloque content extendido del body.
Modificación del layout base de Symfony
Siguiendo el ejemplo, con nuestras propias plantillas, vamos a estructurar nuestro layout en tres niveles. Lo primero será reemplazar el contenido del base layout para insertar dentro nuestras plantillas. Así que copiamos el contenido de /vendor/BalckrockDigital/startbootstrap-sb-admin-2/pages/blank.html a /app/Resources/views/base.html.twig. Tras realizar la copia tenemos que actualizar las rutasen esta a los fuentes JS y CSS, ya que nuestra aplicación HTML ha cambiado la ruta del vendor y sb-admin.
- Reemplazamos en todo el documento “../vendor” por “/vendor”
- Reemplazamos en todo el dfocumento “../dist” por “/vendor/startbootstrap-sb-admin-2”
Ahora levantamos el servidor (php bin/console server:run
) y navegamos a la raíz, el tema debería estar funcionando y veríamos la página. De paso, se puede comprobar que los enlaces a JS están funcionando correctamente al desplegar los menús, recordad que los enlaces del menú no funcionaran, ya que apuntan a páginas estáticas de otro directorio que no hemos copiado.
En principio vamos a dejar de lado el menú, sin modificarlo, vamos a declarar con Twig, donde ubicamos los bloques principales de nuestro template cogidas del antiguo base.
Title
Primero localizamos la etiqueta HTML de título y dentro insertamos el bloque.1
2
3
4
5<title>SB Admin 2 - Bootstrap Admin Theme</title>
<!-- Lo cambiamos por -->
<title>{% block title %}Welcome!{% endblock %}</title>Stylesheets
Ya que el template es nuestro framework cliente, los enlaces a los stylesheets los vamos a respetar, insertamos el bloque bajo el último enlace CSS para que nuestros estilos se añadan a la plantilla.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<!-- Bootstrap Core CSS -->
<link href="/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<!-- MetisMenu CSS -->
<link href="/vendor/metisMenu/metisMenu.min.css" rel="stylesheet">
<!-- Custom CSS -->
<link href="/vendor/startbootstrap-sb-admin-2/css/sb-admin-2.css" rel="stylesheet">
<!-- Custom Fonts -->
<link href="/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
{% block stylesheets %}{% endblock %}favicon
Insertamos este bloque antes del cierre del HEAD para que en caso de querer usarlo podamos inyectarlo. Justo debajo del bloque de estilos, recién modificado.1
2
3
4{% block stylesheets %}{% endblock %}
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
</head>Body
Localizamos el apartado de contenido y reemplazamos el interior del container-fluid por el bloque body1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<!-- Page Content -->
<div id="page-wrapper">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">Blank</h1>
</div>
<!-- /.col-lg-12 -->
</div>
<!-- /.row -->
</div>
<!-- /.container-fluid -->
</div>
<!-- /#page-wrapper -->
<!-- Lo cambiamos por -->
<div id="page-wrapper">
<div class="container-fluid">
{% block body %}{% endblock %}
</div>
<!-- /.container-fluid -->
</div>
<!-- /#page-wrapper -->javascripts
Finalmente antes del cierre del body insertamos el bloque de javascript, justo bajo los enlaces a los fuentes JS de la plantilla original.1
2{% block javascripts %}{% endblock %}
</body>
Ya tenemos un primer layout para hacer algunos tests, nuevamente accedemos al index http://localhost:8000/ para ver como ha cambiado. Ahora Symfony inyecta el contenido al marco central de la pantalla, pero sus propias hojas de estilo modifican el comportamiento estándar de sb-admin y muestran la pagina más pequeña. Ocupa solo un marco central con espacios a ambos lados, con nuestros controladores esto no va suceder, así que de momento no nos preocupamos por ello.
Plantillas de contenido en tres niveles
Ahora que ya tenemos el layout creado vamos a volver a desarrollar con nuestro controlador blog. Lo primero es crear una plantilla común para todas utilizar con las acciones de este BlogController, así que creamos la carpeta blog dentro del directorio /app/Resources/views/ , que es la ruta por defecto para los templates. Y dentro escribiremos una primera plantilla denominada layout.html.twig , será un template común para todos los contenidos de blog.
1 | {# app/Resources/views/blog/layout.html.twig #} |
Solo nos falta crear las plantillas específicas a las acciones, de manera que comenzaremos con el index de noticias, la acción listAction dentro de BlogController. En la carpeta /app/Resources/views/blog creamos el archivo index.html.twig con estos contenidos. Además en la ruta /web/css/blog crearemos el archivo vacío index.css, que nos permitirá modificar los estilos necesarios para este template
1 | {# app/Resources/views/blog/index.html.twig #} |
En el template estamos heredando el layout base del blog que define content, y a su vez hereda el layout base de la aplicación que define stylesheets, así que con esta plantilla reemplazamos los dos bloques indicados. En stylesheets se utliza la funcion asset(), encargada de completar la ruta relativa al elemento público, ya que los elementos que queremos integrar son relativos al directorio público raíz de nuestra aplicación, pero esta puede acabar como Bundle de otra aplicación o integrada dentro de un subdirectorio del servidor. Con esta función nos olvidaremos de tener que modificar estas rutas según las circunstancias.
En el bloque content también utilizamos la función for de Twig para iterar los arrays, muy parecida al foreach de PHP.
1 | {% for value in myarray %} |
En cada iteración value tendrá un valor de myarray, podremos acceder a sus elementos mediante la notacion punto (.). Para acceder a la información específica del bucle, tendremos acceso al array “loop”, en el que podemos comprobar el numero de iteraciones ya pasadas mediante el elemento loop.index (inicio en 1, loop.index0 para consultar el índice contando desde 0). Con loop.length podremos averiguar el numero de elementos del array… podemos ver la información completa aquí.
Volviendo a nuestro BlogController vamos a crear y devolver al template la variable que contiene nuestros posts, de momento vamos a generar un array en código que contenga un índice de tres noticias para ser publicadas en blog/index.html.twig. Así que modificamos nuestra función listAction para que envíe al template las variables y renderice su contenido. Mediante el comando render qué nos retornará un objeto response con el contenido ya montado e inyectado listo para ser devuelto al cliente.
1 | /** |
Si accedemos a la ruta del listAction del blog http://localhost:8000/blog podremos ver el resultado final. El layout con las noticias embebidas, a partir de ahora todos los templates que vayamos generando tendrán un aspecto homogéneo y tendremos garantizado el acceso a todas las funcionalidades del framework Bootstrap.
Hemos llegado al fin
Bueno, por esta sesión creo que es suficiente, me gustaría extenderme más pero creo que lo tratado es suficiente para hacer un punto y aparte. En el próximo post empezaremos con Datatables para generar listados y seguiremos un poco más con Twig, también introduciremos el ORM promulgado por Symfony, Doctrine.