Curso de Symfony3. Por Juan David Rodríguez García (juanda@juandarodriguez.es)

Curso Symfony3

1 Licencia        3

2 Instalación y configuración de Symfony        3

2.1 El instalador de Symfony        3

2.2 Uso del servidor de pruebas        4

2.3 Barra de depuración de Symfony        5

2.4 Versiones y roadmap        5

3 Vista panorámica de Symfony        5

3.1 HTTP: Peticiones y Respuestas        5

3.1.1 Aspecto de peticiones HTTP        5

3.1.2 Aspecto de respuestas HTTP        6

3.2 Estructura de un proyecto Symfony        6

3.3 Configuración        7

3.4 Rutas        8

3.5 Controladores        8

3.6 Mecanismo de sesión        9

3.7 Plantillas y web assets        9

4 Ejercicio 1.        9

5 Inyección de dependencia        11

5.1 El Service Container        11

5.2 Algunos servicios útiles ofrecidos por Symfony        11

5.3 Creación de servicios        12

5.4 Bundles y código de terceros        12

5.5 Creación de un bundle propio        13

6 Ejercicio 2.        13

7 Formularios        14

7.1 Creación de formularios        14

7.2 Formularios definidos en clases: Tipos        14

7.3 Manipulación de formularios        15

7.4 Validación de datos        15

8 Seguridad        15

8.1 Autenticación y Autorización        15

8.2 Mecanismo básico de seguridad        17

8.2.1 Firewalls (autenticación)        17

8.2.2 Proveedor de usuarios        17

8.2.3 Encoders        17

8.2.4 Access control (autorización)        17

8.3 Más cosas sobre seguridad        18

8.3.1 Access control y Roles        18

8.3.2 Obtener el usuario        18

8.3.3 Implementar un formulario de login clásico HTML        18

8.3.4 Obtener usuarios desde una base de datos        18

8.3.5 Autenticar contra LDAP o Directorio Activo        18

9 Otros temas        18

9.1 Event dispatcher y Listeners        18

9.1.1 Escuchar eventos (suscribirse a eventos)        20

9.1.2 Lanzar eventos        21

9.2 Creación de nuevos bundles        23

9.3 Instalación de bundles externos        25

9.4 Traducciones        25

9.5 Testing        26

9.6 Logging        26


1 Licencia

La propiedad intelectual de esta obra pertenece a su autor: Juan David Rodríguez García (juanda@juandarodriguez.es)

Esta obra está bajo una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 4.0 Internacional.

2 Instalación y configuración de Symfony

Recursos

2.1 El instalador de Symfony

En windows se puede instalar así.

# php -r "file_put_contents('symfony', file_get_contents('https://symfony.com/installer'));"
move symfony c:\php
cd c:\php
(
echo @ECHO OFF & echo php "%~dp0symfony" %*) > symfony.bat

Con esta herramienta podemos crear la estructura básica de proyectos symfony en la versión que deseemos.

symfony.bat new <nombre-proyecto> <version-symfony>
symfony.bat
new prueba 3.4
cd prueba

También se puede usar composer para crear un proyecto de symfony:

composer create-project symfony/framework-standard-edition my_project_name "3.4"

Podemos comprobar que la instalación es correcta y tenemos todas las librerías y dependencias necesarias arrancando el servidor de desarrollo (o usando cualquier otro servidor) y accediendo a la ruta /config.php:

bin/console server:run
http:
//localhost:8000/config.php

Existe una aplicación de ejemplo muy completa y útil para aprender symfony. Se puede instalar así:

composer.phar create-project symfony/symfony-demo demo "1.1.1"

O con el propio instalador de symfony:

symfony demo

Esta aplicación sigue las directrices planteadas en el libro de buenas prácticas de symfony:

https://symfony.com/doc/current/best_practices/index.html

cuya lectura se recomienda encarecidamente si se piensa usar symfony de manera correcta.

2.2 Uso del servidor de pruebas

Desde el directorio raíz del proyecto:

        

# php bin/console server:run

Y podemos acceder desde un browser apuntando a:

http:/localhost:8000

Si queremos acceder desde otro equipo (no se recomienda pero puedes ser útil para alguna que otra prueba) y en otro puerto:

# php bin/console server:run  <ip>:<port>

Por ejemplo:

# php bin/console server:run  0.0.0.0:3000

2.3 Barra de depuración de Symfony

Se accede utilizando el controlador frontal de desarrollo de symfony. Si se utiliza el servidor web de desarrollo es precisamente este controlador el que se usa por defecto, y por tanto tenemos disponible la barra de depuración sin más.

2.4 Versiones y roadmap

3 Vista panorámica de Symfony

Recursos:

3.1 HTTP: Peticiones y Respuestas

Symfony es un framework para el desarrollo de aplicaciones HTTP, el principal problema que resuelve es el de tratar con los elementos de este protocolo de una manera sencilla y segura.

3.1.1 Aspecto de peticiones HTTP

GET /pub/WWW/TheProject.html HTTP/1.1
Host: www.w3.org


POST / HTTP/
1.1
Host: foo.com
Content-Type: application/x-www-form-urlencoded
Content-Length:
13

say=Hi&to=Mom

3.1.2 Aspecto de respuestas HTTP

HTTP/1.1 200 OK
Date: Mon,
27 Jul 2009 12:28:53 GMT
Server: Apache/
2.2.14 (Win32)
Last-Modified: Wed,
22 Jul 2009 19:15:56 GMT
Content-Length:
88
Content-Type: text/html
Connection: Closed
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>

HTTP/1.1 404 Not Found
Date: Sun,
18 Oct 2012 10:36:20 GMT
Server: Apache/
2.2.14 (Win32)
Content-Length:
230
Connection: Closed
Content-Type: text/html; charset=iso
-8859-1
<!DOCTYPE HTML
PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head>
  <title>
404 Not Found</title>
</head>
<body>
  <h1>Not Found</h1>
  <p>The requested URL /t.html was not found on this server.</p>
</body>
</html>

Symfony proporciona objetos que encapsulan peticiones, respuestas rutas y otros elementos del protocolo HTTP así como las sesiones y cookies, que son  elementos adicionales usado en las aplicaciones web para mantener el estado, ya que HTTP es un protocolo SIN ESTADO.

3.2 Estructura de un proyecto Symfony

app /
    Contiene cosas como configuración y plantillas. Básicamente, todo lo que no sea código PHP va aquí.


src /
   Tu código PHP vive aquí.El 99% del tiempo, trabajarás en src / (archivos PHP) o en la aplicación / (todo lo demás).


bin/
   El famoso archivo
 bin/console vive aquí (y otros archivos ejecutables menos importantes).


tests/
   Las pruebas automatizadas (por ejemplo, pruebas unitarias) para su aplicación en vivo aquí.


var/
    Aquí es donde se almacenan los archivos creados automáticamente, como los archivos de caché (var/cache /), los registros (var/logs /) y las sesiones (var/sessions/).


vendor/
    Aquí viven bibliotecas de terceros (es decir, "proveedores"). Estos se descargan a través del administrador de paquetes composer.


web/

Esta es la raíz del documento para su proyecto: coloque aquí los archivos accesibles públicamente (por ejemplo, CSS, JS e imágenes). Es donde se encuentra el controlador frontal.

3.3 Configuración

El sistema de configuración de Symfony es muy flexible. Se basa en entornos de ejecución, de manera que el mismo código puede ejecutarse en distintos entornos lo que significa que se aplican distintas configuraciones. Esto es útil para separar, por ejemplo, el entorno de producción del de desarrollo, que son los dos entornos clásicos y que vienen preparados por defecto en cualquier instalación de symfony. No obstante se pueden definir tantos entornos de configuración como queramos.

El entorno de ejecución se define en el controlador frontal que ejecutemos. Estos se encuentran en el directorio web:

app.php es el de producción,

app_dev.php es el de desarrollo.

Se declaran en la línea:

$kernel = new AppKernel("nombre_entorno", false);

Según el entorno que estemos usando, se cargará una configuración u otra. Concretamente se carga el archivo de configuración:

app/config/config_nombre_entorno.yml

Si miramos dentro de cualquiera de los que vienen por defecto con symfony (prod, dev) veremos que ambos incluyen al fichero:

app/config/config.yml

Por tanto en este último fichero se definen los parámetros de configuración comunes a todos los entornos.

3.4 Rutas

Las rutas se mapean en controladores, de manera que cuando se hace una petición HTTP a una determinada ruta se ejecuta el controlador asociado.

Crear una página en Symfony consiste en:

Las rutas se pueden definir en ficheros de configuración (yaml, XML, php) o en el código de los controladores mediante anotaciones.

Se pueden pasar datos a través de las rutas mediante placeholders (elementos variables en las rutas).

Ejemplos de rutas en el siguiente enlace:

https://symfony.com/doc/3.4/routing.html#routing-examples

3.5 Controladores

Un controlador no es más que una clase con métodos que deben terminar con el sufijo Action y que denominamos acciones. Las rutas se mapean contra estos métodos que son ejecutados cuando llega una petición a la ruta en cuestión.

El único requisito de una acción es que debe devolver un objeto de tipo respuesta (Symfony\Component\HttpFoundation\Response);

Ejemplo: https://symfony.com/doc/3.4/controller.html#a-simple-controller

Sin embargo si hacemos que la clase que implementa el controlador derive de Symfony\Bundle\FrameworkBundle\Controller\Controller, tenemos disponibles muchos métodos y servicios que nos ayudarán en nuestros desarrollos.

Ejemplo: https://symfony.com/doc/3.4/controller.html#the-base-controller-classes-services

3.6 Mecanismo de sesión

El servicio session encapsula y sirve para el manejo de las sesiones:

https://symfony.com/doc/3.4/controller.html#managing-the-session

3.7 Plantillas y web assets

Ejemplo plantilla twig:

https://symfony.com/doc/3.4/templating.html#templates

Las plantillas twig ofrecen mecanismos de herencia e inclusión de otras plantillas que facilitan la organización y reutilización del código.

Las plantillas twig ofrecen la posibilidad de incluir archivos javascript y css’s (assets) de manera portable, es decir, con independencia de la url asociada al despliegue del proyecto.

4 Ejercicio 1.

Implementar la aplicación “Gestor de claves web” del ejercicio 5 del curso de PHP utilizando el framework Symfony. Por lo pronto implementaremos únicamente la versión que almacena los datos en un fichero. Los pasos siguientes te ayudarán a resolver el ejercicio:

Algunos snippets que te resultarán útiles:

if($request->getMethod() == "POST"){

...

}

$keyfile = $this->getParameter("keyfile")

/**
* @Route("/add", name="add")
*/

...
"autoload": {
           
"psr-4": {
               
"AppBundle\\": "src/AppBundle",
               
"Acme\\": "src/Acme"
           },
...
}
...

return $this->render('GestorClaves/add.html.twig', array(
     
'message' => $message
));

5 Inyección de dependencia

Recursos:

5.1 El Service Container

Desde un controlador que herede de Symfony\Bundle\FrameworkBundle\Controller\Controller, tenemos disponible el contenedor en: $this->container. Y así podemos instanciar cualquiera de los servicios que tengamos disponible en nuestra aplicación, ya sea propio del framework, desarrollado por terceros o por nosotros mismos.

Ejemplo: $logger = $this->container->get(‘logger’);

Pero también se puede obtener un servicio determinado inyectándolo en los argumentos de una acción indicando la clase a que pertenece:

use Psr\Log\LoggerInterface;

/**
* @Route("/products")
*/
public function listAction(LoggerInterface $logger)
{
   $logger->info('Look! I just used a service');

   // ...
}

Los servicios disponibles los podemos enumerar con la aplicación de consola:

php bin/console debug:container

5.2 Algunos servicios útiles ofrecidos por Symfony

5.3 Creación de servicios

Como desarrolladores podemos y debemos crear nuestros servicios. La idea es que los controladores tengan el mínimo código posible (thin controller) y que toda la lógica de negocio la realicen los servicios.

A partir de Symfony 3.4 la creación de servicios es inmediata. Basta crear una clase que lo implemente e inyectarla en la acción del controlador donde queramos usarla indicando el tipo (la clase) del servicio.

Aquí se puede ver un ejemplo:

https://symfony.com/doc/3.4/service_container.html#creating-configuring-services-in-the-container

5.4 Bundles y código de terceros

La posibilidad de extender fácilmente las funcionalidades de un framework es algo fundamental. En symfony dicha extensión se hace a través de los bundles.

Un bundle es simplemente un conjunto estructurado de archivos dentro de un directorio que implementa una característica única. Puede crear un BlogBundle, un ForumBundle o un paquete para la administración de usuarios (muchos de estos ya existen como paquetes de código abierto). Cada directorio contiene todo lo relacionado con esa característica, incluidos archivos PHP, plantillas, hojas de estilo, archivos JavaScript, pruebas y todo lo demás. Cada aspecto de una característica existe en un paquete y cada característica vive en un paquete.

La propia aplicación que existe en el directorio src es un bundle. Incluso el core del framework es un bundle. La arquitectura de symfony usa el bundle como el elemento básico de construcción.

Los bundles que usemos en nuestra aplicación se deben registrar en el archivo app/AppKernel.php:

class AppKernel extends Kernel

{

        public function registerBundles()

        {

            $bundles = [

                new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),

                new Symfony\Bundle\SecurityBundle\SecurityBundle(),

                new Symfony\Bundle\TwigBundle\TwigBundle(),

                new Symfony\Bundle\MonologBundle\MonologBundle(),

                new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),

                new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),

                new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),

                new AppBundle\AppBundle(),

            ];

            if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {

                $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();

                $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();

                $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();

                if ('dev' === $this->getEnvironment()) {

                    $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();

                    $bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle();

                }

            }

            return $bundles;

        }

Si instalamos un nuevo bundle, debemos registrarlo en este archivo.

Existen multitud de bundles que nos pueden ayudar en nuestros desarrollos. En la documentación de cada uno de ellos se explican los pasos que hay que dar para incorporarlos en nuestra aplicación. Citamos algunos de los más populares:

5.5 Creación de un bundle propio

https://symfony.com/doc/3.4/bundles.html#creating-a-bundle

6 Ejercicio 2.

En este ejercicio refactorizaremos la aplicación del ejercicio 1 de manera que el objeto Acme\KeyStorage\KeyFileStorage se incorpore al framework como servicio de Symfony.

Nota: Acme\KeyStorage\KeyFileStorage depende de Acme\TopSecret\AES256Crypter, por lo que este último también debe registrarse como servicio. Pero para que esto sea posible hay que eliminar la dependencia de la clave (argumento $key) del constructor, ya que la clave es algo que se le pide al usuario en el formulario y no la podemos saber cuando se realiza el bootstraping del framework, por lo que la creación de dicho servicio fallará. La solución está en definir la clave a través de un setter en lugar del constructor, y usarlo antes de abrir el fichero de datos cuando ya se sepa la clave.

Por otro lado, para que la idea anterior funcione también hay que modificar el constructor de la clase Acme\KeyStorage\KeyFileStorage para  que no intente abrir el fichero de datos, ya que en ese punto aún no tenemos la clave. Se puede pasar el código que abre el fichero a una función openDataFile($key), que se llamará cuando dispongamos de la clave. Al no ser el constructor el encargado de abrir el fichero, el contenedor de servicios podrá crear una instancia sin problemas.

7 Formularios

Recursos:

7.1 Creación de formularios

Los formularios se crean a partir de clases planas cuyos atributos serán los datos que manipula el formulario. Esto es especialmente útil cuando utilizamos entidades persistibles  de Doctrine o cualquier otro ORM.

Mediante el servicio de formularios definimos los campos con sus tipos y validaciones que contendrá el formulario y que coincidirán en nombre con los atributos de la clase plana que representará el dato del formulario. Un objeto de dicha clase, con sus atributos definidos o vacíos, se pasará al objeto formulario y este se encarga de asignar los valores de los atributos a los campos que se han definido en su construcción.

El objeto formulario así creado e inicializado con la instancia de la clase plana, puede validarse y renderizarse en un documento HTML.

7.2 Formularios definidos en clases: Tipos

Por otro lado, la creación de formularios la podemos hacer directamente en una acción de un controlador usando el servicio de construcción de formularios o definiendo clases especiales que se denominan tipos, a partir de las cuales se construyen los formularios en una sola línea de código. Esta última manera es más adecuada pues da lugar a una organización más limpia del código y a una menor cantidad de líneas de código en el controlador.

7.3 Manipulación de formularios

https://symfony.com/doc/3.4/forms.html#handling-form-submissions

7.4 Validación de datos

La validación de datos se hace con el servicio de validación de datos. Podemos utilizarlo para validar cualquier dato y en combinación con los formularios.

En el caso de tratar con formularios, los validadores se definen sobre los propios atributos de la clase plana que se usa como dato del formulario. Podemos utilizar anotaciones, con lo cual los definimos en el propio código de la clase, o ficheros yaml, XML o PHP.

Ejemplo:

https://symfony.com/doc/3.4/validation.html#the-basics-of-validation

La validación se hace así:

https://symfony.com/doc/3.4/validation.html#using-the-validator-service

Para usar el servicio de validación hay que asegurarse de que está debidamente configurado en el framework:

https://symfony.com/doc/3.4/validation.html#configuration

También podemos crear nuestros propios validadores:

https://symfony.com/doc/3.4/validation/custom_constraint.html

8 Seguridad

Recursos:

8.1 Autenticación y Autorización

La seguridad en una aplicación es un proceso que consiste en dos pasos sucesivos: Autenticación y Autorización.

La autenticación es el proceso mediante el cual la aplicación comprueba si el usuario que pretende utilizarla es realmente quien dice ser. Es un proceso de identificación. Para ello la aplicación solicita al usuario ciertos parámetros que lo identifiquen y, mediante algún tipo de comprobación, decide si lo considera identificado en el sistema o no.

La autorización es un proceso mediante el cual la aplicación decide qué funcionalidades puede utilizar el usuario que la maneja y qué información le puede presentar. La aplicación toma tal decisión basándose en la identificación del usuario, esto es; decidirá qué recursos puede ofrecerle una vez que ha admitido la autentificación del usuario. Es, por tanto, un segundo nivel de seguridad en el control de acceso.

Algunas aplicaciones seguras ofrecen todos sus recursos a cualquier usuario autenticado, en cuyo caso la autorización se confunde con la autenticación, pero en la mayoría de las aplicaciones no es así.

Symfony proporciona un magnífico servicio para el tratamiento de la seguridad en las aplicaciones web, de forma que podemos resolver gran parte de los problemas que esta plantea a través de un fichero de configuración. A lo largo de esta unidad estudiaremos este componente, el cual, como indican en la documentación oficial de Symfony, su configuración y uso no está exenta de cierta dificultad. Pero una vez que se comprende la flexibilidad y potencia que ofrece merece el esfuerzo.

La estructura básica del funcionamiento de los mecanismos de seguridad en aplicaciones web es prácticamente la misma en todas ellas.

En primer lugar, cuando el usuario solicite algún recurso protegido, la aplicación debe iniciar el proceso de autentificación, solicitando de alguna manera al usuario las credenciales que lo identifican. A continuación la aplicación contrasta dichas credenciales utilizando algún tipo de sistema de información persistente (base de datos, directorio tipo LDAP, fichero de passwords, etcétera) donde residen los datos de identificación y posiblemente otros datos acerca del usuario (permisos asociados, por ejemplo).

Una vez admitida la identidad del usuario, se obtienen del sistema de información los datos del usuario y se hace uso de la sesión para mantener en las sucesivas peticiones del usuario su estado autentificado y sus permisos. De esa manera la aplicación sabrá en cada petición qué recursos puede ofrecer al usuario (autorización) sin preguntarle de nuevo sus credenciales de identificación.

Cuando el usuario solicite finalizar la sesión, o pase un determinado tiempo sin actividad, la aplicación destruirá la sesión, de manera que al realizar una nueva petición a un recurso protegido, la aplicación volverá a pedir las credenciales al usuario repitiendose el proceso.

8.2 Mecanismo básico de seguridad

https://symfony.com/doc/3.4/security.html#a-configuring-how-your-users-will-authenticate

8.2.1 Firewalls (autenticación)

Es el elemento básico del sistema de seguridad. Controla cómo se va a llevar a cabo la autenticación (formulario de login, autenticación basic http, mediante token, …..). Se pueden declarar varios firewalls en una misma aplicación. Cada firewall protege un conjunto de rutas dado por una expresión regular, y cada firewall define su propio contexto, por lo que no se puede pasar de uno a otro directamente, si no que hay que presentar las credenciales que cada firewall determine.

8.2.2 Proveedor de usuarios

Una vez que se hayan obtenido las credenciales hay que constrastarlas contra alguna fuente de datos donde existan los usuarios de la aplicación. El proveedor de usuario representa dicha fuente de datos. Puede ser una base de datos un LDAP un fichero de texto, etcétera. Si las credenciales resultan válidas, el proveedor de usuarios construye un objeto que encapsula la información del usuario. La clase de dicho objeto debe implementar alguna de las siguientes interfaces:

Estas interfaces definen un método getPassword(). Por lo general los passwords se almacenan usando algún tipo de hash por cuestiones de seguridad. Para poder comparar el password que introduce el usuario (durante el mecanismo de autenticación) con el password encriptado en la fuente de datos, hay que aplicar previamente el hash al password introducido. Por ello es necesario conocer el algoritmo que se utilizó cuando se almacenó el password encriptado. En symfony esta tarea es facilitada por los encoders, los cuales se asocian a los objetos usuarios para que el mecanismo de autenticación sepa cómo debe encriptar el password facilitado por el usuario.

8.2.3 Encoders

Como acabamos de decir en el párrafo anterior, a cada clase que represente un usuario obtenido desde algún user provider, se le asocia el codificador con el que se realiza la encriptación del password.

8.2.4 Access control (autorización)

Aquí se definen a qué recursos puede acceder cada usuario en función de los permisos (roles) que tenga asociado.

8.3 Más cosas sobre seguridad

8.3.1 Access control y Roles

https://symfony.com/doc/3.4/security.html#denying-access-roles-and-other-authorization

8.3.2 Obtener el usuario

https://symfony.com/doc/3.4/security.html#retrieving-the-user-object

8.3.3 Implementar un formulario de login clásico HTML

8.3.4 Obtener usuarios desde una base de datos

8.3.5 Autenticar contra LDAP o Directorio Activo

9 Otros temas

Recursos:

9.1 Event dispatcher y Listeners

Un patrón muy conocido y útil de la POO es el publish-subscribe o dispatch-listen. Proporciona una manera de ampliar las aplicaciones y modificar el flujo de ejecución añadiendo nuevas funcionalidades sin tener que alterar el código.

El funcionamiento en esencia es el siguiente. En la ejecución de un proceso determinado, se emiten eventos (o mensajes) en lugares estratégicos del proceso. Dichos eventos contienen información relevante sobre lo que está ocurriendo. Por otro lado se implementan clases o funciones que escuchan (o se suscriben) a dichos eventos. Cuando un evento es lanzado,  estas funciones se ejecutan en el orden de prioridad que se haya especificado en la subscripción, recibiendo como argumento el evento (o mensaje), de manera que puede alterar la información relevante contenida en él.

Symfony proporciona este mecanismo de expansión mediante el componente event dispatcher.

https://symfony.com/doc/3.4/reference/events.html

El componente kernel del framework, a medida que va procesando la request emite varios eventos:

Cada uno de estos eventos emite un objeto event (mensaje) que, aunque derivan de la misma clase Symfony\Component\HttpKernel\Event\KernelEvent, varían ligeramente en cuanto a la información contenida. La siguiente tabla muestra los objetos eventos lanzados por cada evento:

Para saber los listener que están escuchando a cada evento podemos usar el comando:

# php bin/console debug:event-dispatcher [nombre_evento]

Cuanto mayor sea el número de prioridad del listener, antes se ejecutará.

También se puede usar el depurador de symfony para examinar los eventos que se han lanzado y los listener que se han ejecutado.

Además de los eventos lanzados por el kernel de symfony, los bundles que hayamos instalado pueden lanzar sus propios eventos. También nosotros mismos podemos crear y emitir eventos en nuestros bundles y aplicaciones.

9.1.1 Escuchar eventos (suscribirse a eventos)

Symfony proporciona dos maneras de suscribirse a eventos.

La primera, consiste en crear una clase con un método que se llame según el siguiente patrón:

Por ejemplo:

Y una vez creada registrarla como servicio etiquetado en service.yml. Por ejemplo si queremos registrar un listener en el evento kernel.exception, podemos crear una clase AppBundle\EventListener\ExceptionListener con un método onKernelException y registrarla así::

AppBundle\EventListener\ExceptionListener:
           tags:
               - { name: kernel.event_listener, event: kernel.
exception, priority: 16 }

El método onKernelException debe recibir el tipo de evento correcto en función del evento al que se haya registrado:

<?php

namespace AppBundle\EventListener;

use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

class ExceptionListener
{
        
public function onKernelException(GetResponseForExceptionEvent $event)
        {                    
           
           $response =
new Response(
                   sprintf(
'<html><body><h1>Exception</h1><pre>%s</pre></body></html>',
                           print_r($event->getException()->getMessage(),
true)));
           
           
           $event->setResponse($response);
           
           
        }
}

La segunda, más precisa y directa, consiste en crear clases que implementen la interfaz

En ese caso no hay que registrarlos como servicio pues symfony utilizará la característica de autowiring para registrarlo automáticamente.

Estas clases deben implementar el método estático getSubscribedEvents(), donde se indicará a qué eventos se suscribe y qué funciones con qué prioridad se ejecutarán. Estas funciones deben declararse como métodos de la propia clase. Por ejemplo:

<?php

namespace AppBundle\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\Response;

class ExceptionSubscriber implements EventSubscriberInterface {

        
public static function getSubscribedEvents() {
                       
return array(
               KernelEvents::EXCEPTION =>
array(                    
                     
array('processException', 10),
                     
array('logException', 0),
                     
array('notifyException', -10),
                     
array('resourceNotFoundException', 20),
               )
           );
        }

        
public function processException(GetResponseForExceptionEvent $event) {          
   
// code
        }

        
public function logException(GetResponseForExceptionEvent $event) {
   
// code            
        }

        
public function notifyException(GetResponseForExceptionEvent $event) {
   
// code
                   }

        
public function resourceNotFoundException(GetResponseForExceptionEvent $event) {
   
// code
        }

}

9.1.2 Lanzar eventos

https://symfony.com/doc/3.4/components/event_dispatcher.html#creating-and-dispatching-an-event

Para lanzar eventos, primero tenemos que crearlos declarando una clase que extienda de

La clase contendrá los métodos que consideremos necesarios para obtener la información sobre el evento en cuestión. Por ejemplo:

namespace Acme\Store\Event;

use Symfony\Component\EventDispatcher\Event;
use Acme\Store\Order;

/**
* The order.placed event is dispatched each time an order is created
* in the system.
*/

class OrderPlacedEvent extends Event
{
   
const NAME = 'order.placed';

   
protected $order;

   
public function __construct(Order $order)
   {
       
$this->order = $order;
   }

   
public function getOrder()
   {
       
return $this->order;
   }
}

Cualquier listener suscrito a este evento, tendrá acceso al método getOrder().

A continuación tenemos que lanzar el evento mediante el servicio dispatcher, el cual se puede obtener mediante type hinting en los argumentos de la función que vaya a usarlo un objeto con la interfaz:

O usando el contenedor de servicios mediante el id debug.event_dispatcher.

use Acme\Store\Order;
use Acme\Store\Event\OrderPlacedEvent;

// the order is somehow created or retrieved
$order =
new Order();
// ...

// creates the OrderPlacedEvent and dispatches it
$event =
new OrderPlacedEvent($order);
$dispatcher->dispatch(OrderPlacedEvent::NAME, $event);

9.2 Creación de nuevos bundles

# php bin/console generate:bundle

Are you planning on sharing this bundle across multiple applications? [no]: no

Your application code must be written
in bundles. This command helps
you
generate them easily.

Give your bundle a descriptive name, like BlogBundle.
Bundle name: PruebaBundle

Bundles are usually generated into the src/ directory. Unless you're
doing something custom, hit enter to
keep this default!

Target Directory [src/]: src/

What
format do you want to use for your generated configuration?

Configuration
format (annotation, yml, xml, php) [annotation]: annotation

Se crea el directorio src/PruebaBundle con el código esqueleto del bundle.

El comando actualiza el fichero app/config.yml para tener en cuenta el archivo de configuración de servicios del bundle y el app/routing.yml para tener en cuenta las rutas del bundle. Pero hay que actualizar a mano el autoloading en composer.json:

...
"autoload": {
           
"psr-4": {
               
"AppBundle\\": "src/AppBundle",
               
"PruebaBundle\\": "src/PruebaBundle",
               
"Acme\\": "src/Acme"
           },
...

y ejecutar:

# composer.bat dump-autoload

Aún así hay un error en la carga de las plantillas. Resulta que el comando genera las plantillas en el propio directorio del bundle. Para que puedan cargarse en este directorio

, en lugar del directorio por defecto:

Hay que:

  1. Registrar el path de las plantillas en el bundle de twig:

twig:
        
debug: '%kernel.debug%'
        strict_variables:
'%kernel.debug%'
        paths:
'%kernel.project_dir%/src/PruebaBundle/Resources/views': PruebaBundle

  1. Cambiar la referencia a la plantilla que se hace en el controlador creado por el comando (esto es un bug del comando). Hay que poner:

return $this->render('@PruebaBundle/Default/index.html.twig');

en  lugar de:

return $this->render('PruebaBundle:Default.index.html.twig');

9.3 Instalación de bundles externos

9.4 Traducciones

Symfony proporciona un servicio para la internacionalización de las aplicaciones. Su uso implica los siguientes pasos:

La definición del idioma (locale) se hace:

  1. Por configuración en app/config.yml:

framework:
        
translator: { fallbacks: ['es'] }

  1. Usando el placeholder _locale en la ruta de la acción

/**
* @Route(
"/key/{_locale}", name="key")
*
/

  1. Mediante un listener de kernel.request que se dispare antes que

 Symfony\Component\HttpKernel\EventListener\TranslatorListener::onKernelRequest()

usando el método setLocale del objeto request:

$request->setLocale('es');

9.5 Testing

La herramienta más potente en  PHP para realizar tests es phpunit. Y es esa precisamente la que se recomienda usar con symfony. De hecho una instalación del framework viene preparada para usar phpunit por defecto.

La configuración de phpunit se hace en el archivo phpunit.xml.dist ubicado en el directorio raíz del proyecto y se carga desde el autoloading vía vendor/autoload.php.

Los tests se colocan en la carpeta test que cuelga directamente del directorio raíz. Y, por convención, el testeo de una clase se hace en una estructura de directorio idéntica a la de la clase y que cuelga del directorio test.

Para ejecutar los test:

# ./vendor/bin/simple-phpunit

Si queremos un informe sobre la cobertura de los tests:

# ./vendor/bin/simple-phpunit  --coverage-html <dir>
# ./vendor/bin/simple-phpunit --coverage-text=<file>
# ./vendor/bin/simple-phpunit --coverage-xml <file>

Dependiendo del formato en que queramos el informe.

Por defecto los test se deben hacer en la carpeta tests que cuelga directamente de la raíz del proyecto, siguiendo la misma estructura de directorio que el archivo que deseamos testear. Aunque esto se puede cambiar en el archivo phpunit.xml.dist.

9.6 Logging

Cualquier aplicación que dé servicio en un entorno de producción real debe llevar un registro temporal (log) de las cosas importantes que están sucediendo durante su ejecución, especialmente los errores. Los procesos del núcleo de symfony y de todos los bundles (que están bien desarrollados) vuelcan por sí solos estos logs al directorio /var/log. Cada entorno de ejecución tiene su propio fichero de log.

El código de nuestras aplicaciones también debería registrar los eventos importante y errores que suceden cuando se ejecuta. Para ello symfony ofrece el servicio ogger, el cual se obtiene directamente del contenedor de servicios:

        $logger = $this->get('logger');