CursoSymfony2

262
Desarrollo de aplicaciones web con Symfony2 version 1.0 Juan David Rodríguez García 16 de Julio de 2012

Transcript of CursoSymfony2

Page 1: CursoSymfony2

Desarrollo de aplicaciones web conSymfony2version 1.0

Juan David Rodríguez García

16 de Julio de 2012

Page 2: CursoSymfony2
Page 3: CursoSymfony2

ContenidoCurso: Desarrollo de Aplicaciones Web con Symfony2 1

Licencia 2Unidad 1. Inmersión 3

Aplicaciones web 3Desarrollo rápido y de calidad de aplicaciones web 4Presentación del curso 5

A quién va dirigido 5Objetivos del curso 6Plan del curso 7

Sobre Symfony2 8La documentación de Symfony2 8Comentarios 9

Unidad 2: Desarrollo de una aplicación web siguiendo el patrón MVC 10El patrón MVC en el desarrollo de aplicaciones web 10Descripción de la aplicación 11Diseño de la aplicación (I). Organización de los archivos 12Diseño de la aplicación (II). El controlador frontal 12Construcción de la aplicación. Vamos al lío. 13Creación de la estructura de directorios 13El controlador frontal y el mapeo de rutas 14Las acciones del Controlador. La clase Controller. 16La implementación de la Vista. 19

Las plantillas PHP 19El layout y el proceso de decoración de plantillas 20

El Modelo. Accediendo a la base de datos 24La configuración de la aplicación 26Incorporar las CSS's 27La base de datos 27Comentarios 28

Unidad 3: Symfony2 a vista de pájaro 29¿Qué es Symfony2? 29Instalación y configuración de Symfony2 30

El directorio web 32El directorio app 33El directorio vendor 34El directorio src 35

Page 4: CursoSymfony2

El directorio bin 35Los Bundles: Plugins de primera clase 35La aplicación gestión de alimentos en Symfony2 36

Generación de un Bundle 36Anatomía de un Bundle 38Flujo básico de creación de páginas en Symfony2 40Definición de las rutas del bundle 41Creación de la acción en el controlador 43Creación de la plantilla 45Decoración de la plantilla con un layout 46Instalación de los assets de un bundle 50Implementamos el resto de la aplicación 51

La unidad en chuletas 61Generar un bundle 61Registrar un bundle 61Enlazar el routing de un bundle con el routing general de laaplicación

62

Pasos para acoplar un bundle al framework 62Flujo para la creación de páginas en Symfony2 62Nombres lógicos de acciones 62Sintaxis básica de twig 62Herencia en plantilla twig 62Función path de twig 63Iterar una colección (array) de datoso en twig 63Código condicional en twig 63Inclusión de plantillas en twig 63Estructura básica de una ruta 63

Comentarios 63Unidad 4: Inyección de Dependencias 64

Primer paso: inyección de dependencias 64Segundo paso: el contenedor de dependencias 65Integración de la clase Model como un servicio de Symfony2 67El Contenedor de Servicios de Symfony2 68¿Y qué hemos ganado con todo esto? 71¡Servicios al poder! 74Más servicios aún 77La unidad en chuletas 78Comentarios 79

Page 5: CursoSymfony2

Unidad 5: Desarrollo de la aplicación MentorNotas (I). Análisis 80Descripción de la aplicación 80Descripción general 81Catálogo de requisitos 81

Requisitos del frontend 81Requisitos del backend 82

Gestión de usuarios 82Gestión de publicidad 82Gestión de planes de pagos 82

Modelo de datos 83Descripción de los procesos 83

Registro de nuevos usuarios 83Creación de una nota 83Contratación de una cuenta premium 84Presentación de la publicidad 84

Interfaz de usuario 84Pantalla de login 84Pantalla de registro 84Pantalla principal (inicio) 85Pantalla de creación de notas 85Pantalla de modificación de notas 85Pantalla de planes de pago 86Menú de la aplicación de administración 86Gestión de entidades 86

Listado de usuarios 86Creación de usuarios 87Edición de usuarios 87

Recursos para la construcción de la aplicación 87Conclusión 88Comentarios 88

Unidad 6: Desarrollo de la aplicación MentorNotas (II). Rutas yControladores

89

Lo primero: el bundle 89Definimos las rutas y sus acciones asociadas 91

Diseño de la lógica de control para las acciones del controladorNotasController

93

Diseño de la lógica de control para las acciones del controladorLoginController

94

Page 6: CursoSymfony2

Diseño de la lógica de control para las acciones del controladorContratosController

94

Implemetación de la lógica de control del controladorNotasController

94

La acción indexAction() 100La acción nuevaAction 100La acción editarAction 101La acción borrarAction() 102Las acciones miEspacioAction() y rssAction 102

Implemetación de las plantillas del controlador NotasController 102La unidad en chuletas 105

Requerimientos en el Routing 105Generando rutas 106Servicio Request 106Servicio Session 106Ampliando el código de un bloque heredado en una plantilla twig 107Generando redirecciones desde un controlador 107Haciendo forwarding a otra acción 107

Comentarios 107Unidad 7: Desarrollo de la aplicación MentorNotas (III). El modelo y lapersistencia de datos.

108

El Object Relational Mapping (ORM) 108Las entidades 109Construcción de las entidades. El generador de entidades deSymfony2

111

Creación de la base de datos 118El servicio de persistencia Doctrine2 119Refinamos el modelo. Las asociaciones entre objetos 125

La relación One to Many 126La relación Many to One 127La relación Many to Many 127

Más allá de los métodos findBy. El lenguaje DQL 132Organizamos las consultas en repositorios 135Incorporamos el modelo a la aplicación 138La unidad en chuletas 139

Construir entidades 139Configuración de la base de datos 139Crear la base de datos y las tablas 139Persistir un objeto en la base de datos 140

Page 7: CursoSymfony2

Recuperar objetos con métodos find 140Borrar un objeto 140Especificación de las relaciones entre entidades (forma simple ypráctica)

141

Implementación de un método de repositorio 142Comentarios 142

Unidad 8: Desarrollo de la aplicación MentorNotas (IV). Validación yFormularios

143

El servicio de validación 143El servicio de formularios 149

Formularios definidos en la acción 150Definición de tipos para crear formularios 154Validación de formularios 156

La unidad en chuletas 160El servicio de validación 160El servicio de formularios 161Método para la validación de formularios. 163

Comentarios 164Unidad 9: Desarrollo de la aplicación MentorNotas (V). Seguridad -Autentificación y Autorización

165

Componentes y funcionamiento del servicio de seguridad deSymfony2.

165

Los proveedores de usuarios (user providers) 166Los cortafuegos (firewalls) 166El control de acceso (access control) y la jerarquía de roles (rolehierarchy)

167

Los codificadores (encoders) 168La seguridad en acción 168El proveedor de usuarios in memory 169Autentificación 170

Autentificar con HTTP Basic 171Autentificar con un formulario de Login tradicional 173Saliendo de la sesión (logout) 176

Autorización 177Protegiendo los recursos de la aplicación 177Protegiendo los controladores y plantillas de la aplicación 178Exigiendo canal seguro 179

La base de datos como proveedor de usuarios 179El servicio de codificación. Los encoders 184

Page 8: CursoSymfony2

Recuperando el objeto Usuario 185La unidad en chuletas 186

Users providers 186Cortafuegos 187Autorización 188Encoders 188

Comentarios 189Unidad 10: Desarrollo de la aplicación MentorNotas (VI). Esamblando todoel frontend

190

La eclosión de la crisálida 190Primero los activos (assets) 191Después las plantillas 191Adaptación de las pantallas del panel de notas 193El layout del panel de notas 193

Las acciones del panel de notas 196El proceso de registro 207El resto de la aplicación. 214

Unidad 11: Desarrollo de la aplicación MentorNotas (VII). Desarrollo delbackend

215

Estrategias de desarrollo para la parte de administración 215El generador de módulos CRUD de Symfony2 216El generador de módulos CRUD SonataAdminBundle 217

Instalación del SonataAdminBundle 217Inyección de los módulos de administración 221Entidad Contrato 223Entidad Grupo 225Entidad Publicidad 226Entidad Usuario 230

Ejercicios del Curso: Desarrollo de Aplicaciones Web con Symfony2 235Ejercicios de la unidad 2 235

Ejercicio 1 235Ejercicio 2 235Ejercicio 3 235Ejercicio 4 235Ejercicio 5 235Ejercicio 6 235

Ejercicios de la unidad 3 235Ejercicio 1 236

Page 9: CursoSymfony2

Ejercicio 2 236Ejercicio 3 236Ejercicio 4 237Ejercicio 5 237

Ejercicios de la unidad 4 237Ejercicio 1 237Ejercicio 2 237

Ejercicios de la unidad 5 238Ejercicio 1 238Ejercicio 2 238Ejercicio 3 238Ejercicio 4 238

Ejercicios de la unidad 6 239Ejercicio 1 239Ejercicio 2 239Ejercicio 3 241

Ejercicios de la unidad 7 242Ejercicio 1. Fixtures 242

Instalación del bundle DoctrineFixturesBundle 242Creación de los fixtures 244Instalación manual del bundle (sin utilizar bin/vendor) 248

Ejercicio 2 248Ejercicio 3 248

Ejercicios de la unidad 9 248Ejercicio 1 249Ejercicio 2 249Ejercicio 3 249Ejercicio 4 249

Ejercicios de la unidad 10 249Ejercicio 1 250Ejercicio 2 250Ejercicio 3 250

Indices y tablas 251Indices y tablas 252

Page 10: CursoSymfony2
Page 11: CursoSymfony2

Curso: Desarrollo de Aplicaciones Web conSymfony2Contenidos:

Curso: Desarrollo de Aplicaciones Web con Symfony2

1

Page 12: CursoSymfony2

Licencia

Este trabajo:Desarrollo de Aplicaciones web con Symfony2

ha sido elaborado por Juan David Rodríguez García ([email protected])y se encuentra bajo una Licencia:Creative Commons Reconocimiento-NoComercial-CompartirIgual 3.0 Unported.Autor del código: Juan David Rodríguez García <[email protected]>

Licencia

2

Page 13: CursoSymfony2

Unidad 1. InmersiónAplicaciones webLa World Wide Web es un sistema de documentos de hipertexto o hipermedios enlazados ydistribuidos a través de Internet. En sus comienzos, la interacción entre los usuarios de laWWW y sus servidores era muy reducida: a través de un software cliente denominadonavegador web, el usuario se limitaba a solicitar documentos a los servidores y estosrespondían a aquellos con el envío del documento solicitado. A estos documentos quecirculaban por el “espacio web” se les denominó páginas web. Cada recurso, conocido comopágina web, se localiza en este espacio mediante una dirección única que describe tanto alservidor como al recurso: la URL. Cada página web puede incorporar las URL's de otraspáginas como parte de su contenido. De esta manera se enlazan unas con otras.Han pasado casi 20 años desde la aparición de la Word Wide Web y, aunque en esencia sufuncionamiento, basado en el protocolo HTTP, sigue siendo el mismo, la capacidad deinteracción entre usuarios y servidores se ha enriquecido sustancialmente. De la página webhemos pasado a la aplicación web; un tipo de aplicación informática que adopta, de maneranatural, la arquitectura cliente-servidor de la web. De manera que en las peticiones alservidor, el usuario no sólo solicita un recurso, si no que además puede enviar datos. Elservidor los procesa y elabora la respuesta que corresponda en función de ellos. Es decir, elservidor construye dinámicamente la página web que le envía al cliente.Todo el peso de la aplicación reside en el servidor, mientras que el cliente, esto es, elnavegador web, se limita a presentar el contenido que recibe mostrándolo al usuario.Esta evolución comenzó con la aparición de los CGI's, que son aplicaciones escritas encualquier lenguaje de programación y que pueden ser accedidas por el servidor web apetición del cliente, y ha madurado gracias a la aparición de los lenguajes de programacióndel lado del servidor, como PHP, Java o Python, gracias a los cuales los servidores web(apache como ejemplo más conocido y usado) han ampliado su funcionalidades; ya no sóloson capaces de buscar, encontrar y enviar documentos a petición del cliente, si no quetambién pueden procesar peticiones (acceder a base de datos, realizar peticiones a otrosservidores, ejecutar algoritmos, …) y construir los documentos que finalmente seránenviados al cliente en función de los datos que este les ha proporcionado.También es relevante en esta evolución de la web la incorporación de procesamiento en losnavegadores web mediante lenguajes de “scripting” como javascript, que permiten laejecución de ciertos procesos (casi todos relacionados con la manipulación de la interfazgráfica) en el lado del cliente. De hecho, en la actualidad existen aplicaciones que delegangran parte de sus procesos al lado del cliente, aunque de todas formas, todo el código esproporcionado desde la parte servidora la primera vez que se solicita el recurso.Todo esto ha sido bautizado con el omnipresente y manido término de Web 2.0, que enrealidad es una manera de referirse a este aumento de la capacidad de interacción con elusuario, y que ha permitido el desarrollo y explosión de las redes sociales y la blogosferaentre otros muchos fenómenos de la reducida pero incesante historia de la World Wide Web.El panorama actual se resume en un interés creciente por las aplicaciones web, hasta elpunto de que, en muchos casos, han desplazado a la madura aplicación de escritorio. Sonvarias las razones que justifican este hecho y, aunque se trata de un tema que por suamplitud no abordaremos en detalle, si que señalaremos algunas:

• Se mejora la mantenibilidad de las aplicaciones gracias a su centralización. Al residir laaplicación en el servidor, desaparece el problema de la distribución de las mismas. Porejemplo, los cambios en la interfaz de usuario son realizado una sola vez en el servidory tienen efecto inmediatamente en todos los clientes.

Unidad 1. Inmersión

3

Page 14: CursoSymfony2

• Se aumenta la capacidad de interacción y comunicación entre los usuarios de la misma,así como de su gestión.

• Al ser HTTP un protocolo de comunicación ligero y “sin conexión” (conectionless) seevita mantener conexiones abiertas con todos y cada uno de sus clientes, mejorando laeficiencia de los servidores.

• Para utilizar la aplicación, los usuarios tan solo necesitan tener instalado un softwaredenominado navegador web (browser). Esto reduce drásticamente los problemas deportabilidad y distribución. También permite que terminales ligeras, con poca capacidadde proceso, puedan utilizar “grandes” aplicaciones ya que su función se limita a mostrarmediante el navegador los datos que le han sido enviado.

• El desarrollo de dispositivos móviles con conectividad a redes expande el dominio deuso de las aplicaciones web y abre nuevos mercados.

• Se puede acceder a la aplicación desde cualquier punto con acceso a la red dondepreste servicio la aplicación. Si se trata de Internet, desde cualquier parte del mundo, sise trata de una intranet desde cualquier parte del mundo con acceso a la misma. Todoello sin necesidad de instalar nada más que el navegador en la computadora cliente(punto anterior).

• Los lenguajes utilizados para construir las aplicaciones web son relativamente fáciles deaprender. Además algunos de los más utilizados ,como PHP y Python, se distribuyen conlicencias libres y existe una gran cantidad de documentación de calidad disponible en lapropia red Internet.

• Recientemente han aparecido en escena varias plataformas y frameworks de desarrollo(por ejemplo Zend Framework, CakePHP, symfony, Symfony2) que facilitan laconstrucción de las aplicaciones web, reduciendo el tiempo de desarrollo y mejorando lacalidad.

Obviamente no todo son ventajas; incluso algunas de las ventajas que hemos señaladopueden convertirse en desventajas desde otros puntos de vista. Por ejemplo:

• el hecho de que los cambios realizados en una aplicación web sean efectivosinmediatamente en todos los clientes que la usan, puede dejar sin servicio a un grannúmero de usuarios si este cambio provoca un fallo (intencionado si se trata de unataque, o no intencionado si se trata de una modificación que no ha sido debidamenteprobada). Esto repercute en la necesidad de aumentar las precauciones y la seguridadpor parte de los responsables técnicos que mantienen la aplicación.

• La disponibilidad de la aplicación es completamente dependiente de la disponibilidad dela red. Así la aplicación web será útil en entornos donde se garantice la estabilidad de lared. Los programadores necesitan dominar las distintas tecnologías y conceptos que, enestrecha colaboración, conforman la aplicación (HTTP, HTML, XML, CSS, javascript,lenguajes de scripting del lado del servidor como PHP, Java o Python, …)

• La triste realidad de las incompatibilidades entre navegadores.No obstante, la realidad demuestra que el interés por las aplicaciones web es un hechoconsumado, lo cual seduce a los programadores de todo el mundo a formarse en lastecnologías y estrategias que permiten desarrollarlas. Este curso tiene como objetivopresentar una de las más exitosas: el desarrollo de aplicaciones web mediante el uso delframework Symfony2.

Desarrollo rápido y de calidad de aplicaciones webLa experiencia adquirida tras muchos años de construcción de aplicaciones informáticas de escritorio, dio lugar a la aparición de entornos y frameworks de desarrollo que no solo hacían posible construir rápidamente las aplicaciones, si no que además cuidaban la calidad de las

Desarrollo rápido y de calidad de aplicaciones web

4

Page 15: CursoSymfony2

mismas. Es lo que técnicamente se conoce como Desarrollo Rápido de Aplicaciones.Sin embargo, el desarrollo de aplicaciones web es muy reciente, por lo que estasherramientas de desarrollo rápido y de calidad no han aparecido en el mundo de la webhasta hace bien poco. De hecho la construcción de una aplicación web de calidad no haestado exenta de dificultades debido a esta carencia. Afortunadamente contamos desdehace unos pocos años con frameworks de desarrollo de aplicaciones web que facilitan eldesarrollo de las mismas y están haciendo que el concepto de Desarrollo Rápido deAplicaciones en este campo sea una realidad. Symfony2 representa una de las herramientasde más éxito para la construcción de aplicaciones web de calidad con PHP.Desarrollar con Symfony2 hace más sencillo la construcción de aplicaciones web quesatisfagan las siguientes características, deseables en cualquier tipo de aplicacióninformática profesional al margen de sus requisitos específicos.

• Fiabilidad. La aplicación debe responder de forma que sus resultados sean correctos ypodamos fiarnos de ellos. También implica que los datos que introducimos comoentrada sean debidamente validados para asegurar un comportamiento correcto.

• Seguridad. La aplicación debe garantizar la confidencialidad y el acceso a la misma ausuarios debidamente autentificados y autorizados. En el caso de las aplicaciones webesto es especialmente importante puesto que residen en computadores que, alpertenecer a una red, son accesibles a una gran cantidad de personas. Lo que significaque inevitablemente están expuestas a ser atacadas con fines maliciosos. Por ellodeben incorporar mecanismos de protección ante conocidas técnicas de ataque webcomo puede ser el Cross Site Scripting (XSS).

• Disponibilidad. La aplicación debe prestar servicio cuando se le solicite. Es importante,por tanto, que los cambios requeridos por operaciones relacionadas con elmantenimiento (actualizaciones, migraciones de datos, migraciones de la aplicación aotros servidores, etcétera) sean sencillos de controlar. De esa manera se evitarán largastemporadas de inactividad. La disponibilidad es una de las características más valoradasen las aplicaciones web, ya que el funcionamiento de la misma no depende, por logeneral, de sus usuarios si no de los responsables técnicos del sistema donde seencuentre alojada. Hay que pensar en ellos y ponérselo fácil cuando necesiten realizareste tipo de tarea. También es importante que los errores de funcionamiento debidos aerrores de programación (bugs) sean rápidamente diagnosticados y resueltos paramejorar tanto la disponibilidad como la fiabilidad de la aplicación.

• Mantenibilidad. A medida que se usa una aplicación, aparecen nuevos requisitos yfuncionalidades que se desean ofrecer. Un sistema mantenible permite ser extendidosin que ello suponga un coste muy alto, minimizando la probabilidad de introducirerrores en los aspectos que ya estaban funcionando antes de emprender laimplementación de nuevas característcas.

• Escalabilidad, es decir, que la aplicación pueda ampliarse sin perder calidad en losservicios ofrecidos, lo cual se consigue diseñándola de manera que sea flexible ymodular.

Presentación del curso

A quién va dirigidoEste curso va dirigido a personas que ya cuenten con cierta experiencia en la programaciónde aplicaciones web. A pesar de que Symfony2 está construido sobre PHP, no es tanimportante conocer dicho lenguaje como estar familiarizado con las tecnologías de la web ycon el paradigma de la programación orientada a objetos.

Presentación del curso

5

Page 16: CursoSymfony2

En la confección del curso hemos supuesto que el estudiante comprende los fundamentos delas tecnologías que componen las aplicaciones web y las relaciones que existen entre ellas:

• El protocolo HTTP y los servidores web,• Los lenguajes de marcado HTML y XML,• Las hojas de estilo CSS's,• Javascript como lenguaje de script del lado del cliente,• Los lenguajes de script del lado del servidor (PHP fundamentalmente),• Los fundamentos de la programación orientada a objetos (mejor con PHP),• Los fundamentos de las bases de datos relacionales y los sistemas gestores de base de

datos.Obviamente, para seguir el curso, no hay que ser un experto en cada uno de estastecnologías, pero sí es importante conocerlas hasta el punto de saber cual es el papel quedesempeña cada una y como se relacionan entre sí. Cualquier persona que hayadesarrollado alguna aplicación web mediante el archiconocido entorno LAMP o WAMP(Linux/Windows – Apache – MySQL – PHP), debería tener los conocimientos necesarios paraseguir con provecho este curso.

Objetivos del cursoCuando finalices el curso habrás adquirido suficiente conocimiento para desarrollaraplicaciones web mediante el empleo del framework de desarrollo en PHP Symfony2. Ellosignifica a grandes rasgos que serás capaz de construir aplicaciones web que:

• Son altamente modulares, extensibles y escalables.• Separan claramente la lógica de negocio de la presentación, permitiendo que el trabajo

de programación y de diseño puedan realizarse independientemente.• Incorporaran un sistema sencillo, flexible y robusto garantizar la seguridad a los niveles

de autentificación y autorización.• Acceden a las bases de datos a través de una capa de abstracción que permite cambiar

de sistema gestor de base de datos sin más que cambiar un parámetro deconfiguración. No es necesario tocar ni una sola línea de código para ello.

• Cuentan con un flexible sistema de configuración mediante el que se puede cambiargran parte del comportamiento de la aplicación sin tocar nada de código. Esto permite,entre otras cosas, que se puedan ejecutar en distintos entornos: de producción, dedesarrollo y de pruebas, según la fase en la que se encuentre la aplicación.

• Pueden ofrecer el resultado final en varios formatos distintos (HTML, XML, JSON, RSS,txt, …) gracias al avanzado sistema de generación de vistas,

• Cuentan con un potente sistema de gestión de errores y excepciones, especialmenteútil en el entorno de desarrollo.

• Implementan un sistema de caché que disminuye los tiempos de ejecución de losscripts.

• Incorpora por defecto mecanismos de seguridad contra ataques XSS y CSRF.• Pueden ser internacionalizadas con facilidad, aunque la aplicación no se haya

desarrollado con la internacionalización como requisito.• Incorporan un sistema de enrutamiento que proporciona URL's limpias, compuestas

exclusivamente por rutas que ocultan detalles sobre la estructura de la aplicación.

Objetivos del curso

6

Page 17: CursoSymfony2

• Cuentan con un avanzado sistema de autentificación y autorización.El curso cubre una porción suficientemente completa sobre las múltiples posibilidades queofrece Symfony2 para desarrollar aplicaciones web, incidiendo en sus características yherramientas más fundamentales. El objetivo es que, al final del curso, te sientas cómodousando Symfony2 y te resulte sencillo y estimulante continuar profundizando en elframework a medida que tu trabajo lo sugiera.Cuando emprendas el estudio de este curso, debes tener en cuenta que el aprendizaje decualquier framework de desarrollo de aplicaciones, y Symfony2 no es una excepción, esbastante duro en los inicios. Sin embargo merece la pena, pues a medida que se vanasimilando los conceptos y procedimientos propios del framework, la productividad aumentamuchísimo.

Plan del cursoPara conseguir los objetivos que nos hemos propuesto hemos optado por un planteamientocompletamente práctico en el que se está “picando código” funcional desde el principio delcurso.En la unidad 2, sin utilizar Symfony2 para nada, desarrollamos una sencilla aplicación weben PHP. El objetivo de esta unidad es mostrar como se puede organizar el código para quesiga los planteamientos del patrón de diseño Modelo – Vista – Controlador (MVC), gracias alcual separamos completamente la lógica de negocio de la presentación de la información. Esimportante comprender los fundamentos de esta organización ya que las aplicacionesdesarrolladas con Symfony2 suelen seguir este patrón. Además en esta unidad se introducenlos conceptos de controlador frontal, acción, plantilla y layout, ampliamente usados en elresto del curso.En la unidad 3 hacemos una presentación panorámica de Symfony2, exponiendo losconceptos fundamentales. En esta unidad volveremos a escribir, esta vez utilizandoSymfony2, la aplicación de la unidad 2. Dicho ejercicio nos ayudará a realizar la presentacióndel framework a la vez que servirá como referencia para los conceptos básicos. Avisamos:esta unidad es bastante densa.La unidad 4 la hemos dedicado completamente al estudio de un importante patrón dediseño en torno al cual se ha construido Symfony2: La inyección de dependencias. Es muyimportante comprender este concepto para sentirse cómodo con Symfony2 y para poderextender el framework con soltura.En la unidad 5 planteamos el análisis de una aplicación, que aún siendo concebida concriterios pedagógicos, es suficientemente amplia como para ser considerada una aplicaciónprofesional. Se trata de de una aplicación para la gestión de notas (al estilo de los post-it)inspirada en EverNote® una herramienta que últimamente está teniendo mucho éxito entrelos usuarios de plataformas móviles (smartphones y tabletas). Su desarrollo nos servirácomo vehículo para penetrar al interior de Symfony2 durante el resto del curso.En las siguientes unidades se construyen progresivamente las distintas funcionalidades de laaplicación analizada en la unidad 5. Cada unidad incide sobre algún aspecto fundamental deSymfony2.En la unidad 6 profundizamos en el concepto de routing y los controladores, conceptosfundamentales sobre los que descansa la lógica de las aplicaciones construidas conSymfony2.En la unidad 7 se realiza un estudio bastante detallado del ORM Doctrine2 para eltratamiento de los datos persistentes, es decir, para el acceso a bases de datos.Adelantamos aquí que, a pesar de cubrir los aspectos fundamentales para el desarrollo casicualquier tipo de aplicación, Doctrine2 va mucho más allá. Un tratamiento completo de estemagnifico ORM requiere un curso dedicado al mismo.

Plan del curso

7

Page 18: CursoSymfony2

La unidad 8 está dedicada a los servicios de validación de datos y de creación deformularios HTML, herramienta fundamental en cualquier aplicación web.En la unidad 9 estudiamos el novedoso sistema de seguridad de Symfony2, mediante elcual podemos proteger nuestras aplicaciones web en los niveles de autentificación yautorización sin necesidad de picar demasiado código.La unidad 10 integra todos los conocimientos acumulados a lo largo del curso para darforma definitiva a la aplicación de gestión de notas. Aquí incorporamos las plantillas, estilosenriquecidos con javascript (jQuery) y perfilamos los flecos que se nos han ido quedando enlas unidades anteriores.Por último en la unidad 11 explicaremos las distintas estrategias que podemos seguir paradesarrollar el backend, o parte de administración, de las aplicaciones web. Utilizaremos unconocido y potente bundle (los plugins de Symfony2) con el que se pueden construirelegantes y prácticas aplicaciones de backend sin necesidad de picar mucho código.

Sobre Symfony2La primera versión estable de Symfony2 ha sido desarrollada en un tiempo record de 448días. El primer commit tiene fecha de 3/6/2010, mientras que el último, correspondiente a laversión 2.0.1, es del 25/08/2011. Al extraordinario talento del equipo liderado por FabienPotencier, lider del proyecto, se une los más de 6 años de experiencia acumulada durante eldesarrollo de la primera versión de este producto. El resultado ha sido doble; por una partehan construido un conjunto de componentes que actúan como piezas de un Lego para lasolución de problemas relacionados con la web y, por otro, han elaborado un framework parael desarrollo de aplicaciones web utilizando como pilares tales componentes. Por eso cuandose habla de Symfony2 debemos interpretar, en función del contexto como con toda palabrapolisémica, si se hace referencia a los componentes o al framework. Este curso va de losegundo.Symfony 1, el antecesor de Symfony2, ha sido y sigue siendo un framework líder en elmundo PHP. Prueba de ello es la amplia y activa comunidad que lo desarrolla y que lo utiliza.El número de aplicaciones registradas en el trac5 del proyecto también da una idea de laconfianza que muchos desarrolladores han depositado en symfony 1. Pues bien, un análisisde la situación actual parece indicar que Symfony2 va camino de destronar a su antecesor(si no lo ha hecho ya). La actividad del proyecto, que puede seguirse públicamente en surepositorio de github (https://github.com/symfony/symfony) y la proliferación de sitios sobreSymfony2 soportan esta observación. Otro indicativo del éxito que está teniendo esta nuevaversión, es el hecho de que los desarrolladores de Drupal, han decidido utilizar varios de loscomponentes de Symfony2 para el desarrollo de la versión 8 de su producto.

La documentación de Symfony2Posiblemente una de las características más apreciadas de symfony 1 fue la cantidad y lacalidad de documentación oficial que existe. Ello proporciona la tranquilidad de saber queprácticamente cualquier problema que se le presente al programador estará resuelto, o almenos estará resuelto algo parecido, en alguno de los muchos documentos que sobresymfony 1 se han escrito.También en Symfony2, no podía ser de otra forma, se ha prestado mucha atención a lacalidad de la documentación, aunque por el momento únicamente se encuentra en inglés eitaliano. El documento imprescindible es el libro oficial(http://symfony.com/doc/current/book/index.html), que está complementado por el libro derecetas (http://symfony.com/doc/current/cookbook/index.html), una guía de referencia conlas principales opciones de configuración(http://symfony.com/doc/current/reference/index.html), algunos videos demostrativos, y ladocumentación de la API (http://api.symfony.com/2.0/).

Sobre Symfony2

8

Page 19: CursoSymfony2

Todo ello lo encontrarás en el sitio oficial de Symfony2 (http://symfony.com/). Si finalmenteterminas atrapado en las redes de Symfony2 no te quepa duda de que se convertirá en unade tus herramientas imprescindibles. Esperamos que este curso también se encuentre entreellas.Como ya hicimos con el curso "Desarrollo de aplicaciones web con symfony", hemosdesarrollado el texto desde una perspectiva más pedagógica que técnica, ya que es en aquelaspecto donde la documentación oficial de Symfony2 es más endeble. Este curso supone unapoyo pedagógico para aprender a desarrollar aplicaciones web con Symfony2, y nopretende ni sustituir ni desdeñar la documentación oficial. Muy al contrario creemos, comoya hemos señalado, que dicha documentación es muy valiosa y debe formar parte delequipo de recursos necesarios para desarrollar con Symfony2.

ComentariosAquí puedes enviar comentarios, dudas y sugerencias. Utiliza la barra de scroll interna pararecorrer todos los mensajes. El formulario de envío se encuentra al final.Autor del código: Juan David Rodríguez García <[email protected]>

Comentarios

9

Page 20: CursoSymfony2

Unidad 2: Desarrollo de una aplicación web siguiendoel patrón MVCEl patrón MVC en el desarrollo de aplicaciones webMuchos de los problemas que aparecen en la ingeniería del software son similares en suestructura. Y, por tanto, se resuelven de manera parecida. A lo largo de la historia de estadisciplina se han elaborado un buen número de esquemas resolutivos que son conocidos conel nombre des patrones de diseño 1 y cuyo conocimiento y aplicación son de una inestimábleayuda a la hora de diseñar y construir una aplicación informática.Posiblemente uno de los más conocidos y utilizados sea el patrón "Modelo, Vista,Controlador" (MVC), que propone organizar una aplicación en tres partes bien diferenciadasy débilmente acopladas entre sí, de manera que los cambios que se produzcan en una noafecten demasiado a las otras (idealmente nada). El nombre del patrón enumera cada unade las partes:

• El Controlador. En este artefacto se incluye todo lo referente a la lógica de control dela aplicación, que no tiene nada que ver con las características propias del negocio parael que se está construyendo la aplicación. En el caso de una aplicación web, un ejemplosería la manipulación de la request HTTP.

• El Modelo. Donde se implementa todo lo relativo a la lógica de negocio, es decir, losaspectos particulares del problema que la aplicación resuelve. Si, por ejemplo estamosdesarrollando un blog, un ejemplo sería una librería de funciones para la gestión de loscomentarios.

• La Vista. Aquí se ubica el código encargado de "pintar" el resultado de los procesos dela aplicación. En una aplicación web la vista se encarga de producir documentos HTML,XML, JSON, etcétera, con los datos que se hayan calculado previamente en la aplicación.

Para que el conjunto funcione, las partes deben interaccionar entre sí. Y en este puntoencontramos en la literatura distintas soluciones. La que proponemos en este curso es lamostrada en la siguiente figura:

Diagrama del modelo MVC

Unidad 2: Desarrollo de una aplicación web siguiendo el patrón MVC

10

Page 21: CursoSymfony2

El controlador recibe la orden de entrada y se encarga de procesarla utilizando, si es preciso,los servicios del modelo para ello. Una vez que ha realizado el cálculo entrega los datos"crudos" a la vista y esta se encarga de decorarlos adecuadamente. La característica másimportante de esta solución es que la vista nunca interacciona con el modelo.Las aplicaciones web más típicas pueden plantearse según este patrón: el controlador recibeuna petición HTTP y la procesa, haciendo uso del modelo calcula los datos de salida y losentrega a la vista, la cual se encarga de construir una respuesta HTTP con las cabecerasadecuadas y un payload o cuerpo de la respuesta que suele ser un contenido HTML, XML oJSON.Aunque no todas las aplicaciones web se pueden ajustar a este modelo, si es cierto que laidea de separar responsabilidades o las distintas areas de un problema en sistemasdébilmente acoplados, es una estrategia común en las metodología que utilizan elparadigma de programación orientado a objetos. Es lo que se conoce en la terminologíaanglosajona como Separation Of Concerns 2.En esta unidad vamos a plantear y desarrollar una sencilla aplicación web utilizando comoguía de diseño este patrón. De esta manera ilustraremos sus ventajas y nos servirá comomaterial introductorio a la arquitectura de Symfony2. Llegados a este punto hemos deindicar que Fabien Potencier, lider del proyecto, declara que Symfony2, o mejor dicho, laedición standard del framework construido con los componentes de Symfony2, no es unframework MVC, ya que no trata para nada del modelo y deja al programador absolutalibertad para incoporar las librerías que más le convenga. En nuestra humilde opinión,creemos que esta apreciación es el resultado de "hilar muy fino", y que, en términosprácticos, Symfony2 se ajusta al patrón MVC. De hecho, en su edición standard, incorporaDoctrine como ORM. Y aunque es cierto que Doctrine es un proyecto autónomo y externo aSymfony2, no deja de ser un servicio prestado por el framework para la construcción delModelo. Realmente este tipo de discusiones son interesante y nos ayudan a ejercitar nuestramente, pero no creemos que sean vitales para aprender a utilizar el framework.

Nota

En http://fabien.potencier.org/article/49/what-is-symfony2, puedes leer un interesanteartículo de Fabien Potencier en el que describe lo que es Symfony2 y trata su punto devista acerca de estos detalles.

Descripción de la aplicaciónVamos a construir una aplicación web para elaborar y consultar un repositorio de alimentoscon datos acerca de sus propiedades dietéticas. Utilizaremos una base de datos paraalmacenar dichos datos que consistirá en una sola tabla con la siguiente información sobrealimentos:

• El nombre del alimento,• la energía en kilocalorías ,• la cantidad de proteínas,• la cantidad hidratos de carbono en gramos• la cantidad de fibra en gramos y• la cantidad de grasa en gramos,

todo ello por cada 100 gramos de alimento.

Descripción de la aplicación

11

Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Page 22: CursoSymfony2

Aunque se trata de una aplicación muy sencilla, cuenta con los elementos suficientes paratrabajar el aspecto que realmente pretendemos estudiar en esta unidad: la organización delcódigo siguiendo las directrices del patrón MVC. Comprobaremos como esta estrategia nosayuda a mejorar las posibilidades de crecimiento (escalabilidad) y el mantenimiento de lasaplicaciones que desarrollamos.

Diseño de la aplicación (I). Organización de los archivosLa "anatomía" de una aplicación web típica consiste en:

1. El código que será procesado en el servidor (PHP, Java, Python, etcétera) para construirdinámicamente la respuesta.

2. Los Assets, que podemos traducir como "activos" de la aplicación, y que lo constituyentodos aquellos archivos que se sirven directamente sin ningún tipo de proceso. Suelenser imágenes, CSS's y código Javascript.

El servidor web únicamente puede acceder a una parte del sistema de ficheros que sedenomina Document Root. Es ahí donde se buscan los recursos cuando se realiza unapetición a la raíz del servidor a través de la URL http://el.servidor.que.sea/. Sinembargo, el código ejecutado para construir dinámicamente la respuesta puede "vivir" encualquier otra parte, fuera del Document root 3.Tode esto sugiere una manera de organizar el código de la aplicación para que no se puedaacceder desde el navegador más que al código estrictamente imprescindible para que estafuncione. Se trata, simplemente, de colocar en el Document root sólo los activos y losscripts PHP de entrada a la aplicación. El resto de archivos, fundamentalmente libreríasPHP's y ficheros de configuración (XML, YAML, JSON, etcétera), se ubicarán fuera delDocument Root y serán incluidos por los scripts de inicio según lo requieran.Siguiendo estas conclusiones, nuestra aplicación presentará la siguiente estructura dedirectorio:

.├── app└── web ├── css ├── images └── js

Configuraremos nuestro servidor web para que el directorio web sea su Document root, y enapp colocaremos el código PHP y la configuración de la aplicación.

Diseño de la aplicación (II). El controlador frontalLa manera más directa y naïf de construir una aplicación en PHP consiste en escribir unscript PHP para cada página de la aplicación. Sin embargo esta práctica presenta algunosproblemas, especialmente cuando la aplicación que desarrollamos adquiere cierto tamaño ypretendemos que siga creciendo. Veamos algunos de los problemas más significativos deeste planteamiento.Por lo general, todos los scripts de una aplicación realizan una serie de tareas que son comunes. Por ejemplo: interpretar y manipular la request, comprobar las credenciales de seguridad y cargar la configuración. Esto significa que una buena parte del código puede ser compartido entre los scripts. Para ello podemos utilizar el mecanismo de inclusión de ficheros de PHP y fin de la historia. Pero, ¿qué ocurre si en un momento dado, cuando ya tengamos escrito mucho código, queremos añadir a todas las páginas de la aplicación una nueva característica que requiere, por ejemplo, el uso de una nueva librería?. Tenemos,

Diseño de la aplicación (I). Organización de los archivos

12

Page 23: CursoSymfony2

entonces, que añadir dicha modificación a todos los scripts PHP de la aplicación. Lo cualsupone una degradación en el mantenimiento y un motivo que aumenta la probabilidad defallos una vez que el cambio se haya realizado.Otro problema que ocurre con esta estrategia es que si se solicita una página que no tieneningún script PHP asociado, el servidor arrojará un error (404 Not Found) cuyo aspecto nopodemos controlar dentro de la propia aplicación (es decir, sin tocar la configuración delservidor web).Como se suele decir, ¡a grandes males grandes remedios!; si el problema lo genera el hechode tener muchos scripts, que además comparten bastante código, utilicemos uno solo quese encargue de procesar todas las peticiones. A este único script de entrada se le conocecomo controlador frontal.Entonces, ¿cómo puedo crear muchas páginas distintas con un solo script?. La clave está enutilizar la query string de la URL como parte de la ruta que define la página que se solicita. Elcontrolador frontal, en función de los parámetro que lleguen en la query string determinaráque acciones debe realizar para construir la página solicidada.

Nota

La query string es la parte de la URL que contiene los datos que se pasarán a laaplicación web. Por ejemlo, en: http://tu.servidor/index.php?accion=hola, laquery string es: ?accion=hola.

Construcción de la aplicación. Vamos al lío.Pues eso, vamos al lío aplicando todo lo que llevamos dicho hasta el momento:

• El patrón de diseño MVC,• La estructura de directorios que expone únicamente los ficheros indispensables para el

servidor web y,• La idea de que todas la peticiones pasen por un solo script, el controlador frontal

Creación de la estructura de directoriosComenzamos creando la estructura de directorios propuesta anteriormente. Por lo pronto, ennuestro entorno de desarrollo y por cuestiones de comodidad, crearemos la estructura enalguna ubicación dentro del Document root.

Nota

Si estás utilizando como sistema operativo Ubuntu, el Document root se encuentraen /var/www, es ahí donde debes crear un directorio denominado alimentos quealojará la estructura propuesta. Si estás utilizando XAMP en Windows, se encuentra enC:/xampp/htdocs.Es importante resaltar que esto no debería hacerse en un entorno de producción, ya que dejamos al servidor web acceder directamente al directorio app, y es algo que deseamos evitar. Sin embargo, de esta manera podemos añadir todos los proyectos que queramos sin tener que tocar la configuración del servidor web. Lo cual es algo muy agradecido cuando se está desarrollando. En un entorno de producción debemos

Construcción de la aplicación. Vamos al lío.

13

Page 24: CursoSymfony2

asegurarnos de que el directorio web es el Document root del servidor (o delVirtualHost de nuestra aplicación, si es que estamos alojando varias webs en unmismo servidor).

Nuestra implementación del patrón MVC será muy sencilla; crearemos una clase para laparte del controlador que denominaremos Controller, otra para el modelo quedenominaremos Model, y para los parámetros de configuración de la aplicación utilizaremosuna clase que llamaremos Config. Los archivos donde se definen estas clases losubicaremos en el directorio app. Por otro lado las Vistas serán implementadas comoplantillas PHP en el directorio app/templates.Los archivos CSS, Javascript , las imágenes y el controlador frontal los colocaremos en eldirectorio web.Cuando terminemos de codificar, la estructura de ficheros de la aplicación presentará elsiguiente aspecto:

/var/www/alimentos ├── app | ├── templates | ├── Controller.php | ├── Model.php | └── Config.php | └── web ├── css ├── images ├── js └── index.php

El controlador frontal y el mapeo de rutasEn cualquier aplicación web se deben definir las URL's asociadas a cada una de sus páginas.Para la nuestra definiremos las siguientes:

URL Acciónhttp://tu.servidor/alimentos/index.php?ctl=inicio mostrar pantalla iniciohttp://tu.servidor/alimentos/index.php?ctl=listar listar alimentoshttp://tu.servidor/alimentos/index.php?ctl=insertar insertar un alimentohttp://tu.servidor/alimentos/index.php?ctl=buscar buscar alimentoshttp://tu.servidor/alimentos/index.php?ctl=ver&id=x ver el alimento x

A cada una de estas URL's les vamos a asociar un método público de la clase Controller.Estos métodos se suelen denominar acciones. Cada acción se encarga de calculardinámicamente los datos requeridos para construir su página. Podrá utilizar, si le hace falta,lo servicios de la clase Model. Una vez calculados los datos, se los pasará a una plantilladonde se realizará, finalmente, la construcción del documento HTML que será devuelto alcliente.Todos estos elementos serán "orquestados" por el controlador frontal, el cual lo implementaremos en un script llamado index.php ubicado en el directorio web. En concreto,

El controlador frontal y el mapeo de rutas

14

Page 25: CursoSymfony2

la responsabilidad del controlador frontal será:

• cargar la configuración del proyecto y las librerías donde implementaremos la parte delModelo, del Controlador y de la Vista.

• Analizar los parámetros de la petición HTTP (request) comprobando si la páginasolicitada en ella tiene asignada alguna acción del Controlador. Si es así la ejecutará, sino dará un error 404 (page not found).

Llegados a este punto es importante aclara que, el controlador frontal y la claseController, son distintas cosas y tienen distintas responsabilidades. El hecho de que ambosse llamen controladores puede dar lugar a confusiones.El controlador frontal tiene el siguiente aspecto. Crea el archivo web/index.php y copia elsiguiente código.

1 <?php 2 // web/index.php 3 4 // carga del modelo y los controladores 5 require_once __DIR__ . '/../app/Config.php'; 6 require_once __DIR__ . '/../app/Model.php'; 7 require_once __DIR__ . '/../app/Controller.php'; 8 9 // enrutamiento10 $map = array(11 'inicio' => array('controller' =>'Controller', 'action' =>'inicio'),12 'listar' => array('controller' =>'Controller', 'action' =>'listar'),13 'insertar' => array('controller' =>'Controller', 'action' =>'insertar'),14 'buscar' => array('controller' =>'Controller', 'action' =>'buscarPorNombre'),15 'ver' => array('controller' =>'Controller', 'action' =>'ver')16 );17 18 // Parseo de la ruta19 if (isset($_GET['ctl'])) {20 if (isset($map[$_GET['ctl']])) {21 $ruta = $_GET['ctl'];22 } else {23 header('Status: 404 Not Found');24 echo '<html><body><h1>Error 404: No existe la ruta <i>' .25 $_GET['ctl'] .26 '</p></body></html>';27 exit;28 }29 } else {30 $ruta = 'inicio';31 }32 33 $controlador = $map[$ruta];34 // Ejecución del controlador asociado a la ruta35 36 if (method_exists($controlador['controller'],$controlador['action'])) {37 call_user_func(array(new $controlador['controller'], $controlador['action']));38 } else {39 40 header('Status: 404 Not Found');

41 echo '<html><body><h1>Error 404: El controlador <i>' .42 $controlador['controller'] .43 '->' .

El controlador frontal y el mapeo de rutas

15

Page 26: CursoSymfony2

44 $controlador['action'] .45 '</i> no existe</h1></body></html>';46 }

• En las líneas 5-7 se realiza la carga de la configuración del modelo y de loscontroladores.

• En las líneas 10-16 se declara un array asociativo cuya función es definir una tabla paramapear (asociar), rutas en acciones de un controlador. Esta tabla será utilizada acontinuación para saber qué acción se debe disparar.

• En las líneas 19-31 se lleva a cabo el parseo de la URL y la carga de la acción, si la rutaestá definida en la tabla de rutas. En caso contrario se devuelve una página de error.Observa que hemos utilizado la función header() de PHP para indicar en la cabeceraHTTP el código de error correcto. Además enviamos un pequeño documento HTML queinforma del error. También definimos a inicio como una ruta por defecto, ya que si laquery string llega vacía, se opta por cargar esta acción.

Nota

En honor a la verdad tenemos que decir que lo que estamos llamando parseo de laURL, no es tal. Simplemente estamos extrayendo el valor de la variable ctl que se hapasado a través de la petición HTTP. Sin embargo, hemos utilizado este terminoporque lo ideal sería que, en lugar de utilizar parámetros de la petición HTTP pararesolver la ruta, pudiésemos utilizar rutas limpias (es decir, sin caracteres ? ni & ) deltipo:

http://tu.servidor/index.php/iniciohttp://tu.servidor/index.php/buscarhttp://tu.servidor/index.php/ver/5

En este caso sí es necesario proceder a un parseo de la URL para buscar en la tabla derutas la acción que le corresponde. Esto, obviamente, es más complejo. Pero es lo quehace (y muchas cosas más) el componente Routing de Symfony2. En la siguienteunidad realizaremos un primer acercamiento a dicho componente que te dará unaidea de la potencia del mismo.

Las acciones del Controlador. La clase Controller.Ahora vamos a implementar las acciones asociadas a las URL's en la clase Controller. Creael archivo app/Controller.php y copia el siguiente código:

1 <?php 2 3 class Controller 4 { 5 6 public function inicio() 7 { 8 $params = array(

Las acciones del Controlador. La clase Controller.

16

Page 27: CursoSymfony2

9 'mensaje' => 'Bienvenido al curso de Symfony2', 10 'fecha' => date('d-m-yyy'), 11 ); 12 require __DIR__ . '/templates/inicio.php'; 13 } 14 15 public function listar() 16 { 17 $m = new Model(Config::$mvc_bd_nombre, Config::$mvc_bd_usuario, 18 Config::$mvc_bd_clave, Config::$mvc_bd_hostname); 19 20 $params = array( 21 'alimentos' => $m->dameAlimentos(), 22 ); 23 24 require __DIR__ . '/templates/mostrarAlimentos.php'; 25 } 26 27 public function insertar() 28 { 29 $params = array( 30 'nombre' => '', 31 'energia' => '', 32 'proteina' => '', 33 'hc' => '', 34 'fibra' => '', 35 'grasa' => '', 36 ); 37 38 $m = new Model(Config::$mvc_bd_nombre, Config::$mvc_bd_usuario, 39 Config::$mvc_bd_clave, Config::$mvc_bd_hostname); 40 41 if ($_SERVER['REQUEST_METHOD'] == 'POST') { 42 43 // comprobar campos formulario 44 if ($m->validarDatos($_POST['nombre'], $_POST['energia'], 45 $_POST['proteina'], $_POST['hc'], $_POST['fibra'], 46 $_POST['grasa'])) { 47 $m->insertarAlimento($_POST['nombre'], $_POST['energia'], 48 $_POST['proteina'], $_POST['hc'], $_POST['fibra'], 49 $_POST['grasa']); 50 header('Location: index.php?ctl=listar'); 51 52 } else { 53 $params = array( 54 'nombre' => $_POST['nombre'], 55 'energia' => $_POST['energia'], 56 'proteina' => $_POST['proteina'], 57 'hc' => $_POST['hc'], 58 'fibra' => $_POST['fibra'], 59 'grasa' => $_POST['grasa'], 60 ); 61 $params['mensaje'] = 'No se ha podido insertar el alimento. Revisa el formulario'; 62 }

63 } 64 65 require __DIR__ . '/templates/formInsertar.php'; 66 } 67 68 public function buscarPorNombre() 69 { 70 $params = array( 71 'nombre' => '', 72 'resultado' => array(), 73 ); 74

Las acciones del Controlador. La clase Controller.

17

Page 28: CursoSymfony2

75 $m = new Model(Config::$mvc_bd_nombre, Config::$mvc_bd_usuario, 76 Config::$mvc_bd_clave, Config::$mvc_bd_hostname); 77 78 if ($_SERVER['REQUEST_METHOD'] == 'POST') { 79 $params['nombre'] = $_POST['nombre']; 80 $params['resultado'] = $m->buscarAlimentosPorNombre($_POST['nombre']); 81 } 82 83 require __DIR__ . '/templates/buscarPorNombre.php'; 84 } 85 86 public function ver() 87 { 88 if (!isset($_GET['id'])) { 89 throw new Exception('Página no encontrada'); 90 } 91 92 $id = $_GET['id']; 93 94 $m = new Model(Config::$mvc_bd_nombre, Config::$mvc_bd_usuario, 95 Config::$mvc_bd_clave, Config::$mvc_bd_hostname); 96 97 $alimento = $m->dameAlimento($id); 98 99 $params = $alimento;100 101 require __DIR__ . '/templates/verAlimento.php';102 }103 104 }

Esta clase implementa una serie de métodos públicos, que hemos denominado accionespara indicar que son métodos asociados a URL's. Fíjate como en cada una de las acciones sedeclara un array asociativo ( params ) con los datos que serán pintados en la plantilla. Peroen ningún caso hay información acerca de como se pintarán dichos datos. Por otro lado, casitodas las acciones utilizan un objeto de la clase Models para realizar operaciones relativas ala lógica de negocio, en nuestro caso a todo lo relativo con la gestión de los alimentos.Para comprender el funcionamiento de las acciones, comencemos por Analizar la funciónlistar() . Comienza declarando un objeto del modelo (línea 17) para pedirleposteriormente el conjunto de alimentos almacenados en la base de datos. Los datosrecopilados son almacenados en el array asociativo params (líneas 20-22). Por últimoincluye el archivo /templates/mostrarAlimentos.php (línea 24). Tal archivo, quedenominamos plantilla, será el encargado de construir el documento HTML con los datosdel array params. Observa que todas las acciones tienen la misma estructura: realizanoperaciones, recojen datos y llaman a una plantilla para construir el documento HTML queserá devuelto al cliente.Observa también que en las acciones del controlador no hay ninguna operación que tengaque ver con la lógica de negocio, todo lo que se hace es lógica de control.Analicemos ahora la acción insertar(), cuya lógica de control es algo más compleja debidoa que tiene una doble funcionalidad:

1. Enviar al cliente un formulario HTML,2. Validar los datos sobre un alimento que se reciben desde el cliente para insertarlos en la

base de datos.La función comienza por declarar un array asociativo con campos vacíos que coinciden con los de la tabla alimento (líneas 29-36). A continuación comprueba si la petición se ha

Las acciones del Controlador. La clase Controller.

18

Page 29: CursoSymfony2

realizado mediante la operación POST (línea 41), si es así significa que se han pasado datosa través de un formulario, si no es así quiere decir que simplemente se ha solicitado lapágina para ver el formulario de inserción. En este último caso, la acción pasa directamentea incluir la plantilla que pinta el formulario (línea 65). Como el array de parámetros estávacío, se enviará al cliente un formulario con los campos vacíos (cuando veas el código de laplantilla lo verás en directo, por lo pronto basta con saber que es así).Por otro lado, si la petición a la acción insertar() se ha hecho mediante la operación POST,significa que se han enviado datos de un formulario desde el cliente (precisamente delformulario vacío que hemos descrito un poco más arriba). Entonces se extraen los datos dela petición, se comprueba si son válidos (línea 44) y en su caso se realiza la inserción (línea47) y una redirección al listado de alimentos (línea 50). Si los datos no son válidos, entoncesse rellena el array de parámetros con los datos de la petición (líneas 53-60) y se vuelve apintar el formulario, esta vez con los campos rellenos con los valores que se enviaron en lapetición anterior y con un mensaje de error.Todo el proceso que acabamos de contar no tiene nada que ver con la lógica de negocio;esto es, no decide cómo deben validarse los datos, ni cómo deben insertarse en la base dedatos, esas tareas recaen en el modelo (el cual, obviamente debemos utilizar). Loimportante aquí es que debe haber una operación de validación para tomar una decisión:insertar los datos o reenviar el formulario relleno con los datos que envió el usuario y con unmensaje de error. Es decir, únicamente hay código que implementa la lógica de control.

Nota

El esquema de control que se acaba de presentar resulta muy práctico y ordenadopara implementar acciones que consisten en recopilar datos del usuario y realizaralgún proceso con ellos (almacenarlos en una base de datos, por ejemplo). A lo largodel curso aparecerá, con más o menos variaciones, en varias ocasiones.

La implementación de la Vista.

Las plantillas PHPAhora vamos a pasar a estudiar la parte de la Vista, representada en nuestra solución por lasplantillas. Aunque en el análisis que estamos haciendo ya hemos utilizado la palabra"plantilla" en varias ocasiones, aún no la hemos definido con precisión. Así que comenzamospor ahí.Una plantilla es un fichero de texto con la información necesaria para generar documentosen cualquier formato de texto (HTML, XML, CSV, LaTeX, JSON, etcétera). Cualquier tipo deplantilla consiste en un documento con el formato que se quiere generar, y con variablesexpresadas en el lenguaje propio de la plantilla y que representas a lo valores que soncalculados dinámicamente por la aplicación.Cuando desarrollamos aplicaciones web con PHP, la forma más sencilla de implementarplantillas es usando el propio PHP como lenguaje de plantillas. ¿Qué significa esto? Acudimosal refranero popular y decimos aquello de que una imagen vale más que mil palabras. Contodos vosotros un ejemplo de plantilla HTML que usa PHP como lenguaje de plantillas(dedícale un ratito a observarla y analizarla, ¿qué es lo que te llama la atención en elaspecto del código PHP que aparece?)

La implementación de la Vista.

19

Page 30: CursoSymfony2

1 <table> 2 <tr> 3 <th>alimento (por 100g)</th> 4 <th>energía (Kcal)</th> 5 <th>grasa (g)</th> 6 </tr> 7 <?php foreach ($params['alimentos'] as $alimento) :?> 8 <tr> 9 <td><a href="index.php?ctl=ver&id=<?php echo $alimento['id']?>">10 <?php echo $alimento['nombre'] ?>11 </a>12 </td>13 <td><?php echo $alimento['energia']?></td>14 <td><?php echo $alimento['grasatotal']?></td>15 </tr>16 <?php endforeach; ?>17 18 </table>

Esencialmente no es más que un trozo de documento HTML donde la información dinámicase obtiene procesando código PHP. La característica principal de este código PHP es quedebe ser escueto y corto. De manera que no "contamine" la estructura del HTML. Por ellocada instrucción PHP comienza y termina en la misma línea. La mayor parte de estasinstrucciones son echo's de variables escalares. Pero también son muy usuales lautilización de bucles foreach - endforeach para recorrer arrays de datos, así como losbloques condicionales if - endif para pintar bloques según determinadas condiciones.En el ejemplo de más arriba se genera el código HTML de una tabla que puede tener unnúmero variable de filas. Se recoje en la plantilla el parámetro alimentos , que es un arraycon datos de alimentos, y se genera una fila por cada elemento del array con información dela URL de una página sobre el alimento (línea 9), y su nombre, energía y grasa total (líneas10-14).Observa también la forma de construir el bucle foreach, se abre en la línea 7 y se cierra enla 16. Lo particular de la sintaxis de este tipo de bucle para plantillas es que la instrucciónforeach que lo abre terminan con el caracter :. Y la necesidad de cerrarlo con un <?phpendforeach; ?>.

El layout y el proceso de decoración de plantillasEn una aplicación web, muchas de las páginas tienen elementos comunes. Por ejemplo, uncaso típico es la cabecera donde se coloca el mensaje de bienvenida, el menú y el pie depágina. Este hecho, y la aplicación del conocido principio de buenas prácticas deprogramación DRY (Don't Repeat Yourself, No Te Repitas), lleva a que cualquier sistema deplantillas que se utilice para implementar la vista utilice otro conocido patrón de diseño: ElDecorator, o Decorador 4. Aplicado a la generación de vistas la solución que ofrece dichopatrón es la de añadir funcionalidad adicional a las plantillas. Por ejemplo, añadir el menú yel pie de página a las plantillas que lo requieran, de manera que dichos elementos puedanreutilizarse en distintas plantillas. Literalmente se trata de decorar las plantillas conelementos adicionales reutilizables.Nuestra implementación del patrón Decorator es muy simple y, por tanto limitada, perosuficiente para asimilar las bases del concepto y ayudarnos a comprender más adelante lafilosofía del sistema de plantillas de Symfony2, denomindado twig 5.Nuestras plantillas serán ficheros PHP del tipo que acabamos de explicar, y las ubicaremos en el directorio app/templates. Como ya has visto en el código del controlador, las acciones finalizan incluyendo alguno de estos archivos. Comencemos por estudiar la plantilla

El layout y el proceso de decoración de plantillas

20

Page 31: CursoSymfony2

app/templates/mostrarAlimentos.php, que es la que utiliza la acción listar() parapintar los alimentos que obtiene del modelo. Crea el archivoapp/templates/mostrarAlimentos.php con el siguiente código:app/templates/mostrarAlimentos.php

1 <?php ob_start() ?> 2 3 <table> 4 <tr> 5 <th>alimento (por 100g)</th> 6 <th>energía (Kcal)</th> 7 <th>grasa (g)</th> 8 </tr> 9 <?php foreach ($params['alimentos'] as $alimento) :?>10 <tr>11 <td><a href="index.php?ctl=ver&id=<?php echo $alimento['id']?>">12 <?php echo $alimento['nombre'] ?></a></td>13 <td><?php echo $alimento['energia']?></td>14 <td><?php echo $alimento['grasatotal']?></td>15 </tr>16 <?php endforeach; ?>17 18 </table>19 20 21 <?php $contenido = ob_get_clean() ?>22 23 <?php include 'layout.php' ?>

Como ves, las líneas 3-18 son las que se han puesto como ejemplo de plantilla PHP hace unmomento. La novedad son las líneas 1 y 21-23. En ellas está la clave del nuestro proceso dedecoración. Para comprenderlo del todo es importante echarle un vistazo al ficheroapp/templates/layout.php, incluido al final de la plantilla. Créalo y copia el siguientecódigo:app/templates/layout.php

1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> 2 <html> 3 <head> 4 <title>Información Alimentos</title> 5 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 6 <link rel="stylesheet" type="text/css" href="<?php echo 'css/'.Config::$mvc_vis_css ?>" /> 7 8 </head> 9 <body>10 <div id="cabecera">11 <h1>Información de alimentos</h1>12 </div>13 14 <div id="menu">15 <hr/>16 <a href="index.php?ctl=inicio">inicio</a> |17 <a href="index.php?ctl=listar">ver alimentos</a> |

18 <a href="index.php?ctl=insertar">insertar alimento</a> |19 <a href="index.php?ctl=buscar">buscar por nombre</a> |

El layout y el proceso de decoración de plantillas

21

Page 32: CursoSymfony2

20 <a href="index.php?ctl=buscarAlimentosPorEnergia">buscar por energia</a> |21 <a href="index.php?ctl=buscarAlimentosCombinada">búsqueda combinada</a>22 <hr/>23 </div>24 25 <div id="contenido">26 <?php echo $contenido ?>27 </div>28 29 <div id="pie">30 <hr/>31 <div align="center">- pie de página -</div>32 </div>33 </body>34 </html>

El nombre del fichero es bastante ilustrativo, es un layout HTML, es decir, un diseño de undocumento HTML que incluye como elemento dinámico a la variable $contenido (línea 26),la cual esta definida al final de la plantilla mostrarAlimentos.php, y cuyo contenido esprecisamente el resultado de interpretar las líneas comprendidas entre el ob_start() y$contenido = ob_get_clean() . En la documentación de estas funciones(http://php.net/manual/es/function.ob-start.php) puedes ver que el efecto de ob_start() esenviar todos los resultados del script desde la invocación de la función a un buffer interno.Dichos resultados se recojen a través de la función ob_get_clean() . De esa maneraconseguimos decorar la plantilla con el layout. Esta técnica es utilizada en todas lasplantillas, de manera que todos los elementos comunes a todas las páginas son escritos unasóla vez en layout.php y reutilizados con todas las plantillas generadas con los datos decada acción.Observa que el layout que hemos propuesto incluye:

• los estilos CSS (línea 6),• el menú de la aplicación (líneas 14-23)• el pie de página (líneas 29-32)

A continuación mostramos el código del resto de las plantillas:app/templates/inicio.php

1 <?php ob_start() ?>2 <h1>Inicio</h1>3 <h3> Fecha: <?php echo $params['fecha'] ?> </h3>4 <?php echo $params['mensaje'] ?>5 6 <?php $contenido = ob_get_clean() ?>7 8 <?php include 'layout.php' ?>

app/templates/formInsertar.php

1 <?php ob_start() ?> 2 3 <?php if(isset($params['mensaje'])) :?> 4 <b><span style="color: red;"><?php echo $params['mensaje'] ?></span></b> 5 <?php endif; ?> 6 <br/>

El layout y el proceso de decoración de plantillas

22

Page 33: CursoSymfony2

7 <form name="formInsertar" action="index.php?ctl=insertar" method="POST"> 8 <table> 9 <tr>10 <th>Nombre</th>11 <th>Energía (Kcal)</th>12 <th>Proteina (g)</th>13 <th>H. de carbono (g)</th>14 <th>Fibra (g)</th>15 <th>Grasa total (g)</th>16 </tr>17 <tr>18 <td><input type="text" name="nombre" value="<?php echo $params['nombre'] ?>" /></td>19 <td><input type="text" name="energia" value="<?php echo $params['energia'] ?>" /></td>20 <td><input type="text" name="proteina" value="<?php echo $params['proteina'] ?>" /></td>21 <td><input type="text" name="hc" value="<?php echo $params['hc'] ?>" /></td>22 <td><input type="text" name="fibra" value="<?php echo $params['fibra'] ?>" /></td>23 <td><input type="text" name="grasa" value="<?php echo $params['grasa'] ?>" /></td>24 </tr>25 26 </table>27 <input type="submit" value="insertar" name="insertar" />28 </form>29 * Los valores deben referirse a 100 g del alimento30 31 <?php $contenido = ob_get_clean() ?>32 33 <?php include 'layout.php' ?>

app/templates/buscarPorNombre.php 1 <?php ob_start() ?> 2 3 <form name="formBusqueda" action="index.php?ctl=buscar" method="POST"> 4 5 <table> 6 <tr> 7 <td>nombre alimento:</td> 8 <td><input type="text" name="nombre" value="<?php echo $params['nombre']?>">(puedes utilizar '%' como comodín)</td> 9 10 <td><input type="submit" value="buscar"></td>11 </tr>12 </table>13 14 </table>15 16 </form>17 18 <?php if (count($params['resultado'])>0): ?>19 <table>20 <tr>21 <th>alimento (por 100g)</th>22 <th>energía (Kcal)</th>23 <th>grasa (g)</th>24 </tr>25 <?php foreach ($params['resultado'] as $alimento) : ?>26 <tr>27 <td><a href="index.php?ctl=ver&id=<?php echo $alimento['id'] ?>">28 <?php echo $alimento['nombre'] ?></a></td>29 <td><?php echo $alimento['energia'] ?></td>30 <td><?php echo $alimento['grasatotal'] ?></td>31 </tr>32 <?php endforeach; ?>33 34 </table>35 <?php endif; ?>36 37 <?php $contenido = ob_get_clean() ?>38 39 <?php include 'layout.php' ?>

app/templates/verAlimento.php

1 <?php ob_start() ?> 2 3 <h1><?php echo $params['nombre'] ?></h1> 4 <table border="1"> 5 6 <tr>

El layout y el proceso de decoración de plantillas

23

Page 34: CursoSymfony2

7 <td>Energía</td> 8 <td><?php echo $alimento['energia'] ?></td> 9 10 </tr>11 <tr>12 <td>Proteina</td>13 <td><?php echo $alimento['proteina']?></td>14 15 </tr>16 <tr>17 <td>Hidratos de Carbono</td>18 <td><?php echo $alimento['hidratocarbono']?></td>19 20 </tr>21 <tr>22 <td>Fibra</td>23 <td><?php echo $alimento['fibra']?></td>24 25 </tr>26 <tr>27 <td>Grasa total</td>28 <td><?php echo $alimento['grasatotal']?></td>29 30 </tr>31 32 </table>33 34 35 <?php $contenido = ob_get_clean() ?>36 37 <?php include 'layout.php' ?>

Todas las plantillas recurren al uso de las funciones ob_start() y ob_get_clean() y a lainclusión del layout para realizar el proceso de decoración.

El Modelo. Accediendo a la base de datosYa sólo nos queda presentar al Modelo. En nuestra aplicación se ha implementado en laclase Model y esta compuesto por una serie de funciones para persistir datos en la base dedatos, recuperarlos y realizar su validación.Dependiendo de la complejidad del negocio con el que tratemos, el modelo puede ser más omenos complejo y, además de tratar con la persistencia de los datos puede incluir funcionespara ofrecer otros servicios relacionados con el negocio en cuestión. Crea el archivoapp/Model.php y copia el siguiente código:app/Model.php

1 <?php 2 3 class Model 4 { 5 protected $conexion; 6 7 public function __construct($dbname,$dbuser,$dbpass,$dbhost)

El Modelo. Accediendo a la base de datos

24

Page 35: CursoSymfony2

8 { 9 $mvc_bd_conexion = mysql_connect($dbhost, $dbuser, $dbpass); 10 11 if (!$mvc_bd_conexion) { 12 die('No ha sido posible realizar la conexión con la base de datos: ' . mysql_error()); 13 } 14 mysql_select_db($dbname, $mvc_bd_conexion); 15 16 mysql_set_charset('utf8'); 17 18 $this->conexion = $mvc_bd_conexion; 19 } 20 21 22 23 public function bd_conexion() 24 { 25 26 } 27 28 public function dameAlimentos() 29 { 30 $sql = "select * from alimentos order by energia desc"; 31 32 $result = mysql_query($sql, $this->conexion); 33 34 $alimentos = array(); 35 while ($row = mysql_fetch_assoc($result)) 36 { 37 $alimentos[] = $row; 38 } 39 40 return $alimentos; 41 } 42 43 public function buscarAlimentosPorNombre($nombre) 44 { 45 $nombre = htmlspecialchars($nombre); 46 47 $sql = "select * from alimentos where nombre like '" . $nombre . "' order by energia desc"; 48 49 $result = mysql_query($sql, $this->conexion); 50 51 $alimentos = array(); 52 while ($row = mysql_fetch_assoc($result)) 53 { 54 $alimentos[] = $row; 55 } 56 57 return $alimentos; 58 } 59 60 public function dameAlimento($id) 61 {

62 $id = htmlspecialchars($id); 63 64 $sql = "select * from alimentos where id=".$id; 65 66 $result = mysql_query($sql, $this->conexion); 67 68 $alimentos = array(); 69 $row = mysql_fetch_assoc($result); 70 71 return $row; 72 73 } 74

El Modelo. Accediendo a la base de datos

25

Page 36: CursoSymfony2

75 public function insertarAlimento($n, $e, $p, $hc, $f, $g) 76 { 77 $n = htmlspecialchars($n); 78 $e = htmlspecialchars($e); 79 $p = htmlspecialchars($p); 80 $hc = htmlspecialchars($hc); 81 $f = htmlspecialchars($f); 82 $g = htmlspecialchars($g); 83 84 $sql = "insert into alimentos (nombre, energia, proteina, hidratocarbono, fibra, grasatotal) values ('" . 85 $n . "'," . $e . "," . $p . "," . $hc . "," . $f . "," . $g . ")"; 86 87 $result = mysql_query($sql, $this->conexion); 88 89 return $result; 90 } 91 92 public function validarDatos($n, $e, $p, $hc, $f, $g) 93 { 94 return (is_string($n) & 95 is_numeric($e) & 96 is_numeric($p) & 97 is_numeric($hc) & 98 is_numeric($f) & 99 is_numeric($g));100 }101 102 }

Cuando el controlador requiere el uso del modelo, creamos un objeto de tipo $m = newModel() . El constructor de esta clase realiza una conexión con la base de datos y la ponedisponible a todos sus métodos al añadir la conexión creada como atributo del objeto. Cadafunción utiliza esta conexión para realizar su cometido contra la base de datos.La última función de la clase, validarDatos(), es algo distinta, ya que no utiliza para nadala conexión con la base de datos. Simplemente valida datos. Si la aplicación fuera máscompleja sería interesante crear una clase dedicada a la validación. De manera queatendamos al principio de la Separation of Concerns .

La configuración de la aplicaciónA lo largo y ancho de todo el código expuesto, aparece cada tanto una referencia a unosatributos estáticos de la clase Config . Por ejemplo, en la línea 17 del archivoapp/Controller.php, aparece Config::$mvc_bd_hostname, Config::$mvc_bd_usuario,etcétera. Se trata de parámetros de configuración que hemos definido en una clasedenominada Config:app/Config.php

1 <?php 2 3 class Config 4 { 5 static public $mvc_bd_hostname = "localhost"; 6 static public $mvc_bd_nombre = "alimentos"; 7 static public $mvc_bd_usuario = "root"; 8 static public $mvc_bd_clave = "root"; 9 static public $mvc_vis_css = "estilo.css";10 }

Esta clase está disponible durante todo el script de manera que se pueden utilizar susvalores a lo largo del código, y cambiarlos sin más que modificar este fichero.Con esto ya tenemos todo el código de la parte de servidor. Ya sólo nos falta darle un toquede estilo a los documentos HTML que enviamos al cliente y crear la base de datos quealmacenará los datos persistentes sobre los alimentos.

La configuración de la aplicación

26

Page 37: CursoSymfony2

Incorporar las CSS'sCrea el directorio web/css y en él coloca un archivo llamado estilo.css con el siguientecontenido:web/css/estilo.css

body { padding-left: 11em; font-family: Georgia, "Times New Roman", Times, serif; color: purple; background-color: #d8da3d }ul.navbar { list-style-type: none; padding: 0; margin: 0; position: absolute; top: 2em; left: 1em; width: 9em }h1 { font-family: Helvetica, Geneva, Arial, SunSans-Regular, sans-serif }ul.navbar li { background: white; margin: 0.5em 0; padding: 0.3em; border-right: 1em solid black }ul.navbar a { text-decoration: none }a:link { color: blue }a:visited { color: purple }address { margin-top: 1em; padding-top: 1em; border-top: thin dotted }#contenido { display: block; margin: auto; width: auto; min-height:400px;}

Fíjate que en el archivo app/templates/layout.php se incluye (línea 6) este archivo CSSque acabamos de crear. Como dicho layout decora a todas las plantillas, estos estilosafectarán a todas las páginas.

La base de datosEn el Sistema Gestor de Base de Datos MySQL que vayas a utilizar, utilizando algún clienteMySQL crea una base de datos para almacenar los alimentos. Introduce algunos registrospara probar la aplicación.

Incorporar las CSS's

27

Page 38: CursoSymfony2

CREATE TABLE `alimentos` ( `id` int(11) NOT NULL AUTO_INCREMENT, `nombre` varchar(255) NOT NULL, `energia` decimal(10,0) NOT NULL, `proteina` decimal(10,0) NOT NULL, `hidratocarbono` decimal(10,0) NOT NULL, `fibra` decimal(10,0) NOT NULL, `grasatotal` decimal(10,0) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Lo importante para que la conexión funcione, es que los parámetros de conexión que seestablecen en el fichero app/Config.php coincidan con los de tu base de datos.

Sugerencia

Lo más cómodo para desarrollar es tener todo en el mismo computador: tanto elservidor web (apache) como el servidor de base de datos (MySQL). Además es muypráctico, aunque nada seguro, utilizar en el servidor MySQL el usuario root(superadministrador) como usuario de nuestras aplicaciones. Así no hay quepreocuparse por temas de permisos. Repetimos: es lo más cómodo pero, a la vez, lomás inseguro. Esta práctica está justificada ÚNICAMENTE en un entorno de desarrolloen local, donde la seguridad, en principio no es primordial.

Ya puedes juntar todas las piezas y probar la aplicación introduciendo en tu navegador laURL correspondiente:

http://tu.servidor/ruta/a/alimentos/web/index.php

¡Suerte!

ComentariosAquí puedes enviar comentarios, dudas y sugerencias. Utiliza la barra de scroll para recorrertodos los mensajes. El formulario de envío se encuentra al final.

Autor del código: Juan David Rodríguez García <[email protected]>

Comentarios

28

Page 39: CursoSymfony2

Unidad 3: Symfony2 a vista de pájaroEl objetivo fundamental de este curso es que aprendas a desarrollar aplicaciones web decalidad con Symfony2, un potente framework de desarrollo en PHP. Para conocerlo enprofundidad utilizaremos una estrategia de acercamiento en espiral a nuestro objeto deestudio, es decir, lo rodearemos trazando círculos de radio cada vez más pequeños, deforma que cada vuelta que demos nos revelará un conocimiento más profundo de estamagnífica herramienta de desarrollo.Esta estrategia nos proporcionará resultados visibles y prácticos desde el principio, desde laprimera vuelta, aunque ello será a costa de tratar superficialmente algunos conceptos que,progresivamente, irán definiéndose con más precisión a medida que avanza el curso.En esta unidad volveremos a desarrollar la aplicación que construimos en la unidad 2. Peroahora utilizaremos Symfony2. Será nuestra primera vuelta, la menos precisa pero la máspanorámica, que nos proporcionará una primera imagen del framework aún difusa perosuficiente para mostrar sus características fundamentales.

¿Qué es Symfony2?La respuesta rápida, no por ello menos cierta, es que Symfony2 es un framework PHP para eldesarrollo de aplicaciones web. Sin embargo, los creadores de Symfony2 no la darían porbuena. Posiblemente tampoco aprobasen completamente lo que voy a decir a continuación(¡son unos programadores, además de excelentes, muy "quisquillosos"!).El objetivo principal de Symfony2 no es tanto desarrollar otro framework PHP, comodesarrollar un conjunto de componentes estables, independientes, fácilmente acoplables(que no acoplados) para formar sistemas más complejos, mediante los cuales se puedanresolver problemas relacionados con el desarrollo de aplicaciones web en PHP. Obviamenteno es necesario utilizar todos los componentes de Symfony2; gracias a su absolutaindependencia se pueden utilizar únicamente aquellos que se requieran para resolver elproblema en cuestión. Symfony2 se presenta así como una especie de Lego en el mundo deldesarrollo en PHP.Con estos componentes, uno de los problemas que se puede resolver es el de la creación deun framework de desarrollo web. De hecho, uno de los primeros productos que se hanconstruido con estos componentes ha sido el framework de desarrollo conocido comoDistribución Standard de Symfony2, o más brevemente Symfony2 sin más. Y esprecisamente del estudio de esta distribución de lo que trata este curso.Por ello, el término Symfony2 puede referirse a:

1. Los componentes de Symfony2

2. El framework de Symfony2, o distribución standard de Symfony2.En el momento en que se escribe este texto, lo más probable es que se trate de lo segundo.Los componentes de Symfony2, se están utilizando para otros proyectos 6, y su usoaumentará cuando dispongan de una documentación tan exhaustiva y bien elaborada comola que cuenta en estos momentos el framework Symfony2. Es muy probable (ojalá) quecuando este curso vea la luz esta situación haya cambiado 7. Entonces, a lo mejor seríainteresante elaborar otro curso sobre los componentes de Symfony2, pero sería eso: otrocurso. Los componentes son los elementos de bajo nivel del framework Symfony2, y parautilizar dicho framework no es necesario conocerlos.De todas formas vamos a aprovechar este apartado para enumerar los componentes de Symfony2, dando una breve descripción de su funcionalidad. Pero, repetimos: no es necesario conocerlos para utilizar el framework, que es de lo que trata este curso. Damos esta información con el fin de calmar al curioso y de aclarar las posibles confusiones que se

Unidad 3: Symfony2 a vista de pájaro

29

Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Page 40: CursoSymfony2

dan al hablar de Symfony2 entre los componentes y el framework. Obviamente, el contextoaclarará de que se habla en cada ocasión.

Componente DescripciónDependencyInjectionAutomaticación de la Inyección de dependenciasEventDispatcherImplementación del patrón observerHttpFoundationProporciona una capa orientada a objetos para el tratamiento del HTTPDomCrawler Para navegar el DOM de un documento HTMLClassLoader Autoloader que implementa PSR-0 standard (forma standard de autocargar

classes con espacios de nombre)CssSelector Para convertir un CSS selector en un XPathHttpKernel Proporciona la parte dinámica de la especificación HTTPBrowserKit Simula el comportamiento de un BrowserTemplating Proporciona elementos para construir sistemas de plantillasTranslation Soporte para las traduccionesSerializer Para crear arrays a partir de estructuras complejas como gráficosValidator Validaciones basadas en JSR-303 Bean Validation specificationSecurity Infraestructura para el tratamiento de la autentificación y la autorizaciónRouting Potente sistema para asociar rutas a accionesConsole Para desarrollar herramientas CLIProcess Para ejecutar procesos del sistemaConfig Herramientas para cargar configuraciones de distintas fuentesFinder Para encontrar archivos en el sistema de archivosLocale Para tratar la localización cuando no está disponible la extensión intlYaml Para la manipulación de archivos de configuración en formato YAMLForm Herramientas para definir formularios, pintarlos y asociarle datos

Y estas son las "tripas" del monstruo Symfony2. No hablaremos mucho más acerca de estoscomponentes a lo largo del cursos. Pero que sepas que existen. Puede que te ayuden aresolver tu próximo proyecto, y muy probablemente sean los ladrillos fundamentales con losque se construyan muchas de las aplicaciones PHP en un futuro no muy lejano.

Instalación y configuración de Symfony2A partir de este momento, y mientras no lo especifiquemos explicitamente, cuandohablemos de Symfony2 nos estamos refiriendo al framework, concretamente a la ediciónestándard.En este apartado vamos a instalar y configurar Symfony2, y lo dejaremos listo para construirla aplicación de gestión de alimentos sobre él.Bájate de http://symfony.com/download la última versión de la rama 2.0 de Symfony2. Verásque hay una modalidad normal y otra without vendors. Utiliza la primera.

Instalación y configuración de Symfony2

30

Marina
Resaltado
Page 41: CursoSymfony2

Nota

La modalidad normal contiene todas las librerías de terceros (vendors) necesariaspara comenzar a trabajar con el framework, mientras que la modalidad withoutvendors, como su nombre indica, viene sin estas librerías, razón por lo que hay queinstalarlas posteriormente mediante una herramienta incluida con Symfony2(bin/vendors) que utiliza el sistema de control de versiones git para bajar lasúltimas versiones desde el repositorio de github 8, donde se encuentra todo el códigode Symfony2.

Descompríme el archivo descargado en algún directorio accesible al servidor web, esto es,dentro de su Document root. Para que la aplicación funcione, el servidor web debe poderescribir en los directorios app/cache y app/logs. Si estás utilizando un sistema operativotipo UNIX (Ubuntu, MacOSX, etcétera), la forma más fácil de dar dichos permisos es:

chmod -R 777 app/cache app/logs

Nota

Durante toda la unidad suponemos que has hecho esta operación directamente en elDocument root del servidor web, de manera que tendrá la siguiente estructura dedirectorios:

/var/www/ (o donde tengas mapeado tu Document root) | └── Symfony | ├── LICENSE ├── README.md ├── app/ ├── bin/ ├── deps ├── deps.lock ├── src/ ├── vendor/ └── web/

Y que tanto el servidor web como el servidor de MySQL están instalado en la máquinalocal.

A continuación comprobamos que nuestro sistema cumple los requisitos mínimos ejecutandopor la interfaz de comandos la siguiente orden:

php app/check.php

Instalación y configuración de Symfony2

31

Marina
Resaltado
Page 42: CursoSymfony2

Si el resultado nos señala algún error, debemos resolverlo antes de continuar. Una vez quepasemos al menos los requisitos obligatorios (mandatory requirements), podemos ejecutarla demo que viene incorporada en la distribución standard de Symfony2. Para ello apuntacon tu navegador a la siguiente URL:

http://localhost/Symfony/web/app_dev.php

¡Y juega un poquito!, Por ejemplo, pica en Run the demo y navega por los distintos enlaces.Fíjate en la pinta que tienen las URL's. La demo muestra el código que genera las páginas dela propia demo. Fíjate en él detenidamente. Verás que muestra dos partes: el del controladory el de la plantilla (template). ¿Te suena?. Si no es así: GOTO unidad 2.Ya has visto en acción la primera aplicación construida con Symfony2. Ahora vamos adescribir la manera en que Symfony2 organiza el código.Como en el mini-framework que hemos construido en la unidad 2, Symfony2 tambiénorganiza los archivos en dos grupos: los que deben estar directamente accesibles al servidorweb (CSS's, Javascript, imágenes y el controlador frontal) y los que pueden ser incluidosdesde el controlador frontal (librerías PHP y ficheros de configuración). Los primeros viven enel directorio web, y los segundos, según su funcionalidad, están repartidos entre losdirectorios app , src y vendor. Si has asimilado bien todo lo que se ha dicho en la unidad 2,estarás pensando que en una instalación en un entorno de producción, el Document rootdel servidor web (o del Virtual host dedicado para la aplicación), debe coincidir con eldirectorio web, y que el resto de directorios deben ubicarse fuera del Document root. Si esasí, estás en lo cierto, y si no, te sugerimos que antes de continuar con esta unidad, repasesla anterior.

Nota

Para paliar el efecto de posibles despistes o malas prácticas por desconocimiento,pereza y otras fatales causas, los directorios src y app, contienen un fichero.htaccess que indica al servidor web que no debe mostrar su contenido.

Veamos ahora para que se utiliza cada uno de estos directorios.

El directorio webPoco hay que decir ya de este directorio, aquí encontraremos el controlador frontal y todoslos assets de la aplicación: CSS's, Javascipts, imágenes, etcétera.Esta es la estructura del directorio:

web├── app_dev.php├── apple-touch-icon.png├── app.php├── bundles│   ├── acmedemo│   ├── framework│   ├── sensiodistribution│   └── webprofiler├── config.php├── favicon.ico

El directorio web

32

Page 43: CursoSymfony2

└── robots.txt

Podemos ver 3 scripts PHP:

• config.php es un script que asiste en la configuración del framework. No esimprescindible. De hecho cuando uno se siente confortable con Symfony2, es mássencillo realizar la configuración directamente sobre el código fuente. Pero paraempezar puede servir de ayuda. Si lo utilizas ten en cuenta los permisos de los ficherosdel directorio app/config, pues este script debe poder escribir allí.

• app.php es el controlador frontal de la aplicación. No vamos a repetir aquí que esteconcepto. Si no sabes de que hablamos, THEN GOTO Unidad 2.

• app_dev.php también es el controlador frontal de la aplicación. ¿Cómo? ¿doscontroladores frontales? ¡eso no encaja con lo que hemos aprendido!. Bueno tranquilos,tiene su explicación. Se trata de lo que se denomina en Symfony2 el controlador frontalde desarrollo. En principio pinta lo mismo que app.php, pero le añade una barra dedepuración que ofrece muchísima información sobre todo lo relacionado con laejecución del script. Puedes ver la barra de depuración en la demo que has ejecutadohace un momento. Se encuentra abajo de la página. Explórala un poco, te asombrarásde la cantidad de información que te proporciona. Cuando desarrollamos es muyconveniente utilizar este controlador frontal, pero en producción NUNCA debe utilizarse,pues daríamos a los usuario de la web información que podría comprometer nuestrosistema.

Por otro lado los assets se ubicarán en el directorio bundles/nombre_bundle, dondenombre_bundle es el nombre del bundle al que pertenece el asset en cuestión. Vale, ¿y quees un bundle?, pues por lo pronto quedate con que "es la unidad funcional de código queutiliza Symfony2". Algo así como una de las piezas del Lego Symfony2. En la sección LosBundles: Plugins de primera clase hablaremos de estos "personajes" con más detalle.

El directorio appLa finalidad de este directorio es alojar a a los scripts PHP encargados de los procesos decarga del framework (lo que se conoce como bootstraping) y a todo lo que tenga que vercon la configuración general de la aplicación. Los archivos de este directorio son losencargados de unir y dar cohesión a los distintos componentes del framework.Son especialmente importantes los ficheros autoload.php y AppKernel.php, ya que hayque tocarlos cada vez que extendemos el framework con nuevas funcionalidades, es decircada vez que incorporamos nuevos bundles (vamos poniendo en circulación a esta palabrejaque usaremos hasta la saciedad).En autoload.php se mapean los espacios de nombres contra los directorios en los queresidirán las clases pertenecientes a dichos espacios de nombre. De esa manera el procesode autocarga de clases sabrá donde tiene que buscar las clases cuando se usen dichosespacios, sin necesidad de incluir explicitamente (esto es, usando include o require ) losarchivos donde se definen las clases.En AppKernel.php, se declaran los bundles que se utilizarán en la aplicación.En el directorio config se encuentran los archivos de configuración de la aplicación:config.yml, routing.yml y security.yml.El sistema de configuración de Symfony2 permite trabajar con distintos entornos deejecución. Los más típicos son prod, para producción y dev, para desarrollo. Pero se puedendefinir tantos entornos como deseemos. En el controlador frontal se indica qué entornodeseamos utilizar en la ejecución del script. Fíjate en la línea 22 de web/app_dev.php, o enla línea 9 del web/app.php:

El directorio app

33

Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Page 44: CursoSymfony2

...$kernel = new AppKernel('prod', false);...

El primer argumento decide el entorno de ejecución que se utilizará, y el segundo sirve parahabilitar los mensajes de depuración 9. ¿Y para que sirve esto del entorno de ejecución?.Symfony2 utiliza este dato para saber qué ficheros de configuración debe cargar.Supongamos, por ejemplo, que se especifica dev como entorno de ejecución. Entonces, siexiste el fichero config_dev.yml lo cargará, y si no es así cargará config.yml. Lo mismoocurre con los ficheros routing.yml, security.yml y services.yml. Más adelanteestudiaremos para que sirven cada uno de ellos. Por lo pronto nos conformaremos con saberla dinámica de funcionamiento.Los entornos proporcionan mucha flexibilidad a la hora de desarrollar una aplicación. Vamosa ilustrar con un ejemplo esta flexibilidad. Un caso que nos encontramos habitualmente esque la aplicación que estamos construyendo debe enviar e-mails. Es bastante molesto tenerque disponer de cuentas reales y gestionarlas para que podamos probar la aplicaciónmientras desarrollamos. Podemos utilizar este sistema de configuración para indicar alframework que en el entorno de desarrollo se envíen todos los e-mails a una sola cuenta, oincluso que no se envíen. Otro ejemplo típico podría ser el definir unos parámetros deconexión a la base de datos para el entorno de producción y otro para el de desarrollo.Una estrategía muy adecuada para tratar con los ficheros de configuración cuandoqueremos que haya partes comunes y partes diferentes en cada entorno, es definir todos losparámetros comunes en el fichero fichconfig.yml (donde fichconfig es config,security, routing o services), y los particulares de cada entorno en el ficherofichconfig_env.yml (donde env es dev, prod o cualquier otro nombre de entorno queusemos). Por último importamos los primeros (comunes) desde los últimos (particulares) dela siguiente manera:Inicio del fichero fichconfig_env.yml

imports: - { resource: fichconfig.yml } ...

Puedes comprobar que esta es la estrategia utilizada por la distribución standard deSymfony2 con los ficheros config.yml, config_dev.yml y config_prod.yml.Para acelerar la ejecución de los scripts, la configuración, el enrutamiento y las plantillas detwig son compiladas y almacenadas en el directorio cache. Por otro lado, los errores y otrainformación de interés acerca de eventos que ocurren cuando se ejecuta el framework, sonregistrados en archivos que se almacenan en el directorio logs. Por eso estos dosdirectorios deben tener permisos de escritura para el servidor web.Por último, en este directorio tan "denso", encontramos la navaja suiza de Symfony2, laaplicación app/console. Prueba a ejecutarla sin pasarle ningún argumento. Verás una listacon todas las tareas que se pueden lanzar por línea de comandos.

php app/console

El directorio vendorAquí se aloja todo el código funcional que no es tuyo. Es lo que tradicionalmente se conoce como librerías de terceros. Entre otras cosas, el directorio contiene los componentes de

El directorio vendor

34

Page 45: CursoSymfony2

Symfony2, el ORM Doctrine2 y el sistema de plantillas twig. Cuando amplies tu aplicacióncon nuevos bundles de terceros instalados automáticamente con la aplicación bin/vendors,será aquí donde se ubique el código.

El directorio srcEs el directorio donde colocarás tu código. Más concretamente: tus bundles. A base deutilizar este palabro acabarás por asimilarlo antes de que te lo expliquemos :-).

El directorio binEl nombre de este directorio es un clásico en el mundo UNIX. En él se colocan archivosejecutables. La distribución standard solo trae el ejecutable vendors que se utiliza, encombinación con el fichero deps (dependencias), para instalar componentes de terceros(vendors).Y con esto acabamos la descripción de los directorios de Symfony2. Ha llegado el momentode hablar de los bundles, esos grandes desconocidos (¡por ahora!).

Los Bundles: Plugins de primera claseSi los creadores de Symfony2 hubieran elegido la palabra plugin en lugar de bundle, esprobable que te hubieses hecho una idea más concreta de lo que es un bundle. Pues bien,por lo pronto, piensa que un bundle es un plugin, por que no es ni más ni menos que eso.Cualquier framework que se precie debe ofrecer un mecanismo de extensión que permitaampliar la aplicación sin compromenter la escalabilidad. Para ello las piezas que se añadenal sistema deben ser bloques prácticamente autónomos y con una interfaz sencilla paraengancharlos (to plug, en inglés) al sistema. A estos bloques se les conoce a lo largo y anchode la galaxia con el nombre de plugin (o complemento, en castellano). ¿Por qué loscreadores de Symfony2 han decidido llamarles bundles en su lugar? Lo mismo hay algunarazón teórica que se me escapa. Pero de lo que si estoy seguro es de que hay una razónhistórica:El antecesor de Symfony2, el fantástico symfony 1.x organiza el código en aplicaciones, quea su vez están formadas por módulos con la implementación de las acciones. Además ofreceun mecanismo de extensión basado en plugins, los cuales también organizan el código enmódulos con sus acciones. Pero a pesar de este paralelismo las aplicaciones son "másimportantes" que los plugins. De hecho, las aplicaciones pueden usar módulos de losplugins, pero lo contrario no tiene sentido tal y como está concebido symfony 1.x. Con eltiempo los desarrolladores se dieron cuenta de que era más fácil de mantener y organizarlos plugins, ya que son bloques de código autónomos y fácilmente acoplables a la aplicación.Este hecho llevó de forma natural a reorganizar la aplicación colocando todo el códigofuncional en los plugins. Las aplicaciones se quedaban prácticamente vacías de código y tansolo contenían ficheros de configuración.Así pues, en Symfony2 decidieron olvidarse del concepto de aplicación (en el sentido desymfony 1.x), y obligar a que todo el código funcional se organizase en plugins. Es comohacer a los plugins ciudadanos de primera clase del framework. Finalmente, para evitarcualquier confusión y dirimir la diferencia entre plugin y aplicación, decidieron usar lapalabra bundle. Y eso es todo. Si no conoces symfony 1.x, seguro que hubieras preferidollamarle plugin. Y si lo conoces es probable que también.En fin, lo que realmente debes saber:

El directorio src

35

Page 46: CursoSymfony2

Importante

Un bundle no es más que un directorio que aloja todo aquello relativo a unafuncionalidad determinada. Puede incluir clases PHP, plantillas, configuraciones, CSS'sy Javascript.

La aplicación gestión de alimentos en Symfony2Sin más preámbulo vamos a comenzar a reimplementar con Symfony2 la misma aplicaciónque hemos desarrollado en la unidad 2 . Este ejercicio nos va a permitir conocer losconceptos más básicos del framework, muchos de los cuales serán profundizados conformeavancemos en el curso.

Generación de un BundleLa primera idea que debe quedar clara, expresada de manera simplista, es que "todo es unbundle" en Symfony2. Por tanto, si queremos desarrollar una aplicación necesitaremos, porlo menos, tener un bundle para alojar el código de la misma. Comencemos por ahí. Elsiguiente comando de Symfony2 nos ayuda a generar el esqueleto de un bundle de manerainteractiva:

php app/console generate:bundle

A cada pregunta que nos hace le acompaña una pequeña ayuda. En primer lugar nospregunta por el espacio de nombre que compartiran las clases del bundle. Larecomendación, como se dice en el texto de ayuda del comando, es que comience por elnombre del fabricante del bundle, el nombre del proyecto o del cliente, seguido,opcionalemente, por una o más categorías, y finalizar con el nombre del bundle seguido delsufijo Bundle. Es decir el nombre completo del espacio de nombres del bundle debe seguir elsiguiente patrón:

Fabricante/categoria1/categoria2/../categoriaN/nombrebundleBundle

Ilustremos esto con varios ejemplos de nombres de bundles válidos:

AulasMentor/AlimentosBundleAulasMentor/Unidad3/AlimentosBundleAulasMentor/CursoSf2/Unidad3/AlimentosBundleJazzyweb/AulasMentor/AlimentosBundle

Nos quedaremos con el último de los nombres para el bundle que vamos a construir. Coneste nombre se quiere expresar algo así como que el bundle AlimentosBundle ha sidocreado por Jazzyweb (una empresa ficticia) para el cliente AulasMentor. Como ves,cualquier nombre vale siempre que contenga un nombre de fabricante (vendor name) y unnombre de bundle. En medio podemos poner lo que queramos para organizar nuestrotrabajo.Introduce Jazzyweb/AulasMentor/AlimentosBundle como espacio de nombres del bundle.A continuación nos pregunta por el nombre del bundle. Y nos ofrece una recomendación quees el mismo nombre del espacio de nombres anterior pero sin los separadores /. El nombredel bundle es importante pues, en ocasiones, hay que referirse al bundle por este nombre.

La aplicación gestión de alimentos en Symfony2

36

Page 47: CursoSymfony2

Presiona enter para aceptar la sugerencia.El próximo paso es asignarle una ubicación en la estructura de directorios del proyecto. Laflexibilidad de Symfony2 permite que lo coloques donde quieras. Pero es muy recomendableque lo coloques en el directorio src, ya que está pensado para alojar nuestro código. Si lohaces así, te ahorrarás tener que incluir una línea de código en el fichero app/autoload.phppara registrar el espacio de nombre en el sistema de autocarga de clases. Esto último es asíporque en dicho fichero ya se ha contemplado que todas las clases que se aloje en src seanautocargadas asignándole como espacio de nombre raíz el mismo nombre que la estructurade directorios computada desde src.Presiona enter para aceptar la sugerencia. Cuando termines de generar el bundle veráscomo se ha creado en src el directorio Jazzyweb/AulasMentor/AlimentosBundle, es decirun directorio que tiene la misma estructura que el espacio de nombres que hemos asignadoal bundle. Esto es lo que se quiere decir de manera genérica en el párrafo anterior.Los bundles llevarán asociados algo de configuración. Como mínimo será necesarioconfigurar las rutas que mapean las URL's en acciones del bundle. Symfony2 admite 4formas de representar las configuraciones: con ficheros XML, YML o PHP, y medianteanotaciones, que es una manera de expresar parámetros de configuración en el propiocódigo funcional aprovechando para ello los comentarios de PHP.Más adelante tendremos ocasión de utilizar las anotaciones y las entenderás mejor. Llegadosa este punto hemos de decir que la elección es una cuestión de gusto; discutir con alguienacerca de cual es la mejor opción sería una pérdida de tiempo. Para el caso de laconfiguración de los bundles (prácticamente para definir rutas como veremos después)hemos elegido los fichero YAML como formato para la configuración.Selecciona (escribe) yml como formato de configuración.Por último contesta yes a la pregunta de si quieres generar la estructura completa. Estaopción generará algunos directorios y archivos extra que siguen las recomendaciones deSymfony2 para alojar código. Es posible que no los utilices, pero no hacen "daño" y sugierencomo debe organizarse el código. No obstante el programador tiene bastante libertad a lahora de organizar los archivos del bundle como quiera.Confirma la generación del código. Una vez generado, el asistente te realizará dos preguntasmás. Primera pregunta: ¿quieres actualizar automáticamente el Kernel? y segunda pregunta¿quieres actualizar directamente el routing? Contesta a las dos afirmativamente. Vamos aver con más detalle las consecuencias de estas actualizaciones automáticas.Por una parte el bundle, como ya hemos explicado, es un bloque desacoplado y reutilizablede código que agrupa a una serie de funcionalidades. Si queremos utilizarlo en nuestroproyecto debemos "notificarlo" al framework. Es decir, hemos de "engancharlo". Esto sehace registrándolo en el archivo app/AppKernel.php. La primera actualización automáticaha realizado dicho registro. Abre ese archivo y fíjate como al final del métodoregisterBundles() aparece la siguiente línea:

...new Jazzyweb\AulasMentor\AlimentosBundle\JazzywebAulasMentorAlimentosBundle(),...

Dicha línea ha sido insertada automáticamente como consecuencia de haber respondidoafirmativamente a la primera pregunta. El cometido de la línea es registrar el bundle reciencreado en el framework para poder hacer uso del mismo.La segunda actualización automática "enlaza" la tabla enrutamiento general de la aplicación con la tabla de enrutamiento particular del bundle. La tabla de enrutamiento es la

La aplicación gestión de alimentos en Symfony2

37

Page 48: CursoSymfony2

responsable de indicar al framework como deben mapearse las URL's en acciones PHP. Paraver como se ha realizado este enlace mira el fichero app/config/routing.yml:

JazzywebAulasMentorAlimentosBundle: resource: "@JazzywebAulasMentorAlimentosBundle/Resources/config/routing.yml" prefix: /

Estas líneas han sido introducidas automáticamente como consecuencia de contestarafirmativamente a la segunda pregunta. Observa que el apartado resource es la dirección enel sistema de ficheros de la tabla de enrutamiento propia del bundle que acabamos de crear.Symfony2 sabe convertir @JazzywebAulasMentorAlimentosBundle en la ubicación delbundle pues está debidamente registrado.Es importante que conozcas como se acopla un bundle a la aplicación, pues si falla laactualización automática del AppKernel.php y/o del routing.yml, debes realizarlasmanualmente.Ahora puedes echarle un vistazo al fichero routing.yml del bundle(src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/config/routing.yml). Verásque existe una ruta mapeada contra una acción. Después explicaremos los detalles de laruta. Esta última ruta sirve para probar el bundle. Así que accede desde tu navegador web ala siguiente URL (que es la que se corresponde con esta ruta de prueba)

http://localhost/Symfony/web/app_dev.php/hello/alberto

Si todo va bien, obtendrás como respuesta un saludo. Puedes cambiar el nombre del final dela ruta.Resumiendo: Para desarrollar nuestra aplicación hemos de contar al menos con un bundlepara escribir el código. Según la complejidad de la aplicación será más o menos adecuadoorganizar el código en varios bundles. El criterio a seguir es el de agrupar en cada bundlefuncionalidades similares o del mismo tipo. Los bundles son bloques desacoplados y tienenasociado un espacio de nombre. Para acoplar un bundle al framework hay que :

1. Registrar el espacio de nombre en el sistema de autocarga (fichero app/autoload.php.Este paso no es necesario si ubicamos al bundle en el directorio src.

2. Registrar al bundle en el fichero app/AppKernel.php. Esta operación se puede hacerautomáticamente a través del generador interactivo de bundles, pero si fallase poralguna razón (por ejemplo que los permisos de dicho archivo no estén bien definidos).Habría que hacerlo a mano.

3. Importar las tablas de enrutamiento del bundle en la tabla de enrutamiento de laaplicación.

Anatomía de un BundleSi has seguido las indicaciones que hemos dado en esta unidad, debes tener en tu directoriosrc dos directorios: Jazzyweb y Acme . El primero se corresponde con el bundle queacabamos de crear, y el segundo es un ejemplo que viene de serie con la distribuciónstandard de Symfony2 y que contiene el código de la demo con la que has jugado hace unrato. Vamos a utilizar este último para realizar la disección de un bundle, ya que está másrellenito de código que nuestro recien horneado y esquelético bundle.

Acme/└── DemoBundle ├── AcmeDemoBundle.php

Anatomía de un Bundle

38

Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Page 49: CursoSymfony2

├── Controller │   ├── DemoController.php │   ├── SecuredController.php │   └── WelcomeController.php ├── ControllerListener.php ├── DependencyInjection │   └── AcmeDemoExtension.php ├── Form │   └── ContactType.php ├── Resources │   ├── config │   │   └── services.xml │   ├── public │   │   ├── css │   │   │   └── demo.css │   │   └── images │   │   ├── blue-arrow.png │   │   ├── field-background.gif │   │   ├── logo.gif │   │   ├── search.png │   │   ├── welcome-configure.gif │   │   ├── welcome-demo.gif │   │   └── welcome-quick-tour.gif │   └── views │   ├── Demo │   │   ├── contact.html.twig │   │   ├── hello.html.twig │   │   └── index.html.twig │   ├── layout.html.twig │   ├── Secured │   │   ├── helloadmin.html.twig │   │   ├── hello.html.twig │   │   ├── layout.html.twig │   │   └── login.html.twig │   └── Welcome │   └── index.html.twig ├── Tests │   └── Controller │   └── DemoControllerTest.php └── Twig └── Extension └── DemoExtension.php

• AcmeDemoBundle.php es una clase que extiende aSymfony\Component\HttpKernel\Bundle\Bundle y que define al bundle. Se utiliza enel proceso de registro del mismo (recuerda, en el fichero app/AppKernel.php). Todoslos bundles deben incorporar esta clase (bueno, el nombre cambiará según el nombredel bundle)

• Controller es el directorio donde se deben colocar los controladores con las distintas acciones del bundle. Recuerda el concepto de controlador y acción que se estudió en la unidad 2. Lo lógico y recomendado, es crear una clase Controller por cada grupo de funcionalidades. Pero no es una exigencia, si quieres puedes colocar todas tus acciones en el mismo controlador. Cuando se genera un bundle se crea el controlador

Anatomía de un Bundle

39

Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Page 50: CursoSymfony2

DefaultController.• Dependency Injection. Una de las características más sobresaliente de Symfony2 es el

uso intensivo que hace de la Inyección de Dependencias, un potente patrón de diseñomediante el que se facilita la creación y configuración de objetos que prestan serviciosen una aplicación gracias a la gestión automática de sus dependencias. Contribuye acrear un código más desacoplado y coherente. La unidad 4 se ha dedicadoexclusivamente a presentar este concepto. Aunque no es un patrón complicado, esdificil de explicar con precisión y claridad.Symfony2 nos ofrece dos maneras de "cargar" la configuración de las dependencias ylos servicios creados. Una más sencilla y directa, y otra más elaborada y apropiada parael caso en que nuestro bundle vaya a ser distribuido con la intención de que se utiliceen otros proyectos Symfony2. En este directorio se ubican las clases relacionadas coneste segundo método de gestionar las dependencias.

• Resources, es decir, recursos. Entendemos por recursos: los ficheros de configuracióndel bundle (directorio config), los assets que requiere el bundle para enviar en susrespuestas (directorio public) y las plantillas con las que se renderizan (pintan) elresultado de las acciones de los controladores (directorio views). Fíjate como en estebundle, las plantillas están organizadas en tres directorios (Demo, Secured y Welcome)cuyos nombres coinciden con los de los controladores.

• Test, es el directorio donde viven las pruebas unitarias y funcionales del bundle.Estos son los directorios más típicos de cualquier bundle, de hecho son los que se generanautomáticamente con el comando app/console generate:bundle. Sin embargo un bundlepuede tener muchos más directorios y ficheros, organizados como su creador creaconveniente. En el caso del bundle AcmeDemoBundle, puedes ver los siguientes "extras":

• Form es el directorio donde se colocarán las clases que definen los formularios de laaplicación.

• ControllerListener.php describe un event listener que es un mecanismo muyadecuado de extender y alterar el flujo del framework sin tener que tocar el códigooriginal del componente del framework que utiliza dicho sistema. Se trata de unacaracterística avanzada de Symfony2 raramente utilizada cuando uno se esta iniciando.

• Twig es un directorio propio de este bundle, en el que se ha implementado unaextensión del sistema de plantillas.

Ahora ya nos encontramos con un mínimo bagaje para emprender el desarrollo del bundleJazzywebAulasMentorAlimentosBundle y, por tanto de la aplicación.

Flujo básico de creación de páginas en Symfony2La creación de páginas web con Symfony2 involucra tres pasos:

1. Creación de la ruta que mapea la URL de la página en una acción de algún controlador.Dicha ruta se registra en el archivo config/Resources/routing.yml del bundle, que asu vez debe estar correctamente importado en el archivo de rutas general de laaplicación app/config/routing.

2. Creación de dicha acción en el controlador correspondiente. La acción, haciendo uso delmodelo, realizará las operaciones necesarias y obtendrá los datos crudos (raw), es decirsin ningún tipo de formato, que facilitará a una plantilla para ser pintados(renderizados). El código de los controladores debe ubicarse en el directorioControllers del bundle.

3. Creación de dicha plantilla. Esto se hace en el directorio Resources/views. Con el fin de organizar bien las plantillas, es recomendable crear un directorio con el nombre de

Flujo básico de creación de páginas en Symfony2

40

Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Page 51: CursoSymfony2

cada controlador. También es muy recomendable utilizar Twig como sistema deplantillas, aunque también se puede utilizar PHP.

Como puedes comprobar el procedimiento no es muy diferente al que hemos estudiado en launidad anterior.Estos pasos son, por supuesto, una guía general y mínima que debemos seguir en lacreación de las páginas de nuestra aplicación. No obstante, en muchos casos tendremos querealizar otras operaciones que se salen de este flujo y que tienen que ver más con laconstrucción del modelo de la aplicación.

Definición de las rutas del bundleEn la unidad anterior propusimos las siguientes rutas para acceder a las distintas partes dela aplicación:

URL Acciónhttp://tu.servidor/alimentos/index.php?ctl=inicio mostrar pantalla iniciohttp://tu.servidor/alimentos/index.php?ctl=listar listar alimentoshttp://tu.servidor/alimentos/index.php?ctl=insertar insertar un alimentohttp://tu.servidor/alimentos/index.php?ctl=buscar buscar alimentoshttp://tu.servidor/alimentos/index.php?ctl=ver&id=x ver el alimento x

Ahora vamos a volver a definirlas usando el potente sistema de rutas de Symfony2.Comencemos con una observación. El usuario que utiliza el navegador "siente" que la URLque aparece en la barra de direcciones forma parte de la aplicación que está utilizando. Portanto, cualquier URL llena de carácteres extraños y demasiado larga redunda en unadegradación estética. Más allá de estos problemas estéticos, cuando utilizamos query stringsclásicas, es decir, del tipo: ?param1=val1&param2=val2&...&paramN=valN, estamos dandoinformación innecesaria al usuario, ya que el nombre de los parámetros (paramX) es algo quetiene sentido únicamente para la aplicación en el servidor. Esta información extra, ademásde dar lugar a URL's horribles, supone un problema de seguridad, ya que el usuario podríautilizarlas para sacar conclusiones acerca de la aplicación que está en el servidor.El sistema de Routing de Symfony2 nos ofrece la posibilidad de utilizar auténticas rutas paralas URL's de nuestra aplicación, es decir, rutas que sólo utilizan el carácter / comoseparador. Además los nombre de los parámetros que reciben los datos en el servidor, noaparecen en las rutas. Únicamente aparece el valor de dichos parámetros. Las URL's asíconstruidas identifican más elegantemente los recursos del servidor, además de no dar másinformación de la estrictamente necesaria.Además, si utilizamos el módulo Rewrite del servidor web, podemos eliminar de las URL's elnombre del controlador frontal (app.php es el nombre que le da la distribución standard deSymfony2 por defecto). En cuyo caso, además de mejorar el estilo de la URL, ocultamos alusuario información acerca del lenguaje de programación que estamos utilizando en elservidor. Nos quedarían URL's de este tipo:

http://tu.servidor/http://tu.servidor/listarhttp://tu.servidor/ver/4

¡Mucho más legibles y elegantes!

Definición de las rutas del bundle

41

Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Page 52: CursoSymfony2

Nota

En el directorio web existe un fichero .htaccess con el siguiente contenido:

<IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ app.php [QSA,L]</IfModule>

La función de dicho fichero es, precisamente, reescribir las rutas anteponiendoapp.php, de manera que no sea necesario especificar el controlador frontal en la URL.Para que esto funcione es necesario que el servidor web tenga instalado el móduloRewrite, y permita el cambio de directivas a través de ficheros .htaccess.

Por otro lado, para definir la ruta no es necesario especificar la URL completa. De hecho, elsentido de ruta para Symfony2 es la parte del URL a partir del nombre del controladorfrontal. O si se ha eliminado este gracias al uso del módulo Rewrite, la parte de la URLdetrás del dominio.La tabla anterior, con las rutas de Symfony2 quedaría así:

URL Acción/ mostrar pantalla inicio/listar listar alimentos/insertar insertar un alimento/buscar buscar alimentos/ver/x ver el alimento x

En Symfony2 las rutas se definen en el archivo app/config/routing.yml. Para que losbundles no pierdan la autonomía que debe caracterizarlos, las rutas que se mapean en uncontrolador de un determinado bundle deberían definirse dentro del propio bundle.Concretamente en el archivo Resources/config/routing.yml del bundle. Y para hacerlasdisponibles a la aplicación, se importa este último fichero en app/config/routing.yml.

Nota

Aunque el sitio recomendado para ubicar el fichero routing.yml de un bundle esResources/config, Symfony2 no lo exige, ya que en el archivoapp/config/routing.yml, que es el que realmente define las rutas, puedes indicar laruta concreta de los archivos que se quieren importar.

Abre el archivosrc/Jazzyweb/AulasMentor/AlimentosBunle/Resources/config/routing.yml y borra lassiguientes líneas:

Definición de las rutas del bundle

42

Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Page 53: CursoSymfony2

JazzywebAulasMentorAlimentosBundle_homepage: pattern: /hello/{name} defaults: { _controller: JazzywebAulasMentorAlimentosBundle:Default:index }

Las líneas que acabas de borrar definían la ruta de la acción de ejemplo que se creaautomáticamente al generar el bundle. Fíjate en la estructura de la definición de una ruta;consisten en un identificador de la ruta (JazzywebAulasMentorAlimentosBundle_homepage),que puede ser cualquiera siempre que sea único en todo el framework, el patrón de la ruta(pattern: /hello/{name}), que describe la estructura de la ruta, y la declaración delcontrolador sobre el que se mapea la ruta (defaults: { _controller:JazzywebAulasMentorAlimentosBundle:Default:index }).Creamos nuestra primera ruta añadiendo al archivo anterior lo siguiente:

JAMAB_homepage: pattern: / defaults: { _controller: JazzywebAulasMentorAlimentosBundle:Default:index }

Como el nombre de la ruta debe ser único en toda la aplicación, es una buena prácticanombrarlas anteponiendo un prefijo con el nombre del bundle, o con algo que lo identifique.Como el nombre de nuestro bundle es muy largo, hemos optado por usar como prefijo lassiglas JAMAB.Una vez definida la ruta debemos implementar la acción del controlador especificada en lamisma, es decir JazzywebAulasMentorAlimentosBundle:Default:index.

Nota

Fíjate en el patrón que se utiliza para especificar la acción del controlador:JazzywebAulasMentorAlimentosBundle:Default:index. A esto se le llama enSymfony2 un nombre lógico. Está compuesto por el nombre del bundle, el nombre delcontrolador, y el nombre de la acción separados por el caracter :. En este caso, elnombre lógico hace referencia a el método indexAction() de una clase PHP llamadaJazzyweb\AulasMentor\AlimentosBundle\Controller\DefaultController. Esdecir, hay que añadir el sufijo Controller al nombre del controlador, y el sufijoAction al nombre de la acción.

Creación de la acción en el controladorEditamos el ficherosrc/Jazzyweb/AulasMentor/AlimentosBundle/Controller/DefaultController.php, yreescribimos el método indexAction():

1 <?php 2 3 namespace Jazzyweb\AulasMentor\AlimentosBundle\Controller; 4 5 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 7 class DefaultController extends Controller 8 {

Creación de la acción en el controlador

43

Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Page 54: CursoSymfony2

9 10 public function indexAction()11 {12 $params = array(13 'mensaje' => 'Bienvenido al curso de Symfony2',14 'fecha' => date('d-m-yy'),15 );16 17 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:index.html.twig',18 $params);19 }20 }

Analicemos el código anterior. La clase DefaultController "vive" en el espacio de nombresJazzyweb\AulasMentor\AlimentosBundle\Controller, por lo que su nombre completo esJazzyweb\AulasMentor\AlimentosBundle\Controller\DefaultController. La claseextiende de Symfony\Bundle\FrameworkBundle\Controller\Controller, la cual formaparte de Symfony2 y, aunque no es necesario que nuestros controladores deriven de dichaclase, si lo hacemos nos facilitará mucho la vida, ya que esta clase base cuenta con potentesherramientas para trabajar con Symfony2. Posiblemente la más útil sea el Contenedor deDependencias tambien conocido como Contenedor de Servicios, con el que podemos obtenerfácilmente instancias bien configuradas de cualquier servicio del framework, tanto de losincluidos en la distribución estándard, como de los que nosotros creemos o de los que seañadan en las extensiones de terceros (vendors) que podamos instalar. Quédate tranquilocon esto de los servicios pues será un tema que abordaremos más adelante. Por lo pronto essuficiente con que sepas que los servicios son objetos ofrecidos por el framework pararealizar determinadas tareas (como por ejemplo enviar emails o manipular una base dedatos).

Nota

Sobre los espacios de nombre de PHP 5.3 Si en la línea 7 se utiliza únicamente elnombre Controller en lugar del nombre completoSymfony\Bundle\FrameworkBundle\Controller\Controller, es por quepreviamente, en la línea 5, se ha indicado en el archivo que se va a utilizar la claseController de dicho espacio de nombre.

El método indexAction() es una acción en el mismo sentido que ya explicamos en launidad 2, es decir, es un método que está mapeado en una URL a través de una ruta (o tablade rutas), es decir de un fichero routing.yml. En esencia es el mismo que su homólogo dela unidad 2: define un array asociativo con los datos "crudos" (raw) mensaje y fecha, y selos pasa a una plantilla para que los pinte. Esto último se hace en la línea 17 utilizando elmétodo render de la clase padreSymfony\Bundle\FrameworkBundle\Controller\Controller. Este método recibe dosargumentos, el primero es el nombre lógico de la plantilla que se desea utilizar, y el segundoes un array asociativo con los datos. Las acciones terminan con la devolución de un objetoResponse. Precisamente, el método render convierte una plantilla en un objeto de este tipo.El método render es uno de los servicios disponibles en el framework y accesible desde cualquier clase que derive de Symfony\Bundle\FrameworkBundle\Controller\Controller. Es un servicio que usaremos hasta la saciedad. El nombre lógico de una plantilla, es similar al nombre lógico de un controlador; esta compuesto por el nombre del bundle, el nombre del directorio que aloja a la plantilla en el directorio Resources/views (que suele coincidir con el nombre del controlador, en este caso Default), y el nombre del archivo que implementa

Creación de la acción en el controlador

44

Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Marina
Resaltado
Page 55: CursoSymfony2

la plantilla (en este caso index.html.twig). Es decir que el nombre lógico:JazzywebAulasMentorAlimentosBundle:Default:index.html.twig, hace referencia alarchivosrc/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/Default/index.html.twig.

Creación de la plantillaSiguiendo los pasos para la creación de una página en Symfony2, lo siguiente que tenemosque hacer es crear la plantilla. Edita el ficherosrc/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/Default/index.html.twigcon el siguiente contenido:

1 <h1>Inicio</h1>2 <h3> Fecha: {{fecha}} </h3>3 {{mensaje}}

Aunque Symfony2 permite el uso de PHP como sistema de plantillas, en este cursoutilizaremos Twig, que es lo recomendado oficialmente. El código anterior es una plantillatwig. Fíjate que es muy parecida a su homóloga de la unidad 2 (inicio.php). La informaciónque se pinta y el formato HTML son identicos. Pero hay que reconocer que la plantilla twig esmucho más limpia.En twig, el contenido dinámico, es decir, los datos "crudos" que le son pasados desde elcontrolador (segundo argumento del método render en la acción indexAction()), sereferencian con dobles llaves ({{ dato }}). En el ejemplo anterior {{ fecha }} hacereferencia al elemento fecha del array construido en el controlador, y {{ mensaje }}, comoya has deducido, al elemento mensaje de dicho array.Pues con esto hemos terminado. Vamos a probar lo que acabamos de hacer. Introduce en labarra de direcciones de tu navegador la URL correspondiente a la ruta que acabamos decrear. Utiliza el controlador de desarrollo:

http://localhost/Symfony/web/app_dev.php/

¡Vaya! parece que nada de lo que hemos hecho ha funcionado. Vuelve a aparecer laaplicación demo de Symfony2.Ahora prueba con el controlador de producción:

http://localhost/Symfony/web/app.php/

¡Ahora si! Vemos la pantalla de inicio de nuestro bundle. Pero entonces, ¿qué estápansando? las rutas tienen distinto sentido según el controlador frontal que usemos. ¿Porqué?. La respuesta a este comportamiento se encuentra en las distintas configuraciones quese cargan en función del entorno de ejecución. Cuando utilizamos el controlador frontal dedesarrollo app_dev.php, se carga el fichero de routing app/config/routing_dev.php. Si leechas un vistazo al fichero verás que comienza con la siguiente ruta:

_welcome: pattern: / defaults: { _controller: AcmeDemoBundle:Welcome:index }

La cual colisiona con la que nosotros hemos creado, ya que el patrón de la ruta es el mismo: /. El sistema de enrutamiento de Symfony2 va leyendo todas las rutas y cuando encuentra una que coincide con la URL que se ha pedido, ejecuta la acción asociada. No sigue leyendo

Creación de la plantilla

45

Marina
Resaltado
Page 56: CursoSymfony2

más rutas. Por eso, si en un mismo proyecto hay dos rutas, o más precisamente, dospatrones de rutas que coincidan, se ejecutará la primera que se encuentre. Atención por queno se producirá ningún error. Esto hay que tenerlo muy en cuenta cuando se desarrolla conSymfony2 para evitarnos algún que otro dolor de cabeza.En el caso del controlador frontal de producción, el framework lee el fichero routing.yml, yaque no existe routing_prod.yml. Mira el fichero y podrás comprobar que no hay ningunaruta que colisione con la que nosotros hemos definido. Por tanto todo está bien y se ejecutala acción correcta.Una vez que sabemos las causas del problema, si queremos que el controlador de desarrollocargue la ruta de nuestro bundle, cualquier solución que propongamos pasa por evitar lacolisión entre rutas. Y para ello podemos hacer varias cosas:

1. Deshabilitar el plugin AcmeDemoBundle y sus rutas.2. Cambiar el patrón de las rutas del plugin AcmeDemoBundle, anteponiendole a todas

ellas un prefijo (acme, por ejemplo)3. Cambiar el patrón de las rutas del Jazzyweb/AulasMentorAlimentosBundle,

anteponiendole a todas ellas un prefijo (alimentos, por ejemplo)Con el fin de ilustrar una carácteristica del sistema de routing, hemos optado por la 3ªsolución. Podemos añadir un prefijo a todas las rutas del bundle sin más que cambiar elparámetro prefix en la ruta importada en el archivo app/config/routing.yml:

JazzywebAulasMentorAlimentosBundle: resource: "@JazzywebAulasMentorAlimentosBundle/Resources/config/routing.yml" prefix: /alimentos

Ahora, para ver la página de inicio de nuestro bundle, apuntamos nuestro navegador a:

http://localhost/Symfony/web/app_dev.php/alimentos

Y ya está! A partir de ahora todas las rutas de nuestro bundle llevarán el prefijo alimentosdelante.

¡Atención!

Como hemos cambiado un fichero de configuración, para que el cambio se hagaefectivo en el entorno de producción hay que borrar la caché con el siguientecomando:

# app/console cache:clear --env=prod

Decoración de la plantilla con un layoutTe habrás dado cuenta que hemos pintado un bloque HTML incompleto. Si no te haspercatado de ello mira el código fuente HTML que llega al navegador. Nos falta someter a laplantilla al proceso de decoración que hemos estudiado en la unidad 2, mediante el cual sele añade funcionalidad. En el caso de la aplicación de gestión de alimentos hay que añadir lacabecera con el menú, el pie de página y los estilos.

Decoración de la plantilla con un layout

46

Page 57: CursoSymfony2

El sistema de plantillas twig, está provisto de un mecanismo de herencia gracias al cual ladecoración de plantillas resulta de una flexibilidad y versatilidad total. Podemos hacercualquier cosa que nos imaginemos, como por ejemplo fragmentar la vista en distintasplantillas organizadas por criterios funcionales, y combinarlas para producir la vistacompleta. Podemos colocar en una un menú, en otra un pie de página, en otra la estructurabásica del documento HTML, otra puede pintar un listado de twits, etcétera.La herencia es un mecanismo típico de la programación orientada a objetos mediante el queun componente software hereda todas las funcionalidades de otro y puede extenderlas y/ocambiarlas. Es exactamente esto lo que ocurre cuando una plantilla twig hereda de otra.En twig la herencia se implementa mediante el concepto de bloque. En las plantillaspodemos delimitar bloques que comienzan con un {% block nombre_bloque %} y finalizancon {% endblock %}. Las plantillas heredan todas las funcionalidades de las plantillas queextienden y pueden cambiar el código de los bloques heredadados. Como siempre unejemplo vale más que mil palabras.Fíjate en el fichero app/Resources/views/base.html.twig que viene de serie en ladistribución standard de Symfony2:app/Resources/views/base.html.twig

1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 <title>{% block title %}Welcome!{% endblock %}</title> 6 {% block stylesheets %}{% endblock %} 7 <link rel="shortcut icon" href="{{ asset('favicon.ico') }}" /> 8 </head> 9 <body>10 {% block body %}{% endblock %}11 {% block javascripts %}{% endblock %}12 </body>13 </html>

Representa la estructura básica de un documento HTML. Y presenta varios bloques: title,stylesheets, body y javascripts. Esta plantilla es ofrecida por Symfony2 para que sirvade ejemplo. Pero puede utilizarse como plantilla básica de casi cualquier aplicación web.Vamos a modificar nuestra plantilla index.html.twig para que la herede (o para que laextienda, son dos maneras de decir lo mismo):src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/Default/index.html.twig

1 {% extends '::base.html.twig' %}2 3 {% block body %}4 5 <h1>Inicio</h1>6 <h3> Fecha: {{fecha}} </h3>7 {{mensaje}}8 9 {% endblock %}

En la línea 1 se indica la herencia de la plantilla base. Esto significa que la plantilla JazzywebAulasMentorAlimentosBundle:Default:index.html.twig asume todo el contenido de la plantilla ::base.html.twig. Pero además se modifica el contenido del

Decoración de la plantilla con un layout

47

Page 58: CursoSymfony2

bloque body con las líneas 5-7.Si además queremos modificar el bloque title, no tenemos más que añadirlo en nuestraplantilla index.html.twig:src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/Default/index.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block title %} 4 Bienvenido a la aplicación alimentos 5 {% endblock %} 6 7 {% block body %} 8 9 <h1>Inicio</h1>10 <h3> Fecha: {{fecha}} </h3>11 {{mensaje}}12 13 {% endblock %}

Ahora, en la sección <title> del documento se pintará: Bienvenido a la aplicaciónalimentos en lugar de Welcome.Puedes probar a recargar la página a través de la URL:

http://localhost/Symfony/web/app_dev.php/alimentos/

Aunque el aspecto de la página es el mismo que antes, si ves el código fuente HTML en elnavegador, comprobarás que el documento está completo, es decir, con todas sus etiquetasHTML. También puedes comprobar que, al utilizar el controlador frontal de desarrollo,aparece en la parte de abajo de la página la barra de depuración de Symfony2.

Nota

Recuerda el concepto de nombre lógico de una plantilla. Y fíjate en el nombre lógicode la plantilla ::base.html.twig. Como no pertenece a ningún bundle (es común a laaplicación), y está úbicada directamente en el directorio views, no lleva nada ni antesdel primer : ni del segundo.

La herencia de plantillas puede llevarse a cabo a varios niveles, esto es, una plantilla puedeheredar de otra plantilla que a su vez hereda de otra plantilla, etcétera. No obstante no serecomienda llevar a cabo muchos niveles de herencia, ya que puede llegar a ser bastanteconfuso e incontrolable. La estrategia que recomiendan los creadores de Symfony2 es usartres niveles de herencia:

• en el primer nivel se colocan la estructura básica del documento HTML, se correspondecon lo que hace la plantilla ::base.html.twig,

• en el segundo se colocan los elementos específicos de cada sección del sitio, porejemplo el menú de la sección,

• y en el tercero se reserva para los elementos propios de la acción, se corresponde connuestra plantilla JazzywebAulasMentorAlimentosBundle:Default:index.html.twig

Decoración de la plantilla con un layout

48

Page 59: CursoSymfony2

Tan sólo nos falta incluir los menús que serán comunes a todas las páginas de la aplicación.Seguiremos la estrategia de tres niveles de herencia que acabamos de exponer. Creamos laplantilla genéral JazzywebAulasMentorAlimentosBundle::layout.html.twig. Según lalógica de los nombres lógicos, esta se debe ubicar en:src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/layout.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block body %} 4 <div id="cabecera"> 5 <h1>Información de alimentos</h1> 6 </div> 7 8 <div id="menu"> 9 <hr/>10 <a href="{{ path('JAMAB_homepage')}}">inicio</a> |11 <a href="#">ver alimentos</a> |12 <a href="#">insertar alimento</a> |13 <a href="#">buscar por nombre</a> |14 <a href="">buscar por energia</a> |15 <a href="">búsqueda combinada</a>16 <hr/>17 </div>18 19 <div id="contenido">20 {% block contenido %}21 22 {% endblock %}23 </div>24 25 <div id="pie">26 <hr/>27 <div align="center">- pie de página -</div>28 </div>29 30 {% endblock %}

Nota

En la línea 10 hemos usado la función path de twig para construir la URL's del menú.Está función recibe como argumento el nombre de la ruta cuya URL se desea calcular.Únicamente la hemos usado en el primer enlace del menú, pués, por ahora, es laúnica ruta que hemos definido.

Ahora es esta plantilla la que extiende a la plantilla base, por tanto, habrá que cambiar laplantilla JazzywebAulasMentorAlimentosBundle:Default:index.html.twig para queextienda de JazzywebAulasMentorAlimentosBundle::layout.html.twig, y para queredefina el bloque contenido de esta última. Quedaría así:

Decoración de la plantilla con un layout

49

Page 60: CursoSymfony2

1 {% extends 'JazzywebAulasMentorAlimentosBundle::layout.html.twig' %}2 3 {% block contenido %}4 5 <h1>Inicio</h1>6 <h3> Fecha: {{fecha}} </h3>7 {{mensaje}}8 9 {% endblock %}

Vuelve a probar la página. Ya sólo nos falta incorporarle estilos CSS's para que tenga lamisma pinta que en la unidad 2.

Instalación de los assets de un bundleYa hemos dicho que un bundle es un directorio que aloja todo aquello relativo a unafuncionalidad determinada. Puede incluir clases PHP, plantillas, configuraciones, CSS’s yjavascripts.Cuando los bundles incluyen assets, es decir archivos que no son procesados por PHP y sonservidos directamente por el servidor web (CSS's, javascripts e imágenes son los assets máshabituales), estos deben ser copiados dentro del directorio web del proyecto o enlazadosdesde dicho directorio, ya que es ahí únicamente donde el servidor web puede acceder enbusca de archivos (suponiendo que lo hemos configurado correctamente para un entorno deproducción).Por otro lado en un bundle los assets deben ser ubicados en el directorio Resources/public.Si lo examinas verás que tiene la siguiente estructura:

Resources└─ public   ├── css   ├── images   └── js

Se ha reservado un directorio para cada tipo de asset. Copia el archivo estilos.css de launidad 2 en el directorio que le corresponde; Resources/public/css. Para que el servidorweb la pueda cargar, se utiliza el siguiente comando de consola:

php app/console assets:install web --symlink

La función de este comando es realizar una copia o un enlace simbólico (si se especifica laopión --symlink, aunque en la plataforma Windows esto último no es posible) del contenidode los directorios Resouces/public de todos los bundles que se encuentren registrados enel framework. El comando requiere un argumento (web en nuestro caso), que especifica eldirectorio donde se realizará la copia o el enlace simbólico.Dicha copia o enlazado se organiza de la siguiente manera:

web├─ nombre_bundle_1| ├── css| ├── images| └── js

Instalación de los assets de un bundle

50

Page 61: CursoSymfony2

├─ nombre_bundle_2| ├── css| ├── images| └── js...└─ nombre_bundle_N ├── css ├── images └── js

Ya sólo falta incluir una referencia en el código HTML a la CSS que acabamos de incorporar.Aunque es posible incluir el enlace a la CSS directamente en la plantilla ::base.html.twig,el lugar correcto es en la plantillaJazzywebAulasMentosAlimentosBundle::layout.html.twig. Teniendo en cuenta lo quehemos explicado acerca del mecanismo de herencia, habría que añadir un bloquestylesheets (heredado de la plantilla padre ::base.html.twig), en el que se hagareferencia al archivo CSS.src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/layout.html.twig

...{% block stylesheets %} <link href="{{ asset('bundles/jazzywebaulasmentoralimentos/css/estilo.css') }}" type="text/css" rel="stylesheet" />{% endblock %}...

En este código hemos utilizado la función de twig asset, la cual crea la URL correcta queapunta al asset en cuestion. La ruta que toma como argumento la función asset seespecifica tomando como raíz el directorio web.

Nota

Puedes colocar el bloque stylesheets delante o detrás del bloque body.

Recarga la página y la verás con los estilos aplicados.

Implementamos el resto de la aplicaciónSiguiendo estos tres pasos: enrutar, crear código de la acción (controlador) y crear laplantilla, podemos completar lo que nos falta de la aplicación. No obstante, en las accionesque faltan, se necesita acceder a la base de datos para recuperar, modificar y crearalimentos. Recuerda que esto se hacía en la unidad 2 utilizando una clase denominadaModel. La distribución standard de Symfony2 proporciona un potente servicio para el accesoa los datos persistentes, es decir, los que se almacenan en algún tipo de base de datos. Perono obliga a utilizarlo. No solo eso, tampoco forma parte del núcleo de Symfony2, es decir, noes un componente. Por ello es una decisión del desarrollador utilizarlo o no. Es en esesentido que Fabien Potencier, lider del proyecto Symfony2, proclama que este último no esun framework MVC, ya que no dice nada sobre como debes construir tu modelo.Aunque lo recomendable es utilizar Doctrine2 (que es el servicio de persistencia que vieneincorporado en la distribución standard), o Propel, en esta unidad no los vamos a utilizar porlas siguientes razones:

1. Ya llevamos muchos conceptos introducidos y no queremos sobrecargar la unidad. Másadelante estudiaremos y usaremos Doctrine2.

Implementamos el resto de la aplicación

51

Page 62: CursoSymfony2

2. Queremos ilustrar como podemos construir el modelo a nuestro antojo, y comopodemos reutilizar código fácilmente cuando lo tenemos bien organizado bajo laspautas del patrón MVC.

Así pues, antes de implentar el resto de las acciones que componen la aplicación, vamos aelaborar el modelo.Crea un directorio denominado Model (el nombre puede ser cualquiera), y copia ahí elfichero Model.php de la unidad 2. Incorpora la clase Model al espacio de nombres delbundle añadiendo al principio del fichero: namespaceJazzyweb\AulasMentor\AlimentosBundle\Model. Nos quedaría así:src/Jazzyweb/AulasMentor/AlimentosBundle/Model/Model.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\AlimentosBundle\Model; 4 5 class Model 6 { 7 protected $conexion; 8 9 public function __construct($dbname,$dbuser,$dbpass,$dbhost)10 {11 $mvc_bd_conexion = mysql_connect($dbhost, $dbuser, $dbpass);12 13 if (!$mvc_bd_conexion) {14 die('No ha sido posible realizar la conexión con la base de datos: '15 . mysql_error());16 }17 mysql_select_db($dbname, $mvc_bd_conexion);18 19 mysql_set_charset('utf8');20 21 $this->conexion = $mvc_bd_conexion;22 }23 24 25 public function bd_conexion()26 {27 28 }29 30 public function dameAlimentos()31 {32 $sql = "select * from alimentos order by energia desc";33 34 $result = mysql_query($sql, $this->conexion);35 36 $alimentos = array();37 while ($row = mysql_fetch_assoc($result))38 {39 $alimentos[] = $row;40 }41 42 return $alimentos;

Implementamos el resto de la aplicación

52

Page 63: CursoSymfony2

43 }44 45 public function buscarAlimentosPorNombre($nombre)46 {47 $nombre = htmlspecialchars($nombre);48 49 $sql = "select * from alimentos where nombre like '" . $nombre . "' order50 by energia desc";51 52 $result = mysql_query($sql, $this->conexion);53 54 $alimentos = array();55 while ($row = mysql_fetch_assoc($result))56 {57 $alimentos[] = $row;58 }59 60 return $alimentos;61 }62 63 public function dameAlimento($id)64 {65 $id = htmlspecialchars($id);66 67 $sql = "select * from alimentos where id=".$id;68 69 $result = mysql_query($sql, $this->conexion);70 71 $alimentos = array();72 $row = mysql_fetch_assoc($result);73 74 return $row;75 76 }77 78 public function insertarAlimento($n, $e, $p, $hc, $f, $g)79 {80 $n = htmlspecialchars($n);81 $e = htmlspecialchars($e);82 $p = htmlspecialchars($p);83 $hc = htmlspecialchars($hc);84 $f = htmlspecialchars($f);85 $g = htmlspecialchars($g);86 87 $sql = "insert into alimentos (nombre, energia, proteina, hidratocarbono,88 fibra, grasatotal) values ('" .89 $n . "'," . $e . "," . $p . "," . $hc . "," . $f . "," . $g . ")";90 91 $result = mysql_query($sql, $this->conexion);92 93 return $result;94 }95 96 }

El próximo paso es adaptar el código del controlador de la unidad 2. Se trata de añadir elsufijo Action a todas las acciones, y en utilizar el servicio de renderizado de Symfony2 parainvocar a las plantillas. El código del controlador DefaultController quedaría así:src/Jazzyweb/AulasMentor/AlimentosBundle/Controller/DefaultController.php

Implementamos el resto de la aplicación

53

Page 64: CursoSymfony2

1 <?php 2 3 namespace Jazzyweb\AulasMentor\AlimentosBundle\Controller; 4 5 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 use Jazzyweb\AulasMentor\AlimentosBundle\Model\Model; 7 use Jazzyweb\AulasMentor\AlimentosBundle\Config\Config; 8 9 class DefaultController extends Controller 10 { 11 12 public function indexAction() 13 { 14 $params = array( 15 'mensaje' => 'Bienvenido al curso de Symfony2', 16 'fecha' => date('d-m-yy'), 17 ); 18 19 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:index.html.twig', 20 $params); 21 } 22 23 public function listarAction() 24 { 25 $m = new Model(Config::$mvc_bd_nombre, Config::$mvc_bd_usuario, 26 Config::$mvc_bd_clave, Config::$mvc_bd_hostname); 27 28 $params = array( 29 'alimentos' => $m->dameAlimentos(), 30 ); 31 32 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:mostrarAlimentos.html.twig', 33 $params); 34 35 } 36 37 public function insertarAction() 38 { 39 $params = array( 40 'nombre' => '', 41 'energia' => '', 42 'proteina' => '', 43 'hc' => '', 44 'fibra' => '', 45 'grasa' => '', 46 ); 47 48 $m = new Model(Config::$mvc_bd_nombre, Config::$mvc_bd_usuario, 49 Config::$mvc_bd_clave, Config::$mvc_bd_hostname); 50 51 if ($_SERVER['REQUEST_METHOD'] == 'POST') { 52 53 // comprobar campos formulario 54 if ($m->insertarAlimento($_POST['nombre'], $_POST['energia'],

55 $_POST['proteina'], $_POST['hc'], $_POST['fibra'], $_POST['grasa'])) { 56 $params['mensaje'] = 'Alimento insertado correctamente'; 57 } else { 58 $params = array( 59 'nombre' => $_POST['nombre'], 60 'energia' => $_POST['energia'], 61 'proteina' => $_POST['proteina'], 62 'hc' => $_POST['hc'], 63 'fibra' => $_POST['fibra'], 64 'grasa' => $_POST['grasa'], 65 ); 66 $params['mensaje'] = 'No se ha podido insertar el alimento. Revisa el formulario'; 67 }

68 } 69

Implementamos el resto de la aplicación

54

Page 65: CursoSymfony2

70 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:formInsertar.html.twig', 71 $params); 72 73 } 74 75 public function buscarPorNombreAction() 76 { 77 $params = array( 78 'nombre' => '', 79 'resultado' => array(), 80 ); 81 82 $m = new Model(Config::$mvc_bd_nombre, Config::$mvc_bd_usuario, 83 Config::$mvc_bd_clave, Config::$mvc_bd_hostname); 84 85 if ($_SERVER['REQUEST_METHOD'] == 'POST') { 86 $params['nombre'] = $_POST['nombre']; 87 $params['resultado'] = $m->buscarAlimentosPorNombre($_POST['nombre']); 88 } 89 90 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:buscarPorNombre.html.twig', 91 $params); 92 93 } 94 95 public function verAction() 96 { 97 if (!isset($_GET['id'])) { 98 throw new Exception('Página no encontrada'); 99 }100 101 $id = $_GET['id'];102 103 $m = new Model(Config::$mvc_bd_nombre, Config::$mvc_bd_usuario,104 Config::$mvc_bd_clave, Config::$mvc_bd_hostname);105 106 $alimento = $m->dameAlimento($id);107 108 $params = $alimento;109 110 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:verAlimento.html.twig',111 $params);112 113 }114 115 }

Para que podamos utilizar la clase Model en el controlador sin necesidad de referirnos a ellapor su nombre completo, Jazzyweb\AulasMentor\AlimentosBundle\Model\Model, hemosutilizado (línea 6) la directiva use de PHP 5.3 en dicho fichero. Así podemos utilizar la claseModel directamente en el controlador.Crea el directorio src/Jazzyweb/AulasMentor/AlimentosBundle/Config, y coloca ahí elfichero Config.php de la unidad 2. Añádelo al espacio de nombresJazzyweb\AulasMentor\AlimentosBundle\Config. Este fichero es usado por el controladoranterior para inicializar el modelo (el objeto Model). En la línea7 puedes ver que hemosañadido el espacio de nombre de esta clase.src/Jazzyweb/AulasMentor/AlimentosBundle/Config/Config.php

<?php

namespace Jazzyweb\AulasMentor\AlimentosBundle\Config;

class Config{ static public $mvc_bd_hostname = "localhost";

Implementamos el resto de la aplicación

55

Page 66: CursoSymfony2

static public $mvc_bd_nombre = "alimentos"; static public $mvc_bd_usuario = "root"; static public $mvc_bd_clave = "root"; static public $mvc_vis_css = "estilo.css";}

Como puedes ver hemos comenzado por el 2º paso del flujo básico de desarrollo de páginascon Symfony2 es decir, escribir el controlador. En realidad el orden no importa mucho; alfinal hay que tener los tres pasos resueltos antes de que funcione. Así que vamos a por elprimer paso: definir las rutas. Esto lo hacemos editando el ficherosrc/Jazzyweb/AulasMentor/AlimentosBundle/Resources/config/routing.yml yplasmando ahí la tabla de rutas. Recuerda:

URL Acción/ mostrar pantalla inicio/listar listar alimentos/insertar insertar un alimento/buscar buscar alimentos/ver/x ver el alimento x

El archivosrc/Jazzyweb/AulasMentor/AlimentosBundle/Resources/config/routing.yml quedaasí:

JAMAB_homepage: pattern: / defaults: { _controller: JazzywebAulasMentorAlimentosBundle:Default:index }

JAMAB_listar: pattern: /listar defaults: { _controller: JazzywebAulasMentorAlimentosBundle:Default:listar }

JAMAB_insertar: pattern: /insertar defaults: { _controller: JazzywebAulasMentorAlimentosBundle:Default:insertar }

JAMAB_buscar: pattern: /buscar defaults: { _controller: JazzywebAulasMentorAlimentosBundle:Default:buscarPorNombre }

JAMAB_ver: pattern: /ver/{id} defaults: { _controller: JazzywebAulasMentorAlimentosBundle:Default:ver }

La última ruta (JAMAB_ver) utiliza una funcionalidad muy interesante del sistema de Routingde Symfony2 que se utiliza continuamente. Se trata de introducir en la propia ruta losparámetros que se pasarán por GET al servidor web. Los valores encerrados entre llaves, ennuestro caso {id}, se denominan placeholders. El sistema de Routing parsea las URL's quecoincidan con la ruta y asigna el valor que venga en la posición de cada placeholder a unavariable denominada con el nombre especificado entre las llaves. Veámoslo con un ejemplo.La siguiente ruta:

http://localhost/Symfony/web/app_dev.php/alimentos/ver/5

Implementamos el resto de la aplicación

56

Page 67: CursoSymfony2

Coincide con la ruta JAMAB_ver (recuerda que a todas las rutas del bundle les hemoscolocado el prefijo alimentos). El sistema de Routing, al parsearla, asignará al objetoRequest de Symfony2 una variable denominada id, con un valor 5. Además, esta variable sepasará como argumento al controlador especificado en la ruta, en nuestro caso aJazzywebAulasMentorAlimentosBundle:Default:ver. Se consigue, además de usar URL'selegantes en la que sólo se utiliza el caracter /, eliminar el nombre de las variables de laquery string, ocultando información que no es necesaria para el cliente.Symfony2 mapea esta ruta en una acción llamada verAction($id) a la que se le pasa elargumento id. Vamos a cambiar la acción verAction() para que su código sea máscorrecto y symfónico:src/Jazzyweb/AulasMentor/AlimentosBundle/Controller/DefaultController.php

1 <?php 2 ... 3 public function verAction($id) 4 { 5 $m = new Model(Config::$mvc_bd_nombre, Config::$mvc_bd_usuario, 6 Config::$mvc_bd_clave, Config::$mvc_bd_hostname); 7 8 $alimento = $m->dameAlimento($id); 9 10 if(!$alimento)11 {12 throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();13 }14 15 $params = $alimento;16 17 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:verAlimento.html.twig', $params);18 19 }20 ...

En la línea 3 hemos introducido un argumento para recoger la variable creada por el sistemade Routing, y en las líneas 9-12 hemos utilizado las excepciones de Symfony2 para tratar elcaso de que el registro no exista. Fíjate que de esta manera no necesitamos utilizar lavariable superglobal $_GET de PHP.

Nota

En lugar del nombre completo\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException(),puedes utilizar AccessDeniedHttpException(), si referencias el espacio de nombre alprincipio del fichero mediante la directiva use.

Nota

Las acciones buscarPorNombreAction y insertarAction, hacen uso de la variable global de PHP $_POST. Esto es una mala práctica en Symfony2, ya que en su lugar se debe utilizar el objeto Request del framework, que es una abstracción de la petición (request) HTTP en la que se han "limpiado" los valores de sus atributos de posibles cadenas potencialmente peligrosas (código malicioso). Será la primera y ultima vez que haremos esto. Sirva como ejemplo de que el hecho de utilizar un framework ayuda pero no es suficiente para generar un código de calidad. Es el programador

Implementamos el resto de la aplicación

57

Page 68: CursoSymfony2

quien, conociendo y aplicando las buenas prácticas de programación, produce unbuen código.

Y ahora a por las plantillas. Igual que hemos hecho con las acciones del controlador,adaptaremos las plantillas de la unidad 2 al sistema twig.src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/Default/verAlimento.html.twig

1 {% extends 'JazzywebAulasMentorAlimentosBundle::layout.html.twig' %} 2 3 {% block contenido %} 4 5 <h1>{{ nombre }}</h1> 6 <table border="1"> 7 8 <tr> 9 <td>Energía</td>10 <td>{{ energia }} </td>11 </tr>12 <tr>13 <td>Proteina</td>14 <td>{{ proteina }}</td>15 </tr>16 <tr>17 <td>Hidratos de Carbono</td>18 <td>{{ hidratocarbono }}</td>19 </tr>20 <tr>21 <td>Fibra</td>22 <td>{{ fibra }}</td>23 </tr>24 <tr>25 <td>Grasa total</td>26 <td> {{grasatotal}} </td>27 </tr>28 </table>29 30 {% endblock %}

src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/Default/mostrarAlimentos.html.twig

1 {% extends 'JazzywebAulasMentorAlimentosBundle::layout.html.twig' %} 2 3 {% block contenido %} 4 5 <table> 6 <tr> 7 <th>alimento (por 100g)</th> 8 <th>energía (Kcal)</th> 9 <th>grasa (g)</th>10 </tr>

Implementamos el resto de la aplicación

58

Page 69: CursoSymfony2

11 {% for alimento in alimentos %}12 <tr>13 <td><a href="{{ path('JAMAB_ver', {'id': alimento.id}) }}">{{alimento.nombre}}</a></td>14 <td>{{ alimento.energia }}</td>15 <td>{{ alimento.grasatotal }}</td>16 </tr>17 {% endfor %}18 19 </table>20 21 {% endblock %}

En esta última plantilla hemos introducido tres elementos nuevos del sistema twig:

• La navegación por un array. Fíjate que la acción que utiliza esta plantilla,listarAction(), le pasa como parámetros una colección (array) de alimentosdevueltos por el método dameAlimentos del modelo. Las colecciones, es decir losarrays indexados (no asociativos), pueden ser iterados en una plantilla twig mediante laconstrucción {% for dato in datos %} - {% endfor %}, donde datos es el array quellega a la plantilla.

• Por otro lado, cada elemento del array alimentos es un array asociativo. Suselementos pueden ser accedido mediante la notación dato.propiedad. Unacaracterística interesante de esta notación es que se puede utilizar no solo con arraysasociativos, sino con objetos provistos de getters sobre sus propiedades. Este hecho seutiliza intensa y extensamente en Symfony2.

• Por último se utiliza la función path() de twig, que sirve para calcular la URL correcta apartir del nombre de la ruta. Así cuando cambiemos la aplicación de servidor o deubicación, la ruta será calculada correctamente. Los argumentos de la ruta se pasan ala función path usando la sintaxis de un objeto JSON, es decir: { 'param1': val1,..., `paramN': valN }. Esta función será otro de los elementos omnipresentes encualquier aplicación web construida con Symfony2 y twig.

src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/Default/formInsertar.html.twig

{% extends 'JazzywebAulasMentorAlimentosBundle::layout.html.twig' %}

{% block contenido %}

{% if mensaje is defined %}<b><span style="color: red;">{{ mensaje }}</span></b>{% endif %}<br/><form name="formInsertar" action="{{ path('JAMAB_insertar') }}" method="POST"> <table> <tr> <th>Nombre</th> <th>Energía (Kcal)</th> <th>Proteina (g)</th> <th>H. de carbono (g)</th> <th>Fibra (g)</th> <th>Grasa total (g)</th> </tr> <tr> <td><input type="text" name="nombre" value="{{ nombre }}" /></td> <td><input type="text" name="energia" value="{{ energia }}" /></td> <td><input type="text" name="proteina" value="{{ proteina }}" /></td>

Implementamos el resto de la aplicación

59

Page 70: CursoSymfony2

<td><input type="text" name="hc" value="{{ hc }}" /></td> <td><input type="text" name="fibra" value="{{ fibra }}" /></td> <td><input type="text" name="grasa" value="{{ grasa }}" /></td> </tr>

</table> <input type="submit" value="insertar" name="insertar" /></form>* Los valores deben referirse a 100 g del alimento

{% endblock %}

En esta plantilla hemos introducido otro elemento nuevo; la construcción {% if data isdefined %} - {% endif %}, que como puedes deducir, comprueba si la variable data hasido definida. A lo largo del curso veremos más expresiones lógicas utilizadas en los bloquesif - endif.También hemos vuelto a utilizar la función path para escribir el parámetro action delformulario HTML.Llegados a este punto hemos de aclarar que Symfony2 proporciona un potente servicio parala construcción de formularios que estudiaremos en su momento. Por lo pronto nosquedamos con esta manera sencilla y directa de crear formularios.Vamos a por la siguiente plantilla:src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/Default/buscarPorNombre.html.twig

1 {% extends 'JazzywebAulasMentorAlimentosBundle::layout.html.twig' %} 2 3 {% block contenido %} 4 5 <form name="formBusqueda" action="{{ path('JAMAB_buscar') }}" method="POST"> 6 7 <table> 8 <tr> 9 <td>nombre alimento:</td>10 <td><input type="text" name="nombre" value="{{ nombre }}">(puedes utilizar '%' como comodín)</td>11 12 <td><input type="submit" value="buscar"></td>13 </tr>14 </table>15 16 </table>17 18 </form>19 20 {% if resultado %}21 {% include 'JazzywebAulasMentorAlimentosBundle:Default:_tablaAlimentos.html.twig' with {'alimentos': resultado} %}22 {% endif %}23 24 {% endblock %}

Otro elemento nuevo; la inclusión de plantillas en otras plantillas. Esto lo hacemos en la línea21 mediante la función include de twig, la cual requiere como argumento el nombre lógicode la plantilla que se desea incluir. Los parámetros que necesita la plantilla incluida se pasanen un array con sintaxis JSON despues del token with.Este mecanismo de inclusión combinado con la herencia proporciona una gran flexibilidad alprogramador, otorgándole las herramientas necesarias para elaborar un código bienorganizado y reusable.La plantilla incluida es la siguiente:

src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/Default/_tablaAlimentos.html.twig

Implementamos el resto de la aplicación

60

Page 71: CursoSymfony2

1 <table> 2 <tr> 3 <th>alimento (por 100g)</th> 4 <th>energía (Kcal)</th> 5 <th>grasa (g)</th> 6 </tr> 7 {% for alimento in alimentos %} 8 <tr> 9 <td>{{ alimento.nombre }}</td>10 <td>{{ alimento.energia }}</td>11 <td>{{ alimento.grasatotal }}</td>12 </tr>13 {% endfor %}14 15 </table>

Y, por último, utilizando esta última plantilla que pinta un listado de alimentos, podemossimplificar la plantilla mostrarAlimentos.html.twig evitando la repetición de códigoinnecesariamente.src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/views/Default/mostrarAlimentos.html.twig

1 {% extends 'JazzywebAulasMentorAlimentosBundle::layout.html.twig' %}2 3 {% block contenido %}4 5 {% include 'JazzywebAulasMentorAlimentosBundle:Default:_tablaAlimentos.html.twig' with {'alimentos': alimentos} %}6 7 {% endblock %}

Y con esto ya tenemos la aplicación de gestión de alimentos terminada y construida enSymfony2. Aún puede hacerse de un modo más symfónico, utilizando los servicios depersistencia de datos (Doctrine), de creación de formularios y de validación de datos. Pero laintención de esta unidad es mostrar los elementos básicos para la creación de páginas enSymfony2, y por tanto vamos a dar por buena la aplicación tal y como está. En las próximasunidades tendremos ocasión de estudiar estos servicios y de ilustrarlos con el desarrollo deuna aplicación más completa y compleja.Únicamente faltaría usar la función path() de twig para completar los enlaces de losmenús. Pero eso vamos a dejar que lo hagas tú.

La unidad en chuletas

Generar un bundle

php app/console generate:bundle

Registrar un bundleSe hace en el archivo app/AppKernel.php, de la siguiente manera:

...new Jazzyweb\AulasMentor\AlimentosBundle\JazzywebAulasMentorAlimentosBundle(),...

La unidad en chuletas

61

Page 72: CursoSymfony2

Enlazar el routing de un bundle con el routing general de la aplicaciónSe hace añadiendo al archivo app/config/routing.yml (o routing_{env}.yml):

JazzywebAulasMentorAlimentosBundle:resource: "@JazzywebAulasMentorAlimentosBundle/Resources/config/routing.yml"prefix: /

Pasos para acoplar un bundle al framework

1. Registrar el espacio de nombre en el sistema de autocarga. Este paso no es necesario siubicamos al bundle en el directorio src.

2. Registrar al bundle en el fichero app/AppKernel.php. Esta operación se puede hacerautomáticamente a través del generador interactivo de bundles, pero si fallase poralguna razón (por ejemplo que los permisos de dicho archivo no estén bien definidos).Habría que hacerlo a mano.

3. Importar las tablas de enrutamiento del bundle en la tabla de enrutamiento de laaplicación.

Flujo para la creación de páginas en Symfony2

1. Creación de la ruta en config/Resources/routing.yml del bundle, o directamente enapp/config/routing.yml.

2. Creación de la acción en el controlador correspondiente en una clase que debe ubicarseen un fichero del directorio Controllers del bundle.

3. Creación de una plantilla en el directorio Resources/views.

Nombres lógicos de accionesNombreBundle:NombreControlador:NombreAcción.Ejemplo:AcmeDemoBundle:Secured:login se mapea en la acción loginAction() de la claseAcme\DemoBundle\Controller\SecuredController definida (normalmente) ensrc/Acme/DemoBundle/Controller/SecuredController.php.

Sintaxis básica de twig{{ parametro }} -> pinta el valor de la variable parametro.{% comando %} ... {% endcomando %} -> ejecuta la acción expresada por comando en elbloque definido desde su declaración hasta {% endcomando%}.

Herencia en plantilla twigEsta plantilla hereda de JazzywebAulasMentorAlimentosBundle::layout.html.twig, ymodifica el bloque contenido que allí se declara.

1 {% extends 'JazzywebAulasMentorAlimentosBundle::layout.html.twig' %}2 3 {% block contenido %}4 5 <h1>Inicio</h1>6 <h3> Fecha: {{fecha}} </h3>

Enlazar el routing de un bundle con el routing general de la aplicación

62

Page 73: CursoSymfony2

7 {{mensaje}}8 9 {% endblock %}

Función path de twig

{{ path('JAMAB_listar', {'id': alimento.id}) }}

Iterar una colección (array) de datoso en twig

{% for alimento in alimentos %} <tr> <td><a href="{{ path('JAMAB_listar', {'id': alimento.id}) }}">{{alimento.nombre}}</a></td> <td>{{ alimento.energia }}</td> <td>{{ alimento.grasatotal }}</td> </tr>{% endfor %}

Código condicional en twig

{% if data is defined %} ...{% endif %}

Inclusión de plantillas en twig

{% include 'JazzywebAulasMentorAlimentosBundle:Default:_tablaAlimentos.html.twig' with {'resultado': resultado} %}

Estructura básica de una ruta

nombre_unico_de_la_ruta: pattern: /el/patron/de/la/ruta/{param1}/{param2}/.../{paramN} defaults: { _controller: NombreLogico:del:Controlador }

Ejemplo:

JAMAB_ver: pattern: /ver/{id} defaults: { _controller: JazzywebAulasMentorAlimentosBundle:Default:ver }

ComentariosAquí puedes enviar comentarios, dudas y sugerencias. Utiliza la barra de scroll para recorrertodos los mensajes. El formulario de envío se encuentra al final.

Autor del código: Juan David Rodríguez García <[email protected]>

Función path de twig

63

Page 74: CursoSymfony2

Unidad 4: Inyección de DependenciasInyección de Dependencia (Dependency Injection), Inversión de Control (Control Inversion),Contenedor de Servicios (Service Container) y Contenedor de Dependencias (DependencyContainer) son palabros que te vas a encontrar continuamente cuando te sumerges en elmundo de Symfony2. Todas hacen referencia a una misma cosa: un patrón de diseño, en el"que se suministran objetos a una clase en lugar de ser la propia clase quien cree losobjetos" (definición encontrada en la Wikipedia y bastante acertada para lo cortita que es).Este patrón es muy popular en el mundo de Java y ha sido muy bien explicado por un famosogurú del desarrollo de software: Martin Fowler 10. Los creadores de Symfony2 hancomprendido su eficacia para organizar sistemas compuestos por muchos objetos quepresentan dependencias entre ellos y han decidido diseñar la arquitectura del frameworkSymfony2 alrededor de este patrón.Y ya está bien de palabritas de presentación. Vamos a contarlo. Lo haremos directamentecon ejemplos, aquí se aplica al cien por cien aquello de que una imagen vale más que milpalabras.En un sistema software orientado a objetos, como puede ser un framework de desarrolloweb, se llevan a cabo tareas de distinta índole: interpretar la petición HTTP, construir unarespuesta HTTP, manipular la sesión, controlar la autentificación y autorización, persistir losdatos en una base de datos, contruir formularios, validar datos, crear documentos HTML,JSON, XML y de otros tipos, enviar e-mails, mapear las URL's en acciones, y otras tareas quepueden ser incorporadas por el desarrollador de la aplicación. Cada una de estas tareas sedelega en distintos objetos atendiendo al principio de separación de ámbitos (Separation OfConcern). Este tipo de objetos que realizan algún tipo de tarea "global" en el sistema sedenominan Servicios, para diferenciarlos de los objetos que tienen utilidad en partesconcretas de la aplicación, esto es que se crean y se destruyen en el momento en que hanrealizado su labor. Estos últimos objetos son más propios de la lógica de negocio de laaplicación (por ejemplo un objeto Documento en un aplicación de gestión documental).Es muy usual, prácticamente lo normal, que los servicios dependan de parámetros deconfiguración e incluso de otros servicios que a su vez dependen de parámetros deconfiguración y, posiblemente, de otros servicios. Por ejemplo, un servicio de persistencia dedatos va a depender de los parámetros de conexión a la base de datos, un servicio demailing dependerá de los parámetros de conexión al servidor SMTP, un servicio diseñadopara registrar usuarios puede depender del servicio de persistencia y del servicio de mailing.Y así hasta que construyamos nuestro sistema completo.

Primer paso: inyección de dependenciasEl primer paso en la aplicación del patrón consiste en diseñar los servicios de manera queNO construyan ellos mismos los servicios de los cuales dependen, sino que los serviciosdependientes se pasen debidamente construidos a través del constructor. Es decir si unservicio S2 depende de otro servicio S1, lo siguiente no cumple el patrón:

1 <?php 2 3 class S1 { 4 5 public __construct($p11,$p12, ..., $p1n) 6 {...} 7 8 ... 9 }

Unidad 4: Inyección de Dependencias

64

Page 75: CursoSymfony2

10 11 class S2 {12 13 public __construct($p21,$p22, ..., $p2m)14 {15 ...16 $s2 = new S1($p11,$p12, ..., $p1n);17 ...18 }

En su lugar las clases anteriores se escribirían así:

1 <?php 2 3 class S1 { 4 5 public __construct($p11,$p12, ..., $p1n) 6 {...} 7 8 ... 9 }10 11 class S2 {12 13 public __construct(S1 $s1 $p21,$p22, ..., $p2m)14 {... }

Esto es, para instanciar un objeto S2, hay que crear previamente un objeto S1 y, entoncespasarselo como argumento en la construcción del objeto S2. Esta manera de tratar lasdependencias, mediante inyección en los objetos, redunda en un sistema másdesacoplado. Si en un momento dado tenemos que cambiar, la clase S1 por S1bis, en elprimer plantemiento tenemos que tocar el código de la clase S2, mientras que en elsegundo, si la clases S1bis mantiene la misma interfaz que S1 todo seguiría funcionandosin más que pasarle al constructor de S2 un objeto de S1bis en lugar de uno de S1.Esta es la parte fácil de patrón. De hecho, es muy probable que hayas usado más de una vezesta manera de construir los objetos sin haber oído nunca hablar de la "inyección dedependencias"

Segundo paso: el contenedor de dependenciasNo obstante, cuando el sistema contenga un número considerable de servicios, lasrelaciones de dependencia pueden llegar a ser bastante complejas. Entonces, lo que hemosganado en flexibilidad al separar las tareas en distintos objetos o servicios, lo perdemos encomplejidad a la hora de instanciarlos, pues hay que tener en cuenta todas las dependenciaspara instanciar correctamente un servicio.La siguiente figura ilustra una imagen gráfica de lo que acabamos de decir. Los servicios(objetos) se han representado con un círculo, y los parámetros de configuración con uncuadrado. Observese que un mismo parámetro de configuración puede ser utilizado porvarios servicios, y un mismo servicio puede ser utilizado por varios servicios.

Segundo paso: el contenedor de dependencias

65

Page 76: CursoSymfony2

Sistema de servicios/objetos dependientes

Si en alguna parte de la aplicación necesitamos utilizar el servicio S4 en PHP haríamos algoasí:

1 <?php 2 ... 3 4 $params = array( 5 'p1' => 'v1', 6 'p2' => 'v2', 7 'p3' => 'v3', 8 'p4' => 'v4', 9 'p5' => 'v5',10 'p6' => 'v6',11 );12 13 $s1 = new S1($params['p1'], $params['p2']);14 15 $s2 = new S2($params['p3']);16 17 $s4 = new S4($s1, $s2, $params['p6']);18 19 // y ya podemos usar $s4

Como puedes comprobar resulta un poco engorroso; tenemos que conocer las relaciones dedependencias entre los servicios para realizar una instancia correcta. ¿Qué podemos hacerpara seguir gozando de la flexibilidad ofrecida por un conjunto de objetos desacoplados yevitar, a la vez, tener que instanciar todas las dependencias de un objeto cada vez que lonecesitamos?La respuesta a esta pregunta es la segunda parte del patrón, y es lo que, en nuestra opinión, lo hace realmente útil. Se trata de elaborar un objeto "inteligente" que conozca las dependencias de los servicios y sea capaz de construir cualquier servicio que le pidamos y entregarnóslo bien configurado, listo para su uso, sin que nos tengamos que preocupar de instanciar e "inyectar"sus dependencias. Este objeto tan fantástico y listo se denomina

Segundo paso: el contenedor de dependencias

66

Page 77: CursoSymfony2

"Injector de Dependencias", "Contenedor de Dependencias", "Contenedor de Servicios" o,simplemente "Contenedor". Nosotros utilizaremos preferentemente el término: "Contenedorde Servicios". Si contásemos con un " Contenedor de Servicios" implementado, por ejemplo,en una clase llamada Container, el código anterior quedaría así.

1 <?php2 ...3 4 $container = new Container('/ruta/a/fichero/de/configuracion');5 6 $s4 = $container->get('S4');7 8 // y ya podemos usar $s4

Es decir, creamos un "Contenedor de Servicios" y le pedimos una instancia del servicio S4que es el que vamos a usar en ese momento. ¡Y ya está!. El contenedor se encarga deconstruir los objetos necesarios con los parámetros de configuración correctos.

Importante

Con la aplicación de este patrón, se ha separado la configuración de losservicios de su uso. Reflexiona acerca de esta última frase; resume bastante bien lafinalidad del patrón Inyección de Dependencias

Bueno, en realidad esto suena muy bien pero no hemos hablado del principal problema:¿cómo diseñamos y elaboramos un objeto tan magnífico e inteligente?. El problema no esnada sencillo. Lo que está claro es que ese objeto debe conocer todas las dependencias delos servicios de la aplicación. Por eso hemos pasado como argumento a nuestro hipotéticocontenedor un, también hipotético, fichero de configuración donde estarían definidas dichasdependencias.No vamos a invertir tiempo en contar el diseño y construcción de un "Contenedor deServicios". Aunque es un ejercicio realmente interesante e instructivo, el curso va deSymfony2, así que contaremos, directamente, como funciona el que trae incorporado esteframework a través de su componente DependencyInjection. Mostraremos su uso integrandola clase Model de la aplicación de gestión de alimentos como un servicio.

Integración de la clase Model como un servicio de Symfony2La clase Model de nuestra aplicación realiza un tipo de tarea "global" en el framework;conectarse a la base de datos MySQL para realizar operaciones sobre sus datos. Por tanto setrata de un servicio en el sentido que hemos explicado anteriormente. Para convertirla en unservicio de Symfony2 no hay que tocar nada de su código, ya que todas sus dependenciasson pasadas (inyectadas) a través del constructor, tal y como se ha explicado en el puntoanterior. Tan sólo debemos "decirle" al Contenedor de Servicios Symfony2 que la añada alconjunto de servicios que es capaz de manipular. Y es lo que haremos.

Integración de la clase Model como un servicio de Symfony2

67

Page 78: CursoSymfony2

Nota

Para que el Contenedor de Dependencias pueda crear instancias de la clase quevamos a integrar como servicio, dicha clase debe construirse como se ha indicado enel apartado Primer paso: inyección de dependencias, es decir, los objetos que requierala clase no deben ser instanciados por ella, sino pasados ya construidos a través de suconstructor.

Todos los bundles construidos con el generador de bundles de Symfony2, proporcionan elfichero Resources/config/services.yml. Échale un vistazo. Trae algunas líneascomentadas para que sirvan como ejemplo. En este fichero se describen los servicios delbundle y sus dependencias. El fichero tiene dos secciones; parameters y services. Laprimera sirve para declarar parámetros de configuración, y la segunda para declarar losservicios (los nombres lo dicen todo). Los parámetros declarados en la sección parameters,pueden ser referenciados en cualquier fichero de configuración del framework rodeando sunombre con el caracter %. Para incorporar la clase Model como servicio de Symfony2debemos añadir al fichero services.yml el siguiente código.src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/config/services.yml

parameters: jamab.database_name: alimentos jamab.database_user: root jamab.database_password: root jamab.database_host: localhost jamab.model.class: Jazzyweb\AulasMentor\AlimentosBundle\Model\Model

services: jamab.model: class: %jamab.model.class% arguments: [%jamab.database_name%, %jamab.database_user%, %jamab.database_password%, %jamab.database_host%]

La sección parameters es autodescriptiva. El nombre de los parámetros de configuraciónpuede ser cualquiera. Hemos antepuesto el prefijo jamab para evitar posibles colisiones conotros bundles. Observa que hemos parametrizado incluso el nombre de la clase. La idea esque la definición de todo lo que sea parametrizable, es decir, susceptible de ser cambiado,se encuentre juntito y fácilmente localizable.En la sección services, cada servicio es declarado con un nombre único. Podemos elegir elque queramos siempre que no coincida con otro que ya exista en el sistema. Por eso essiempre una buena práctica usar un prefijo que haga referencia al bundle. En nuestro casohemos llamado al servicio: jamab.model.A continuación hay que indicar qué clase implementa el servicio y qué argumentos necesitadicha clase para crear objetos de su tipo. Las directivas class y arguments recogen dichainformación. Fíjate la forma de referenciar los parámetros de configuración con el carácter %.Esta información es todo lo que necesita Symfony2 para incorporar la clase Model comoservicio del framework. Ahora podrás obtener instancias de la misma a través del"Contenedor de Servicios" de Symfony2.

El Contenedor de Servicios de Symfony2La clase Symfony\Bundle\FrameworkBundle\Controller\Controller del framework,proporciona en un atributo público llamado container, una instancia del contenedor dedependecias. Por tanto, desde cualquier clase derivada deSymfony\Bundle\FrameworkBundle\Controller\Controller se puede obtener unainstancia del Contenedor de Dependencias así:

El Contenedor de Servicios de Symfony2

68

Page 79: CursoSymfony2

$c = $this->container;

Y una vez que lo tengamos podemos instanciar cualquier servicio existente a través delmétodo get() del Contenedor de Servicios. A este método se le pasa como argumento unastring con el nombre que se le ha dado a dicho servicio en la declaración del mismo. Porejemplo para obtener una instancia del servicio que acabamos de crear con la clase Model,haríamos lo siguiente:

$c = $this->container->get('jamab.model');

O más sencillo aún:

$c = $this->get('jamab.model');

Ya que el método get() de la claseSymfony\Bundle\FrameworkBundle\Controller\Controller, es un wrapper, es decir llamadirectamente al método get() del Contenedor de Dependencia de Symfony2.Como nuestro controlador DefaultController extiende a la claseSymfony\Bundle\FrameworkBundle\Controller\Controller, podemos obtener instanciasde la clase Model a través del Contenedor de Servicios de Symfony2. El código de la claseDefaultController quedaría así:src/Jazzyweb/AulasMentor/AlimentosBundle/Controller/DefaultController.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\AlimentosBundle\Controller; 4 5 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 7 class DefaultController extends Controller 8 { 9 10 public function indexAction() 11 { 12 $params = array( 13 'mensaje' => 'Bienvenido al curso de Symfony2', 14 'fecha' => date('d-m-yyy'), 15 ); 16 17 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:index.html.twig', $params); 18 } 19 20 public function listarAction() 21 { 22 $m = $this->get('jamab.model'); 23 24 $params = array( 25 'alimentos' => $m->dameAlimentos(), 26 ); 27 28 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:mostrarAlimentos.html.twig',

29 $params); 30 } 31 32 public function insertarAction() 33 { 34 $params = array( 35 'nombre' => '',

El Contenedor de Servicios de Symfony2

69

Page 80: CursoSymfony2

36 'energia' => '', 37 'proteina' => '', 38 'hc' => '', 39 'fibra' => '', 40 'grasa' => '', 41 ); 42 43 $m = $this->get('jamab.model'); 44 45 if ($_SERVER['REQUEST_METHOD'] == 'POST') { 46 47 // comprobar campos formulario 48 if ($m->insertarAlimento($_POST['nombre'], $_POST['energia'], 49 $_POST['proteina'], $_POST['hc'], $_POST['fibra'], $_POST['grasa'])) { 50 $params['mensaje'] = 'Alimento insertado correctamente'; 51 } else { 52 $params = array( 53 'nombre' => $_POST['nombre'], 54 'energia' => $_POST['energia'], 55 'proteina' => $_POST['proteina'], 56 'hc' => $_POST['hc'], 57 'fibra' => $_POST['fibra'], 58 'grasa' => $_POST['grasa'], 59 ); 60 $params['mensaje'] = 'No se ha podido insertar el alimento. Revisa el formulario'; 61 } 62 } 63 64 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:formInsertar.html.twig', 65 $params); 66 } 67 68 public function buscarPorNombreAction() 69 { 70 $params = array( 71 'nombre' => '', 72 'resultado' => array(), 73 ); 74 75 $m = $this->get('jamab.model'); 76 77 if ($_SERVER['REQUEST_METHOD'] == 'POST') { 78 $params['nombre'] = $_POST['nombre']; 79 $params['resultado'] = $m->buscarAlimentosPorNombre($_POST['nombre']); 80 } 81 82 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:buscarPorNombre.html.twig', 83 $params); 84 } 85 86 public function verAction($id) 87 { 88 $m = $this->get('jamab.model'); 89 90 $alimento = $m->dameAlimento($id); 91 92 if (!$alimento) { 93 throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException(); 94 } 95 96 $params = $alimento; 97 98 return $this->render('JazzywebAulasMentorAlimentosBundle:Default:verAlimento.html.twig', 99 $params);100 }101 102 }

Observa (líneas 22, 43, 75 y 88) que ya no tenemos que pasar parámetros de configuración para obtener una instancia de la clase Model, puesto que el Contenedor de Servicios sabe hacerlo y lo hace por nosotros. Como consecuencia, ya no es necesario la clase Jazzyweb\AulasMentor\AlimentosBundle\Config\Config, puesto que los parámetros de configuración han sido definidos en el archivo

El Contenedor de Servicios de Symfony2

70

Page 81: CursoSymfony2

src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/config/services.yml. Así quepuedes borrarla (¡menos mal! era una clase que "chirriaba" demasiado).

¿Y qué hemos ganado con todo esto?Es muy probable que estes preguntándote qué hemos ganado con todo esto de la inyecciónde dependencias. Y es normal, por que el concepto del Contenedor de Servicios es unanueva forma de entender la programación orientada a objetos.En esta sección vamos a indicar algunas de las cosas que hemos ganado al convertir enservicio de Symfony2 nuestra clase Model. Después seguiremos mostrando las ventajas deContenedor de Servicios implementando nuevos servicios para la aplicación de gestión dealimentos.La ventaja más inmediata es el hecho de poder instanciar un objeto Model sin tener quepasarle nada ni conocer nada sobre sus dependencias, tan solo hay que pedírselo alcontenedor.Otra cosa buena es que los parámetros de configuración son declarados en un fichero deconfiguración y son accesibles desde cualquier otro fichero de configuración usando elcarácter % alrededor del nombre del parámetro. También se puede obtener el valor de undeterminado parámetro de configuración desde un controlador utilizando el métodogetParameter().

...$database = $this->getParameter('jamab.database_name');...

En ocasiones resulta muy útil cambiar la clase que implementa un servicio. Por ejemplo, siqueremos realizar pruebas aisladas sobre la clase DefaultController para comprobar quela lógica de control funciona como se espera, es decir, pruebas en las que no interfiera labase de datos, podemos utilizar en lugar de la clase Model, una clase "tonta" con la mismainterfaz pero independiente de la conexión a la base de datos. Por ejemplo la siguiente:src/Jazzyweb/AulasMentor/AlimentosBundle/Model/ModelMock.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\AlimentosBundle\Model; 4 5 class ModelMock 6 { 7 protected $conexion; 8 9 public function __construct($dbname,$dbuser,$dbpass,$dbhost)10 {11 12 }13 14 public function dameAlimentos()15 {16 $alimentos = array(17 array(18 'id' => 1,19 'nombre' => 'pera',20 'energia' => '90',

¿Y qué hemos ganado con todo esto?

71

Page 82: CursoSymfony2

21 'proteina' => '80',22 'hidratocarbono' => '78',23 'fibra' => '89',24 'grasatotal' => '98',25 ),26 array(27 'id' => 2,28 'nombre' => 'manzana',29 'energia' => '94',30 'proteina' => '60',31 'hidratocarbono' => '38',32 'fibra' => '83',33 'grasatotal' => '48',34 ),35 );36 37 38 return $alimentos;39 }40 41 public function buscarAlimentosPorNombre($nombre)42 {43 return $this->dameAlimentos();44 }45 46 public function dameAlimento($id)47 {48 $alimento = array(49 'id' => 1,50 'nombre' => 'manzana',51 'energia' => '94',52 'proteina' => '60',53 'hidratocarbono' => '38',54 'fibra' => '83',55 'grasatotal' => '48',56 );57 58 return $alimento;59 60 }61 62 public function insertarAlimento($n, $e, $p, $hc, $f, $g)63 {64 }65 66 }

Fíjate que la clase ModelMock clase no requiere una conexión a la base de datos, peromantiene la misma interfaz que Model. Puedes probar a cambiar en el archivo deconfiguración de los servicios del bundle, el nombre de la clase que implementa el serviciojamab.model:src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/config/services.yml

¿Y qué hemos ganado con todo esto?

72

Page 83: CursoSymfony2

1 parameters: 2 jamab.database_name: alimentos 3 jamab.database_user: root 4 jamab.database_password: root 5 jamab.database_host: localhost 6 jamab.model.class: Jazzyweb\AulasMentor\AlimentosBundle\Model\ModelMock 7 8 services: 9 jamab.model:10 class: %jamab.model.class%11 arguments: [%jamab.database_name%, %jamab.database_user%, %jamab.database_password%, %jamab.database_host%]

Si ejecutas la aplicación ahora puedes comprobar que todo sigue funcionando, aunque losalimentos que devuelve son los especificados a "capón" en los arrays de la clase ModelMock.Esto nos puede servir, como ya hemos dicho antes, para construir tests con PHPUnit quecomprueben la funcionalidad del controlador DefaultController sin necesidad de"contaminar" las pruebas con posibles fallos con la conexión a la base de datos. Todo esto esposible gracias al uso de objetos (servicios) absolutamente desacoplados y que son"juntados" entre sí mediante la inyección de dependencias.Imáginemos ahora que, después de que nuestra aplicación de gestión de alimentos hacrecido un montón, por "exigencias del guíon" nos han migrado los datos a un sistemagestor de base de datos Postgresql. O que nos exigen hacerla compatible también con estesistema. En tal caso podríamos crear una clase ModelPostgresql, con la misma interfaz queModel, pero con los métodos adaptados para atacar una base de datos Postgresql. Hechoesto, bastaría con cambiar en la configuración la clase que implementa el serviciojamab.model.Terminamos la sección "symfonizando" un poco más nuestro bundle. La distribuciónstandard de Symfony2 proporciona el archivo app/config/parameters.ini, con el objetivode que se declaren ahí los parámetros globales de la aplicación. Si lo abres verás quepropone como tales a los parámetros de conexión a una base de datos y a un servidor decorreo. Por tanto deberíamos utilizar estos parámetros como argumentos de nuestro serviciojamab.modle. Para ello basta con realizar el cambio en el fichero services.yml del bundle:src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/config/services.yml

1 parameters:2 jamab.model.class: Jazzyweb\AulasMentor\AlimentosBundle\Model\ModelMock3 4 services:5 jamab.model:6 class: %jamab.model.class%7 arguments: [%database_name%, %database_user%, %database_password%, %database_host%]

Ya no necesitamos los parámetros jamab.database_name, etcétera. Obviamente debescolocar los valores correctos para la conexión a la base de datos en el fichero deconfiguración global parameters.ini:app/config/parameters.ini

1 [parameters] 2 database_driver = pdo_mysql 3 database_host = localhost 4 database_port = 5 database_name = alimentos 6 database_user = root 7 database_password = root 8 9 mailer_transport = smtp

¿Y qué hemos ganado con todo esto?

73

Page 84: CursoSymfony2

10 mailer_host = localhost11 mailer_user =12 mailer_password =13 14 locale = en15 16 secret = ThisTokenIsNotSoSecretChangeIt

¡Servicios al poder!Continuamos mostrando la potencia de los servicios. Ahora vamos a construir un servicioque combinará el que acabamos de crear jamab.model, y el servicio de mailing que vieneincluido en Symfony2. Denominaremos al nuevo servicio jamab.infosender, y su cometidoserá enviar por correo electrónico la información que tenemos en nuestro sistema sobre undeterminado alimento.En primer lugar vamos a crear la clase que implementará el servicio. La llamaremosInfoSender y la colocaremos en el espacio de nombresJazzyweb\AulasMentor\AlimentosBundle\Model, se alojará por tanto en el directoriosrc/Jazzyweb/AulasMentor/AlimentosBundle/Model.Dicha clase, para que cumpla el patrón y pueda integrarse como servicio, debe recibir en suconstructor un objeto jamab.model y otro mailer . Con el primero buscará informaciónsobre alimentos en el sistema, y con el segundo enviará el e-mail con dicha información. Laclase que nos hemos inventado para este menester es la siguiente:src/Jazzyweb/AulasMentor/AlimentosBundle/Model/InfoSender.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\AlimentosBundle\Model; 4 5 class InfoSender 6 { 7 8 protected $model; 9 protected $mailer;10 11 public function __construct($model, $mailer)12 {13 $this->model = $model;14 $this->mailer = $mailer;15 }16 17 public function send($terminoBusqueda, $direccionEmail)18 {19 $alimentos = $this->model->buscarAlimentosPorNombre($terminoBusqueda);20 21 $texto = '';22 foreach ($alimentos as $alimento)23 {24 $texto = implode(',', $alimento);25 $texto .= PHP_EOL;26 }27

¡Servicios al poder!

74

Page 85: CursoSymfony2

28 $message = \Swift_Message::newInstance()29 ->setSubject('Información sobre alimentos')30 ->setFrom('[email protected]')31 ->setTo($direccionEmail)32 ->setBody($texto)33 ;34 35 $this->mailer->send($message);36 }37 }

El constructor de la clase (líneas 11-15) recibe los objetos $model y $mailer (o dicho deotra forma; los servicios jamab.model y mailer) ya creados y bien configurados, ysimplemente los asigna como atributos para que puedan utilizarlos los métodos que se iránañadiendo al servicio.El método send() del servicio (líneas 17-36), recibe como parámetros un término debúsqueda y una dirección de correos. Entonces, utiliza el servicio jamab.model (línea 19)para realizar una busqueda de alimentos en el sistema. El resultado es usado para construiruna cadena que servirá como cuerpo del e-mail (líneas 21-26). A continuación se prepara unmensaje de e-mail (líneas 28-33) con los campos correspondientes; subject, from, to y body,y se usa el servicio de mailing de Symfony2 (línea 35) para enviar el mensaje.

Nota

El servicio de mailing incorporado con Symfony2 esta basado en la librería SwiftMailer,y para el envío de mensaje utiliza el método send() cuyo argumento debe ser unainstancia de Swift_Message. La creación de esta instancia es lo que se realiza en laslíneas 28-33.

Algo importante y que hemos de resaltar. Durante el proceso de construcción de la claseInfoSender no nos hemos preocupados por detalles de la configuración de los servicios,simplemente sabíamos que existian y conocíamos sus funciones, las hemos utilizado y seacabó. Es la consecuencia de haber separado la configuración del uso. Gracias a ello, escribirun servicio que dependa de otro, o simplemente utilizar un servicio es realmente sencillo.El próximo paso es integrar la clase que acabamos de construir como servicio del framework.Si has estado atento a todo lo que llevamos dicho en esta unidad, sabrás que esto se haceañadiéndola al fichero de configuración services.yml del bundle.src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/config/services.yml

1 parameters: 2 jamab.model.class: Jazzyweb\AulasMentor\AlimentosBundle\Model\Model 3 jamab.infosender.class: Jazzyweb\AulasMentor\AlimentosBundle\Model\InfoSender 4 5 services: 6 jamab.model: 7 class: %jamab.model.class% 8 arguments: [%database_name%, %database_user%, %database_password%, %database_host%] 9 10 jamab.infosender:11 class: %jamab.infosender.class%12 arguments: [ @jamab.model, @mailer ]

¡Servicios al poder!

75

Page 86: CursoSymfony2

La única explicación que merece el código anterior es que hay que tener en cuenta que losargumentos que son servicios se representan anteponiendo el carácter @ a su nombre.El último paso es asegurarnos de que todos los servicios que estamos usando están bienconfigurados. Los de nuestra factura; jamab.model y jamab.infosender, obviamente loestán (véase el código anterior). Nos falta comprobar el servicio de mailing. Este servicio seconfigura en el archivo de configuración global app/config/config.yml, en la secciónswiftmailer.Echale un vistazo y fíjate que hace referencia a los parámetros%mailer_transport%, %mailer_host%, %mailer_user% y %mailer_password%. ¿Adivinasdonde se definen tales parámetros? Espero que así sea, significa que has estado leyendo launidad con atención. Se hace en el archivo src/config/parameters.ini, aunque podríamoscolocar los valores concretos directamente en el fichero config.yml, los administradores desistemas que van a instalar nuestra aplicación agredecerán muchísimo que todos losparámetros de configuración que tengan que ver con los servicios del sistema se encuentrenjuntitos en un sólo fichero (cuidado que ahora la palabra servicio la estamos usando en uncontexto de sistemas de información, esto es, nos referimos a servicios de base da datos, decorreo, etcétera).Hemos utilizado una cuenta de Gmail para enviar emails. Para configurarla es preciso añadirdos parámetros más: el tipo de encriptación y el modo de autenticación (esto lo dice ladocumentación de Gmail). Por otro lado, en la documentación oficial de Symfony2 se diceque estos datos son mapeados por swiftmailer a través de los parámetros encryption yauth_mode. La sección swiftmailer del archivo de configuración global config.yml quedaasí:app/config/config.yml

...swiftmailer: transport: %mailer_transport% username: %mailer_user% password: %mailer_password%...

Y la declaración de los parámetros:app/config/parameters.ini

1 ...2 mailer_transport = gmail3 mailer_user = tuusername4 mailer_password = tupassword5 ...

Y ya tenemos el servicio de mailing bien configurado y listo para ser usado. Y ahora tocaprobarlo. Construiremos una ruta y una acción asociada para ello. Estaría bien queintentases realizar tu mismo esta prueba sin mirar lo que viene a continuación hasta que nolo hayas conseguido o hasta que te hayas desesperado. Si vas asimilando todo lo que hastaahora hemos estudiado en el curso, deberías ser capaz de hacerlo.En src/Jazzyweb/AulasMentor/AlimentosBundle/Resources/config/routing.ymlañadimos la ruta:

JAMAB_testinfosender: pattern: /testinfosender defaults: { _controller: JazzywebAulasMentorAlimentosBundle:Default:testInfoSender }

¡Servicios al poder!

76

Page 87: CursoSymfony2

Y en el DefaultController implementamos la acción correspondiente:src/Jazzyweb/AulasMentor/AlimentosBundle/Controller/DefaultController.php

<?php...public function testInfoSenderAction() { $infosender = $this->get('jamab.infosender');

$infosender->send('%naranja%', '[email protected]');

return new \Symfony\Component\HttpFoundation\Response( '<html><body><h2>Se ha enviado información a [email protected]</h2></body></html>'); }...

Lanza esta ruta en tu navegador y se enviará información sobre el alimento naranja a ladirección de e-mail que hayas elegido.

Nota

Sustituye tuusername, tupassword y [email protected] por losvalores propioso de tu cuenta de e-mail y por la dirección de correo contra la quedeseas hacer pruebas.

Nota

Observa que no hemos construido una plantilla para pintar la acción. Como es unaprueba sencilla, hemos decidido construir directamente un objeto Response.

El uso del servicio jamab.infosender, como todos los servicios de Symfony2 no puede sermás sencillo. Le dices al Contenedor de Servicios: "dame el servicio fulanito", él te lodevuelve educadamente, sin hacerte preguntas, y tu lo usas en tu aplicación. Que quieresusar otro servidor para enviar los mensajes. No tienes que cambiar ni una línea de tu código.Cambias la configuración del servicio de mailing y todo sigue funcionando. Que ahora resultaque no quieres enviar mails mientras estás haciendo pruebas. Añades a la configuración deswiftmailer el parámetro: disable_delivery: true y se deshabilita el envío. Son posiblessituaciones prácticas que se te pueden dar en el desarrollo de tus aplicaciones y que, graciasa la inyección de dependencias, se resuelven de una manera muy sencilla y segura en elsentido de que realizas grandes cambios y todo sigue funcionando, nada se rompe.

Más servicios aúnNo te apures ya hemos terminado, tan sólo queremos recalcar que Symfony2 está compuesto por muchos servicios como los que hemos estudiado en esta unidad. Y que, por supuesto, los bundles de terceros más famosos utilizan extensiva e intensivamente la inyección de dependencia. Comprender bien este concepto te facilitará el aprendizaje y tu

Más servicios aún

77

Page 88: CursoSymfony2

experiencia con Symfony2.

Nota

Si quieres saber todos los servicios que vienen con Symfony2 ejecuta este comando:

php app/console container:debug

Es verdad que se pueden desarrollar aplicaciones con Symfony2 sin necesidad de "echarmucha cuenta" a la inyección de dependencias. Basta con conocer los servicios másimportantes proporcionados por Symfony2, instanciarlos en las acciones del controladorcuando los necesitas y utilizarlos. Sin embargo, si no prestas suficiente atención a esteconcepto y lo dejas un poco de lado, sentirás que no consigues controlar del todo alframework y en ocasiones se te escapa. Comprender la inyección de dependencia, elconcepto de servicio y de contenedor de servicios, es fundamental para tomar las riendasdel framework y cabalgar sobre él confortablemente. Este es el motivo esta unidad y de suubicación a la mitad del curso, justo antes de comenzar a desarrollar una aplicación máscompleja que servirá de vehículo vertebrador para aprender a utilizar Symfony2.En las unidades anteriores ya hemos utilizado los servicios de Symfony2 sin haberlosmencionados. Cuando renderizamos los resultados de una acción con una plantilla, estamoshaciendo uso del servicio templating a través del wrapper render() de la claseController. De hecho hacer esto:

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

es equivalente a esto:

$t = $this->get('templating');

return $t->renderResponse('JazzywebAulasMentorAlimentosBundle:Default:index.html.twig', $params);

Pero más cortito.En las próximas unidades estudiaremos los servicios más importantes que ofrece Symfony2.Con ellos se puede hacer casi de todo. No obstante a veces tendremos que ampliar elframework con nuevas funcionalidades. Y esto, si queremos hacerlo bien, debemos hacerlousando la inyección de dependencias y los servicios.

La unidad en chuletas

• Servicio. Un tipo de objetos que realizan algún tipo de tarea “global” en el sistema.• Las clases que se quieran integrar como servicios, se definen en el archivoResources/config/services.yml del bundle.

• Ese archivo tiene dos secciones: parameters y services. La primera sirve para definirparámetros de configuración del bundle (que pueden ser utilizados por los servicios o enotras partes del bundle), y la segunda para declarar los servicios.

• Las clases que vayan a incorporarse como servicios deben "inyectar" sus dependencias(parámetros y otros servicios) a través de su constructor o de algún setter.

Ejemplo:

La unidad en chuletas

78

Page 89: CursoSymfony2

parameters: jamab.database_name: alimentos jamab.database_user: root jamab.database_password: root jamab.database_host: localhost jamab.model.class: Jazzyweb\AulasMentor\AlimentosBundle\Model\Model

services: jamab.model: class: %jamab.model.class% arguments: [%jamab.database_name%, %jamab.database_user%, %jamab.database_password%, %jamab.database_host%]

• Desde un controlador (que extienda la claseSymfony\Bundle\FrameworkBundle\Controller\Controller, se puede obtenercualquier servicio usando el método get():

$servicio = $this->get('nombre_del_servicio');

• Desde ese controlador también se puede obtener los valores de los parámetros usandoel método clase getParameter():

$parametro = $this->getParameter('nombre_del_parametro');

• Los argumentos que son servicios se especifican colocando el carácter @ delante delnombre:

...services: jamab.infosender: class: %jamab.infosender.class% arguments: [ @jamab.model, @mailer ]...

ComentariosAquí puedes enviar comentarios, dudas y sugerencias. Utiliza la barra de scroll para recorrertodos los mensajes. El formulario de envío se encuentra al final.

Autor del código: Juan David Rodríguez García <[email protected]>

Comentarios

79

Page 90: CursoSymfony2

Unidad 5: Desarrollo de la aplicación MentorNotas (I).AnálisisContinuaremos el estudio de Symfony2 desarrollando una aplicación web con unasfuncionalidades que permitan aplicar la mayor parte de las herramientas que el frameworkofrece. Tal aplicación requiere de un análisis previo que, mediante los modelos apropiados,describa con cierto detalle las características de la misma. El primer objetivo de este capítuloes elaborar dicho análisis.En el análisis abordaremos:

• el catálogo de requisitos funcionales• el modelo de datos• el modelo de procesos

La tarea de diseño quedará reducida a proponer las pantallas que conformarán la aplicacióny los elementos que deben incluir, ya que los elementos clave del diseño arquitectónico losestablece el propio framework. En la unidad 3 hemos estudiado los elementos más básicosde esta arquitectura y se irán completando a medida que avancemos en la aplicación.Por último facilitaremos también los recursos que van más allá del código de loscontroladores, entidades y plantillas que conforman el grueso de la aplicación:

• las CSS's para dotar de estilo a la aplicación.• las librerías javascript para enriquecer la interfaz de usuario.• Algunos recursos gráficos (logotipo, iconos, …).

Estos recursos se denominan “activos” (assets) de la aplicación, y son servidos por elservidor web al cliente directamente, sin ningún tipo de proceso intermedio.En definitiva, en esta unidad desarrollaremos rápidamente pero con sufciente precisión, lasfases previas a la construcción del software; análisis y diseño, así como la elaboración de lainterfaz gráfica de usuario. La intención es poder centrarnos en la fase de construcción,donde un framework de desarrollo de aplicaciones es realmente útil.

Descripción de la aplicaciónA partir de este momento y a lo largo del resto del curso, nuestra labor será el desarrollo deuna aplicación para la gestión de notas (al estilo de los post-it). Nos hemos inspirado en unaherramienta que últimamente está teniendo mucho éxito entre los usuarios de plataformasmóviles (smartphones y tabletas). Se trata de EverNote® 11, una aplicación que permite asus usuarios almacenar notas y etiquetarlas para facilitar su búsqueda y recuperaciónposterior. Esta aplicación se distribuye para los principales sistemas operativos yplataformas, tanto móviles como fijas, y también ofrecen una aplicación web que puede serejecutada desde los navegadores más populares. Posiblemente la característica mássobresaliente de EverNote®, sea la posibilidad de mantener sincronizadas las notas de unusuario en todos los dispositivos en los que utilice la aplicación, gracias a la existencia de unrepositorio remoto donde se almacenan sus notas.La versión web de EverNote® nos ha servido como motivo de inspiración para la aplicación que analizaremos en esta unidad y construiremos en las siguientes. Obviamente no pretendemos ni hacer una “copia”, ni reinventar la rueda, ni hacer la competencia ni nada parecido. Nuestro objetivo es fundamentalmente pedagógico; queremos que aprendas a desarrollar aplicaciones web con Symfony2, y estamos convencidos de que la mejor manera de aprender a programar con una determinada tecnología es mediante la elaboración de una aplicación con suficiente “cuerpo”, que plantee en su desarrollo los principales problemas a los que te enfrentarás como programador web. Dada su popularidad, EverNote® nos ha

Unidad 5: Desarrollo de la aplicación MentorNotas (I). Análisis

80

Page 91: CursoSymfony2

servido como un buen modelo. Por otro lado, dado nuestro interés pedagógico, nuestraaplicación será bastante más modesta, aunque, como podrás comprobar, es totalmenteprofesional e incluso apta para ser usada en un entorno de producción.Por último hemos optado por un gestor de notas como aplicación vertebradora del curso,porque engloba un amplio conjunto de funcionalidades para tratar los aspectos másrelevantes de Symfony2. Además es una aplicación eminentemente práctica, a pesar de quela hemos concebido desde una perspectiva pedagógica. De hecho, y gracias a lasposibilidades de extensión de Symfony2, una vez finalizado el curso, la aplicación puede sermejorada, modificada y/o adaptada hasta el punto que uno desee. También podrá serutilizada como modelo para el desarrollo de otras aplicaciones que, en principio, no tienennada que ver con la gestión de notas; basta con que el programador tenga la suficientecapacidad de abstracción para identificar estructuras análogas en dominios distintos.

Descripción generalCon MentorNotas los usuarios registrados podrán crear, editar y eliminar notas al estilo depost-it's “enriquecidos”. El usuario podrá asociar las etiquetas que desee a sus notas, con elobjetivo de facilitar la búsqueda y recuperación de las mismas. Los usuarios podránregistrarse facilitando una dirección de correo electrónico. Además, podrán tener acceso acaracterísticas premium si se abonan a alguno de los planes ofrecidos por el sistema.El sistema será administrado por una aplicación que permitirá gestionar los usuarios, losplanes de pago para el uso de características premium, y los banners de publicidad queserán presentados a los usuarios no premium.Utilizando la jerga clásica de los desarrolladores web, llamaremos backend a la aplicación deadministración, y frontend a la aplicación en sí, es decir al gestor de notas que usan losusuarios registrados.

Catálogo de requisitos

Requisitos del frontend

1. Desde una página pública, los usuarios podrán:

• entrar en la aplicación presentando sus credenciales (nombre de usuario y clave),• si aún no está registrado, crear una nueva cuenta a partir de una dirección de

correo electrónico.2. Existen dos tipos de usuarios:

• registrados, simplemente han obtenido una cuenta con su correo electrónico.• premium, se han abonado a alguno de los planes de pago

3. El usuario podrá crear, editar y borrar sus notas. Dichas notas contendrán los siguienteselementos:

• título,• fecha, que pondrá automáticamente el sistema,• texto enriquecido, es decir, con posibilidad de formato,• un archivo adjunto (únicamente para usuarios registrados)

4. En el momento de crear una nota el usuario podrá asociarle las etiquetas que desee.Para ello podrá utilizar las etiquetas que ya existan o crear otras nuevas sobre lamarcha, mientras está creando la nota.

Descripción general

81

Page 92: CursoSymfony2

5. En una misma pantalla se mostrarán:

• un listado con todas las etiquetas del usuario,• un buscador mediante el que se podrán buscar notas que contengan un término de

búsqueda en el texto y/o en el título,• un listado con las notas que satisfacen el criterio de búsqueda que el usuario haya

elegido: todas las notas asociadas a una etiqueta o todas las notas que contienenun término de búsqueda,

• un detalle con la nota seleccionada, o con la primera nota que se encuentre si no seha seleccionado ninguna.

6. Cuando el usuario haga click en una etiqueta, el listado de notas mostrará todas lasnotas pertenecientes a dicha categoría.

7. Cuando el usuario haga una búsqueda, el listado de notas mostrará todas las notas quecontienen en su texto o título ese término.

8. Cuando el usuario haga click en una nota del listado de notas, se mostrará el detalle deesa nota en un espacio bien visible.

9. La aplicación mostrará a los usuarios no premium banners de publicidad.10. Los usuarios premium dispondrán de un apartado para consultar sus pagos y otros

elementos propios de la cuenta premium

11. Se habilitará un servicio RSS para difundir noticias relacionadas con la aplicación

Requisitos del backendSólo los usuarios administradores podrán acceder al backend para:

• gestionar usuarios,• gestionar publicidad,• gestionar planes de pago

Gestión de usuarios

12. Crear y modificar los datos de los usuarios.13. Borrar usuarios.14. Activar/desactivar cuentas.15. Asociar permisos (administración, cuenta premium). Los usuarios podrán tener más de

un permiso.16. Consultar los pagos realizados por el usuario.

Gestión de publicidad

17. La publicidad consistirá en banners que son imágenes en formato jpg, png o gif.18. Desde el backend se podrán crear, modificar y borrar estos banners.

Gestión de planes de pagos

19. Cada plan de pago consistirá en un precio para un periodo de tiempo.20. Desde el backend se podrán crear nuevos planes de pago. Se podrán modificar o borrar

si no existe ningún usuario abonado.

Requisitos del backend

82

Page 93: CursoSymfony2

Modelo de datosLa siguiente figura representa el modelo de datos propuesto:

Modelo de datos

Cada usuario puede tener las notas que quiera, pero cada nota pertenece a un sólo usuario.Las etiquetas de las notas son también propias de cada usuario. Las notas pueden tenerasociadas tantas etiquetas como se deseen, a la vez que una misma etiqueta puede sercompartida entre las notas siempre que pertenezcan a un mismo usuario.Un mismo usuario podrá pertenecer a distintos grupos y, evidentemente, un grupo podrá sercompartido por los usuarios.Cada usuario podrá realizar uno o muchos contratos, y cada contrato tendrá aplicada unasóla de las tarifas propuestas.Por último la publicidad no tiene relación con el resto de las entidades. Simplemente será unrepositorio de banners que serán mostrado de manera aleatoria a los usuarios no premium.

Descripción de los procesos

Registro de nuevos usuariosEl usuario introduce en un formulario su dirección de correo electrónico, su nombre deusuario y el password (por duplicado) con el que desea entrar en la aplicación.Si el formulario es válido:

• se creará un nuevo usuario en la aplicación con estado inactivo y al que se le asignaráun token único.

• se le enviará a la dirección de correo electrónico que ha facilitado un mensaje debienvenida con una url que lleva adjunta dicho token único para activar su cuenta.

Si el formulario no es válido volverá a mostrarse indicando la razón del error.Cuando el usuario enlaza con la URL facilitada en el mensaje de bienvenida, la aplicacióncomprobará el token facilitado, y si es correcto se activará la cuenta. A partir de esemomento el usuario podrá entrar en la aplicación a través del formulario de login.

Creación de una notaEl usuario rellena un formulario con los siguientes datos:

• título de la nota,• fecha, que se autorrellenará con la fecha del sistema, aunque podrá ser cambiada por el

usuario.

Modelo de datos

83

Page 94: CursoSymfony2

• el texto de la nota, con posibilidad de darle formato.• las etiquetas de la nota,• si es usuario premium, podrá subir un archivo.

El proceso de asociar etiquetas a las notas será como sigue:En un cuadro de texto el usuario escribirá todas las notas. A medida que escribe cada nota,la aplicación irá autocompletando sugiriendo las etiquetas que haya utilizado en otras notasanteriores y que coincidan con el patrón que escribe. Al pulsar la tecla intro, la etiqueta sehabrá terminado de escribir y podrá escribir una nueva. El proceso se repite hasta que elusuario se harte.Cuando termine y guarde la nota, las etiquetas nuevas serán dadas de alta en el sistema yserán asociadas a la nota.

Contratación de una cuenta premiumEl usuario podrá contratar una cuenta premium picando en un enlace bien visible que sehabilitará para ello. Cuando el usuario pique en dicho enlace, será redirigido a una páginadonde se le informe de las tarifas y las funcionalidades premium. El usuario elige la tarifaque desea y es redirigido al servicio de pago virtual, donde realiza el pago. Una vez realizadoel pago, dicho servicio vuelve a redirigir al usuario a la aplicación indicando que se harealizado el pago correctamente. Entonces, se actualizará a premium la cuenta del usuariopor el periodo de tiempo que haya abonado. A partir de ese momento podrá asociar archivosa sus notas.

Presentación de la publicidadLa publicidad será presentada a los usuarios que no dispongan de cuenta premium de unamanera aleatoria, según una distribución de probabilidad uniforme.

Interfaz de usuarioEn este apartado mostraremos mediante bocetos de pantallas, los distintos escenarios quepresentará la aplicación:

Pantalla de login.

Pantalla de login

Pantalla de registro.

Contratación de una cuenta premium

84

Page 95: CursoSymfony2

Pantalla de registro

Pantalla principal (inicio).

Pantalla de inicio

Pantalla de creación de notas.

Pantalla de creación de notas

Pantalla de modificación de notas.

Pantalla principal (inicio)

85

Page 96: CursoSymfony2

Pantalla de modificación de notas

Pantalla de planes de pago.

Pantalla de planes de pago

Menú de la aplicación de administración.

Menú de la aplicación de administración

Gestión de entidades.La gestión de las entidades usuarios, grupos, planes de pago y publicidad es, esencialmente,idéntica. Se trata de poder crearlas, modificarlas y listarlas. Por tanto utilizaremos sólo unade ellas para mostrar los bocetos.

Listado de usuarios.

Listado de usuarios

Pantalla de planes de pago

86

Page 97: CursoSymfony2

Creación de usuarios.

Creación de usuarios

Edición de usuarios.

Edición de usuarios

Recursos para la construcción de la aplicaciónQueremos que nuestra aplicación tenga un aspecto profesional y moderno. Por ello, paraconstruir las páginas, iremos más allá del HTML y utilizaremos jQuery 12 para ayudarnos enla construcción de la interfaz gráfica de usuario.Como dicen en la página oficial, “jQuery es una librería javascript, rápida y concisa quesimplifica el manejo del HTML, la manipulación de eventos, la animación, y la interacciónAjax con el fin de desarrollar aplicaciones web rápidamente”.Además utilizaremos jQuery UI, que es una librería construida sobre jQuery y que ofreceútiles elementos para la confección de interfaces de usuario (widgets), tales como menús,desplegables de varios tipos, calendarios, etcétera.Por último, para confeccionar el layout con tres columnas, cabecera y pie que se especificaen el análisis, utilizaremos un plugin de jQuery denominado jQuery UI Layout Plugin 13.Tanto jQuery UI como jQuery UI Layout Plugin, al ser extensiones de jQuery destinadas alenriquecimiento de interfaces web (HTML), cuentan con sus propias CSS's, y son las queutilizaremos para confeccionar la interfaz de usuario de nuestra aplicación.En el archivo interfaz_de_usuario.zip tienes disponibles los layouts (plantillas) HTML que,enriquecidos con estas librerías, implementan las pantallas que hemos esbozado en elanálisis realizado en esta unidad.La estructura de directorio, una vez desplegado es la siguiente:

Creación de usuarios

87

Page 98: CursoSymfony2

ui├── layouts -> layouts *HTML*| └── images -> imágenes utilizadas por los layouts anteriores├── src│   ├── css -> *CSS* desarrolladas por nosotros│   ├── images -> imágenes desarrolladas por nosotros│   └── js -> javascript desarrollados por nosotros└── vendors -> librerías *javascript* de terceros, estas ├── CLEditor1_3_0 cuentan con sus propias *CSS's* e imágenes ├── jquery ├── jquery-layout └── tagit

Si abres directamente con el navegador los archivos del directorio layouts:

• inicio.html• inicio-editar_nota.html• login.html• registro.html

podrás ver la interfaz gráfica completamente acabada pero, obviamente, sinfuncionalidades. El objetivo de las siguientes unidades es, precisamente dotar defuncionalidades a esta interfaz utilizando Symfony2. Estos archivos nos servirán como basepara elaborar las plantillas de la aplicación.

ConclusiónEn esta unidad hemos realizado un análisis de la aplicación cuya construcción nos servirádurante el resto del curso para estudiar Symfony2. Hemos planteado un catálogo derequisitos, un modelo de datos, unos proceso. Hemos propuesto unos bocetos para lainterfaz de usuario. Y por último se han facilitado los assets (CSS's, javascripts e imágenes) ylas plantillas HTML con las que construiremos la interfaz gráfica de la aplicación.

ComentariosAquí puedes enviar comentarios, dudas y sugerencias. Utiliza la barra de scroll para recorrertodos los mensajes. El formulario de envío se encuentra al final.

Autor del código: Juan David Rodríguez García <[email protected]>

Conclusión

88

Page 99: CursoSymfony2

Unidad 6: Desarrollo de la aplicación MentorNotas (II).Rutas y Controladores

Advertencia

Atención!!!! Falta por colocar la url del curso de symfony 1.4 en una nota del final.

La construcción de la aplicación que se ha analizado en la unidad anterior puede llevarse acabo por muchos caminos. Lo importante es que elegir alguno de los que llevan a "Roma" sinsufrir demasiado.El camino que vamos a seguir en las próximas unidades no es el más corto, ya que su diseñoobedece a criterios pedágogicos, en el sentido de que intentamos presentar los conceptosde manera secuencial. Sin embargo, en la práctica esto no es lo más adecuado: lo ideal estener un conocimiento global de la herramienta y utilizar cada una de sus partes en losmomentos adecuados. Esperamos que cuando finalices el curso tengas los conocimientossufciente para usar Symfony2 eficientemente y construyas aplicaciones a la velocidad delrayo. Pero por lo pronto tendremos que dar algunos rodeos con el propósito de presentarconceptos fundamentales.

Lo primero: el bundleEn Symfony2 todo es un bundle. Aunque es una frase simplista nos recuerda que lo primeroque necesitamos para desarrollar la aplicación es, por lo menos, un bundle donde escribir sucódigo.En el análisis de la unidad anterior se especificó que el sistema debía contar con una partepara los usuarios, que es la aplicación de notas en sí, y otra parte de administración para losresponsables de la aplicación. A la primera se le suele denominar frontend y a la segundabackend. La primera decisión que hemos tomado en el diseño de la aplicación es dividirla endos bundles, uno para las funcionalidades del frontend y otro para las del backend. Estadivisión no es estrictamente necesaria, se puede meter todas las funcionalidades en un solobundle. Pero atendiendo al principio de la Separation of Concerns, preferimos separar elcódigo del frontend y del backend.

Nota

Por lo pronto, si alguien que no conoce la aplicación comienza a explorarla y amodificarla, tendrá claro donde debe ir para ver el código de cada parte.

Para desarrollar la aplicacion utilizaremos el mismo proyecto Symfony2 que venimosutizando en el curso. Así podrás ver como un sólo proyecto puede dar cabida a muchasaplicaciones.También utilizaremos el espacio de nombres de la empresa ficticia Jazzyweb y la categoriaAulasMentor para la ubicación de los bundles de la aplicación MentorNotas. Así todo elcódigo del curso quedará en un sólo proyecto. Si más adelante quieres separarlo,únicamente tienes que crear un nuevo proyecto de Symfony2 y enchufarle los bundles queutilicen la aplicación que quieras separar.

Unidad 6: Desarrollo de la aplicación MentorNotas (II). Rutas y Controladores

89

Page 100: CursoSymfony2

Este primer conjunto de decisiones de diseño nos permiten emprender la primera acción:crear los bundles. Utiliza el generador de bundles de Symfony2 tal como hicimos en launidad 3.

php app/console generate:bundle

Y usa los siguientes valores para contestar a las preguntas:

Parámetro ValorBundle namespace Jazzyweb/AulasMentor/NotasFrontendBundleBundle name JAMNotasFrontendBundleTarget directory /tu/ruta/a/cursosf2/srcConfiguration format ymldirectory structure yesautomatic update of your Kernel yesautomatic update of the Routing yes

Por comodidad, hemos acortado un poco el nombre del bundle. Si todo ha ido bien códigodel bundle se ha generado en/tu/ruta/a/cursosf2/src/Jazzyweb/AulasMentor/NotasFrontendBundle, se ha debidoregistrar en el fichero app/AppKernel.php, y debe haber una entrada en el ficheroapp/config/routing.yml que incluye el routing del bundle:

JAMNotasFrontendBundle: resource: "@JAMNotasFrontendBundle/Resources/config/routing.yml" prefix: /

Ahora construimos el bundle para las funcionalidades del backend. Vuelve a lanzar elgenerador de bundles y contesta con los siguientes datos:

Parámetro ValorBundle namespace Jazzyweb/AulasMentor/NotasBackendBundleBundle name JAMNotasBackendBundleTarget directory /tu/ruta/a/cursosf2/srcConfiguration format ymldirectory structure yesautomatic update of your Kernel yesautomatic update of the Routing yes

Aunque ya hemos generado el esqueleto para el bundle donde se implementará el backend,este último será desarrollado en la última unidad del curso. Desde ahora y hasta la unidad10, todo el desarrollo tendrá que ver con el frontend.Para que no haya colisión con las rutas del bundle de ejemplo AcmeDemoBundle, vamos acambiar la ruta _welcome del archivo app/config/routing_dev.yml por la siguiente:app/config/routing_dev.yml

Unidad 6: Desarrollo de la aplicación MentorNotas (II). Rutas y Controladores

90

Page 101: CursoSymfony2

_welcome: pattern: /welcome defaults: { _controller: AcmeDemoBundle:Welcome:index }

Y ya tenemos la ruta / disponible para nuestra aplicación. Esa ruta es muy especial, es laruta inicial, y debe ser la de entrada en nuestra aplicación. Por eso la queremos y se laquitamos a la acción AcmeDemoBundle:Welcome:index, que al fin y al cabo es de ejemplo yno tiene tanta importancia como nuestra aplicación.

Definimos las rutas y sus acciones asociadasLa siguiente decisión que hemos tomado es agrupar las acciones de nuestro frontend en lossiguientes controladores:

• NotasController, para las acciones relacionadas con la manipulación de notas.• LoginController, para las acciones relacionadas con el proceso de registro de

usuarios.• ContratosController, para las acciones relacionadas con los contratos premium

De nuevo, todas las acciones pueden ubicarse en un mismo controlador, pero las buenasprácticas de programación recomiendan separar las funcionalidades en grupos biendefinidos cuando esto sea posible.Tras el estudio del análisis de la aplicación proponemos el siguiente conjunto de rutas:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/config/routing.yml

1 jamn_homepage: 2 pattern: / 3 defaults: { _controller: JAMNotasFrontendBundle:Notas:index } 4 requirements: 5 _method: GET 6 7 jamn_conetiqueta: 8 pattern: /conetiqueta/{etiqueta} 9 defaults: { _controller: JAMNotasFrontendBundle:Notas:index }10 requirements:11 id: \d+12 _method: GET13 14 jamn_buscar:15 pattern: /buscar16 defaults: { _controller: JAMNotasFrontendBundle:Notas:index, etiqueta: termino }17 requirements:18 _method: POST19 20 jamn_nota:21 pattern: /nota/{id}22 defaults: { _controller: JAMNotasFrontendBundle:Notas:index }23 requirements:24 id: \d+25 _method: GET26 27 jamn_nueva:28 pattern: /nueva

29 defaults: { _controller: JAMNotasFrontendBundle:Notas:nueva }30

Definimos las rutas y sus acciones asociadas

91

Page 102: CursoSymfony2

31 jamn_editar:32 pattern: /editar/{id}33 defaults: { _controller: JAMNotasFrontendBundle:Notas:editar }34 requirements:35 id: \d+36 37 jamn_borrar:38 pattern: /borrar/{id}39 defaults: { _controller: JAMNotasFrontendBundle:Notas:borrar }40 requirements:41 id: \d+42 43 jamn_espacio_premium:44 pattern: /miespacio45 defaults: { _controller: JAMNotasFrontendBundle:Notas:espacioPremium }46 47 jamn_rss:48 pattern: /rss49 defaults: { _controller: JAMNotasFrontendBundle:Notas:rss }50 requirements:51 _method: GET52 _format: xml53 54 jamn_registro:55 pattern: /registro56 defaults: { _controller: JAMNotasFrontendBundle:Login:registro }57 58 jamn_activar_cuenta:59 pattern: /activar/{token}60 defaults: { _controller: JAMNotasFrontendBundle:Login:activar }61 62 jamn_tarifas:63 pattern: /tarifas64 defaults: { _controller: JAMNotasFrontendBundle:Contratos:tarifasPremium }65 requirements:66 _method: GET67 68 jamn_contratar:69 pattern: /contratar70 defaults: { _controller: JAMNotasFrontendBundle:Contratos:contratarPremium }

Ya hemos hablado de las rutas, sabemos lo que son, para que sirven y como se especifican.Pero estas rutas utilizan nuevas carácterísticas del sistema de routing de Symfony2. Vamosa verlas.En muchas de las rutas se utiliza la directiva requirements. Como su nombre indica,establece un requisito que debe cumplir la ruta para que sea válida. Uno de los usos másfrecuentes de los requirements es limitar los valoresde los placeholders medianteexpresiones regulares 14. Por ejemplo, en la ruta jamn_nota (líneas 20-25) el requisitoid: \d+, esta limitando ese placeholder a valores enteros positivos. De manera que lasiguiente URL:

http://tu.servidor/nota/mi_nota

no casa con la ruta jamn_nota, pues mi_nota no es un entero positivo, mientras que lasiguiente si lo hace:

Definimos las rutas y sus acciones asociadas

92

Page 103: CursoSymfony2

http://tu.servidor/nota/2

El uso de requisitos proporciona una definición más precisa de la ruta. Otro requisito muy útiles _method que especifica el método HTTP que debe utilizar la petición (request) para casarcon la ruta. En nuestro caso, las rutas jamn_homepage, jamn_conetiqueta, jamn_nota,jamn_tarifas y jamn_rss casan con las URL's especificadas en ellas únicamente si lapetición se ha realizado a través del método GET de HTTP.Por último el requisito _format indica al framework el formato de la petición del objetoRequest. Esto, como veremos más adelante nos servirá para seleccionar el tipo dedocumento que debemos construir en la respuesta.Ahora que ya sabemos algo más acerca del sistema de Routing de Symfony2, vamos adescribir la funcionalidad que debe implementar las acciones asociadas para satisfacer lascondiciones del análisis.

Diseño de la lógica de control para las acciones del controlador NotasControllerLos requisito 3-8 de la aplicación definen gran parte de la lógica de control de la aplicación ytambién especifica como deben mostrarse los datos. Las rutas y acciones diseñadasproponen una solución posible a dichos requisitos.Para mantener entre peticiones HTTP el filtro aplicado para obtener el listado de notas, y laidentificación de la nota seleccionada cuyos detalles se muestran, utilizaremos elmecanismo de sesión 15.Las rutas jamn_homepage, jamn_conetiqueta y jamn_buscar se mapean sobre la mismaacción JAMNotasFrontendBundle:Notas:index, la diferencia entre ellas radica en losparámetros que se pasan en la petición.Si la acción indexAction() es invocada por la ruta jamn_homepage, buscará todas las notasque coincidan con el filtro de búsqueda almacenado en la sesión y las mostrará en el listadode notas. Si ese filtro aún no existe (por ejemplo en la primera petición) se mostrarán todaslas notas.La ruta jamn_conetiqueta pasa por la URL el valor de la variable etiqueta, y seráinterpretado por la acción JAMNotasFrontendBundle:Notas:index como que debe buscar ymostrar en el listado de notas sólo aquellas asociadas a la etiqueta con id especificado portal variable. Si el valor de esta variable es todas mostrará todas las etiquetas. Esta acciónactualizará la sesión con el valor de la etiqueta que ha recibido.La ruta jamn_buscar pasa por POST la variable termino. Esto será interpretado porJAMNotasFrontendBundle:Notas:index como que debe buscar y mostrar todas las notasque contengan en su título o contenido la cadena definida por dicha variable. Esta acciónactualizará la sesión con el valor del término de búsqueda que ha recibido.En todos los casos anteriores se mostrará el detalle de la nota seleccionada que seespecifique en la sesión. Si en la sesión no se especifica ninguna, mostrará la primera dellistado de notas.La ruta jamn_nota también se mapea contra la acciónJAMNotasFrontendBundle:Notas:index, la cual buscará la nota con el id especificado porla variable id pasado por GET a través de la URL y la mostrará en el lugar reservado para eldetalle de la nota seleccionada. El listado de notas se calculará de acuerdo a lo especificadoen la sesión. Esta acción actualizará el valor de la sesión correspondiente a la notaseleccionada.La ruta jamn_nueva se mapea contra la acción JAMNotasFrontendBundle:Notas:nueva,mostrará un formulario para crear nuevas notas, y se encargará también de recibir los datosdel formulario para crear la nota.

Diseño de la lógica de control para las acciones del controlador NotasController

93

Page 104: CursoSymfony2

La ruta jamn_editar se mapea contra la acción JAMNotasFrontendBundle:Notas:editar,mostrará un formulario para editar los datos de la nota cuyo id coincide con el valor de lavariable id pasado por GET, y se encargará de grabar los datos recibidos para actualizar lanota.La ruta jamn_borrar se mapea contra la acción JAMNotasFrontendBundle:Notas:borrar, yservirá para eliminar la nota especificada por el valor de la variable id pasado por GET. Estaacción borrará el valor de la sesión correspondiente a la nota seleccionada, ya que en elmomento de borrar la nota, era precisamente esa la nota seleccionada, y una vez borrada yano puede seguir siéndolo.Todas las acciones asociadas a estas rutas pintarán un listado con todas las etiquetas delusuario y un listado de notas según lo especificado en los filtros de búsqueda almacenadosen la sesión.La ruta jamn_espacio_premium se mapea contra la acciónJAMNotasFrontendBundle:Notas:espacioPremium y será el punto de acceso a lasfuncionalidades premium.la ruta jamn_rss se mapea contra JAMNotasFrontendBundle:Notas:rss y será laencargada de suministrar el documento XML con las noticias RSS.

Diseño de la lógica de control para las acciones del controlador LoginControllerLa ruta jamn_registro se mapea contra la acciónJAMNotasFrontendBundle:Login:registro que se encargará de recoger los datos de unnuevo usuario, registrarlo en la aplicación y enviarle un e-mail de bienvenida con una URLconstruida exclusivamente para el usuario y mediante la cual tendrá que activar su cuenta.La ruta jamn_activar_cuenta se mapea contra la acciónJAMNotasFrontendBundle:Login:activar y servirá para activar la cuenta de un usuarioque ha solicitado la activación usando su URL de activación.

Diseño de la lógica de control para las acciones del controladorContratosControllerLa ruta jamn_tarifas se mapea contra la acciónJAMNotasFrontendBundle:Contratos:tarifasPremium y servirá para mostrar al usuario lastarifas existentes.La ruta jamn_contratar se mapea contra la acciónJAMNotasFrontendBundle:Contratos:contratarPremium, la cual realizará un nuevocontrato premium.

Implemetación de la lógica de control del controladorNotasControllerEl siguiente paso sería implementar las acciones y sus plantillas asociadas y fin del trabajo.Suena bien pero las cosas no son siempre tan sencillas.Para empezar aún no hemos construido nuestro modelo con el que llevaremos a cabo lasoperaciones propias de la lógica de la aplicación (manipulación de notas, registro, contratos)y la persistencia de datos. Estudiaremos este tema en la siguiente unidad, que versará sobreel servicio de persistencia de datos Doctrine.Algunas de las acciones requieren la manipulación de formularios y validaciones, tema queserá estudiado en la unidad 8.También es preciso incorporar un sistema para el tratamiento de la seguridad, que garanticeque sólo los usuarios registrados pueden usar la aplicación, y dentro de estos, sólo los

Diseño de la lógica de control para las acciones del controlador LoginController

94

Page 105: CursoSymfony2

premium que han pagado pueden usar las características premium. El tema de laautenticación y la autorización lo trataremos en la unidad 9.Cuando hayamos estudiado todos estos aspectos, estaremos en condiciones de escribir elcódigo definitivo de las acciones. Mientras tanto construiremos la lógica de control de lasacciones del controlador NotasController. Para lo cual utilizaremos un modelo de negociosimplificadísimo, carente de funcionalidad, pero suficiente para permitirnos construir lalógica de control:Las etiquetas serán representadas por arrays asociativos con las claves id y nombre, y lasnotas con arrays asociativos con las clasve id y titulo. Para cualquier búsqueda realizada,ya sea por etiqueta o por término de búsqueda, se devolverá siempre el mismo conjunto denotas.Este modelo super simple, aunque es totalmente inservible como modelo defintivo, será muyútil para centrarnos en el paso de datos entre cliente y servidor y construir una primeraversión de las acciones con la lógica de control totalmente funcional. Por otro lado, paradiseñar la jerarquía de plantillas twig tampoco es necesario contar con el modelo definitivo.Por eso también propondremos una primera versión de plantillas twig en los siguientesapartados.Durante el desarrollo de estas primeras versiones de las acciones del controladorNotasController aprenderemos:

• cómo se debe utilizar el servicio request, omnipresente en todos los controladores,• cómo se pasan parámetros a las plantillas desde las acciones,• cómo se construyen URL's correctas mediante el sistema de Routing, en las plantillas

twig y en los controladores,• la relación entre las peticiones del cliente y las respuestas del servidor,• cómo se utiliza el servicio session para mantener constancia del cliente que está

utilizando la aplicación y poder mantener el estado del mismo.Comenzamos el trabajo cambiando el nombre al controlador creado por defecto durante lageneración automática del bundle. En lugar de DefaultController lo llamaremosNotasController. Esto significa cambiar el nombre del ficherosrc/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/DefaultController.phpporsrc/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/NotasController.php, ycambiar el nombre de clase que se declara en dicho fichero por NotasController.A continuación copia el siguiente código en su interior:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/NotasController.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Controller; 4 5 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 7 class NotasController extends Controller 8 { 9 10 public function indexAction() 11 { 12 $request = $this->getRequest(); // equivalente a $this->get('request');

Diseño de la lógica de control para las acciones del controlador LoginController

95

Page 106: CursoSymfony2

13 $session = $this->get('session'); 14 15 $ruta = $request->get('_route'); 16 17 switch ($ruta) 18 { 19 case 'jamn_homepage': 20 21 break; 22 23 case 'jamn_conetiqueta': 24 $session->set('busqueda.tipo', 'por_etiqueta'); 25 $session->set('busqueda.valor', $request->get('etiqueta')); 26 $session->set('nota.seleccionada.id', ''); 27 28 break; 29 30 case 'jamn_buscar': 31 $session->set('busqueda.tipo', 'por_termino'); 32 $session->set('busqueda.valor', $request->get('termino')); 33 $session->set('nota.seleccionada.id', ''); 34 35 break; 36 case 'jamn_nota': 37 $session->set('nota.seleccionada.id', $request->get('id')); 38 break; 39 } 40 41 list($etiquetas, $notas, $nota_seleccionada) = $this->dameEtiquetasYNotas(); 42 43 return $this->render('JAMNotasFrontendBundle:Notas:index.html.twig', array( 44 'etiquetas' => $etiquetas, 45 'notas' => $notas, 46 'nota_seleccionada' => $nota_seleccionada, 47 )); 48 } 49 50 public function nuevaAction() 51 { 52 $request = $this->getRequest(); 53 $session = $this->get('session'); 54 55 if ($request->getMethod() == 'POST') { 56 57 // si los datos que vienen en la request son buenos guarda la nota 58 59 $session->setFlash('mensaje', 'Se debería guardar la nota:' 60 . $request->get('nombre') . '. Como aun no disponemos de un 61 servicio para persistir los datos, mostramos la nota 1'); 62 63 return $this->redirect($this->generateUrl('jamn_nota', array('id' => 1))); 64 } 65 66 list($etiquetas, $notas, $nota_seleccionada) = $this->dameEtiquetasYNotas();

67 68 return $this->render('JAMNotasFrontendBundle:Notas:nueva.html.twig', array( 69 'etiquetas' => $etiquetas, 70 'notas' => $notas, 71 'nota_seleccionada' => $nota_seleccionada, 72 )); 73 }

Diseño de la lógica de control para las acciones del controlador LoginController

96

Page 107: CursoSymfony2

74 75 public function editarAction() 76 { 77 $request = $this->getRequest(); 78 $session = $this->get('session'); 79 80 // Se recupera la nota que viene en la request para ser editada 81 82 $nota = array( 83 'id' => $request->get('id'), 84 'titulo' => 'nota', 85 ); 86 87 88 if ($request->getMethod() == 'POST') { 89 90 // si los datos que vienen en la request son buenos guarda la nota 91 92 $session->setFlash('mensaje', 'Se debería editar la nota:' 93 . $request->get('titulo') . 94 '. Como aún no disponemos de un servicio para persistir los 95 datos, la nota permanece igual'); 96 97 return $this->redirect($this->generateUrl('jamn_nota', array('id' => $request->get('id')))); 98 } 99 100 list($etiquetas, $notas, $nota_seleccionada) = $this->dameEtiquetasYNotas();101 102 return $this->render('JAMNotasFrontendBundle:Notas:editar.html.twig', array(103 'etiquetas' => $etiquetas,104 'notas' => $notas,105 'nota_a_editar' => $nota,106 ));107 }108 109 public function borrarAction()110 {111 $request = $this->getRequest();112 $session = $this->get('session');113 114 // borrado de la nota $request->get('id');115 116 $session->setFlash('mensaje', 'Se debería borrar la nota ' . $request->get('id'));117 $session->set('nota.seleccionada.id', '');118 119 return $this->forward('JAMNotasFrontendBundle:Notas:index');120 }121 122 public function miEspacioAction()123 {124 $params = 'Los datos de la página de inicio del espacio premium';125 return $this->render('JAMNotasFrontendBundle:Notas:index', array('params' => $params));126 }127

128 public function rssAction()129 {130 131 }132 133 /**134 * Función Mock para poder desarrollar y probar la lógica de control.135 *136 * La función real que finalmente se implemente, utilizará el filtro almacenado137 * en la sesión y el modelo para calcular la etiquetas, notas y nota seleccionada138 * que en cada momento se deban pintar.139 */140 protected function dameEtiquetasYNotas()141 {

Diseño de la lógica de control para las acciones del controlador LoginController

97

Page 108: CursoSymfony2

142 $session = $this->get('session');143 144 $etiquetas = array(145 array(146 'id' => 1,147 'texto' => 'etiqueta 1',148 ),149 array(150 'id' => 2,151 'texto' => 'etiqueta 2',152 ),153 array(154 'id' => 3,155 'texto' => 'etiqueta 3',156 ),157 );158 159 $notas = array(160 array(161 'id' => 1,162 'titulo' => 'nota 1',163 ),164 array(165 'id' => 2,166 'titulo' => 'nota 2',167 ),168 array(169 'id' => 3,170 'titulo' => 'nota 3',171 ),172 );173 174 $nota_selecionada_id = $session->get('nota.seleccionada.id');175 if(!$nota_selecionada_id)176 {177 $nota_selecionada_id = 1;178 }179 180 $nota_seleccionada = array(181 'id' => $nota_selecionada_id,182 'titulo' => 'nota '. $nota_selecionada_id,183 );184 return array($etiquetas, $notas, $nota_seleccionada);185 }186 }

Fíjate como al principio de todas las acciones se le pide al contenedor de servicios deSymfony2 una instancia de los servicios Request ($this->getRequest()) y Session($this->get('session')). El primero de ellos, Request, es un objeto que encapsula lainformación obtenida de la petición HTTP (request) y proporciona métodos para manipularla.El segundo, Session, es un objeto que encapsula y extiende con más funcionalidades elmecanismo de sesión nativo de PHP.

Diseño de la lógica de control para las acciones del controlador LoginController

98

Page 109: CursoSymfony2

Nota

Un problemita que tiene la técnica de instanciar objetos a través del contenedor deservicios es que la asistencia automática de código de los IDE's (como Netbeans oEclipse), deja de funcionar, pues la herramienta no puede saber que tipo de objetodevuelve el contenedor de servicios. Sin embargo la clase Controller de Symfony2implementa wrappers para los objetos más usados que son funciones que devuelvenun determinado servicio o función del servicio llamando ellas mismas al contenedor deservicio. Estos wrappers indican mediante comentarios de DocBlock 16 el tipo deobjeto devuelto y/o los argumentos que requiere la función. Esta información es usadapor los IDE's para la asistencia automática.Por ejemplo, estas dos líneas de código devuelven el mismo objeto Request:

$request = $this->get('request');$request = $this->getRequest();

Pero si utilizas la segunda (que es el wrapper), la asistencia de código automática detu IDE funcionará sobre el objeto $request.

Estos dos servicios son omnipresentes en las aplicaciones Symfony2. En la Request seencuentran los datos enviados por el cliente web y todo lo relacionado con la petición: la IPdel cliente, el navegador utilizado, los tipos de contenidos aceptados por el navegador, ymás cosas. Lo mejor para saber todas estas cosas es revisar la documentación sobre elservicio Request en la API de Symfony2 (http://api.symfony.com).De todas las cosas que se pueden hacer con él, lo más habitual es pedir los datos enviadospor el usuario mediante su método get(). Es lo que se hace, por ejemplo en las líneas 32 y37 del código anterior. Otro uso interesante es obtener el nombre de la ruta correspondientea la URL de la petición pasando _route como parámetro a dicho método. Esto se hace en lalínea 15.En la Session encontramos datos que la aplicación va colocando en el lado del servidor y queestán asociados a una cookie, es decir, a un cliente. De esa manera la aplicación puedemantener estados y relacionar peticiones del mismo cliente a pesar de que HTTP es unprotocolo sin estado. De nuevo recomendamos echar un vistazo a la API de Symfony2 paraver todas las cosas que se pueden hacer con este servicio. Lo más utilizado son lossiguientes métodos:

• set(string $name, mixed $value), para crear un parámetro en la sesión.• get(string $name, mixed $default = null) para recuperar un parámetro de la

sesión. Si se proporciona el segundo argumento se utilizará por defecto en caso de queno exista en la sesión tal parámetro.

• setFlash(string $name, string $value), define un parámetro flash en la sesión.Estos parámetros tienen un tiempo de vida de una petición. A la segunda peticióncontada desde el momento en que se creo el parámetro, se eliminan de la sesión. Sonmuy útiles para almacenar mensajes de error y otras notificaciones sin temor a llenar lasesión de parámetros que son utilizados una sola vez.

• getFlash(string $name, string|null $default = null), para recuperar unparámetro flash de la sesión. Si se proporciona el segundo argumento se utilizará pordefecto en caso de que no exista en la sesión tal parámetro.

Diseño de la lógica de control para las acciones del controlador LoginController

99

Page 110: CursoSymfony2

Ya nos encontramos en disposición de explicar el código propuesto, en una primera versión,para el controlador NotasController.

La acción indexAction()Tal y como lo hemos planteado, la acción indexAction() es la que más lógica de controlimplementa. Lo primero que hace es comprobar desde qué ruta ha sido invocada (línea 17).Si la petición viene de la ruta jamn_homepage no hay que hacer nada especial (líneas 19-21).Si viene de jam_conetiqueta hay que definir el filtro de búsqueda en la sesión con el valordel id de la etiqueta pasada en la petición (líneas 23-28). Si la ruta de origen esjamn_buscar, hay que definir el filtro de búsqueda en la sesión con el término de búsquedapasado en la petición (líneas 30-33). Finalmente si es jamn_nota, hay que especificar en lasesión la nota seleccionada para mostrar su detalle (líneas 36-38).

Nota

Una aclaración sobre el diseño del filtro de búsqueda. Ya que la búsqueda puedesolicitarse de dos formas distintas: por etiqueta o por término de búsqueda, hemosdecidido usar el parámetro de sesión busqueda.tipo para almacenar este dato.Después, el valor del id de la etiqueta o del término de búsqueda se almacenará enel parámetro de sesión busqueda.valor. Conseguimos de esta manera un formatohomogéneo y unívoco de expresar el filtro de búsqueda.

Una vez definido en la sesión el filtro de búsqueda, se obtienen el listado de etiquetas, el denotas y la nota seleccionada que corresponda al filtro de búsqueda existente en esemomento (línea 41). Lo hacemos mediante la función dameEtiquetasYNotas. Fíjate en suimplementación. En ella se define y utiliza el modelo sencillo pero vacío de funcionalidad quehemos propuesto más arriba. Fíjate que siempre devolverá los mismos listados de etiquetasy notas. En esto consiste el concepto de mockup; algo que presenta la interfaz pero noimplementa funcionalidad, o lo hace de manera muy simplificada.Finalmente (líneas 43-47) se pasan los datos recuperados a la plantillaJAMNotasFrontendBundle:Notas:index.html.twig que se encargará de construir undocumento HTML con toda esta información y enviarlo como respuesta al cliente. Despuésexplicaremos como hemos diseñado las plantillas de la aplicación en esta primera versión.

La acción nuevaActionEsta acción es responsable de dos funcionalidades distintas: enviar al cliente un formulariovacío para recoger los datos de la nueva nota, y procesar los datos que el cliente coloca endicho formulario. La acción sabe lo que tiene que hacer mirando el tipo de petición que lellega mediante la función getMethod() del objeto Request (línea 55). Si el el método HTTPutilizado por el cliente en la petición es GET, sólo tiene que devolver el formulario. Si esPOST significa que se están enviando los datos de la nueva nota y hay que guardarlos, hecholo cual realiza una redirección a la ruta jamn_nota con el id de la nota recien creada, paraque se muestre en el detalle.Como aún no disponemos de un servicio de persistencia ni de un modelo funcional, laacción, en realidad, no guarda nada, pero construye un mensaje que almacena en unparámetro flash de la sesión (líneas 59-61) que informa de este hecho.La línea 63 es muy interesante pues en ella se hace uso de dos carácteristicas muy útiles de Symfony2; la redirección y la generación de una ruta desde un controlador. Una redirección consiste en enviar al cliente una respuesta con código HTTP 300 y una URL de redirección.

La acción indexAction()

100

Page 111: CursoSymfony2

Este código indica al navegador que debe realizar una nueva petición a la URL indicada. EnSymfony2 esto se realiza devolviendo una respuesta con la función redirect() del objetoController:

return $this->redirec('http://www.google.es');

El código anterior realizaría una redirección a la página principal de google.Si queremos crear desde un controlador la URL de alguna de las rutas de nuestro proyectodebemos utilizar la función generateURL() de la clase Controller. Esta función es, enrealidad, un wrapper al método generate() del servicio Router de Symfony2.

Nota

Como ya te habrás imaginado, la clase Controller implementa wrappers a losservicios y funciones de servicios más utilizados.

La línea 63 es una combinación de estas dos funciones; redirect() y generateUrl(), y elresultado es indicar al navegador cliente que realice una redirección a la URLcorrespondiente a la ruta jamn_nota. Esta redirección no se realiza caprichosamente.Gracias a ella se evita que, si una vez creada la nota el usuario recarga accidentalmente lapágina en el navegador, se envién por segunda vez los datos del formulario, y se vuelva acrear la misma nota de nuevo.Esta acción se pintará con la plantilla JAMNotasFrontendBundle:Notas:nueva.html.twig.Dicha plantilla, muestra los mismos elementos que la plantillaJAMNotasFrontendBundle:Notas:index.html.twig, con la diferencia de que en el lugar deldetalle de la nota seleccionada se dibuja el formulario para la creación de la nota. Por eso laacción nuevaAction() necesita pasarle a su plantilla el listado de etiquetas y notascorrespondientes al filtro de búsqueda almacenado en la sesión. Ese es la razón de ser de lalínea 66; idéntica a la línea 41.

La acción editarActionEsta acción es prácticamente igual que la anterior. Tiene dos funciones: dibujar unformulario para editar la nota que se haya seleccionado, y grabar los datos enviados por elcliente para actualizar la nota. Para ello usa la misma estratégica que la acciónnuevaAction(): discriminar por el método HTTP usado en la petición mediante la funcióngetMethod() del objeto request.Las diferencias son:

1. Debe recuperar la nota que viene en la petición. Esto se hace en las líneas 82-85. Comoaún no disponemos de un modelo funcional ni de una base de datos de donde recuperarlas entidades, hemos hecho una simulación en la que se recoge el id del objetorequest y se crea una nota según el modelo desprovisto de funcionalidad (mock) quevenimos usando durante todo el rato. Al fin y al cabo nos estamos centrando en el pasode variables entre cliente y servidor y para eso no necesitamos más,

2. El tercer parámetro que se le pasa a su plantillaJAMNotasFrontendBundle:Notas:editar.html.twig, es la nota a editar en lugar de lanota seleccionada.

De nuevo la redirección después de actualizar la nota es importante para evitar reenvíosaccidentales de datos por recarga de la página desde el navegador.

La acción editarAction

101

Page 112: CursoSymfony2

La plantilla asociada a esta acción también pintará el listado de etiquetas y notascorrespondientes al filtro que haya almacenado en la sesión. Dichos listados se recuperan enla línea 100 (igual a las líneas 41 y 66). El formulario de edición se pintará en el lugar deldetalle de la nota.

La acción borrarAction()Esta acción recoge del objeto request el id de la nota que se desea borrar y realiza laoperación de borrado. Como aún no disponemos de un modelo funcional, por lo pronto nohace nada más que indicar lo que se debe hacer en un parámetro flash de la sesión que semostrará en la respuesta. Una vez borrada la nota, se borra también el parámetro de sesiónnota.seleccionada.id, para que no se produzca un error cuando se vaya a pintar el detallede la nota (línea 117). Finalmente se realiza una redirección a la página principal. De nuevoesta redirección es importante para evitar reenvíos accidentales de datos por recarga de lapágina desde el navegador.

Las acciones miEspacioAction() y rssActionLa lógica de control de estas acciones no ofrece ningún problema. Así que, por lo pronto, lasdejamos sin implementar.

Implemetación de las plantillas del controlador NotasControllerTodas las acciones que acabamos de implemetar en una primera versión calculan el listadode etiquetas y notas correspondiente al filtro de búsqueda que haya almacenado en lasesión, ya que las páginas que generan deben mostrar dichos listados. La diferencia entreellas es que donde unas pintan un detalle de la nota seleccionada (indexAction() yborrarAction()), otras dibujan un formulario para recoger datos (nuevaAction() yeditarAction()).Este hecho, junto con la herencia en tres niveles recomendada para organizar las plantillas yel principio DRY (Don't Repeat Yourself), nos sugiere el siguiente diseño para las plantillas.

• Una plantilla general para la aplicación con la estructura HTML básica:app/Resources/views/base.html.twig.

• Una plantilla que herede la anterior y que muestre el listado de etiquetas y notas y el formulario de búsqueda denotas:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/layout-etiquetas-notas.html.twig.

• Las plantillas especificadas en las acciones del controlador ControllerNotas queheredarán de la anterior los elementos comunes. Así no repetimos código entre lasplantillas.

El resultado de aplicar estos principios es el siguiente código para las plantillas:app/Resources/views/base.html.twig

1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 <title>{% block title %}Welcome!{% endblock %}</title> 6 {% block stylesheets %}{% endblock %} 7 <link rel="shortcut icon" href="{{ asset('favicon.ico') }}" /> 8 </head> 9 <body>10 {% block body %}{% endblock %}

La acción borrarAction()

102

Page 113: CursoSymfony2

11 {% block javascripts %}{% endblock %}12 </body>13 </html>

src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/layout-etiquetas-notas.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block stylesheets %} 4 5 <style> 6 .marco { 7 border-style: solid; 8 padding: 5px; 9 margin: 5px;10 float: left;11 width: 500px;12 height: 250px;13 }14 15 .detalle_nota{16 background-color: #DFDFDF;17 }18 19 .mensaje{20 width: 1025px;21 height: 20px;22 background-color: #f66;23 24 }25 26 </style>27 28 {% endblock %}29 30 {% block body %}31 32 <h1>Implementación de la lógica de control de la aplicación</h1>33 34 {% if app.session.getFlash('mensaje') %}35 <div id="etiquetas" class="marco mensaje">36 <b>{{ app.session.getFlash('mensaje') }}</b>37 </div>38 {% endif %}39 40 <div id="etiquetas" class="marco">41 <h2>Listado de etiquetas</h2>42 <p>Siempre se muestran todas las etiquetas</b></p>43 <ul>44 {% for etiqueta in etiquetas %}45 <li>46 <a href="{{ path('jamn_conetiqueta', {etiqueta: etiqueta.id}) }}">47 {{ etiqueta.texto}}

48 </a>49 </li>50 {% endfor %}51 </ul>

La acción borrarAction()

103

Page 114: CursoSymfony2

52 </div>53 54 <div id="notas" class="marco">55 <h2>Listado de notas</h2>56 <p> Se mostraran las notas que coincidan con el siguiente criterio de búsqueda:</p>57 tipo de búsqueda: <b>{{ app.session.get('busqueda.tipo') }}</b>,58 valor de la búsqueda: <b>{{ app.session.get('busqueda.valor') }}</b>59 60 <ul>61 {% for nota in notas %}62 <li>63 <a href="{{ path('jamn_nota', {id: nota.id}) }}">{{ nota.titulo }}</a>64 </li>65 {% endfor %}66 </ul>67 68 </div>69 70 <div id="buscar" class="marco">71 <h2>Formulario de búsqueda de notas</h2>72 <form action="{{ path('jamn_buscar') }}" method="POST">73 74 <input type="text" id="termino" name="termino" />75 <input type="submit" value="buscar" />76 77 </form>78 </div>79 80 {% endblock %}

src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/index.html.twig.

1 {% extends 'JAMNotasFrontendBundle:Notas:layout-etiquetas-notas.html.twig' %} 2 3 {% block body %} 4 5 {{ parent() }} 6 7 <div id="detalle_nota" class="marco detalle_nota"> 8 <h2>Detalle de nota</h2> 9 {% if nota_seleccionada %}10 <a href="{{ path('jamn_nueva') }}">Nueva nota</a>11 <a href="{{ path('jamn_editar', {'id': nota_seleccionada.id }) }}">Editar nota </a>12 <a href="{{ path('jamn_borrar', {'id': nota_seleccionada.id }) }}">Borrar nota</a>13 14 <table border="1">15 <tr>16 <td>ID</td>17 <td>{{ nota_seleccionada.id }}</td>18 </tr>19 <tr>20 <td>nombre</td>21 <td>{{ nota_seleccionada.titulo }}</td>22 </tr>23 </table>24 </div>25 {% endif %}26 27 {% endblock %}

src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/nueva.html.twig.

La acción borrarAction()

104

Page 115: CursoSymfony2

1 {% extends 'JAMNotasFrontendBundle:Notas:layout-etiquetas-notas.html.twig' %} 2 3 {% block body %} 4 5 {{ parent() }} 6 7 <div id="detalle_nota" class="marco detalle_nota"> 8 <h2>Detalle de nota</h2> 9 <h2>Formulario crear Nueva Nota</h2>10 <form action="{{ path('jamn_nueva') }}" method="POST">11 nombre:<input type="text" id="nombre" name="nombre" />12 <input type="submit" value="guardar" />13 14 </form>15 </div>16 17 {% endblock %}

src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/editar.html.twig.

1 {% extends 'JAMNotasFrontendBundle:Notas:layout-etiquetas-notas.html.twig' %} 2 3 {% block body %} 4 5 {{ parent() }} 6 7 <div id="detalle_nota" class="marco detalle_nota"> 8 <h2>Detalle de nota</h2> 9 <h2>Formulario editar Nota {{ nota_a_editar.id}}</h2>10 <form action="{{ path('jamn_editar', {id: nota_a_editar.id}) }}" method="POST">11 <input type="text" id="titulo" name="titulo" value="{{ nota_a_editar.titulo }}" />12 <input type="submit" value="guardar" />13 </form>14 </div>15 16 {% endblock %}

Fíjate en los bloques body de las tres últimas plantillas. Para que hereden el código de laplantilla padre no limitandose a cambiarlo, se utiliza la función de twig parent(), cuyafunción es colocar en su lugar el código del bloque padre. Es la manera de añadir más códigoa los bloques heredados en lugar de sustituirlos completamente.Ya puedes comprobar el funcionamiento de la lógica de control que hemos propuesto y decirque has terminado la unidad 6.

La unidad en chuletas

Requerimientos en el Routing

nombre_ruta: pattern: /patron/de/la/{ruta}.{_format}/{culture} defaults: { _controller: NombreBundle:Controlador:accion, _format: html } requirements: ruta: ^[a-zA-Z0-9]+$ _method: GET _format: html|rss culture: es|fr|en

Si un placeholder es incluido en la sección defaults se convierte en opcional.

La unidad en chuletas

105

Page 116: CursoSymfony2

Generando rutasEn una plantilla twig:

<!-- Ruta relativa --><a href="{{ path('nombre_ruta', { 'param': 'val' }) }}"> ...</a>

<!-- Ruta absoluta --><a href="{{ url('nombre_ruta', { 'param': 'val' }) }}"> ...</a>

En una acción de un controlador:

<?php...$router = $this->get('router');$url = $router->generate('nombre_ruta', array('param' => 'val'), true);

// es equivalente a usar el wrapper:

$url = $this->generateUrl('nombre_ruta', array('param' => 'val'), true);

Servicio RequestDesde una acción de un controlador:

<?php...$request = $this->getRequest(); // equivalente a $this->get('request');

$request->get('param', 'valor_defecto'); // Obtiene el parámetro 'param'. // Usa 'valor_defecto' si no existe.

$request->get('_route'); // obtiene el nombre de la ruta que ha disparado la acción.

$request->getMethod(); // obtiene el método HTTP que se usó en la petición

// Mira la API, hay muchos más

Servicio SessionDesde la acción de un controlador:

<?php...$session = $this->get('session'); // equivalente a $this->getRequest()->getSession();

$session->get('param', 'valor_defecto'); // Obtiene el parámetro de sesión 'param'. // Usa 'valor_defecto' si no existe.

$session->set('param', 'val'); // Define un parámetro de las sesión

Generando rutas

106

Page 117: CursoSymfony2

$session->getFlash('param'); // Obtiene el parámetro de sesión flash 'param'.

$session->setFlash('param', 'val'); // Define un parámetro flash de la sesión

Ampliando el código de un bloque heredado en una plantilla twigPara ello se usa la función parent().

...{% block body %}

{{ parent() }}

código nuevo

{% endblock %}

Generando redirecciones desde un controlador

Haciendo forwarding a otra acción. code-block: php

<?phpreturn $this->forward('JAMNotasFrontendBundle:Notas:index');

ComentariosAquí puedes enviar comentarios, dudas y sugerencias. Utiliza la barra de scroll para recorrertodos los mensajes. El formulario de envío se encuentra al final.

Autor del código: Juan David Rodríguez García <[email protected]>

Ampliando el código de un bloque heredado en una plantilla twig

107

Page 118: CursoSymfony2

Unidad 7: Desarrollo de la aplicación MentorNotas (III).El modelo y la persistencia de datos.Ya hemos comentado en un par de ocasiones que Symfony2 no es un framework MVC ya queúnicamente proporciona servicios para la parte del controlador y la vista, dejando alprogramador la tarea de construir el modelo sus herramientas asociadas.Sin embargo la distribución standard de Symfony2, que es la que estamos utilizando en estecurso, incorpora como herramienta de terceros (vendors) un potente framework para eltratamiento de la persistencia de datos denominado Doctrine2. No obstante optar por su usopara la construcción del modelo, no hace que la aplicación sea más o menos "symfónica".Podemos utilizar otras herramientas 17 concebidas para acceder y manipular el sistema deinformación que utilicemos como backend de persistencia de nuestra aplicación.Gran parte de las funcionalidades del modelo de una aplicación web tienen que ver con losdatos persistentes, es decir, con los datos que se deben almacenar en algún tipo de base dedatos. Por ello es muy frecuente identificar el modelo con la herramienta que se utilice paraacceder a los datos y con la estructura datos en sí, aunque esto no sea estrictamente cierto.Por ello a veces se habla de Doctrine2 como el modelo de la aplicación cuando en realidades una herramienta que nos ayuda a construir y manipular nuestro modelo.Esta unidad trata de la construcción del modelo y de la persistencia de los datos conDoctrine2. Al final de la misma tendremos disponibles una serie de clases, denominadasentidades, que responden al modelo de datos especificado en el análisis de la aplicación(unidad 5) y un servicio con el que podemos manipular y persistir en la base de datos talesentidades.

El Object Relational Mapping (ORM)Doctrine2 es un Object Relational Mapping, ORM a partir de ahora. Sin entrar a proponer unatraducción en castellano para el término, diremos que la finalidad de un ORM es la deproporcionar una capa orientada a objetos encima de una base de datos relacional, demanera que las filas o tuplas de las tablas sean tratados como objetos. La asociaciónexistente entre dichos objetos se corresponde con las relaciones entre las tablas de la basede datos. La siguiente imagen, tomada del libro oficial de Symfony2, ilustra bastante bien loque acabamos de decir:

Object Relational Mapping

La gran ventaja de usar un ORM es la reutilización de los objetos en distintas partes de la aplicación e incluso en distintas aplicaciones, proporcionando un modelo que encaja perfectamente en un desarrollo orientado a objetos. La manera de recuperar, introducir y modificar datos es independiente del sistema gestor de base de datos que utilicemos; la sintaxis utilizada es siempre la misma y es el propio ORM quien construye la query (SQL)

Unidad 7: Desarrollo de la aplicación MentorNotas (III). El modelo y la persistencia de datos.

108

Page 119: CursoSymfony2

adecuada al sistema gestor de base de datos que en cada momento utilice la aplicación.Esto proporciona una gran portabilidad a nuestros proyectos, ya que si deseamos hacerlosfuncionar con otro sistema de base de datos distinto bastará con indicarlo en los ficheros deconfiguración del ORM, y no tendremos que cambiar nada de nuestro código.Los elementos comunes a cualquier ORM son las entidades (es así como se suelendenominar a los objetos del modelo) y los metadatos que definen las relaciones entre lasentidades y la base de datos. Cada ORM, en su documentación, debe explicar como sedeben crear dichas entidades, y como se deben especificar los metadatos que mapean lasentidades con las tablas de la base de datos. Por otro lado, el ORM debe proporcionar algúntipo de mecanismo sencillo, flexible y versatil para realizar búsquedas de objetos quesatisfacen un criterio y para realizar operaciones de persistencia, esto es; guardar cambiosrealizados en los objetos, crear otros nuevos y eliminarlos. En esta unidad estudiaremoscomo se realiza todo esto con Doctrine2 desarrollando el modelo de nuestra aplicaciónMentorNotas.Antes de continuar tenemos que aclarar que Doctrine2 es un producto muy potente, y porello, bastante complejo. Su estudio en profundidad abarcaría un curso completo dedicado enexclusiva a Doctrine2. Para que te hagas una idea de lo que estamos hablando, echa unvistazo a la documentación oficial.No tengas la menor duda de que si decides usar Doctrine2 como ORM para construir tumodelo tendrás que visitar esta web en más de una ocasión. Por tanto, el estudio quehacemos en esta unidad no puede considerarse como un compendio completo de Doctrine2,se trata más bien de una introducción a fondo que trata la mayor parte de los conceptosbásicos imprescindibles para manejarse con Doctrine2 en una aplicación construida conSymfony2.

Las entidadesEl modelo de datos especificado en el análisis de la unidad 5 presenta las entidades quemodelan nuestra aplicación y las relaciones de asociación entre ellas. Cada entidad estacompuesta por unos atributos o campos y, posiblemente, por relaciones con otras entidades.Así, por ejemplo, la entidad Nota está compuesta por los atributos titulo, texto y fecha yademás esta relacionada con la entidad Usuario de manera que muchas Nota's puedenpertenecer a un único Usuario (relación muchos a uno), y con la entidad Etiqueta demanera que muchas Etiqueta's pueden estar asociadas a muchas Nota's (relación muchos amuchos). Además, estas entidades, como objetos que son, pueden definir los métodosnecesarios para dotarlas de funcionalidad.Doctrine2 no exige nada especial sobre las entidades del modelo, de manera que pueden serimplementadas como objetos planos de PHP, es decir no tienen por que extender ningunaclase propia del ORM como se hace comunmente en la mayoría de ORM's existentes. Estehecho proporciona una gran flexibilidad al programador así como la posibilidad de reutilizarestos objetos en otras partes de la aplicación que no tienen que ver nada con la persistenciao incluso en otras aplicaciones.Lo único que exige Doctrine2 a las entidades que van a ser persistidas en la base de datoses que dispongan de métodos getter's y setter's típicos de cualquier objeto para poderacceder a sus atributos. Es decir, si una entidad tiene un atributo llamado atributo, debedefinir un método getAtributo() para recuperarlo, y otro setAtributo($valor) paradefinirlo. Por supuesto, la entidad puede definir todos los métodos que se deseen, aunque nose vayan a utilizar nunca con Doctrine2.Para concretar un poco; la siguiente clase, a falta de implementar sus métodos, sería unaentidad válida para ser persistida por Doctrine2:

Las entidades

109

Page 120: CursoSymfony2

<?php

class Nota{ private id; private titulo; private texto; private fecha;

public function getId() {...}

public function setId($id) {...}

public function getTitulo() {...}

public function setTitulo($t) {...}

public function getTexto() {...}

public function setTexto($t) {...}

public function getFecha() {...}

public function setFecha($f) {...} }

Como ves no tiene nada de especial. Entonces, ¿cómo sabe Doctrine2 de qué forma laentidad y sus atributos deben ser mapeados sobre la base de datos? Tranquilos por que nohay magia por ningún lado, los chicos de Symfony2 y Doctrine2 no son partidarios de losobjetos mágicos y prefieren que sea el programador quien controle todos los aspectos de laaplicación. La relación entre el objeto y la base de datos relacional, es decir el ObjectRelational Mapping, se lleva a cabo a través de la configuración. ¿Te suena de algo esto deseparar la configuración del uso?. En efecto, nos estamos refiriendo al patrón Injección deDependencias.Una vez especificadas en la configuración las relaciones entre entidades y base de datos, elservicio de persistencia Doctrine2 ya está listo para manipular los datos de la base de datosa través de las entidades (objetos) del modelo.Así pues, la secuencia de pasos para poder utilizar Doctrine2 es:

1. Diseñar el modelo de datos especificando las asociaciones entre sus entidades (uno amuchos, muchos a uno, muchos a muchos).

2. Construir las entidades como objetos planos de PHP con sus getters y setters de accesoa las propiedades.

Las entidades

110

Page 121: CursoSymfony2

3. Especificar por configuración las relaciones entre las entidades y la base de datos. Setrata de indicar con qué tabla se corresponde cada entidad y de qué tipos son susatributos.

En las siguientes secciones propondremos la estrategia que hemos seguido para construir unmodelo según lo especificado en el análisis y persistible con Doctrine2.

Construcción de las entidades. El generador de entidades deSymfony2En principio, para crear las clases correspondientes a las entidades de nuestro modelo no esnecesario más que el editor de textos de tu IDE. Sin embargo Symfony2 nos puede ayudarbastante si utilizamos el generador automático de entidades de Doctrine2. Se trata de unaherramienta que nos proporciona una primera versión de las entidades con los atributos y laconfiguración requerida para el mapeo sobre la base de datos. Las asociaciones entreentidades debemos resolverlas posteriormente a mano.Lanza el siguiente comando en la raíz del proyecto:

php app/console generate:doctrine:entity

Lee con atención las preguntas que te hace y contesta con las siguientes respuestas:

Entity shortcut name: JAMNotasFrontendBundle:NotaConfiguration format: annotation

New field name : tituloField type : stringField length : 255

New field name : textoField type : text

New field name : fechaField type : datetime

New field name : pathField type : stringField length : 255

generate an empty repository class: yes

• El Entity shortcut name es el nombre de la entidad junto con el bundle donde que laalojará. Es la misma idea que los nombres lógicos para los controladores y las plantillasque hemos visto en unidades anteriores. Las entidades se generan siempre en eldirectorio Entity del bundle. Por eso, para especificar su nombre corto (o lógico) sólose require el nombre del bundle y el de la propia entidad.

• El Configuration format establece la manera en que se van a especificar los metadatos con la información de mapeo entre la entidad y la base de datos, es decir, como se va a especificar la configuración. Como todos los ficheros de configuración en Symfony2 se puede optar entre yml, xml, php y anotaciones. Esta última forma, las anotaciones, es distinta conceptualmente a las demás. Estamos acostumbrado a que las configuraciones se especifiquen en ficheros dedicados, ya sean yml, xml, php, sin

Construcción de las entidades. El generador de entidades de Symfony2

111

Page 122: CursoSymfony2

embargo las anotaciones plantean especificar la configuración en el propio archivo en elque se declara y define una clase. Si se trata de una entidad, en el propio archivo de laentidad, utilizando una sintaxis especial en los bloques de comentarios que es capaz deinterpretar el contenedos de servicios. Esto no siempre tiene sentido, pero para el casode los metadatos de mapeos de entidades en base de datos es especialmente útil, yaque de esa manera tenemos en un mismo fichero la declaración de la clase con losatributos y la manera en que debe realizarse el mapeo sobre la base de datos.Indicamos, por último, que el conjunto de todos estos metadatos se conoce en elcontexto de los ORM's con el nombre de schema.

• A continuación vamos introduciendo los atributos de nuestra entidad y los tiposasociados. Con esta información, el generador de entidades automático, creará lainformación del schema, esto es, los metadatos de mapeo.

• Cuando hemos insertado el último atributo, pulsamos una vez más return ycontestamos a la últmima pregunta: "¿generate an empty repository class?", sicontestamos afirmativamente, además de una clase para la entidad, se generará unarchivo cuya finalidad será la implementación de métodos para la recuperación decolecciones de objetos de esa entidad que satisfacen distintos criterios. Al conjunto deestos métodos Doctrine2 les llama Repositorio.

Ahora debes tener en el bundle un nuevo directorio denominado Entity con dos archivos:Nota.php y NotaRepository, el primero representa la entidad, y el segundo su repositorioasociado (por lo pronto vacío). Vamos a echar un vistazo al ficherosrc/Jazzyweb/AulasMentor/NotasFrontendBundle/Entity/Notas.php.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Entity/Notas.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Entity; 4 5 use Doctrine\ORM\Mapping as ORM; 6 7 /** 8 * Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Nota 9 * 10 * @ORM\Table() 11 * @ORM\Entity(repositoryClass="Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\NotaRepository") 12 */ 13 class Nota 14 { 15 /** 16 * @var integer $id 17 * 18 * @ORM\Column(name="id", type="integer") 19 * @ORM\Id 20 * @ORM\GeneratedValue(strategy="AUTO") 21 */ 22 private $id; 23 24 /** 25 * @var string $titulo 26 * 27 * @ORM\Column(name="titulo", type="string", length=255) 28 */

29 private $titulo; 30 31 /** 32 * @var text $texto 33 *

Construcción de las entidades. El generador de entidades de Symfony2

112

Page 123: CursoSymfony2

34 * @ORM\Column(name="texto", type="text") 35 */ 36 private $texto; 37 38 /** 39 * @var datetime $fecha 40 * 41 * @ORM\Column(name="fecha", type="datetime") 42 */ 43 private $fecha; 44 45 /** 46 * @var string $path 47 * 48 * @ORM\Column(name="path", type="string", length=255) 49 */ 50 private $path; 51 52 53 /** 54 * Get id 55 * 56 * @return integer 57 */ 58 public function getId() 59 { 60 return $this->id; 61 } 62 63 /** 64 * Set titulo 65 * 66 * @param string $titulo 67 */ 68 public function setTitulo($titulo) 69 { 70 $this->titulo = $titulo; 71 } 72 73 /** 74 * Get titulo 75 * 76 * @return string 77 */ 78 public function getTitulo() 79 { 80 return $this->titulo; 81 } 82 83 /** 84 * Set texto 85 * 86 * @param text $texto 87 */

Construcción de las entidades. El generador de entidades de Symfony2

113

Page 124: CursoSymfony2

88 public function setTexto($texto) 89 { 90 $this->texto = $texto; 91 } 92 93 /** 94 * Get texto 95 * 96 * @return text 97 */ 98 public function getTexto() 99 {100 return $this->texto;101 }102 103 /**104 * Set fecha105 *106 * @param datetime $fecha107 */108 public function setFecha($fecha)109 {110 $this->fecha = $fecha;111 }112 113 /**114 * Get fecha115 *116 * @return datetime117 */118 public function getFecha()119 {120 return $this->fecha;121 }122 123 /**124 * Set path125 *126 * @param string $path127 */128 public function setPath($path)129 {130 $this->path = $path;131 }132 133 /**134 * Get path135 *136 * @return string137 */138 public function getPath()139 {140 return $this->path;141 }

Construcción de las entidades. El generador de entidades de Symfony2

114

Page 125: CursoSymfony2

142 }

La línea 3 ubica a la clase que se está definiendo en el archivo al espacio de nombresJazzyweb\AulasMentor\NotasFrontendBundle\Entity, reservado para las entidades. Y enla línea 5 se hace uso de las clases perteneciente al espacio de nombresDoctrine\ORM\Mapping, definiendo el alias ORM para referirse al mismo en el resto delfichero. Fíjate bien en los bloques de comentario. Como comentarios que son, no afectanpara nada a la clase en sí. Sin embargo utilizan una sintaxis especial para describirinformación de mapeo de la entidad en la base de datos.

• En la línea 8 se establece el nombre completo de la entidad.• En la línea 10 se especifica que la entidad se corresponde con una tabla

(@ORM\Table()). Como no se le ha pasado ningún argumento, el nombre de la tablaserá igual que el nombre de la entidad: Nota. Si quisiéramos cambiarloespecificaríamos: ORM\Table('name='nombre_de_la_tabla').

• En la línea 11, se indica el nombre del repositorio asociado a la entidad. El repositorio noes más que una clase que contendrá métodos para recuperar colecciones de objetos deesa entidad.

• En cada uno de los atributos de la clase, se especifica la forma en que se debe mapearel atributo en una columna de la tabla de la base de datos. Lo mejor para explorar lasintaxis de mapeo en los distintos formatos de configuración es consultando ladocumentación oficial sobre el mapping. Examina detalladamente cada uno de losatributos creados y sus metadatos de mapeo correspondientes. Hay que aclarar que elprimer atributo: id, lo ha insertado el generador de entidades automáticamente, con elfin de que sirva de clave principal en la tabla asociada.

• Por último, se han generado los getters y setters correspondientes a cada atributo.Generamos a continuación el resto de las entidades. Lanza para cada una de ellas elcomando:

php app/console generate:doctrine:entity

Y sigue estas indicaciones para la definición de las entidades:Contrato

Entity shortcut name: JAMNotasFrontendBundle:ContratoConfiguration format: annotations

New field name : fechaField type : date

New field name : referenciaField type : stringField length : 255

generate an empty repository class: yes

Etiqueta

Entity shortcut name: JAMNotasFrontendBundle:EtiquetaConfiguration format: annotations

Construcción de las entidades. El generador de entidades de Symfony2

115

Page 126: CursoSymfony2

New field name : textoField type : stringField length : 255

generate an empty repository class: yes

Grupo

Entity shortcut name: JAMNotasFrontendBundle:GrupoConfiguration format: annotations

New field name : nombreField type : stringField length : 255

New field name : rolField type : stringField length : 20

generate an empty repository class: yes

Publicidad

Entity shortcut name: JAMNotasFrontendBundle:PublicidadConfiguration format: annotations

New field name : nombreField type : stringField length : 255

New field name : textoField type : stringField length : 255

New field name : pathField type : stringField length : 255

generate an empty repository class: yes

Tarifa

Entity shortcut name: JAMNotasFrontendBundle:TarifaConfiguration format: annotations

New field name : nombreField type : stringField length : 255

New field name : periodoField type : integer

Construcción de las entidades. El generador de entidades de Symfony2

116

Page 127: CursoSymfony2

New field name : precioField type : float

New field name : validoDesdeField type : date

New field name : validoHastaField type : date

generate an empty repository class: yes

Usuario

Entity shortcut name: JAMNotasFrontendBundle:UsuarioConfiguration format: annotations

New field name : nombreField type : stringField length : 255

New field name : apellidosField type : stringField length : 255

New field name : saltField type : stringField length : 255

New field name : usernameField type : stringField length : 255

New field name : passwordField type : stringField length : 255

New field name : emailField type : stringField length : 255

New field name : isActiveField type : boolean

New field name : tokenRegistroField type : stringField length : 255

generate an empty repository class: yes

Con esto ya tenemos una primera versión del modelo con anotaciones sobre como se debe realizar el mapeo en la base de datos. Aún tenemos que definir las asociaciones entre las entidades para que el modelo sea completo. Sin embargo, antes de dar este paso vamos a crear una primera versión de la base de datos correspondiente a este modelo. Más adelante,

Construcción de las entidades. El generador de entidades de Symfony2

117

Page 128: CursoSymfony2

cuando hayamos completado el modelo con la información de las asociaciones, volveremosa generar la base de datos. Esto nos dará una idea de la potencia y versatilidad de Doctrine2para manipular y modificar la base de datos incrementalmente a partir de los metadatos demapeo.

Creación de la base de datosAntes de crear la base de datos hay que configurar el servicio Doctrine2 con los parámetrosde conexión correctos. Esto se hace en el archivo global app/config/parameters.ini (¿teacuerdas de él?).app/config/parameters.ini

; These parameters can be imported into other config files; by enclosing the key with % (like %database_user%); Comments start with ';', as in php.ini[parameters] database_driver = pdo_mysql database_host = localhost database_port = database_name = mentornotas database_user = root database_password = root ...

Para que las operaciones que vamos a hacer a continuación puedan llevarse a cabo,asegurate de que el usuario de la base de datos tenga permisos para crear bases de datosen el sistema.

Nota

Nosotros estamos utilizando el usuario root de MySQL, de esa manera no tendremosproblemas con los permisos. Pero atentos, esto es justificable únicamente en elentorno de desarrollo, donde podemos relajarnos con el tema de la seguridad. Una vezque pasemos la aplicación a un entorno de producción, estas licencias no deben serpermitidas.

Para crear la base de datos (vacía) lanza el comando de consola:

php app/console doctrine:database:create

Comprueba que se ha creado una base de datos con nombre mentornotas en tu sistemagestor de base de datos.Y ahora con este comando se generarán las tablas de acuerdo a la información especificadaen las anotaciones de cada entidad:

php app/console doctrine:schema:create

Échale un vistazo a la base de datos. Ya tiene tablas. Es posible que, más adelante, nos demos cuenta de que algunos campos deberían poder ser nulos. O que nos hemos equivocado en el tipo. No pasa nada; cambiamos las anotaciones pertinentes y actualizamos

Creación de la base de datos

118

Page 129: CursoSymfony2

el schema, es decir, la definición de la base de datos, mediante:

php app/console doctrine:schema:update

De hecho, durante al inicio de la construcción del modelo, esta instrucción se suele lanzarmuchas veces pues estamos continuamente refinando el modelo hasta que lo dejamos comorealmente queremos. A medida que la aplicación se estabiliza, estas actualizaciones sonmenos frecuentes, pero se siguen haciendo. La gracia de Doctrine2 es que lanza sobre labase de datos los comandos necesarios para realizar los cambios incrementalmente sinperder datos. De todas formas hay que tener cuidado con esto pues no siempre funciona.Por eso todos los cambios que se realicen deben hacerse en el entorno de desarrollo (lo cuales una norma general en cualquier ámbito de la informática).Aunque lo normal ahora sería terminar de construir el modelo añadiendo las asociacionesentre las entidades, no lo vamos a hacer. Nos conformaremos con el modelo inclompleto quetenemos en este momento para mostrar como se crean, editan y eliminan objetos usando elservicio de persistencia de Doctrine2 en combinación con las entidades recien generadas.Después, cuando tengamos algunos datos en la base de datos, vamos a completar el modeloy actualizar la estructura de la base de datos con la nueva información sobre lasasociaciones (o relaciones en el contexto de la base de datos). El objetivo que perseguimoscon este procedimiento "desordenado", es ilustrar lo que suele ocurrir en la práctica: queuna vez que tenemos el modelo funcionando con su base de datos asociada, hay querealizar cambios que afectan a la estructura de la base de datos. Veremos como Doctrine2nos proporciona un ayuda inestimable a la hora de ejecutar dichos cambios estructurales(que son los más fastidiosos) sin que haya pérdida de datos.

El servicio de persistencia Doctrine2Para no contaminar el controlador NotasController con los ejemplos que vamos a utilizaren esta unidad para estudiar el ORM, crearemos otro nuevo controlador denominadoEstudioORMController y en él escribiremos el código que es ajeno a la aplicación. La rutade este controlador será:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/EstudioORMController.php.También crearemos un directorio para almacenar las vistas de este controlador. Su rutaserá: src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/view/EstudioORM.Ahora vamos a crear una ruta y una acción asociada para mostrar la manera de crearentidades y persistirlas con Doctrine2.Añade al archivosrc/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/config/routing.yml, lasiguiente ruta:

jamn_ORM_crear: pattern: /estudio_orm/crear defaults: { _controller: JAMNotasFrontendBundle:EstudioORM:crear }

y la acción crearAction() al controlador EstudioORMController.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/EstudioORMController.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Controller; 4

El servicio de persistencia Doctrine2

119

Page 130: CursoSymfony2

5 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 use \Doctrine\Common\Collections\ArrayCollection; 7 use Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Nota; 8 9 class EstudioORMController extends Controller10 {11 public function crearAction()12 {13 $nota1 = new Nota();14 $nota1->setFecha(new \DateTime());15 $nota1->setTitulo('Nota de prueba 1');16 $nota1->setTexto('Texto para la nota de pruebas 1');17 $nota1->setPath('ruta/a/nota1');18 19 $nota2 = new Nota();20 $nota2->setFecha(new \DateTime());21 $nota2->setTitulo('Nota de prueba 2');22 $nota2->setTexto('Texto para la nota de pruebas 2');23 $nota2->setPath('ruta/a/nota2');24 //Por lo pronto no le asociamos usuario25 // Ahora hay que persistirlo26 // solicitamos el servicio de persistencia (ORM)27 $em = $this->getDoctrine()->getEntityManager();28 // Le enviamos a dicho servicio el objeto que queremos persistir (no se29 // realiza query aún30 $em->persist($nota1);31 $em->persist($nota2);32 33 // Al final del proceso, cuando hayamos enviado a todos los objetos34 // al servicio de persistencia, lo enviamos efectivamente a la base de35 // datos (insert)36 $em->flush();37 38 $notas = new ArrayCollection();39 40 $notas->add($nota1);41 $notas->add($nota2);42 43 return $this->render('JAMNotasFrontendBundle:EstudioORM:crear.html.twig',44 array('notas' => $notas));45 }46 }

Por último la plantilla asociada es:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/EstudioORM/crear.html.twig

{% extends '::base.html.twig' %}

{% block body %}

<h1>Prueba del ORM para crear objetos</h1>

<h2>Creación de notas</h2>

<h3>Se han creado y persistido en base de datos la notas siguientes:</h3>

<ul>

El servicio de persistencia Doctrine2

120

Page 131: CursoSymfony2

{% for nota in notas %} <li>{{nota.titulo}}</li>{% endfor %}</ul>

{% endblock %}

Vamos a analizar la acción que acabamos de crear.La línea 6 indica que se va a usar la clase ArrayCollection de Doctrine2, que es unaimplementación más eficiente de un array . Doctrine2 aconseja usar este ArrayCollectionpara construir colecciones de objetos.La línea 7 indica que se va a usar la entidad Nota de nuestro modelo.En las líneas 13-17 y 19-23 se crean dos objetos de la clase Nota, y se les define suspropiedades utilizando los setters. En ese momento ya disponemos de dos notas enmemoria. Si no queremos perderlas cuando finalice el script debemos persistirlas en la basede datos. Ahí es donde entra en juego el servicio de persistencia Doctrine2. En la línea 28 seobtiene, a través del contenedor de servicios, un objeto EntityManager con el cual serealiza la persistencia de las entidades (lineas 31-32) usando su método persist(). En estepunto el servicio de persistencia ya tiene información de que se desea persistir los objetosnota1 y nota2, pero la escritura en la base de datos no se lleva a cabo hasta la la línea 37donde se invoca el método flush().Como regla general, no se debe usar flush() hasta que no se hayan realizado todos lospersist() sobre los objetos que se están creando, actualizando y/o borrando. Doctrine2hace esto así por razones de eficiencia. Con los persist(), se van preparando las query's, ycon el flush() se lanzan todas de una vez de manera optimizada.En las líneas 38-41, se crea un tipo de array más apropiado para las colecciones de objetosque el array propio de PHP. Hemos usado este tipo de array con el objetivo de introducirlos,ya que las colecciones de objetos devueltos por los repositorios de Doctrine2 son de estetipo.Por último, en las líneas 43-44, se invoca a la plantilla que va a pintar el resultado. El códigode esta plantilla, que reproducimos más arriba, no merece ninguna explicación especial.Ejecuta la acción a través de la URL:

http://localhost/Symfony/web/app_dev.php/estudio_orm/crear

Y comprueba que se hayan insertado los dos registros en la tabla Nota.Como ves la creación de entidades no tiene mucha complicación, se trata de "inyectarlas" enel servicio de persistencia Doctrine2 mediante el método persist() y realizar un flush()antes de que se termine la acción. Más tarde veremos como la actualización de objetos y elborrado se realiza de forma análoga.Ahora vamos a estudiar como se recuperan objetos de la base de datos. Para ello se utilizaotro elemento de Doctrine2 denominado repositorio (repository). Cada entidad tieneasociada por defecto un repositorio, que es un objeto de la claseDoctrine\ORM\EntityRepository, con un conjunto común de métodos para recuperarobjetos de la base de datos. Además, cada entidad, puede declarar su propio repositorio,que es una clase que extiende a Doctrine\ORM\EntityRepository, para añadir nuevosmétodos con criterios de búsqueda más elaborados o sencillamente que no estendisponibles en la clase padre. Estos repositorios especificos de cada entidad son los quehemos generado junto con las entidades al usar el generador de entidades automático.

El servicio de persistencia Doctrine2

121

Page 132: CursoSymfony2

Los métodos comunes del repositorio son:

• find($id), recupera el objeto con el id especificado por el argumento.• findOneBy{Atributo}($valor), recupera en primer objeto que tienen como Atributo

el parámetro pasado en el argumento. Por ejemplo, findOneByTitulo(), ofindOneByTexto() son dos métodos de este tipo disponibles en el repositorio de laentidad Nota.

• findBy{Atributo}($valor), recupera en un ArrayCollection todos los objetos quetienen como Atributo el parámetro pasado en el argumento.

• findAll() recupera todos los objetos.Veámoslo en la práctica. Añade las siguientes ruta, acción y plantilla.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/config/routing.yml

jamn_ORM_recuperar: pattern: /estudio_orm/recuperar/{id} defaults: { _controller: JAMNotasFrontendBundle:EstudioORM:recuperar }

src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/EstudioORMController.php

1 <?php 2 ... 3 public function recuperarAction() 4 { 5 $id = $this->getRequest()->get('id'); 6 7 $em = $this->getDoctrine()->getEntityManager(); 8 9 $nota = $em->getRepository('JAMNotasFrontendBundle:Nota')->find($id);10 if (!$nota) {11 throw $this->createNotFoundException('No existe nota con id ' . $id);12 }13 14 15 return $this->render('JAMNotasFrontendBundle:EstudioORM:recuperar.html.twig',16 array('nota' => $nota));17 }18 ...

src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/EstudioORM/recuperar.html.twig

{% extends '::base.html.twig' %}

{% block body %}

<h1>Prueba del ORM para mostrar una nota</h1>

<h2>Nota {{ nota.id }}</h2>

<table border="1"> <tr> <td>Titulo:</td> <td>{{ nota.titulo }}</td> </tr>

El servicio de persistencia Doctrine2

122

Page 133: CursoSymfony2

<tr> <td>Texto:</td> <td>{{ nota.texto }}</td> </tr>

<tr> <td>Path:</td> <td>{{ nota.path }}</td> </tr>

</table>

{% endblock %}

En la línea 5 de la acción recuperarAction() se recupera el id que se ha pasado a travésde la URL. A continuación pedimos un objeto EntityManager de Doctrine2 (línea 7). En lalínea 9 obtenemos el repositorio de la entidad JAMNotasFrontendBundle:Nota y utilizamossu método find() para recuperar el objeto Nota con ese id asociado. Podría ocurrir que nohubiese ningún objeto asociado a tal id, en ese caso la variable $nota de la línea 9 seríanull. Entonces disparamos una excepción NotFoundException (líneas 10-13) con el métodocreateNotFoundEception() de la clase Controller. El framework, cuando atrapa estaexcepción construye y envía una respuesta HTTP 400 (page not found). Pero si existe unanota asociada a ese id, en la línea 9 se recuperará un objeto válido y lo pintaremos con laplantilla recuperar.html.twig (líneas 15-16). El código de dicha plantilla no necesitaninguna explicación.Prueba la acción que acabamos de crear.

http://localhost/Symfony/web/app_dev.php/estudio_orm/recuperar/1

Esta es la forma rápida de recuperar objetos si se conoce su id. Ahora vamos a recuperaruna colección con todas las notas almacenadas.Añade la ruta, la acción y la plantilla siguientes al proyecto.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/config/routing.yml

jamn_ORM_recuperar_notas: pattern: /estudio_orm/recuperar_notas defaults: { _controller: JAMNotasFrontendBundle:EstudioORM:recuperarNotas }

src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/EstudioORMController.php

1 <?php 2 ... 3 public function recuperarNotasAction() 4 { 5 $em = $this->getDoctrine()->getEntityManager(); 6 7 $notas = $em->getRepository('JAMNotasFrontendBundle:Nota')->findAll(); 8 if (!$notas) { 9 throw $this->createNotFoundException('No existen notas');10 }

El servicio de persistencia Doctrine2

123

Page 134: CursoSymfony2

11 12 13 return $this->render('JAMNotasFrontendBundle:EstudioORM:recuperarNotas.html.twig',14 array('notas' => $notas));15 }16 ...

src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/EstudioORM/recuperarNotas.html.twig

{% extends '::base.html.twig' %}

{% block body %}

<h1>Prueba del ORM para mostrar todas las nota</h1>

<h2>Notas</h2>

{% for nota in notas %}<table border="1">

<tr> <td>Titulo:</td> <td>{{ nota.titulo }}</td> </tr>

<tr> <td>Texto:</td> <td>{{ nota.texto }}</td> </tr>

<tr> <td>Path:</td> <td>{{ nota.path }}</td> </tr>

</table>

<hr/>{% endfor %}

{% endblock %}

Simplemente hemos utilizado el método findAll() del repositorio (línea 7), y hemospintado en la plantilla una colección de objetos Nota. Ahora puedes probar tú con losmétodos findByNombre(), findByTexto() y findByPath(), que también devuelvencolecciones y con los métodos findOneByNombre(), findOneByTexto() y findOneByPath()que devuelven un sólo objeto (el primero que se encuentra).Prueba la acción que acabamos de crear.

http://localhost/Symfony/web/app_dev.php/estudio_orm/recuperar_notas

Si una vez que hemos recuperado un objeto, queremos modificarlo, no tenemos más queutilizar sus métodos setters para cambiar las propiedades y persistirlo con los métodospersist() y flush() del EntityManager:

El servicio de persistencia Doctrine2

124

Page 135: CursoSymfony2

1 <?php 2 ... 3 4 $em = $this->getDoctrine()->getEntityManager(); 5 6 $nota = $em->getRepository('JAMNotasFrontendBundle:Nota')->find($id); 7 if (!$nota) { 8 throw $this->createNotFoundException('No existe esa nota'); 9 }10 11 $nota->setTitulo('nuevotitulo');12 $nota->setTexto('nuevo texto');13 14 $em->persist($nota);15 16 $em->flush();17 18 ...

Si lo que queremos es borrar el objeto:

1 <?php 2 ... 3 4 $em = $this->getDoctrine()->getEntityManager(); 5 6 $nota = $em->getRepository('JAMNotasFrontendBundle:Nota')->find($id); 7 if (!$nota) { 8 throw $this->createNotFoundException('No existe esa nota'); 9 }10 11 $em->remove($nota);12 13 $em->flush();14 15 ...

Refinamos el modelo. Las asociaciones entre objetosYa sabemos como persistir nuestras entidades en la base de datos y como recuperarlascuando el criterio de búsqueda encaja dentro de lo que ofrecen los métodos "encontrar Porlos atributos" de un objeto (findBy{Attributo}). Pero recuerda que el modelo que hemoscreado con la ayuda del generador automático de entidades no cumple lo especificado en elanálisis, pues no hemos tenido en cuenta las asociaciones entre objetos. Es lo que haremosen este apartado.Los objetos pueden asociarse, o relacionarse, de distintas formas:

• un sólo objeto del tipo A puede estar asociado a muchos objetos del tipo B, pero uno deltipo B solo puede estar asociado a uno del tipo A. Esto es una relación uno-a-muchos(one to many),

• muchos objetos del tipo A pueden estar asociados a un sólo objeto del tipo B, y uno deltipo B puede estar asociados a muchos del tipo A. Esto es una relación muchos-a-uno(many to one), y obviamente es la opuesta a la anterior,

Refinamos el modelo. Las asociaciones entre objetos

125

Page 136: CursoSymfony2

• uno o varios objetos del tipo A pueden estar asociados a uno o varios objetos del tipo By viceversa. Esto es una relación muchos a muchos (many to many),

• un sólo objeto del tipo A puede estar asociado con un sólo objeto del tipo B. Esto es unarelación uno a uno (one to one).

La relación semántica de cada asociación viene determinada por el contexto y la naturalezade cada entidad. Identífiquemos algunas de las relaciones entre las entidades de nuestromodelo:

• Una Nota puede contener varios Archivo's y un Archivo sólo puede ser de una Nota.Se trata de una relación one to many desde la Nota al Archivo.

• Varias Nota's pueden pertenecer a un Usuario, y un Usuario puede poseer muchasNota's . Es una relación many to one desde la Nota al Usuario.

• Una o varias Nota's pueden tener una o varias Etiqueta's asociadas, y al revés, una ovarias Etiqueta's pueden etiquetar a una o varias Nota's. Es una relación many tomany.

En las anteriores sentencias se ha marcado en negrita la cualidad semántica de la relación.Como ejercicio puedes continuar describiendo el resto de relaciones. Observa que cadarelación one to many tiene su inversa many to one. Por ejemplo, la inversa de la primerarelación sería: varios Archivo's pueden pertenecer a una sóla Nota.Para completar la implementación del modelo con las asociaciones entre objetos, debemosagregar los atributos correspondientes a las entidades asociadas y las anotacionespertinentes para poder mapearlas sobre la base de datos. Doctrine2 permite especificar lainformación de mapeo con un alto grado de precisión, por lo que la sintaxis de lasanotaciones para las asociaciones puede llegar a ser bastante compleja. En este apartadovamos a dar unas reglas sencillas que funcionan en la mayor parte de las situaciones. Perodebe quedar claro que Doctrine2 permite más posibilidades y que, posiblemente en algunode tus proyectos, tengas que recurrir a la documentación oficial sobre asociaciones.

La relación One to ManyTomemos como ejemplo la relación: "un Usuario posee muchas Nota's", el código que hayque añadir a la clase Usuario sería (código PHP y anotación):

<?php

...use Doctrine\Common\Collections\ArrayCollection;

class Usuario{...

/** * @ORM\OneToMany(targetEntity="Nota", mappedBy="usuario") */ private $notas;

public function __construct() { $this->notas = new ArrayCollection(); }

La relación One to Many

126

Page 137: CursoSymfony2

Es importante en la relación One to Many, inicializar la parte Many como una collección deobjetos Doctrine2 (ArrayCollection). Esto se hace utilizando el constructor de la clase. Enla anotación se debe especificar a través de la directiva mappedBy, como se llama el atributo(usuario) en la clase target (Nota).

La relación Many to OneTomemos como ejemplo la inversa a la anterior: "varias Nota's pueden pertenecer a un sóloUsuario. el código que hay que añadir a la clase Usuario sería (código PHP y anotación):

<?php...class Nota{.../** * @ORM\ManyToOne(targetEntity="Usuario") */ private $usuario;...

El resultado de estas dos anotaciones complementarias cuando se mapean en la base dedatos es la creación de un campo denominado usuario_id en la tabla Nota que será claveforanea del campo id de la tabla Usuario.

La relación Many to ManyTomemos como ejemplo la relación: "varias Notas pueden ser etiquetadas con variasEtiqueta's y viceversa". Este tipo de relación es completamente simétrica e implica a losdos objetos. Por ello hay que agregar el siguiente código a la clase Nota:

<?phpuse Doctrine\Common\Collections\ArrayCollection;...class Nota{...

/** * @ORM\ManyToMany(targetEntity="Etiqueta", inversedBy="notas") */ private $etiquetas;

public function __construct() { $this->etiquetas = new ArrayCollection(); }

Y a la clase etiquetas:

<?php...use Doctrine\Common\Collections\ArrayCollection;

class Etiqueta{...

La relación Many to One

127

Page 138: CursoSymfony2

/** * @ORM\ManyToMany(targetEntity="Etiqueta", mappedBy="etiquetas") */ private $notas;

public function __construct() { $this->notas = new ArrayCollection(); }

En ambos casos es necesario declarar en el constructor las entidades asociadas comoArrayCollection, pues en los dos casos los atributos asociados son colecciones.El resultado de estas dos anotaciones complementarias cuando se mapean en la base dedatos es la creación de una tabla intermedia denominada nota_etiqueta, con dos camposnota_id y etiqueta_id que son clave foranea de los campos id de las tablas Nota yEtiqueta respectivamente. Es decir, la estrategia que se sigue habitualmente paraimplementar las relaciones N-M (otra forma más propia del lenguaje de las bases de datospara decir Many to Many) en una base de datos relacional.Siguiendo estas reglas podemos completar el resto de las entidades con las asociacionesque establece el modelo.src/Jazzyweb/AulasMentor/JAMNotasFrontendBundle/Entity/Contrato

<?php...class Contrato{...////ASOCIACIONES////

/** * @ORM\ManyToOne(targetEntity="Tarifa") */ private $tarifa;

/** * @ORM\ManyToOne(targetEntity="Usuario") */ private $usuario;

////FIN ASOCIACIONES//// ...

src/Jazzyweb/AulasMentor/JAMNotasFrontendBundle/Entity/Etiqueta

<?php...use Doctrine\Common\Collections\ArrayCollection;

class Etiqueta{...////ASOCIACIONES////

La relación Many to One

128

Page 139: CursoSymfony2

/** * @ORM\ManyToMany(targetEntity="Nota", mappedBy="etiquetas") */ private $notas;

/** * @ORM\ManyToOne(targetEntity="Usuario") */ private $usuario;

////FIN ASOCIACIONES////

public function __construct() { $this->notas = new ArrayCollection(); }

src/Jazzyweb/AulasMentor/JAMNotasFrontendBundle/Entity/Grupo

<?php...use Doctrine\Common\Collections\ArrayCollection;

class Grupo{... ////ASOCIACIONES////

/** * @ORM\ManyToMany(targetEntity="Usuario", mappedBy="grupos") */ private $usuarios;

////FIN ASOCIACIONES////

public function __construct() { $this->usuarios = new ArrayCollection(); }

src/Jazzyweb/AulasMentor/JAMNotasFrontendBundle/Entity/Nota

<?php...use Doctrine\Common\Collections\ArrayCollection;

class Nota{...////ASOCIACIONES////

/** * @ORM\ManyToOne(targetEntity="Usuario") */

La relación Many to One

129

Page 140: CursoSymfony2

private $usuario;

/** * @ORM\ManyToMany(targetEntity="Etiqueta", inversedBy="notas") */ private $etiquetas;

////FIN ASOCIACIONES////

public function __construct() { $this->etiquetas = new ArrayCollection(); }

src/Jazzyweb/AulasMentor/JAMNotasFrontendBundle/Entity/Tarifa

<?php...use Doctrine\Common\Collections\ArrayCollection;

class Tarifa{...////ASOCIACIONES////

/** * @ORM\OneToMany(targetEntity="Contrato", mappedBy="tarifa") */ private $contratos;

////FIN ASOCIACIONES////

public function __construct() { $this->contratos = new ArrayCollection(); }

src/Jazzyweb/AulasMentor/JAMNotasFrontendBundle/Entity/Usuario

<?php...use Doctrine\Common\Collections\ArrayCollection;

class Usuario{ ////ASOCIACIONES////

/** * @ORM\OneToMany(targetEntity="Nota", mappedBy="usuario") */ private $notas;

/** * @ORM\OneToMany(targetEntity="Contrato", mappedBy="usuario") */

La relación Many to One

130

Page 141: CursoSymfony2

private $contratos;

/** * @ORM\OneToMany(targetEntity="Etiqueta", mappedBy="usuario") */ private $etiquetas;

/** * @ORM\ManyToMany(targetEntity="Grupo", inversedBy="usuarios") */ private $grupos;

////FIN ASOCIACIONES////

public function __construct() { $this->notas = new ArrayCollection(); $this->contratos = new ArrayCollection(); $this->etiquetas = new ArrayCollection(); $this->grupos = new ArrayCollection(); }

Una vez declaradas las asociaciones, tanto como atributos de las entidades comoanotaciones para el mapeo relacional, debemos crear los métodos para acceder a ellas, esdecir, los getters y setters. Podemos hacerlo a mano, o mejor aún, utilizando el siguientecomando de consola:

php app/console doctrine:generate:entities JAMNotasFrontendBundle

Este comando crea, a partir de la configuración de las entidades, es decir, de las anotacionesen nuestro caso, los atributos y métodos de acceso getters y setters que falten por definir enla entidad. Compruébalo mirando el código de las entidades después de lanzar estecomando.

Nota

Recuerda que además de anotaciones, los metadatos que definen el mapeo se puedendeclarar en un fichero de configuración YAML, XML o PHP.

Y ahora que ya tenemos las entidades completamente definidas, actualizamos la estructurade la base de datos mediante el comando:

php app/console doctrine:schema:update --force

Esta operación es comprometida, se trata de actualizar la estructura de una base de datos, yello podría dar lugar a una pérdida de datos. Por ello Doctrine2 exige indicar si realmentequieres atacar a la base de datos usando el modificador --force, o si sólo quieres volcar enpantalla las sentencias SQL mediante el modificador --dump-sql, de manera que puedasexaminarlas antes de lanzarlas.

La relación Many to One

131

Page 142: CursoSymfony2

Comprueba como después de lanzar el comando se ha actualizado la base de datos contodas las relaciones que modelan las asociaciones entre objetos y, además, no ha habidopérdida de datos (mira el contenido de la tabla Nota). Y es que Doctrine2, además de ser unexplendido ORM es una gran herramienta para la manipulación de bases de datos.

Más allá de los métodos findBy. El lenguaje DQLLos métodos que ofrece el repositorio por defecto asociado a cada entidad son realmenteútiles, y se pueden hacer muchas cosas con ellos. Sin embargo en muchas ocasiones se tevan a quedar cortos. Tendrás que recuperar una serie de objetos que cumplan un criteriomás complejo que el simple "buscarPor" (findBy). Dicho en lenguaje de base de datos,habrá que realizar consultas con clausulas where más complicadas y/o con joins entretablas.Ningún ORM estaría completo si no proporcionase algún mecanismo para realizar cualquiertipo de consultas sobre la base de datos. O dicho ahora en el lenguaje de los ORM's, derecuperar colecciones de objetos que satisfagan criterios de de busquedas de cualquier tipo.El mecanismo de consulta de Doctrine2 se llama DQL, acrónimo de Doctrine QueryLanguage. Se trata de un lenguaje de consulta sobre objetos 18. Y hemos de tener muypresente la palabra objeto, pues el hecho de que DQL utilice las construccionescaracterísticas del SQL: SELECT, UPDATE y DELETE, tiende a confundir al principiante que loutiliza como si fuera una variante de este último.

Sugerencia

No debes pensar en término de tablas, columnas y joins entre tablas, si no entérminos de tu modelo de objetos. Lo veremos con los ejemplos.

Realizaremos un estudio del DQL en la onda de esta unidad, es decir, presentando loselementos básicos que cubren un amplio espectro de situaciones, pero dejando claro queDoctrine2 da para mucho más (parece que un curso de Doctrine2 no vendría mal ;-)).Cuando precises más detalles para construir esa fastidiosa query que necesitas para tuúltimo proyecto, tendrás que consultar la documentación oficial sobre DQL.Comienza por crear una ruta llamada jamn_ORM_DQL y asociala a la acción dqlAction() delcontrolador estudioORM. Seguro que ya sabes cómo se hace esto ¿no?.Comenzamos por algo sencillo; recuperar todas las notas, es decir, lo mismo que seconseguiría con el método findAll() del repositorio;src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/EstudioORMController.php

1 <?php 2 ... 3 public function dqlAction() 4 { 5 $em = $this->getDoctrine()->getEntityManager(); 6 7 // Todas la notas usando DQL 8 $query = $em->createQuery( 9 'SELECT n FROM JAMNotasFrontendBundle:Nota n');10 11 $notasTodas = $query->getResult();

Más allá de los métodos findBy. El lenguaje DQL

132

Page 143: CursoSymfony2

12 13 return $this->render('JAMNotasFrontendBundle:EstudioORM:recuperarNotas.html.twig',14 array('notas' => $notasTodas));15 16 }

En la línea 5 obtenemos un objeto EntityManager a través de nuestro todopoderosoContenedor de Servicios. Entonces creamos una consulta en DQL haciendo uso de sumétodo createQuery() (líneas 8-9). Finalmente obtenemos la colección de objetoscorrespondiente a la consulta en la línea 11. Como la acción recupera una colección denotas, lo mismo que la acción recuperarNotasAction(), hemos reutilizado la plantillaJAMNotasFrontendBundle:EstudioORM:recuperarNotas.html.twig para pintar elresultado.Seguro que el párrafo anterior no te ha servido de mucho, el código es bastanteautoexplicativo. Lo que nos interesa es analizar la sintaxis de la consulta DQL. Si conocesaunque sea un poco de SQL (esperemos que así sea) encontrás muchas similitudes entre elDQL y el SQL. Y las hay. Pero aún así no te dejes llevar demasiado por los parecidos y nopienses en términos de columnas. Debemos pensar en recuperar objetos. En la consultaanterior simplemente se dice que se seleccionen todos los objetosJazzywebMentorNotasBundle:Nota identificadas por el alias n. Fíjate que no se pide ningunacolumna en concreto, pues ese término no tiene sentido en el contexto de los objetos. Sedevolverán, simplemente, objetos notas, con todas sus cosas. Una vez que los tengas en lasmanos, puedes pedirles lo que quieras, o más bien, lo que te permitan, usando sus métodosgetters. La consulta devuelve todos los objetos pues no se le ha límitado con ningunacondición. Vamos a por una un poquito más compleja; todas las notas que tienen en el títulola palabra "ipsum".

1 <?php 2 ... 3 public function dqlAction() 4 { 5 $em = $this->getDoctrine()->getEntityManager(); 6 // Todas las notas que tienen en el título la palabra "ipsum" con DQL 7 $query = $em->createQuery( 8 "SELECT n FROM JAMNotasFrontendBundle:Nota n where n.texto LIKE :termino") 9 ->setParameter('termino', '%ipsum%');10 11 $notasIpsum = $query->getResult();12 13 return $this->render('JAMNotasFrontendBundle:EstudioORM:recuperarNotas.html.twig',14 array('notas' => $notasIpsum));15 }

Ahora pedimos (SELECT) todos los objetos JAMNotasFrontendBundle:Nota identificados porel alias n, que tienen en su propiedad texto el término ipsum. La forma de referenciar unapropiedad del objeto es mediante la sintaxis punto (n.texto). La clausula LIKE tiene elmismo significado que en SQL. Y por último se ha optado por parametrizar la consulta. Paraello se precede con el carácter : el nombre del parámetro y posteriormente se establece suvalor mediante el método setParameter(). De nuevo reutilizamos la plantillaJAMNotasFrontendBundle:EstudioORM:recuperarNotas.html.twig.Vamos a por otro ejemplo. Todas las notas del usuario alberto.

Más allá de los métodos findBy. El lenguaje DQL

133

Page 144: CursoSymfony2

Nota

Para poder probar lo que viene a continuación, introduce en la base de datos con tucliente MySQL favorito, un usuario con username alberto, algunas etiquetasasociadas a ese usuario y algunas notas asociadas al usuario y etiquetadas conalgunas de las etiquetas anteriores.

1 <?php 2 ... 3 public function dqlAction() 4 { 5 $em = $this->getDoctrine()->getEntityManager(); 6 7 // Todas las notas del usuario alberto con DQL 8 $query = $em->createQuery( 9 "SELECT n FROM JAMNotasFrontendBundle:Nota n JOIN n.usuario u WHERE10 u.username = :username")11 ->setParameter('username', 'alberto');12 13 $notasAlberto = $query->getResult();14 15 return $this->render('JAMNotasFrontendBundle:EstudioORM:recuperarNotas.html.twig',16 array('notas' => $notasAlberto));17 }

De nuevo queremos recuperar objetos JAMNotasFrontendBundle:Nota. Y es lo que pedimosen la SELECT. Pero esta vez queremos filtrar por la propiedad usuario, y resulta que estapropiedad es a su vez un objeto del tipo JAMNotasFrontendBundle:Usuario. Por ellodebemos usar la clausula JOIN, para "unir" unos objetos con otros. A los objetos n.usuariolos referenciamos con el alias u el cual utilizamos a continuación para establecer lacondición WHERE u.username = :username, es decir "donde la propiedad ``username`` delobjeto usuario sea lo especificado por el parámetro ``:username``". Este último parámetro,como hemos visto antes, se establece a través del método setParameter().Vamos aumentando la complejidad de la consulta. Ahora vamos a recuperar todas las notasdel usuario alberto que tienen la etiqueta php.

1 <?php 2 ... 3 public function dqlAction() 4 { 5 $em = $this->getDoctrine()->getEntityManager(); 6 // Todas la notas del usuario 'alberto' que tienen etiqueta 'php' con DQL 7 $query = $em->createQuery( 8 "SELECT n FROM JAMNotasFrontendBundle:Nota n 9 JOIN n.usuario u10 JOIN n.etiquetas e11 WHERE e.texto = :texto AND u.username=:username")12 ->setParameters(array('texto' => 'php', 'username' => 'alberto'));13 14 $notasAlberto = $query->getResult();15 16 return $this->render('JAMNotasFrontendBundle:EstudioORM:recuperarNotas.html.twig',17 array('notas' => $notasAlberto));18 }

Más allá de los métodos findBy. El lenguaje DQL

134

Page 145: CursoSymfony2

De nuevo pedimos en el SELECT los objetos de tipo JAMNotasFrontendBundle:Nota. Estavez queremos unirlos con los objectos JAMNotasFrontendBundle:Usuario yJAMNotasFrontendBundle:Etiqueta, ambos relacionados con los objetos de tipoJAMNotasFrontendBundle:Nota a través de sus propiedades usuario y etiquetasrespectivamente. Eso es lo que se hace en las líneas 9 y 10 del código anterior, dondeademás se identifican con los alias u y e. Luego, en la línea 11 se establecen las condiciones(WHERE) sobre las propiedades texto y username de tales objetos. Cuando utilizamos másde un parámetro en la consulta, como es el caso en esta ocasión, utilizamos el métodosetParameters(), el cual recibe como argumento un array asociativo con los nombre yvalores de los parámetros.Fíjate en esta última consulta. En ningún momento se ha hecho un JOIN sobre la tablaintermedia que relaciona las notas con las etiquetas. Sencillamente por que DQL no trata detablas y columnas. Y no hay nada en el modelo de datos (entidades) parecido a una tablaintermedia. De hecho, repetimos hasta correr el riesgo de que nos llamen "pesao", que lapalabra "tabla" no tiene sentido en DQL. Olvídala. Lo que tiene sentido es que una notapuede tener asociadas una o muchas etiquetas y un usuario. Y eso está representado en laentidad a través de sus propiedades etiquetas y usuarios. Y es en eso en lo que debemospensar.En resumen: En las sentencias DQL pedimos la recuperación (SELECT) de objetos de algunode los tipos de nuestras entidades y los referenciamos con un alias (FROM). Posteriormentepodemos unir (JOIN) las propiedades que, a su vez sean objetos, con las entidadescorrespondientes, y filtrar (WHERE) por el valor de las propiedades escalares (las que no sonobjetos).Son muchas las posibilidades del DQL, incluyendo clausulas para borrar (DELETE) y actualizar(UPDATE) objetos que vienen muy bien cuando queremos hacer una operación de borrado oactualizado en masa. Sin embargo en la mayor parte de las ocasiones, los métodospersist(), delete() y flush() son suficiente para borrar y actualizar objetos.

Organizamos las consultas en repositoriosNada nos impide utilizar en las acciones del controlador sentencias DQL para recuperarobjetos. Tal y como lo hemos hecho en la sección anterior. Nada salvo las buenas prácticasde programación y el deseo de que cada cosa esté en su sitio, localizable, testeable, fácil demantener y sin contaminar la lógica de control de las acciones con sentencias DQL que, pormuy bonitas que sean, no dejan de ser otro lenguaje embebido en el código principal.Este es el papel de los repositorios asociados a las entidades. Ya hemos visto que cadaentidad cuenta con un repositorio por defecto con una serie de funciones findBy queresuelven muchos casos. Podemos extender dicho repositorio con nuevas funcionesdesarrolladas por nosotros que encapsulan consultas DQL, y utilizarlas en las acciones de lamisma forma que hemos utilizado en secciones anteriores los métodos findBy.Veremos como se hace esto aprovechando la explicación para completar el modelo denuestra aplicación MentorNotas. Una de las cosas que necesitaremos es recuperar todas lasnotas de un usuario que tengan asociada una determinada etiqueta. Abre el archivosrc/Jazzyweb/AulasMentor/JAMNotasFrontendBundle/Entity/NotaRepository.php yañade el siguiente código:

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Entity; 4 5 use Doctrine\ORM\EntityRepository; 6

Organizamos las consultas en repositorios

135

Page 146: CursoSymfony2

7 /** 8 * NotaRepository 9 *10 * This class was generated by the Doctrine ORM. Add your own custom11 * repository methods below.12 */13 class NotaRepository extends EntityRepository14 {15 16 public function findByUsuarioAndEtiqueta($usuario, $etiqueta)17 {18 if ($usuario instanceof Usuario) {19 $username = $usuario->getUsername();20 } else {21 $username = $usuario;22 }23 24 if ($etiqueta instanceof Etiqueta) {25 $id_etiqueta = $etiqueta->getId();26 } else {27 $id_etiqueta = $etiqueta;28 }29 30 $query = $this->getEntityManager()->createQuery(31 "SELECT n FROM JAMNotasFrontendBundle:Nota n32 JOIN n.etiquetas e33 JOIN n.usuario u34 WHERE e.id = :id_etiqueta and u.username=:username")35 ->setParameters(array('username' => $username, 'id_etiqueta' => $id_etiqueta));36 37 return $query->getResult();38 }39 40 }

La función findByUsuarioAndEtiqueta() recibe dos argumentos. Se ha construido demanera que puedan ser tanto objetos como cadenas. Las condiciones de las líneas 18-22 y24-28, se encargan de extraer los términos de búsqueda correcto según se hayan pasadoobjetos o cadenas. La parte importante está en las líneas 30-35, donde se construye laconsulta DQL de la misma manera que hemos hecho en la sección anterior. La diferencia esque ahora el objeto EntityManager se obtiene directamente del objeto NotaRepository, esdecir, no es necesario utilizar el Contenedor de Servicios.

Nota

Ni es necesario, ni es posible, ya que este objeto no proporciona ninguna manera deacceder al Contenedor de Servicios de Symfony2

Finalmente (línea 37) se devuelve la colección de notas que cumplen el criterio de búsqueda.Ahora ya podemos utilizar esta función en cualquier acción de un controlador de la siguientemanera:

<?php...$notas = $em->getRepository('JAMNotasFrontendBundle:Nota') ->findByUsuarioAndEtiqueta($usuario, $etiqueta);...

Organizamos las consultas en repositorios

136

Page 147: CursoSymfony2

Otras funciones que necesitaremos en nuestro repositorio serán las que nos permitan buscarlas notas de un usuario ordenadas por fechas y las notas de un usuario que contienen undeterminado término de búsqueda. Esto es lo que hacen los siguientes métodos delrepositorio de la entidad Nota.

<?php...class NotaRepository extends EntityRepository { ...

public function findByUsuarioOrderedByFecha($usuario) { $query = $this->getEntityManager()->createQuery( "SELECT n FROM JAMNotasFrontendBundle:Nota n JOIN n.usuario u WHERE u.username=:username ORDER BY n.fecha DESC") ->setParameters(array('username' => $usuario->getUsername()));

return $query->getResult(); }

public function findByUsuarioAndTermino($usuario, $termino) { if ($usuario instanceof Usuario) { $username = $usuario->getUsername(); } else { $username = $usuario; }

$query = $this->getEntityManager()->createQuery( "SELECT n FROM JAMNotasFrontendBundle:Nota n JOIN n.usuario u WHERE u.username=:username AND (n.texto LIKE :termino OR n.titulo LIKE :termino)") ->setParameters(array('username' => $username, 'termino' => '%' . $termino . '%'));

return $query->getResult(); } }

También necesitaremos la colección de etiquetas de un determinado usuario. La funciónfindByUsuarioOrderedByTexto() del repositorio de la entidad Etiqueta resuelve esteproblema:src/Jazzyweb/AulasMentor/JAMNotasFrontendBundle/Entity/EtiquetaRepository.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Entity; 4 5 use Doctrine\ORM\EntityRepository; 6 7 /** 8 * EtiquetaRepository 9 *10 * This class was generated by the Doctrine ORM. Add your own custom11 * repository methods below.12 */13 class EtiquetaRepository extends EntityRepository14 {15 public function findByUsuarioOrderedByTexto($username)16 {17 $query = $this->getEntityManager()->createQuery(18 "SELECT e FROM JAMNotasFrontendBundle:Etiqueta e

Organizamos las consultas en repositorios

137

Page 148: CursoSymfony2

19 JOIN e.usuario u where u.id = :username ORDER BY e.texto ASC")20 ->setParameters(array('username' => $username));21 22 return $query->getResult();23 }24 }

Incorporamos el modelo a la aplicaciónA lo largo de la unidad hemos procurado ofrecer algo más que una introducción a Doctrine2.El objetivo principal ha sido enseñarte como puedes utilizarlo en tus proyectos y, a la vez,construir un modelo completamente funcional para nuestra aplicación. Ahora que lo tenemoses el momento de incorporarlo al código del controlador NotasController. Recuerda que lafunción dameEtiquetasYNotas() de este controlador es la que se encarga de calcular lacolección de etiquetas, notas y la nota seleccionada que debe mostrarse en el detalle, enfunción del filtro de búsqueda que haya definido en la sesión. Hasta ahora estábamosusando un modelo sin funcionalidades que nos permitió implementar la lógica de control.Ahora vamoso a usar en su lugar el modelo que hemos desarrollado a lo largo de estainterminable unidad. El código que proponemos es el siguiente:

1 <?php 2 ... 3 protected function dameEtiquetasYNotas() 4 { 5 $session = $this->get('session'); 6 $em = $this->getDoctrine()->getEntityManager(); 7 8 $usuario = $em->getRepository('JAMNotasFrontendBundle:Usuario') 9 ->findOneByUsername('alberto');10 11 12 $busqueda_tipo = $session->get('busqueda.tipo');13 14 $busqueda_valor = $session->get('busqueda.valor');15 16 // Etiquetas. Se pillan todas17 $etiquetas = $em->getRepository('JAMNotasFrontendBundle:Etiqueta')->18 findByUsuarioOrderedByTexto($usuario);19 20 // Notas. Se pillan según el filtro almacenado en la sesión21 if ($busqueda_tipo == 'por_etiqueta' && $busqueda_valor != 'todas') {22 $notas = $em->getRepository('JAMNotasFrontendBundle:Nota')->23 findByUsuarioAndEtiqueta($usuario, $busqueda_valor);24 } elseif ($busqueda_tipo == 'por_termino') {25 $notas = $em->getRepository('JAMNotasFrontendBundle:Nota')->26 findByUsuarioAndTermino($usuario, $busqueda_valor);27 } else {28 $notas = $em->getRepository('JAMNotasFrontendBundle:Nota')->29 findByUsuarioOrderedByFecha($usuario);30 }31 32 33 $nota_selecionada_id = $session->get('nota.seleccionada.id','1');34

Incorporamos el modelo a la aplicación

138

Page 149: CursoSymfony2

35 $nota_seleccionada = $em->getRepository('JAMNotasFrontendBundle:Nota')->36 findOneById($nota_selecionada_id);37 38 39 return array($etiquetas, $notas, $nota_seleccionada);40 }

La líneas 6 y 7 recuperan el servicio session y el EntityManager. La línea 7 se declara elusuario que ha iniciado la sesión en la aplicación. Como aún no hemos tratado el tema delinicio de sesión y la autentificación, proponemos uno cualquiera "a capón". Es decir, usamosde nuevo un objeto mock. Cuando, dentro de dos unidades, hayamos aprendido a obtener elusario que ha iniciado la sesión, cambiaremos este comportamiento sin funcionalidad por elcorrecto. Por lo pronto nos vale así.Las líneas 14-15 obtienen la colección de etiquetas completa del usuario que ha iniciado lasesión. Para lo cual utilizamos la función findByUsuarioOrderedByTexto del repositorio dela entidad Etiqueta.En las líneas 18-27 se lleva a cabo la búsqueda de notas en función del filtro que estéalmacenado en la sesión. El código es autoexplicativo. El único detalle digno de sercomentado es que cuando buscamos por etiqueta, y el valor de búsqueda es todas, esequivalente a que no haya ningún filtro en la sesión y, por tanto, haya que recuperar todaslas notas del usuario.En las líneas 30-33 se recupera la nota seleccionada que será mostrada en el detalle. Yfinalmente, en la línea 36 de devuelve el resultado de todos los cálculos.Prueba la aplicación. Con esto ya tenemos gran parte de la lógica de negocio de la aplicaciónimplementada.

La unidad en chuletas

Construir entidades

php app/console generate:doctrine:entity

Genera en el directorio Entity del bundle la entidad cuyo nombre especifiquemos y,opcionalmente una clase Repository donde podemos escribir consultas que obedezcan adistintos criterios.

Configuración de la base de datosSe realiza en el fichero app/config/config.yml. Pero allí puedes ver que se utilizan losparámetros %database_driver%, %database_host%, %database_port%, %database_name%,%database_user%, %database_password%. que a su vez se encuentran en el ficheroapp/connfig/parameters.ini.

Crear la base de datos y las tablas

1. Crear la base de datos

php app/console doctrine:database:create

2. Crear el esquema (conjunto de tablas y sus relaciones) de la base de datos

La unidad en chuletas

139

Page 150: CursoSymfony2

php app/console doctrine:schema:create

Las tablas se crean según lo especificado en los metadatos de las entidades. Estosmetadatos se pueden especificar mediante anotaciones o ficheros de configuración YAML,XML o PHP.

3. Actualizar el esquema

php app/console doctrine:schema:update --force

Persistir un objeto en la base de datos

$nota1 = new Nota();$nota1->setFecha(new \DateTime());$nota1->setTitulo('Nota de prueba 1');$nota1->setTexto('Texto para la nota de pruebas 1');$nota1->setPath('ruta/a/nota1');

$em = $this->getDoctrine()->getEntityManager();// Le enviamos a dicho servicio el objeto que queremos persistir (no se// realiza query aún$em->persist($nota1);$em->flush(); //se lanza al final, cuando todos los objetos hayan sido persistidos

Recuperar objetos con métodos find

$id = $this->getRequest()->get('id');

$em = $this->getDoctrine()->getEntityManager();

$nota = $em->getRepository('JAMNotasFrontendBundle:Nota')->find($id);

$notas = $em->getRepository('JAMNotasFrontendBundle:Nota')->findByTitulo('el titulo');

$nota = $em->getRepository('JAMNotasFrontendBundle:Nota')->findOneByTitulo('el titulo');

$notas = $em->getRepository('JAMNotasFrontendBundle:Nota')->findByLoQueQuierasYComoQuieras($id);

Los métodos findBy devuelven colecciones y los métodos findOneBy recuperan un soloobjeto. Atención a esto que es causa común de fallos. Por otro lado, en los repositoriospodemos implementar todos los métodos find que queramos y como queramos

Borrar un objeto

<?php...

$em = $this->getDoctrine()->getEntityManager();

$nota = $em->getRepository('JAMNotasFrontendBundle:Nota')->find($id);if (!$nota) { throw $this->createNotFoundException('No existe esa nota');}

$em->remove($nota);

Persistir un objeto en la base de datos

140

Page 151: CursoSymfony2

$em->flush();

Especificación de las relaciones entre entidades (forma simple y práctica)

1. One to Many

<?php...use Doctrine\Common\Collections\ArrayCollection;

class Usuario{...

/** * @ORM\OneToMany(targetEntity="Nota", mappedBy="usuario") */ private $notas;

public function __construct() { $this->notas = new ArrayCollection(); }

2. Many to One

<?php...class Nota{.../** * @ORM\ManyToOne(targetEntity="Usuario") */ private $usuario;...

2. Many to Many

<?phpuse Doctrine\Common\Collections\ArrayCollection;...class Nota{...

/** * @ORM\ManyToMany(targetEntity="Etiqueta", inversedBy="notas") */ private $etiquetas;

public function __construct() {

Especificación de las relaciones entre entidades (forma simple y práctica)

141

Page 152: CursoSymfony2

$this->etiquetas = new ArrayCollection(); }

Implementación de un método de repositorio

<?php...

public function findByUsuarioAndTermino($usuario, $termino){ if ($usuario instanceof Usuario) { $username = $usuario->getUsername(); } else { $username = $usuario; }

$query = $this->getEntityManager()->createQuery( "SELECT n FROM JAMNotasFrontendBundle:Nota n JOIN n.usuario u WHERE u.username=:username AND (n.texto LIKE :termino OR n.titulo LIKE :termino)") ->setParameters(array('username' => $username, 'termino' => '%' . $termino . '%'));

return $query->getResult();}...

ComentariosAquí puedes enviar comentarios, dudas y sugerencias. Utiliza la barra de scroll para recorrertodos los mensajes. El formulario de envío se encuentra al final.

Autor del código: Juan David Rodríguez García <[email protected]>

Implementación de un método de repositorio

142

Page 153: CursoSymfony2

Unidad 8: Desarrollo de la aplicación MentorNotas (IV).Validación y FormulariosCon lo que llevamos visto hasta el momento ya podemos crear páginas web que presentenformularios al usuario y recoger los datos que este envía para que nuestra aplicación losprocese. Lo hemos hecho en la unidad 3. Sin embargo con Symfony2 se puede hacer mejor.El servicio de formularios junto con el de validación facilitan enormente esta tarea, a la vezque ayudan a tener un código mejor organizado y legible.El propósito del servicio de validación es comprobar si los atributos de un objeto cumplen ono una seríe de reglas que hemos definido previamente. Mientras que la finalidad delservicio de formularios es la de crear cómodamente formularios que, por una parte seránenviados en un documento HTML en la respuesta, y por otra servirán para asociar los datosque el cliente envía en la petición HTTP y comprobar su validez antes de realizar ningunaoperación. Para esta última función el servicio de formulario usa (depende de) el servicio devalidación.Como puedes imaginar después de todo lo que llevamos, ambos servicios son accesibles através del contenedor de servicios. El funcionamientos básico de los dos sigue el mismopatrón que el servicio de persistencia: partimos de un objeto plano de PHP que deseamosprocesar (persistir, validad o crear un formulario), lo inyectamos, es decir, lo pasamos comoargumento al servicio en cuestión, y este último se encarga de procesarlo (persistirlo,validarlo o crear un formulario). Para que el servicio pueda realizar su funciónadecuadamente necesita cierta información que se le proporciona a través de ficheros deconfiguración (YAML, XML, PHP) o anotaciones.

Nota

Un objeto planto es un objeto que no tiene nada que ver con el framework sino másbien con nuestra lógica de negocio. Nuestro objeto Nota es un buen ejemplo.

El servicio de validaciónCuando aplicamos el servicio validador sobre un objeto, este lee la/s regla/s que se hanasociado a sus atributos mediante la configuración o las anotaciones, verificando si dichodato las cumple. Si el resultado es negativo, el validador devuelve un array de errores quepuedes utilizar para, por ejemplo, mostrarlos al usuario en la respuesta HTTP.La mejor manera de ver el funcionamiento de este servicio es mediante un ejemplo. Vamosa realizar la validación de datos sobre nuestro objeto Usuario. Recordemos los atributos deeste objeto:

1 <?php 2 ... 3 class Usuario 4 { 5 6 private $id; 7 private $nombre; 8 private $apellidos; 9 private $username;10 private $salt;

Unidad 8: Desarrollo de la aplicación MentorNotas (IV). Validación y Formularios

143

Page 154: CursoSymfony2

11 private $password;12 private $email;13 private $isActive;14 private $tokenRegistro;15 16 ...

Nota

Para centrarnos en lo que nos interesa y ahorrar espacio hemos eliminado lasanotaciones para Doctrine. No obstante tu no tienes que hacer esto. Mantén lasanotaciones de este objeto, ¡son fundamentales para que funcione Doctrine!

Lo primero que tenemos que hacer es definir (configurar) las reglas que aplicaremos a cadauno de sus atributos. Symfony2 dispone de un amplio conjunto de reglas (constraints) quepuedes utilizar. Además también permite que definas las tuyas propias con lo que lasposibilidades de validación son totales. En la documentación oficial de Symfony2 puedes vertodas estas reglas de validación junto con su manera de uso.Utilizaremos las reglas que nos vayan haciendo falta a medida que avanzamos en la unidad.Pretendemos que los atributos del objeto Usuario satisfagan los siguientes requisitos:

nombre No se puede quedar vacío y no puede tener más de 255 caracteresapellidos No puede tener más de 255 caracteresusername No se puede quedar vacío y no puede tener más de 20 caracteres

alfanuméricos o guionessalt No puede tener más de 255 caracterespassword No se puede quedar vacío y no puede tener más de 20 caracteres

alfanuméricos o guionesemail Debe ser una dirección de email váidaisActive Debe ser un valor booleanotokenRegistroNada en especial

Vamos a utilizar como método de configuración las anotaciones, ya que nos parecen másútiles, pues podemos verlas directamente en el mismo fichero fuente donde se declara elobjeto.

Nota

Para utilizar anotaciones hay que indicarlo explicitamente en el ficheroapp/config/config.yml

...framework: validation: { enable_annotations: true }

Unidad 8: Desarrollo de la aplicación MentorNotas (IV). Validación y Formularios

144

Page 155: CursoSymfony2

...

Además hemos de usar el espacio de nombresSymfony\Component\Validator\Constraints en el fichero fuente donde se declare elobjeto que vamos a validar :

<?php...use Symfony\Component\Validator\Constraints as Assert;...

El siguiente trozo de código muestra como se aplicarían, mediante anotaciones, las reglasnecesarias para satisfacer los requisitos de la tabla anterior.

1 <?php 2 ... 3 use Symfony\Component\Validator\Constraints as Assert; 4 ... 5 /** 6 * @Assert\NotBlank() 7 * @Assert\MaxLength(255) 8 */ 9 private $nombre;10 11 /**12 * @Assert\MaxLength(255)13 */14 private $apellidos;15 16 /**17 * @Assert\NotBlank()18 * @Assert\MaxLength(255)19 * @Assert\Regex(20 * pattern="/^[\w-]+$/",21 * message="El nombre de usuario no puede contener más que caracteres alfanuméricos y guiones")22 */23 private $username;24 25 /**26 * @Assert\MaxLength(255)27 */28 private $salt;29 30 /**31 * @Assert\NotBlank()32 * @Assert\MaxLength(255)33 * @Assert\Regex(34 * pattern="/^[\w-]+$/",35 * message="El password no puede contener más que caracteres alfanuméricos y guiones")

36 */37 private $password;38 39 /**40 41 * @Assert\NotBlank()42 * @Assert\MaxLength(255)43 * @Assert\Email(

Unidad 8: Desarrollo de la aplicación MentorNotas (IV). Validación y Formularios

145

Page 156: CursoSymfony2

44 * message = "La dirección '{{ value }}' no es válida.")45 */46 private $email;47 48 /**49 * @Assert\Type(type="bool", message="El valor {{ value }} debe ser {{ type }}.")50 */51 private $isActive;

Nota

Estas anotaciones se añaden a las que ya teníamos definidas para el servicioDoctrine2. Cada servicio conoce qué anotaciones del objeto debe utilizar. No lasmostramos aquí por que nos estamos centrando exclusivamente en la validación. Alfinal de la unidad presentaremos el código fuente de la clase Notas completo, lasanotaciones para su configuración.

Un ejercicio que deberías hacer es contrastar el conjunto de anotaciones que hemosutilizado con la documentación oficial de las reglas de validación. Como ves no todas lasreglas tienen el mismo grado de complejidad, algunas (NotBlank por ejemplo) son deaplicación inmediatas, mientras que otras (Regex por ejemplo) requieren más datos para serdefinidas. Por otro lado, todas las reglas admiten la opción message para definir el mensajegenerado cuando la regla no se cumple.Entre todas las reglas que hemos utilizado, merece la pena destacar Regex, que permiteaplicar expresiones regulares para validar los datos.Una vez que ya hemos configurado las reglas que se aplicarán a cada atributo de la claseUsuario, podemos utilizar el servicio de validación. La forma de utilizar el servicio devalidación es la siguiente. Supongamos que estamos en una acción y tenemos un objetoUsuario ya creado:

1 <?php2 ...3 4 $validador = $this->get('validator');5 6 $errors = $validador-> validate($usuario);

Una vez que el contenedor de servicio nos proporciona el servicio de validación (línea 4),pasamos el objeto que queremos validad ($usuario) al método validate del servicio (línea6). Si el objeto no cumple lo que se ha especificado en las anotaciones de validación, lavariable $error recogería en un array una lista de mensajes que indican las reglas que nose han cumplido.Veamos esto en la práctica. Igual que hicimos cuando estudiamos Doctrine2, vamos a crearun controlador para realizar el estudio de los validadores y formularios. Lo llamaremosEstudioValidacionYFormulariosController. Creamos en él la acciónvalidaUsuarioAction() y la mapeamos en una ruta:Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/config/routing.yml

Unidad 8: Desarrollo de la aplicación MentorNotas (IV). Validación y Formularios

146

Page 157: CursoSymfony2

...jamn_EVF: pattern: /estudio_valyforms/valida_usuario defaults: { _controller: JAMNotasFrontendBundle:EstudioValidacionYFormulario:validaUsuario }

En el siguiente código validamos varios objetos Usuario que no cumplen las reglasimpuestas. En la plantilla asociada se muestran los errores generados por el servicio devalidación basándose en las reglas que hemos definido en las anotaciones.Jazzyweb/AulasMentor/JAMNotasFrontendBundle/Controller/EstudioValidacionYFormularioController.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Controller;; 4 5 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 use Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario; 7 8 class EstudioValidacionYFormularioController extends Controller 9 {10 11 public function validaUsuarioAction()12 {13 $usuario1 = new Usuario();14 15 // Primero validamos un usuario sin propiedades16 17 $validator = $this->get('validator');18 19 $errors1 = $validator->validate($usuario1);20 21 // Vemos que la única propiedad que ha pasado es Apellidos22 // Rellenamos las demás:23 $usuario2 = new Usuario();24 25 $usuario2->setNombre('Alberto Pablo');26 $usuario2->setEmail('albertopablo');27 $usuario2->setIsActive(10.23);28 $usuario2->setSalt('lasalt');29 $usuario2->setUsername('alberto pablo');30 $usuario2->setPassword('alberto pablo');31 32 $errors2 = $validator->validate($usuario2);33 34 //Pasó la propiedad nombre y salt35 36 $usuario3 = new Usuario();37 38 $usuario3->setNombre('Alberto Pablo');39 $usuario3->setEmail('[email protected]');40 $usuario3->setIsActive(true);41 $usuario3->setSalt('lasalt');42 $usuario3->setUsername('albertopablo');43 $usuario3->setPassword('albertopablo');44

Unidad 8: Desarrollo de la aplicación MentorNotas (IV). Validación y Formularios

147

Page 158: CursoSymfony2

45 $errors3 = $validator->validate($usuario3);46 47 $usuarios = array(48 array('num' => 1, 'user' => $usuario1, 'errors' => $errors1),49 array('num' => 2, 'user' => $usuario2, 'errors' => $errors2),50 array('num' => 3, 'user' => $usuario3, 'errors' => $errors3),51 );52 53 return $this->render(54 'JAMNotasFrontendBundle:EstudioValidacionYFormulario:validaUsuario.html.twig',55 array('usuarios' => $usuarios));56 }57 58 }

La plantilla'JAMNotasFrontendBundle:EstudioValidacionYFormulario:validaUsuario.html.twig,la ubicamos, atendiendo a su nombre lógico, en el directorioJazzyweb/AulasMentor/NotasFrontend/Resources/view/EstudioValidacionYFormulario

Jazzyweb/AulasMentor/NotasFrontend/Resources/views/EstudioValidacionYFormulario/validaUsuario.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block body %} 4 5 <h1>Prueba de validación de objetos Usuario</h1> 6 7 {% for u in usuarios%} 8 9 <h3>Datos del usuario {{u.num}}</h3>10 11 <table border="1">12 <tr>13 <td>id:</td>14 <td>{{u.user.id}}</td>15 </tr>16 <tr>17 <td>nombre:</td>18 <td>{{u.user.nombre}}</td>19 </tr>20 <tr>21 <td>apellidos:</td>22 <td>{{u.user.apellidos}}</td>23 </tr>24 <tr>25 <td>username:</td>26 <td>{{u.user.username}}</td>27 </tr>28 <tr>29 <td>password:</td>30 <td>{{u.user.password}}</td>31 </tr>32 <tr>33 <td>salt:</td>34 <td>{{u.user.salt}}</td>

Unidad 8: Desarrollo de la aplicación MentorNotas (IV). Validación y Formularios

148

Page 159: CursoSymfony2

35 </tr>36 <tr>37 <td>email:</td>38 <td>{{u.user.email}}</td>39 </tr>40 <tr>41 <td>esActivo:</td>42 <td>{{u.user.isActive}}</td>43 </tr>44 45 </table>46 47 {% if u.errors|length > 0 %}48 El usuario no es válido porque:49 <ul>50 {% for error in u.errors %}51 <li>{{ error.propertyPath }} {{ error.message }} </li>52 {% endfor %}53 </ul>54 {% else %}55 El usuario si es válido56 {% endif %}57 58 <hr/>59 {% endfor %}60 61 {% endblock %}

Introduce la ruta recién creada en tu navegador y observa el funcionamiento de losvalidadores que hemos utilizado en la acción.Existen muchas más reglas de validación que puedes utilizar. Pero la filosofía de uso essiempre la misma. En los ejercicios de esta unidad tendrás ocasión de explorar muchas deellas. Pero la teoría acerca de la validación se termina aquí.Para terminar diremos que el uso más frecuente de la validación en las aplicaciones web esla comprobación de datos enviados por el cliente a través de formularios. Por ello, comoveremos en breve, el servicio de formularios utiliza el servicio de validación de formatrasparente para realizar esta tarea. Lo cual significa que casi nunca se utiliza el servicio devalidación en exclusiva. Sin embargo, en ocasiones, puedes que tengas que utilizarlo paraotros menesteres distintos a la validación de datos de un formulario. Por ejemplo, supón quetu aplicación tiene una función muy compleja cuyo resultado debemos rechazar si no cumpleciertas restricciones. El servicio de validación puede servirnos perfectamente paracomprobar tales restricciones. Otro ejemplo que también puede darse es la validación dedatos devueltos por posibles servicios web consultados por la aplicación.Por último, además de las reglas propuestas por el servicio, también puedes crear tuspropias reglas. Para saber como hacer esto puedes mirar: como crear reglas a medida

El servicio de formulariosUn formulario HTML consiste en un conjunto de campos de entrada en los que el usuariopuede introducir datos de distintas maneras: escribiéndolo, seleccionándolo de una lista,picando con el ratón en alguna/s casilla/s, etcétera. Una de los objetivos del framework deformularios de Symfony2 es ayudarnos a construir dicho conjunto de campos.

El servicio de formularios

149

Page 160: CursoSymfony2

Por otro lado, una vez que el usuario introduce los datos en el formulario y los envía alservidor, por cuestiones de seguridad, la aplicación debe validarlos antes de realizar ningunaacción que pueda comprometer al sistema. El servicio de formularios también se utiliza pararealizar dichas comprobaciones.En Symfony2 podemos crear los formularios de dos maneras:

1. directamente en una acción y,2. definiendo en una clase un tipo (Type) que recoge los tipos de datos de cada campo del

formulario, y creando el formulario a través de dicho tipo.Aunque la primera estrategia es más directa, la segunda es más adecuada pues permite lareutilización de formularios en distintas partes del código de la aplicación, e incluso entredistintas aplicaciones.Comenzaremos estudiando como se crea un formulario directamente en la acción.

Formularios definidos en la acciónEn primer lugar, un formulario debería estar asociado a un objeto plano de nuestro modelo19 al que se le hayan asociado reglas de validación. Por tanto lo primero es asignar reglas devalidación al objeto que nos servirá como semilla del formulario. A continuación inyectamosdicho objeto al servicio de formularios y definimos los campos y tipos de campos quedeseamos añadir al formulario. Entonces ya tendremoso disponible un objeto formulario quepuede ser renderizado en la plantilla y usado también para realizar la validación de losdatos.De nuevo, una imagén vale más que mil palabras. Véamos lo que acabamos de decir enfuncionamiento. Utilizaremos como ejemplo el objeto Usuario cuyas reglas de validación yahemos definido en la sección anterior. Define la siguiente ruta:

jamn_EVF_form_usuario: pattern: /estudio_valyforms/form_usuario defaults: { _controller: JAMNotasFrontendBundle:EstudioValidacionYFormulario:formUsuario }

Y añade la acción formUsuarioAction() al controladorJAMNotasFrontendBundle:EstudioValidacionYFormularioController

Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/EstudioValidacionYFormularioController.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Controller; 4 5 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 use Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario; 7 8 class EstudioValidacionYFormsController extends Controller 9 {10 ...11 12 public function formUsuarioAction()13 {14 $usuario = new Usuario();15 16 $usuario->setNombre('Alberto');17 $usuario->setApellidos('Einstein');

Formularios definidos en la acción

150

Page 161: CursoSymfony2

18 $usuario->setEmail('[email protected]');19 $usuario->setIsActive(true);20 $usuario->setUsername('alberto');21 $usuario->setPassword('alberto');22 23 $form = $this->createFormBuilder($usuario)24 ->add('nombre', 'text')25 ->add('apellidos', 'text')26 ->add('email', 'email')27 ->add('isActive', 'checkbox')28 ->add('username', 'text')29 ->add('password', 'password')30 ->getForm();31 32 return $this->render(33 'JAMNotasFrontendBundle:EstudioValidacionYFormulario:formUsuario.html.twig',34 array(35 'form' => $form->createView(),36 ));37 }38 39 }40 ?>

En las líneas 14-21 hemos creado un usuario y le hemos definido algunos de sus campos. Apartir de este objeto Usuario, creamos un formulario en las línea 23. Para ello usamos elmétodo createFormBuilder(), que es un wrapper al contenedor de servicios de Symfony2,pasándole como argumento el objeto que acabamos de crear. A continuación (líneas 24-29)añadimos los campos que constituirán el formulario mediante los métodos add(). El primerargumento de este método es el nombre del atributo del objeto que estamos utilizando($usuario en nuestro caso), y el segundo el tipo de campo que queremos acociarle.También podemos pasarle un tercer argumento con un array de opciones. Estas opcionesdependen de cada tipo de campo. Los tipos de campos y sus opciones, pueden consultarseen la documentación oficial de Symfony2.Fíjate que no hemos añadido todos los campos del objeto Usuario. No es ni necesario niadecuado, que el formulario disponga de todos los campos del objeto que utiliza comosemilla. Este objeto es utilizado por el formulario para

1. Asignar los valores de sus atributos a los campos del formulario2. Utilizar las reglas de validación asociadas a sus atributos en el proceso de validación.

Así que es el programador quien, en función de sus necesidades, decide qué camposcontendrá el formulario.Los objetos formularios pueden convertirse en variables para las plantillas mediante elmétodo createView(). Es lo que hacemos en la línea 34. En la plantilla podemos usar lavariable form de tres maneras distintas para pintar el formulario. La más sencilla, peromenos flexible, es a través de la función form_widget() de twig, la cual se encarga degenerar todo el código de los campos del formulario automáticamente.Jazzyweb/AulasMentor/NotasFrontend/Resources/views/EstudioValidacionYFormulario/formUsuario.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block body %} 4 5 <h1>Como se pintan los formularios: El formulario usuario </h1> 6

Formularios definidos en la acción

151

Page 162: CursoSymfony2

7 <h2>La manera más rápida, pero con menos control sobre los widgets</h2> 8 <form action="#" method="post" {{ form_enctype(form) }}> 9 {{ form_widget(form) }}10 11 <input type="submit" />12 </form>13 14 {% endblock %}

La línea 9 es la encargada de pintar

• todos los campos del formulario,• las etiquetas de los campos• los errores asociados a los campos en caso de que la validación no haya sido

satisfactoria,• los errores generales del formulario• un campo oculto que se genera automáticamente para proteger el formulario contra

ataques CSRF.Es decir, lo hace todo. El problema es que el código HTML generado no lo controlamosnosotros y, aunque para hacer pruebas puede venir muy bien, es un problema para afinaruna versión definitiva de la plantilla. Otra manera de pintar el formulario que proporcionamás flexibilidad en el código de la plantilla es utilizando en combinación las funcionesform_errors(), form_row() y form_rest(). La plantilla anterior quedaría así:Jazzyweb/AulasMentor/NotasFrontend/Resources/view/EstudioValidacionYFormulario/formUsuario.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block body %} 4 5 <h1>Como se pintan los formularios: El formulario usuario </h1> 6 7 <h2>Una manera más "verbosa", pero con más control sobre los widgets</h2> 8 <form action="#" method="post" {{ form_enctype(form) }}> 9 {{ form_errors(form) }}10 11 {{ form_row(form.nombre) }}12 {{ form_row(form.apellidos) }}13 {{ form_row(form.email) }}14 {{ form_row(form.isActive) }}15 {{ form_row(form.username) }}16 {{ form_row(form.password) }}17 18 {{ form_rest(form) }}19 20 <input type="submit" />21 </form>22 23 {% endblock %}

Ahora se han pintado por separado los errores generales del formulario (línea 9), las filas correspondientes a los campos del formulario se pintan por separado en las líneas 11-16,

Formularios definidos en la acción

152

Page 163: CursoSymfony2

cada línea se compone de la etiqueta del formulario, el mensaje de error cuando no pasa lavalidación, y el campo en sí. Por último en la línea 18 se genera automáticamente el resto delos campos del formulario que no se hayan pintado con form_row. El uso de form_restimportante para pintar el campo oculto que utiliza Symfony2 para el control de ataquesCSRF.Hemos ganado algo de flexibilidad pero en muchos casos tampoco será suficiente. Para esoscasos podemos utilizar las funciones form_label(), form_errors() y form_widget() paradesglosar cada una de los campos.Jazzyweb/AulasMentor/NotasFrontend/Resources/view/EstudioValidacionYFormulario/formUsuario.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block body %} 4 5 <h1>Como se pintan los formularios: El formulario usuario </h1> 6 7 <h2>La manera más "verbosa", pero con más control sobre los widgets</h2> 8 <form action="#" method="post" {{ form_enctype(form) }}> 9 {{ form_errors(form) }}10 11 <div>12 {{ form_label(form.nombre) }}13 {{ form_errors(form.nombre) }}14 {{ form_widget(form.nombre) }}15 </div>16 17 <div>18 {{ form_label(form.apellidos) }}19 {{ form_errors(form.apellidos) }}20 {{ form_widget(form.apellidos) }}21 </div>22 23 <div>24 {{ form_label(form.email) }}25 {{ form_errors(form.email) }}26 {{ form_widget(form.email) }}27 </div>28 29 <div>30 {{ form_label(form.isActive) }}31 {{ form_errors(form.isActive) }}32 {{ form_widget(form.isActive) }}33 </div>34 35 <div>36 {{ form_label(form.username) }}37 {{ form_errors(form.username) }}38 {{ form_widget(form.username) }}39 </div>40 41 <div>42 {{ form_label(form.password) }}43 {{ form_errors(form.password) }}

Formularios definidos en la acción

153

Page 164: CursoSymfony2

44 {{ form_widget(form.password) }}45 </div>46 47 {{ form_rest(form) }}48 49 <input type="submit" />50 </form>51 52 {% endblock %}

Definición de tipos para crear formulariosSi tuviésemos que utilizar el mismo formulario que acabamos de construir en otra parte de laaplicación, tendríamos que copiarlo y pegarlo allá donde lo necesitamos. Supongo que al leerla frase anterior se te habrán erizado todos los bellos de la piel. En efecto, repetir código pordoquier no es una buena idea. Si queremos reutilizar un formulario la solución es crear loque en Symfony2 se conoce como un tipo (Type).Un tipo es una clase que deriva de Symfony\Component\Form\AbstractType en la que sedefinen los campos que debe poseer cualquier formulario construido a partir de dicho tipo. Elservicio de formularios de Symfony2 proporciona el método createForm() el cual, tomandocomo argumentos un Type y un objeto del modelo, construye un formulario.Vamos a construir el mismo formulario para el objeto Usuario utilizando esta estrategiamás adecuada para la reusabilidad. Comenzamos por definir un tipo para el objeto Usuario.Es decir, creamos una clase que llamaremos UsuarioType y que extiende aSymfony\Component\Form\AbstractType. La ubicaremos en el directorioJazzyweb/AulasMentor/NotasFrontend/Form/Type.Jazzyweb/AulasMentor/NotasFrontend/Form/Type/UsuarioType.php.

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Form\Type; 4 5 use Symfony\Component\Form\AbstractType; 6 use Symfony\Component\Form\FormBuilder; 7 8 class UsuarioType extends AbstractType 9 {10 11 public function buildForm(FormBuilder $builder, array $options)12 {13 $builder->add('nombre', 'text')14 ->add('apellidos', 'text')15 ->add('email', 'email')16 ->add('isActive', 'checkbox')17 ->add('username', 'text')18 ->add('password', 'password');19 }20 21 public function getName()22 {23 return 'usuario';24 }

Definición de tipos para crear formularios

154

Page 165: CursoSymfony2

25 26 // Esto no es siempre necesario, pero para construir formularios embebidos27 // es imprescindibles, así que no cuesta nada acostumbrarse a ponerlo28 public function getDefaultOptions(array $options)29 {30 return array(31 'data_class' => 'Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario',32 );33 }34 }

El elemento fundamental de un tipo es su método buildForm() donde se especifican loscampos que debe incluir el formulario que se construya a partir de dicho tipo. El métodogetName() debe devolver un nombre único que identifique al tipo . Y el métodogetDefaultOptions(), aunque no es estrictamente necesario para la mayor parte deoperaciones que se hacen con los tipos, es requerido cuando se utilizan formulariosembebidos. Por ello no viene mal acostumbrarse a ponerlo.Ahora que tenemos el tipo UsuarioType podemos utilizarlo para construir un formulario deusuarios en cualquier acción de nuestra aplicación. La acción formUsuario() de la secciónanterior quedaría ahora así:Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/EstudioValidacionYFormularioController.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Controller; 4 5 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 use Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario; 7 use Jazzyweb\AulasMentor\NotasFrontendBundle\Form\Type\UsuarioType; 8 9 class EstudioValidacionYFormsController extends Controller10 {11 ...12 13 public function formUsuarioAction()14 {15 $usuario = new Usuario();16 17 $usuario->setNombre('Alberto');18 $usuario->setApellidos('Einstein');19 $usuario->setEmail('[email protected]');20 $usuario->setIsActive(true);21 $usuario->setUsername('alberto');22 $usuario->setPassword('alberto');23 24 $form = $this->createForm(new UsuarioType(), $usuario);25 26 return $this->render(27 'JAMNotasFrontendBundle:EstudioValidacionYFormulario:formUsuario.html.twig',28 array(29 'form' => $form->createView(),30 ));31 }32 33 }

Y el resultado es exactamente igual que antes. La diferencia es que

1. El código es más legible, no se contamina con los detalles de creación del formulario.

Definición de tipos para crear formularios

155

Page 166: CursoSymfony2

2. El formulario es reutilizable. Podemos crear un formulario usuario en cualquier otraparte de la aplicación. Si tenemos que cambiarlo, añadiendo por ejemplo un nuevocampo, bastaría con modificar la clase UsuarioType para que todos los formulariosconstruidos a partir de ella se actualicen.

Importante

No olvides añadir la línea: useJazzyweb\AulasMentor\NotasFrontendBundle\Form\Type\UsuarioType; en elcontrolador para poder usar la clase UsuarioType.

Validación de formulariosSi una aplicación envía un formulario al cliente, es por que espera recibir datos para realizaralgún proceso con ellos. Las aplicaciones web, normalmente están más expuestas que lasaplicaciones de usuario, ya que utilizan alguna red para dar su servicio, y son muchos losusuarios que pueden acceder a la red aumentando el riesgo de ataques. Por ello es muyimportante en el desarrollo de aplicaciones web que todos los datos enviados desde elcliente sean cuidadosamente validados antes de ser procesados, pues cualquier tipo deexploit de una aplicación se produce por sus vías de entradas de datos.Vamos a explicar esto con más detalle:La función de los formularios HTML es recoger datos del usuario cliente y enviarlos alservidor para hacer algo con ellos (almacenarlos en una base de datos, realizar un complejocálculo para confeccionar una carta astral o cualquier otra cosa).El navegador web envía los datos realizando una petición HTTP al servidor, la cual, según laespecificación del protocolo HTTP, puede ser de varios tipos en función de lo que el clientetenga intención de hacer con los datos enviados sobre el servidor. Las operaciones HTTPmás conocidas que un servidor web pone a disposición de los clientes a través de su interfazuniforme son GET, POST, PUT y DELETE, la cuales pueden ser comparadas semánticamentecon las operaciones en una base de datos:

MétodoHTTP

Operación en base dedatos Descripción

GET Retrieve Recupera un recurso/registro. No hay modificación delmismo

POST Update Modifica un recurso/registro.PUT Create Crear un recurso/registroDELETE Delete Elimina un recurso/registro

Los navegadores web actuales tan sólo soportan las dos primeras operaciones (GET y POST)para realizar peticiones al servidor y enviarle datos. Ahora bien, ¿cuál de las dos debemosutilizar en nuestros formularios HTML? La respuesta ya la hemos insinuado unas línea másatrás al comparar los métodos HTTP con las operaciones de una base de datos: si en elproceso de servidor que recibe los datos se va a llevar a cabo algún tipo de modificación (yasea en bases de datos, ficheros o cualquier otro tipo de recurso) debemos utilizar POST quese correspondería con una operación de actualización, pero si dicho proceso utiliza los datosenviados tan solo para recuperar algún recurso, sin que haya ningún tipo de modificación enlos datos que gestiona la aplicación, entonces debemos utilizar GET.

Validación de formularios

156

Page 167: CursoSymfony2

Además de los matices semánticos de ambas operaciones, existe una diferencia bien visible:En el caso de una petición del tipo GET, los datos se envían como parámetros que formanparte de la URL, tal y como mostramos en este ejemplo:

http://www.elservidor.es/recurso?param1=valor1&param2=valor2

mientras que en las peticiones POST los datos viajan encapsulados en la sección de datos dela petición HTTP. Además mediante POST se pueden enviar ficheros al servidor indicando enla petición que el tipo de contenido (content-type) de los datos enviados esmultipart/form-data. Esta indicación se realiza a través del parámetro enctype delelemento form.Es muy importante que comprendamos que las peticiones HTTP se realizan desde un clientesobre el cual, obviamente, el servidor no tiene ningún tipo de control directo, de manera queel cliente puede enviar al servidor los parámetros que quiera, saltándose lo prescrito por elformulario. Expliquemos esto con más detalle. La secuencia normal que seguiría unaaplicación web para pedir datos al cliente y procesarlos sería:

1. El servidor envía un formulario HTML al cliente con los campos que el usuario utilizarápara introducir los datos.

2. El navegador interpreta el documento HTML y presenta el formulario al usuario3. El usuario introduce los datos y envía el formulario relleno, es decir, pica en el botón

submit y el navegador construye una petición HTTP al proceso de servidor indicado en elparámetro action del formulario y con los datos que el usuario ha introducido.

4. El servidor recibe la petición con los datos esperados y los procesa.Sin embargo no hay nada que impida saltarse el paso 3 y construir en el cliente, sin utilizarel navegador, una petición HTTP al proceso de servidor con cualquier tipo de datos. Estosignifica que el servidor nunca debe fiarse de los datos que traen las peticiones, ya que hanpodido ser manipuladas en el cliente y no tienen por que obedecer a lo que el programadorespera, dando lugar a posibles brechas de seguridad en la aplicación.Conclusión: Los procesos de servidor deben validar TODOS los datos que le llegan antes derealizar ninguna operación con ellos.

Nota

Observa que el hecho de introducir validadores en el cliente utilizando códigojavascript no vale de nada, ya que como acabamos de indicar, el usuario puede“puentear” completamente el uso del navegador, y por tanto del formulario enviado,para construir y lanzar la petición con los datos que desee. Este hecho junto con eldesconocimiento, la pereza o la escasa destreza del programador para blindar susaplicaciones con los adecuados validadores del lado del servidor, dan lugar a una delas vulnerabilidades más comunes de las aplicaciones web.

Nota

Sobre la validación HTML5.

Validación de formularios

157

Page 168: CursoSymfony2

Desde la introducción del HTML5 muchos navegadores pueden realizar nativamenteuna validación en el lado del cliente. Para activarla se añade el atributo required alcampo que se desea validar. Symfony2 añade automáticamente dicho atributo en loscampos que son requeridos, por lo que si utilizas un navegador compatible con HTML5se producirá una validación en el cliente antes de enviar los datos al servidor. Aún así,y por las razones expuesta más arriba, hay que implementar la validación de losformularios en el lado del servidor.La validación en el lado del cliente se puede deshabilitar añadiendo el atributonovalidate a la etiqueta form, o formnovalidate a la etiqueta submit. Hacer esto esespecialmente importante cuando queremos testear la validación en el lado delservidor. Es decir, lo que nos porponemos hacer ahora. Por tanto añadiremos elatributo no validate a la etiqueta form en nuestras plantillas.

Importante

Añade el atributo novalidate a la etiqueta form en la plantillaformUsuario.html.twig:

...<form novalidate="true" action="#" method="post" {{ form_enctype(form) }}>...

A continuación vamos a explicar un procedimiento que puedes aplicar siempre que necesitesrecoger y validar datos del usuario mediante formularios. Utilizaremos, como no, el objetoUsuario y su tipo asociado UsuarioType como ejemplo.La estrategia consiste en utilizar una misma acción tanto para mostrar el formulario comopara procesar los datos que el usuario ha introducido en él. Si la petición de la acción se harealizado con el método GET, interpretaremos que el usuario está solicitando el formulario.Pero si la petición se hace vía POST querrá decir que el usuario ha enviado datos y entoncestenemos que validarlo.Vamos a modificar la acción formUsuarioAction() para implementar esta estrategia:Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/EstudioValidacionYFormularioController

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Controller; 4 5 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 use Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario; 7 use Jazzyweb\AulasMentor\NotasFrontendBundle\Form\Type\UsuarioType; 8 9 class EstudioValidacionYFormsController extends Controller10 {11 ...12

Validación de formularios

158

Page 169: CursoSymfony2

13 public function formUsuarioAction()14 {15 $request = $this->getRequest();16 $usuario = new Usuario();17 18 $form = $this->createForm(new UsuarioType(), $usuario);19 20 if($request->getMethod() == 'POST')21 {22 $form->bindRequest($request);23 if ($form->isValid())24 {25 // Se procesa el formulario26 27 $this->get('session')->setFlash('mensaje','El formulario era válido');28 return $this->redirect($this->generateUrl('jamn_EVF_form_usuario'));29 }30 }31 32 return $this->render(33 'JAMNotasFrontendBundle:EstudioValidacionYFormulario:formUsuario.html.twig',34 array(35 'form' => $form->createView(),36 ));37 }38 39 }

Para completar el procedimiento hay que indicar en el atributo action del formulario HTML,la URL donde se enviarán los datos. En la línea 8 de la plantilla siguiente se usa la funciónpath de twig para establecer dicha URL. También hemos incluido el atributo novalidate ala etiqueta form, y la visualización condicional del mensaje establecido en una variable flashde sesión que indica que el formulario era válido.Jazzyweb/AulasMentor/NotasFrontend/Resources/view/EstudioValidacionYFormulario/formUsuario.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block body %} 4 5 <h1>Como se pintan los formularios: El formulario usuario </h1> 6 7 {% if app.session.hasFlash('mensaje') %} 8 <div style="background-color: rosybrown"> 9 {{ app.session.flash('mensaje') }}10 </div>11 {% endif %}12 <h2>Una manera más "verbosa", pero con más control sobre los widgets</h2>13 <form novalidate="true" action="{{ path('jamn_EVF_form_usuario') }}" method="post" {{ form_enctype(form) }}>14 {{ form_errors(form) }}15 16 {{ form_row(form.nombre) }}17 {{ form_row(form.apellidos) }}18 {{ form_row(form.email) }}19 {{ form_row(form.isActive) }}20 {{ form_row(form.username) }}21 {{ form_row(form.password) }}22 23 {{ form_rest(form) }}24 25 <input type="submit" />26 </form>27 28 {% endblock %}

Ahora ya podemos probar el procedimiento y explicarlo. Cuando la petición se realiza vía método GET, la acción interpreta que se está solicitando el envío del formulario para rellenar. Nosotros lo hemos enviado vacío, pero si se quiere enviar con algún campo relleno,

Validación de formularios

159

Page 170: CursoSymfony2

basta con definir en el objeto $usuario el atributo correspondiente antes de construir elformulario (línea 16 de la acción). Pero si la petición se ha realizado vía método POST,significa que el usuario envió el formulario picando en el botón submit. Entonces se rellenael formulario con los valores que vienen en la petición (línea 22) y acto seguido se validancon las reglas definidas para el objeto Usuario (línea 23). Si la validación no es correcta,entonces se vuelve a pintar el formulario con los mismos datos que se enviaron y ademáscon los mensaje de error que explican porqué no es válido el formulario. Por último, si sepasa la validación, entonces se realiza el proceso de los datos y se realiza una redirección aalguna acción. Aunque, como en este caso, la redirección se haga a la misma acción, es muyimportante realizarla para evitar que, al refrescar por accidente el navegador, se vuelvan aenviar por POST los mismos datos y vuelvan a procesarse (ten en cuenta que unaredirección siempre se realiza mediante la operación GET).Este procedimiento, con más o menos variación, lo vas a encontrar continuamente en elcódigo de las aplicaciones Symfony2 que utilicen formularios.

La unidad en chuletas

El servicio de validaciónPara definir reglas de validación sobre un objeto plano hay que añadir en el fichero donde sedefine la clase:

<?php...use Symfony\Component\Validator\Constraints as Assert;...

Para saber cuales son las regas de validación, consulta este enlace:reglas de validaciónPara usarlas en una acción:

<?php...$usuario = new Usuario();$validator = $this->get('validator');$errors1 = $validator->validate($usuario1);...

Y para pintarlas en una plantilla:

...{% if u.errors|length > 0 %}El usuario no es válido porque:<ul>{% for error in u.errors %} <li>{{ error.propertyPath }} {{ error.message }} </li>{% endfor %}</ul>{% else %} El usuario si es válido{% endif %} <hr/>

La unidad en chuletas

160

Page 171: CursoSymfony2

{% endfor %}...

El servicio de formulariosPara definir un formulario en la acción:

public function formUsuarioAction(){ $usuario = new Usuario();

$usuario->setNombre('Alberto'); $usuario->setApellidos('Einstein'); $usuario->setEmail('[email protected]'); $usuario->setIsActive(true); $usuario->setUsername('alberto'); $usuario->setPassword('alberto');

$form = $this->createFormBuilder($usuario) ->add('nombre', 'text') ->add('apellidos', 'text') ->add('email', 'email') ->add('isActive', 'checkbox') ->add('username', 'text') ->add('password', 'password') ->getForm();

return $this->render( 'JAMNotasFrontendBundle:EstudioValidacionYFormulario:formUsuario.html.twig', array( 'form' => $form->createView(), ));}

Para pintarlo en una plantilla (manera más "verbosa"):

{% extends '::base.html.twig' %}

{% block body %}

<h1>Como se pintan los formularios: El formulario usuario </h1>

<h2>La manera más "verbosa", pero con más control sobre los widgets</h2> <form action="#" method="post" {{ form_enctype(form) }}> {{ form_errors(form) }}

<div> {{ form_label(form.nombre) }} {{ form_errors(form.nombre) }} {{ form_widget(form.nombre) }} </div>

<div> {{ form_label(form.apellidos) }} {{ form_errors(form.apellidos) }} {{ form_widget(form.apellidos) }}

El servicio de formularios

161

Page 172: CursoSymfony2

</div>

<div> {{ form_label(form.email) }} {{ form_errors(form.email) }} {{ form_widget(form.email) }} </div>

<div> {{ form_label(form.isActive) }} {{ form_errors(form.isActive) }} {{ form_widget(form.isActive) }} </div>

<div> {{ form_label(form.username) }} {{ form_errors(form.username) }} {{ form_widget(form.username) }} </div>

<div> {{ form_label(form.password) }} {{ form_errors(form.password) }} {{ form_widget(form.password) }} </div>

{{ form_rest(form) }}

<input type="submit" /> </form>

{% endblock %}

Para definirlo en una clase como tipo. Se crea la clase en el directorio Form\Type del bundle:

<?php

namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Form\Type;

use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder;

class UsuarioType extends AbstractType {

public function buildForm(FormBuilder $builder, array $options) { $builder->add('nombre', 'text') ->add('apellidos', 'text') ->add('email', 'email') ->add('isActive', 'checkbox') ->add('username', 'text') ->add('password', 'password');

El servicio de formularios

162

Page 173: CursoSymfony2

}

public function getName() { return 'usuario'; }

// Esto no es siempre necesario, pero para construir formularios embebidos // es imprescindibles, así que no cuesta nada acostumbrarse a ponerlo public function getDefaultOptions(array $options) { return array( 'data_class' => 'Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario', ); } }

Y se utiliza en una acción:

<?php...use Jazzyweb\AulasMentor\NotasFrontendBundle\Form\Type\UsuarioType;...$usuario = new Usuario();

$usuario->setNombre('Alberto');$usuario->setApellidos('Einstein');

$form = $this->createForm(new UsuarioType(), $usuario);

Método para la validación de formularios.En una acción:

<?php...public function formUsuarioAction(){ $request = $this->getRequest(); $usuario = new Usuario();

$form = $this->createForm(new UsuarioType(), $usuario);

if($request->getMethod() == 'POST') { $form->bindRequest($request); if ($form->isValid()) { // Se procesa el formulario

$this->get('session')->setFlash('mensaje','El formulario era válido'); return $this->redirect($this->generateUrl('jamn_EVF_form_usuario')); } }

Método para la validación de formularios.

163

Page 174: CursoSymfony2

return $this->render( 'JAMNotasFrontendBundle:EstudioValidacionYFormulario:formUsuario.html.twig', array( 'form' => $form->createView(), )); }

ComentariosAquí puedes enviar comentarios, dudas y sugerencias. Utiliza la barra de scroll para recorrertodos los mensajes. El formulario de envío se encuentra al final.

Autor del código: Juan David Rodríguez García <[email protected]>

Comentarios

164

Page 175: CursoSymfony2

Unidad 9: Desarrollo de la aplicación MentorNotas (V).Seguridad - Autentificación y AutorizaciónLa seguridad en una aplicación es un proceso que consiste en dos pasos sucesivos:Autentificación y Autorización.La autentificación es el proceso mediante el cual la aplicación comprueba si el usuario quepretende utilizarla es realmente quien dice ser. Es un proceso de identificación. Para ello laaplicación solicita al usuario ciertos parámetros que lo identifiquen y, mediante algún tipo decomprobació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é funcionalidadespuede utilizar el usuario que la maneja y qué información le puede presentar. La aplicacióntoma tal decisión basándose en la identificación del usuario, esto es; decidirá qué recursospuede ofrecerle una vez que ha admitido la autentificación del usuario. Es, por tanto, unsegundo nivel de seguridad en el control de acceso.Algunas aplicaciones seguras ofrecen todos sus recursos a cualquier usuario autentificado,en cuyo caso la autorización se confunde con la autentificación, pero en la mayoría de lasaplicaciones no es así. La que estamos desarrollando exige en sus requisitos este doble nivelde seguridad, ya que dependiendo del perfil que tenga asociado, el usuario podrá utilizarmás o menos recursos de la aplicación.Symfony2 proporciona un magnífico servicio para el tratamiento de la seguridad en lasaplicaciones web, de forma que podemos resolver gran parte de los problemas que estaplantea a través de un fichero de configuración. A lo largo de esta unidad estudiaremos estecomponente y lo aplicaremos a la aplicación que estamos desarrollando.

Componentes y funcionamiento del servicio de seguridad deSymfony2.La estructura básica del funcionamiento de los mecanismos de seguridad en aplicacionesweb es prácticamente la misma en todas ellas.En primer lugar, cuando el usuario solicite algún recurso protegido, la aplicación debe iniciarel proceso de autentificación, solicitando de alguna manera al usuario las credenciales que loidentifican. A continuación la aplicación contrasta dichas credenciales utilizando algún tipode sistema de infomación persistente (base de datos, directorio tipo LDAP, fichero depasswords, etcétera) donde residen los datos de identificación y posiblemente otros datosacerca del usuario (permisos asociados, por ejemplo).

Nota

El procedimiento típico para autentificar a un usuario consiste en solicitarle un nombrede usuario (username) y una contraseña (password) asociada, que constituyen un parconocido tan sólo por el usuario para evitar suplantaciones de identidad. A pesar deser el mecanismo más utilizado, es el más inseguro, pues la filtración de passwordobtenidos por distintas técnicas, desde las más rudimentaras como passwords quequedan anotados en papeles que se tiran a la papelera, hasta las más sofisticadascomo la inyeccción de troyanos que leen lo que el usuario teclea (keyloggers), está ala orden del día.Por ello hay una tendencia y un interés creciente por adoptar mecanismos basados en certificados digitales pertenecientes y custodiados por del usuario y protegidos por clave. El DNI electrónico contiene un certificado de este tipo, siendo uno de los

Unidad 9: Desarrollo de la aplicación MentorNotas (V). Seguridad - Autentificación yAutorización

165

Page 176: CursoSymfony2

propósitos del mismo identificar a las personas en aplicaciones informáticas. Estosmétodos son bastante más seguros que el basado en username y password, sinembargo no están exentos de ser vulnerados.Un nivel aún más seguro lo constituyen los mecanismos de control biométrico, comopuede ser la lectura de la huella dactilar, que complementa o sustituye a los queacabamos de describir.No obstante, en el momento de escribir estas líneas, la mayor parte de lasaplicaciones web basan su control de identidad en el par de parámetros nombre deusuario y contraseña y será este el que utilizaremos en nuestra aplicación.

Una vez admitida la identidad del usuario, se obtienen del sistema de información los datosdel usuario y se hace uso de la sesión para mantener en las sucesivas peticiones del usuariosu estado autentificado y sus permisos. De esa manera la aplicación sabrá en cada peticiónqué recursos puede ofrecer al usuario (autorización) sin preguntarle de nuevo suscredenciales de identificación.Cuando el usuario solicite finalizar la sesión, o pase un determinado tiempo sin actividad, laaplicación destruirá la sesión, de manera que al realizar una nueva petición a un recursoprotegido, la aplicación volverá a pedir las credenciales al usuario repitiéndose el proceso.El servicio de seguridad de Symfony2 sigue este mismo patrón de funcionamiento. Para locual utiliza una serie de conceptos que separan las distintas funcionalidades del proceso. Enprimer lugar presentaremos y describiremos dichos conceptos sin entrar en el detalle decomo se especifican en el código. Es importante, antes de nada, entenderlos bien. Después,utilizando nuestra aplicación de ejemplo, veremos con detalle como se aplican talesconceptos.

Los proveedores de usuarios (user providers)Un proveedor de usuarios es un objeto que se comunica con el sistema de informaciónpersistente donde se almacenan los datos de los usuarios relativos a la seguridad y que seutiliza para contrastar las credenciales de acceso y obtener los permisos del usuario(denominados roles en Symfony2). Este sistema de información puede ser:

• un archivo,• una base de datos• un directorio LDAP

• un directorio activo• un servicio externo como Facebook o twitter

• un proveedor de identidad de alguna federación de aplicaciones• etcétera

La edición estándar de Symfony2 proporciona dos proveedores de usuarios: uno basado enel fichero de configuración y otro que utiliza alguna de las base de datos configuradas en elframework. Sin embargo, dado el diseño modular y extensible del framework es posibleañadir nuevos proveedores de usuarios al sistema.

Los cortafuegos (firewalls)Los recursos de una aplicación Symfony2 se protegen mediante cortafuegos (firewalls). Uncortafuegos es un conjunto de directivas o parámetros de configuración que definen

Los proveedores de usuarios (user providers)

166

Page 177: CursoSymfony2

1. qué recursos (rutas) de la aplicación están protegidos,2. qué mecanismo de autentificación debe utilizarse para recoger las credenciales del

usuario y3. sobre qué proveedor de usuarios se deben constrastar dichas credenciales.

Symfony2 incorpora, por lo pronto, los siguientes mecanismos de autentificación. Noobstante, dado su diseño modular y extensible, se podrían añadir sin mucho esfuerzo otrosnuevos.

1. Autentificación básica HTTP (basic HTTP autentication). En este tipo de autentificación elservidor envía al navegador la orden de pedir en un formulario un nombre de usuario yuna contraseña. Por ello es el propio navegador el que proporciona el formulario delogin en una ventana emergente.

2. Autentificación digest HTTP (digest HTTP authentication), que es similar al anterior peromás seguro, ya que aplica funciones hash a las contraseñas antes de enviarlas por lared.

3. Autentificación mediante un formulario HTML. Es el mecanismo clásico. En este caso eldesarrollador tendrá que construir dicho formulario e indicar al cortafuegos como seaccede a él.

4. Autentificación mediante certificados X.509. Es un mecanismo más seguro basado encertificados digitales X.509, como los emitidos por la FNMT (Fábrica Nacional de Moneday Timbre) o el que proporciona el DNI electrónico.

En la configuración de los cortafuegos también se pueden establecer otros procedimientos yparámetros propios del proceso de autentificación como por ejemplo; el proceso de logout(fin de sesión), la incorporación de un filtro Remember me o la URL de la página de error.En definitiva, los cortafuegos son los responsables del proceso de autentificación en elservicio de seguridad de Symfony2 y utilizan algún proveedor de usuarios para contrastar lascredenciales del usuario.

Importante

La aplicación puede definir todos los cortafuegos que precise. Cada cortafuego puedeproteger un conjunto de rutas diferentes, utilizar el mecanismo de autentificación quele venga bien, y contrastar las credenciales contra uno de los proveedores de usuariosdisponibles en la aplicación.

El control de acceso (access control) y la jerarquía de roles (role hierarchy)Una vez que el el usuario ha conseguido demostrar su identidad durante el proceso deautentificación, entra en juego el proceso de autorización.La autorización permite proteger el acceso a los recursos (rutas) mediante el concepto derol, el cual es sencillamente una palabra clave que nos inventamos para asociarla a losusuarios y a los recursos que deseamos proteger. De manera que el acceso a un recurso quetenga asociado un rol (o una combinación lógica de roles), será permitido únicamente a losusuarios que posean dicho rol (o cumplan la combinación lógica de roles).Por ello la configuración del proceso de autorización tiene dos partes:

El control de acceso (access control) y la jerarquía de roles (role hierarchy)

167

Page 178: CursoSymfony2

1. La asignación de roles a los usuarios, para lo que se utiliza el sistema de informaciónpersisitente donde se encuentra información acerca de los roles (permisos) asociados alusuario.

2. La asociación de roles a los recursos, que se definen en la configuración del servicio deseguridad de Symfony2.

Symfony2 permite una mayor granularidad en el proceso de autorización, siendo posibleproteger con roles incluso partes concretas de las acciones y/o plantillas y los atributos delas entidades mapeadas con Doctrine2.El servicio de seguridad de Symfony2 permite organizar los roles en jerarquías, de maneraque un sólo rol puede agrupar a varios y el proceso de autorización tratará al usuario que loposea como si tuviese todos los roles que se hayan definido en la jerarquía.Además el concepto de autorización va más allá de la protección por roles. También sepuede exigir que los recursos sean accedidos únicamente desde ciertas IP's y/o dominos yque sea necesario utilizar web segura (https).

Los codificadores (encoders)Sea cual sea el sistema de información utilizado, las contraseñas de los usuarios deberíanalmacenarse codificadas. De esta manera se previene un eventual filtrado de contraseñaspor parte de quien tiene acceso a dichos sistemas de información, ya sea legalmente por seradministrador o ilegalmente por que ha conseguido penetrar en ellos.La encriptación o codificación de las contraseñas se realiza mediante algoritmos hash cuyaprincipal característica es que codifican cadenas de caracteres en un único sentido. Es decir,no es posible la decodificación. Por ello, si se almacena la versión codificada de lacontraseña, no se puede obtener a partir de esta la contraseña original.Entonces, ¿cómo se puede utilizar las contraseñas codificadas en el proceso deautentificación?. Muy sencillo, se trata de codificar la cadena que el usuario facilita con elmismo algoritmo que se utilizó para su almacenamiento, y comparar el resultado con laconstraseña codificada en el proveedor de usuarios.El sistema de seguridad de Symfony2 también nos ayuda con la tarea de codificar lacontraseña en el proceso de autentificación. Proporciona para ello un servicio de codificación(encoders) que se aplica sobre cada proveedor de usuarios para codificar la contraseñafacilitada por el usuario con el mismo algoritmo que se utilizó en el sistema de informaciónasociado al proveedor de usuarios en cuestión.

La seguridad en acciónLlegó el momento de aplicar estos conceptos en la práctica. La aplicación y definición de laseguridad en Symfony2 se hace a través del fichero app/config/security.yml. Ahírealizaremos la configuración de todos los elementos que acabamos de ver para protegernuestra aplicación, es decir, la configuración de los servicios que constituyen el sistema deseguridad.

Nota

En la distribución estándar de Symfony2, el fichero security.yml es incluido desde elfichero de configuración app/config/config.yml. Por lo que también se podríarealizar la configuración de la seguridad directamente en este último archivo.

Los codificadores (encoders)

168

Page 179: CursoSymfony2

Abre el fichero app/config/security.yml que trae como ejemplo la distribución estándardde Symfony2. Échale un vistazo e identifica los conceptos que hemos estudiado en lasección anterior: user providers, firewalls, access control, role hierarchy y encoders. Parapoder comenzar a jugar necesitamos, por lo pronto un proveedor de usuarios. Ya dijimos queSymfony2 proporciona dos tipos; uno basado en el propio fichero de configuración y otro queutiliza una base de datos. Comenzaremos utilizando el primero, denominado en Symfony2:proveedor de usuarios in memory, pues el sistema lee la información de los usuarios delpropio fichero de configuración y la ubica en memoria durante la ejecución del script.Una vez que tengamos varios usuarios en nuestro sistema, mostraremos como proteger losrecursos de nuestra aplicación, utilizando para recoger las credenciales del usuario, primeroel mecanismo de autentificación HTTP Basic y después un formulario HTML. Será este últimoel que utilizaremos finalmente en nuestra aplicación.Después utilizaremos el mecanismo de autorización para proteger ciertas recursos de laaplicación con roles (permisos). También veremos como podemos exigir que el acceso a losrecursos que deseemos se realice desde las IP's, dominios y canal (seguro/inseguro, esto eshttp/https) que deseemos.Finalmente sustituiremos el proveedor de usuarios in memory por una base de datos con lasclaves debidamente codificadas que constituirá el proveedor de usuarios definitivo denuestra aplicación.Al final de la unidad tendremos disponible el sistema de seguridad de nuestra aplicacióndebidamente configurado, habiendo dado un paso más para su acabado definitivo.

El proveedor de usuarios in memoryEste proveedor de usuarios utiliza el más simple de todos los sistemas de información, unarchivo de texto. Concretamente el mismo archivo security.yml.En el ejemplo que viene de serie con la distribución estándar de Symfony2 puedes ver comose utiliza:app/config/security.yml

security: ... providers: in_memory: users: user: { password: userpass, roles: [ 'ROLE_USER' ] } admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } ...

La sintáxis requiere pocas explicaciones. Bajo el item users del proveedor de usuariosin_memory se añaden arrays asociativos cuyas claves son los usernames de los usuario ycuyos valores son otros arrays asociativos con información de las contraseñas y de los roles.Vamos a adaptar este proveedor de usuario a nuestra aplicación. Siguiendo los requisitos dela unidad 5 llegamos a la conclusión de que necesitamos tres roles: usuario registrado,usuario premium y administrador. Vamos a crear un usuario con cada tipo de rol. Nosquedaría lo siguiente:app/config/security.yml

security: ...

El proveedor de usuarios in memory

169

Page 180: CursoSymfony2

providers: in_memory: users: alberto: { password: pruebas, roles: [ 'ROLE_REGISTRADO' ] } maria: { password: pruebas, roles: [ 'ROLE_PREMIUM' ] } miguel: { password: pruebas, roles: [ 'ROLE_ADMIN' ] } ...

Pues ya está. Ya tenemos una fuente de datos para la identificación en nuestra aplicación.Realmente sencilla ¿no?. En efecto es demasiado sencilla. Si el número de usuarios es alto ynecesitamos almacenar más atributos del usuario, este proveedor de usuarios no es el másadecuado. Pero puede ser suficiente para determinadas aplicaciones que no requieranmuchos usuarios y/o no precisen más datos que los nombres de usuarios.Por lo pronto, con objeto de no despistar demasiado, vamos a dejar las contraseñas sincodificar. Más adelante veremos como hacerlo.

Autentificación

Nota

Queremos mostrar en esta unidad como pueden coexistir en un mismo proyecto deSymfony2 varios bundles con sus propios mecanismos de seguridad. Sin embargo,como suele ocurrir en casi cualquier aspecto de la vida, pedir más significa pagar más.En efecto, para evitar colisiones entre las rutas de nuestro bundle y las del que vienede ejemplo con Symfony2, tendremos que anteponer un prefijo (notas, por ejemplo) atodas las rutas de nuestro bundle. Tampoco es que haya que realizar un esfuerzosobrehumano. Basta con colocar en el archivo app/config/routing.yml el prefijonotas en la definición de las rutas del bundle JAMNotasFrontendBundle:

...JAMNotasFrontendBundle: resource: "@JAMNotasFrontendBundle/Resources/config/routing.yml" prefix: /notas...

La cosa es que da un poco de "coraje" tener que alargar la ruta. No obstante, si noquisiéramos mantener funcionando el bundle de ejemplo, no sería necesario estapequeña modificación. Pero, repetimos, la hacemos porque queremos mostrar laenorme escalabilidad y flexibilidad del framework.

Antes de poder configurar los cortafuegos y los controles de acceso, necesitamos un modelopara la seguridad de nuestra aplicación. La tabla siguente recoge qué recursos se debenproteger y qué roles se requieren para accederlos.

Recurso Acceso/notas /notas/conetiqueta/notas/buscar /notas/nota/notas/borrar

Para usuarios registrados, pero los usuarios nopremium tendrán publicidad

Autentificación

170

Page 181: CursoSymfony2

/notas/nueva notas/editar Para usuarios registrados , pero los premiumpodrán subir archivos

/notas/miespacio Para usuarios premiumnotas/rss Público, pero contenidos extras en función del

perfil/notas/registro /notas/activar/notas/tarifas /notas/contratar

Público

/notas/administracion Para administradores del sistema

Por otro lado, los usuario premium tendrán disponibles todas las funcionalidades de losregistrados y algunas extras más. Los administradores, sólo pueden acceder a laadministración de la aplicación.Como ya hemos dicho anteriormente, podemos definir tantos cortafuegos como queramos,cada uno puede utilizar un mecanismo de autentificación distinto, un proveedor de usuariosdistintos, y por supuesto, configuraciones distintas. Lo importante es que no protejan losmismos recursos, pues habría una colisión que se resolvería seleccionando el primercortafuegos que tenga una ruta coincidente con la que se ha solicitado. Observa que es elmismo comportamiento que tiene el servicio Routing.Fíjate que en el archivo app/config/security.yml hay tres cortafuegos definidos (dev,login y secured_area). Son los que vienen de serie con el bundle AcmeDemoBundle. Lo queharemos será añadir otro cortafuegos para las rutas de nuestros bundles. Lo llamaremosjamn_area_protegida.

Nota

Los cortafuegos que vienen de ejemplo no son en absoluto necesarios para nuestraaplicación. Los mantenemos para ilustrar que varios cortafuegos pueden convivir sinningún problema siempre que no colisionen las rutas que protegen.

Los cortafuegos admiten muchos parámetros. Al final de esta sección mostraremos unlistado con todos ellos. No obstante son imprescindibles los siguientes:

• pattern, que mediante una expresión regular expresa las rutas que se van a protegercon dicho cortafuegos,

• Uno de los siguientes: x509, http_basic, http_digest o login_form, para indicar elmecanismo que se utilizará para pedir las credenciales de identificación. A su vez, estosparámetros tienen distintas opciones.

Si hubiera más de un proveedor de usuarios habría que indicarlo con el parámetro provider.Como solo tenemos uno, no es necesario por lo pronto.

Autentificar con HTTP BasicCon todo esto en mente vamos a proponer la configuración del cortafuegos que nos parecemás acertada. Hay que aclarar que hay más de una que resuelve el problema. Añade elsiguiente cortafuegos al fichero security.yml:app/config/security.yml

Autentificar con HTTP Basic

171

Page 182: CursoSymfony2

1 firewalls:2 ...3 4 jamn_area_protegida:5 pattern: ^/notas/6 http_basic:7 realm: "Area protegida"8 ...

Nota

Ten cuidado con las identaciones en este archivo, fíjate que el cortafuegosjamn_area_protegida pertenece a la sección firewalls y que antes hay varioscortafuegos definidos representados por los puntos suspensivos. El itemjamn_area_protegida, debe estar al mismo nivel que los otros cortafuegos quevienen de ejemplo (secured_area, login y dev)

Por lo pronto, hemos protegido todas las rutas de la aplicación. Es decir el parámetropattern es ^/notas/. El carácter ^ al principio indica al patrón que las rutas coincidentesson las que comienzan por /notas/, y no las que contienen /notas.Además hemos utilizado como método de autentificación el básico de HTTP. Si ahoraintentas acceder a cualquier ruta del bundle JAMNotasFrontendBundle el navegador tesolicitará que ingreses un nombre de usuario y una contraseña. Para acceder puedes utilizarcualquiera de los usuarios que tenemos en nuestro proveedor de usuarios.

Nota

Con la autentificación HTTP, una vez que te hayas autentificado el navegadormantendrá el token de autentificación hasta que lo cierres y lo vuelvas a abrir. Tenesto en cuenta cuando hagas pruebas. Puedes entrar en una ruta protegida cuandoesperas que te salga el formulario de autentificación y volverte un poco loco hasta quete das cuenta de que tienes que reiniciar el navegador para cerrar la sesión.

El problema es que de esta manera hemos protegido también las rutas que deben serpúblicas, como por ejemplo /notas/registro/. La solución a este problema consiste enañadir un "anti-cortafuegos", que no es más que otro cortafuegos en el que se indica unpatrón de rutas al que no se debe activar la autentificación:app/config/security.yml

1 firewalls: 2 ... 3 jamn_area_publica: 4 pattern: ^/notas/(registro|activar|tarifas|contratar) 5 security: false 6

Autentificar con HTTP Basic

172

Page 183: CursoSymfony2

7 jamn_area_protegida: 8 pattern: ^/notas/ 9 http_basic:10 realm: "Area protegida"11 ...

El nuevo cortafuegos (líneas 3-5) actúa, como anti-cortafuegos del que le sigue (líneas 7-10),desprotegiendo, es decir, haciendo públicas las rutas

• ^/notas/registro

• ^/notas/activar

• ^/notas/tarifas

• ^/notas/contratarpues son las que coinciden con la expresión regular especificada en su parámetro patterns.El parámetro security: false (línea 5) es el que desactiva la protección de estas rutas.

Importante

Para que esto funcione es imprescindible que el cortafuegos jamn_area_publica sedeclare antes que jamn_area_protegida, pues al ser el patrón del primero másespecífico que el del segundo, si lo colocamos después nunca entraría enfuncionamiento.

Nota

Esta estrategia de proteger todo y, posteriormente, abrir las puerta que se precisen esmuy típica en el mundo de la seguridad y concretamente en la configuración decortafuegos de red reales.

Nota

Cuando hagas pruebas, verás que las rutas que hemos desprotegido arrojan un error,pero es debido a que aún no hemos escrito sus acciones asociadas. No tienen nadaque ver con el cortafuegos. Lee bien el error.

Ahora el problema que tenemos es que cualquier usuario autentificado puede acceder atodos los recursos protegidos de la aplicación. Pero eso no es un problema de laautentificación, sino de la autorización.

Autentificar con un formulario de Login tradicionalAntes de resolver el problema de la autorización, vamos a cambiar el mecanismo de autentificación por un formulario HTML que podremos diseñar a nuestro gusto y que estará

Autentificar con un formulario de Login tradicional

173

Page 184: CursoSymfony2

más integrado con la aplicación.Para ello cambiamos el parámetro http_basic por form_login. Y agregamos laconfiguración mínima que necesita este mecanismos de autentificación para funcionar: unaruta que pinte el formulario de login, y otra ruta a la que enviar las credenciales introducidaspor el usuario en dicho formulario. El fichero security.yml quedaría así:app/config/security.yml

1 firewalls: 2 ... 3 jamn_area_publica: 4 pattern: ^/notas/(demo|registro|activar|tarifas|contratar|login$) 5 security: false 6 7 jamn_area_protegida: 8 pattern: ^/notas/ 9 form_login:10 login_path: /notas/login11 check_path: /notas/login_check12 ...

En las líneas 9-11 se realiza la especificación del formulario de login. Se trata de definir dosrutas: login_path y check_path, la primera debe dirigir a la acción que muestre elformulario de login, y la segunda se utiliza para comprobar las credenciales enviadas pordicho formulario.El mecanismo de autentificación funciona ahora de la siguiente manera: cuando un usuariono autentificado solicita un recurso protegido por el cortafuegos, este le redirige a la URLasociada a la ruta login_path, la cual se encarga de pintar el formulario de login. El atributoaction de este formulario debe corresponderse con la URL de la ruta asociada al parámetrocheck_path. Así pues, los datos facilitados por el usuario son enviados a esta URL, la cual esmuy especial, ya que es el sistema de seguridad de Symfony2 quien se encarga deinterceptarla para llevar a cabo el proceso de autentificación. Por eso no hay que asociarningún controlador a la ruta check_path, tan solo hay que añadirla al fichero routing.yml.Añadimos, por tanto, al fichero de rutas de nuestro bundle las siguientes rutas (lo hacemosal principio del mismo):src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/config/routing.yml

1 jamn_login:2 pattern: /login3 defaults: { _controller: JAMNotasFrontendBundle:Login:login }4 5 jamn_login_check:6 pattern: /login_check

Para que el sistema funcione es muy importante:

1. Que la ruta asociada al parámetro login_path sea pública. En caso contrario el procesoquedaría atrapado en un bucle infinito. Por esta razón se ha añadido a la expresiónregular del pattern del (anti)cortafuegos jamn_area_publica la ruta notas/login(final de la línea 4).

2. Que la ruta asociada al parámetro check_path esté detrás del cortafuegos. En caso contrario el sistema se quejaría diciendo que no hay ningún controlador asociado a la ruta. Por eso se ha añadido el código $ en la expresión regular de la línea 4. Sin ese

Autentificar con un formulario de Login tradicional

174

Page 185: CursoSymfony2

código la ruta notas/login_check también cumpliría la expresión regular y, por tanto,pertenecería al (anti)cortafuegos.

3. Que las variables asociadas a los campos username y password se llamen _username y_password respectivamente (se puede cambiar por configuración estos nombres).

Tan sólo nos queda crear la acción y la plantilla que pinta el formulario de login y que estánasociadas a la ruta jamn_login (es decir, asociada al parámetros del cortafuegoslogin_path). Como puedes ver en la definición de esta ruta, hemos optado por crear laacción de login en un controlador distinto denominado LoginController. El código de estecontrolador quedaría así:Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/LoginController.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Controller; 4 5 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 use Symfony\Component\Security\Core\SecurityContext; 7 8 class LoginController extends Controller 9 {10 11 public function loginAction()12 {13 if ($this->get('request')->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {14 $error = $this->get('request')->attributes->get(SecurityContext::AUTHENTICATION_ERROR);15 } else {16 $error = $this->get('session')->get(SecurityContext::AUTHENTICATION_ERROR);17 }18 19 return $this->render('JAMNotasFrontendBundle:Login:login.html.twig', array(20 'last_username' => $this->get('request')->getSession()->get(SecurityContext::LAST_USERNAME),21 'error' => $error,22 23 ));24 }25 }

Lo único que hace esta acción es comprobar si en la Request existe el atributoSecurityContext::AUTHENTICATION_ERROR. En caso afirmativo se recupera de la propiaRequest un array con los errores de autentificación, y en caso contrario, estos errores serecuperan de la sesión. Dichos errores se pasan, junto con el último nombre de usuarioalmacenado en la sesión, a la plantilla. Obviamente si no hay errores, el array está vacío.La plantilla correspondiente a la acción anterior sería:Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/view/Login/login.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block body %} 4 <h1>Formulario de Login</h1> 5 6 {% if error %} 7 <div class="error">{{ error.message }}</div> 8 {% endif %} 9 10 <form action="{{ path("jamn_login_check") }}" method="post" id="login">11 <div>12 <label for="username">Username</label>13 <input type="text" id="username" name="_username" value="{{ last_username }}" />14 </div>

Autentificar con un formulario de Login tradicional

175

Page 186: CursoSymfony2

15 16 <div>17 <label for="password">Password</label>18 <input type="password" id="password" name="_password" />19 </div>20 21 <input type="submit" class="symfony-button-grey" value="LOGIN" />22 </form>23 {% endblock %}

Observa como las variables asociadas a los campos username y password se llaman_username y _password respectivamente (líneas 13 y 18), y como el atributo action delformulario se corresponde con la ruta apuntada por check_path, es decir;jamnl_login_check (línea 10).Y ya tenemos habilitado un formulario HTML para el proceso de autentificación de nuestraaplicación. En la siguiente unidad mejoraremos este formulario utilizando el servicio deformularios de Symfony2. Así podremos añadir reglas de validación fácilmente a cada uno delos campos. También integraremos el formulario con el diseño de la aplicación. Por lo pronto,para los propósitos de esta unidad, este nos vale tal y como está. Ya puedes probar el nuevomecanismo de autenticación. Recuerda borrar la caché de tu navegador para borrar lasesión.

Saliendo de la sesión (logout)Otro elemento muy útil que se puede definir en los cortafuegos es el mecanismo de cierre desesión (logout). Activarlo es realmente sencillo. Basta con indicar la ruta que realizará ellogout y añadirla al fichero routing.yml. No tenemos que asociar ningún controlador a estaruta puesto que el mecanismo de seguridad de Symfony2 la intercepta y pone en marcha elfin de la sesión.app/config/security.yml

1 firewalls: 2 ... 3 jamn_area_publica: 4 pattern: ^/notas/(demo|registro|activar|tarifas|contratar|login$) 5 security: false 6 7 jamn_area_protegida: 8 pattern: ^/notas 9 form_login:10 login_path: /notas/login11 check_path: /notas/login_check12 logout:13 path: /notas/logout14 target: /notas

Las líneas 12-14 activan el proceso de logout. Se definen dos rutas, la que inicia el proceso(path) y la ruta a la que se redirige la aplicación una vez finalizada la sesión (target).src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/config/routing.yml

1 ...2 jamn_logout:

Saliendo de la sesión (logout)

176

Page 187: CursoSymfony2

3 pattern: /logout4 ...

Ahora podemos cerrar la sesión solicitando la ruta jamn_logout. Lo más adecuado es incluirun enlace a esta ruta en el layout de la aplicación, para que el usuario sepa como puedehacer logout.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/layout-etiquetas-notas.html.twig,

1 ...2 {% block body %}3 <a href="{{path('jamn_logout')}}">Cerrar sesión </a>4 <h1>Implementación de la lógica de control de la aplicación</h1>5 6 ...

AutorizaciónLa autorización se configura en las secciones access_control y role_hierarchy delarchivo security.yml. Vamos a comenzar por definir nuestra jerarquía de roles.Según hemos establecido en el modelo de seguridad: los usuario premium tendrándisponibles todas las funcionalidades de los registrados y algunas extras más. Losadministradores, sólo pueden acceder a la administración de la aplicación.Estos requisitos pueden resolverse de varias formas. La jerarquía de roles nos ayudapermitiendo que el rol ROL_PREMIUM contenga al rol ROL_REGISTRADO.app/config/security.yml

1 ...2 role_hierarchy:3 ROLE_PREMIUM: [ ROLE_REGISTRADO ]4 ...

Con esta configuración los usuarios que dispongan del rol ROLE_PREMIUM también secomportará de cara al proceso de autorización como si tuvieran el rol ROLE_REGISTRADO.Dependiendo de las necesidades de la aplicación respecto a la autorización, nuestrajerarquía de roles puede ser más o menos compleja. En ciertos casos puede que resulteinnecesaria. Pero la verdad es que, incluso para casos sencillos como el de nuestraaplicación, resulta muy util para organizar los permisos.

Protegiendo los recursos de la aplicaciónAhora vamos a utilizar la tabla con el modelo de seguridad que hemos presentado en estamisma unidad para llevar a cabo la configuración del control de accesos. El resultado es elsiguiente:app/config/security.yml

1 ...2 access_control:3 - { path: ^/notas/miespacio, roles: ROLE_PREMIUM }4 - { path: ^/notas/administracion, roles: ROLE_ADMIN }5 - { path: ^/notas, roles: ROLE_REGISTRADO }

Autorización

177

Page 188: CursoSymfony2

Advertencia

El orden en el que hemos puesto las rutas es imprescidible para que nuestro modelode autorización funcione correctamente. Es decir, hay que colocar las rutas de formaque las expresiones regulares vayan desde las más específicas a las más generales,pues si se hace al revés siempre se cumplirá la más general antes que las másespecífica, facilitando la entrada en recursos con los permisos no deseados.

Protegiendo los controladores y plantillas de la aplicaciónEn el modelo de seguridad establecimos que las rutas:

• jamn_homepage

• jamn_conetiqueta

• jamn_buscar

• jamn_nota

• jamn_borrarmostrarían publicidad a los usuarios no premium, y las rutas

• jamn_nueva

• jamn_editarpermitirían a los usuarios premium adjuntar archivos en las notas.Esta mayor granularidad en el acceso a los recursos se puede conseguir mediante el serviciosecurity.context de Symfony2. La manera de comprobar si un usuario tiene undeterminado rol en una acción es:

<?phpuse Symfony\Component\Security\Core\Exception\AccessDeniedException;...

public function holaAction(){ if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { // Aquí el código para el usuario que no tenga el rol, por // ejemplo lanzar una excepción de acceso denegado:

// throw new AccessDeniedException(); }

...}

También podemos poner condiciones de control de acceso en las plantillas twig mediante lafunción is_granted:

Protegiendo los controladores y plantillas de la aplicación

178

Page 189: CursoSymfony2

{% if is_granted('ROLE_ADMIN') %} <a href="...">Delete</a>{% endif %}

Modificando la plantilla layout-etiquetas-notas.html.twig, de esta manera conseguimoscumplir el requisito referente a la presentación de publicidad a usuarios no premium:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/layout-etiquetas-notas.html.twig,

1 ...2 {% block body %}3 4 <h1>Implementación de la lógica de control de la aplicación</h1>5 6 {% if not is_granted('ROLE_PREMIUM') %}7 <h2>PUBLICIDAD AQUÍ</h2>8 {% endif %}9 ...

En la siguiente unidad le daremos la forma definitiva a esta funcionalidad y tambiénincorporaremos la subida de adjuntos según el rol. Por lo pronto valga esto para mostrarcomo se puede proteger el acceso a partes de controladores y plantillas.

Exigiendo canal seguroEn las especificaciones de la aplicación no se decía nada acerca de usar web segura. Sinembargo es una buena idea exigir este requisito, al menos en la parte de administración. Lamanera de hacer esto es añadiéndo el parámetro requires_channel a la ruta delaccess_control que queramos exigir un canal seguro:app/config/security.yml

1 ...2 access_control:3 - { path: ^/miespacio, roles: ROLE_PREMIUM }4 - { path: ^/administracion, roles: ROLE_ADMIN, requires_channel: https }5 - { path: ^/, roles: ROLE_REGISTRADO }

También podemos exigir que se acceda únicamente desde ciertas IP's y/o desde ciertosdominios con los parámetros ip y host respectivamente.

La base de datos como proveedor de usuariosEn esta sección vamos a cambiar el sencillo pero útil proveedor de usuarios in-memory, poruno más sofisticado y completo basado en una clase entidad de Doctrine2 que, por tanto,utiliza una base de datos para comprobar y obtener los datos de los usuarios. Y lo bueno esque todo lo que hemos hecho para crear el mecanismo de autentificación basado en unformulario HTML seguirá siendo válido.Cada proveedor de usuarios tiene asociada una clase User que "sabe" manipular los datosdel usuario en el sistema de información que utilice.

Exigiendo canal seguro

179

Page 190: CursoSymfony2

Nota

Para el proveedor in-memory que hemos estudiado más arriba, esta clase esSymfony\Component\Security\Core\User\User. Si tienes curiosidad puedes verla enel archivovendor/symfony/src/Symfony/Component/Security/Core/User/User.php.

En el caso del proveedor in-memory dicha clase está perfectamente construida y acabada, yni siquiera es necesario conocer su existencia para utilizarlo. Basta con definir correctametesu configuración en el fichero security.yml.Sin embargo, en el caso del proveedor de usuarios basado en entidad, la clase User la tieneque construir el programador. Esto es así puesto que el proveedor de usuarios no conoce apriori como están organizados los datos del usuario en la base de datos en cuestión. Por ello,hay que "enseñarle" para que pueda realizar su función con éxito.Como esta clase entidad, será utilizada internamente en los procesos de autorización yautentificación, los diseñadores del componente de seguridad de Symfony2 han tenido laprecaución de definir una Interfaz de PHP que obligatoriamente debe ser implementada porla entidad que vayamos a utilizar en el proceso.En realidad el componente de seguridad proporciona dos interfaces que podemos utilizarsegún las funcionalidades que requiramos. La más sencilla se denomina UserInterface, yexige la implementación de los siguientes métodos:

• equals($user), método que debe devolver true cuando el argumento sea igual alobjeto this. El criterio de igualdad lo decide el programador siguiendo lascaracterísticas de su modelo de usuarios.

• getPassword(), debe devolver el password del usuario.• getUsername(), debe devolver el username del usuario.• getRoles(), debe devolver un array con los roles del usuario.• getSalt(), debe devolver la salt utilizada para codificar el password del usuario (en

caso de que se haya codificado)• eraseCredentials(), para eliminar datos sensitivos que se puedan almacenar en el

objeto. No suele ser necesario implementar este método.La otra interfaz proporcionada se denomina AdvancedUserInterfaz, y define los mismosmétodos que la anterior más algunos referentes a las condiciones deactivación/desactivación de usuarios.

• isAccountNonExpired(), comprueba si la cuenta del usuario ha expirado.• isAccountNonLocked(), comprueba si la cuenta del usuario ha sido bloqueada.• isCredentialsNonExpired(), comprueba si la credencial (password, por ejemplo) ha

expirado.• isEnabled(), comprueba si el usuario está habilitado.

La semántica de las operaciones anteriores, es decir lo que signifique cuenta no expirada,cuenta bloqueada, credencial no expirada y usuario habilitado, la proporciona elprogramador de la aplicación en función de su modelo de usuarios.Si decidimos utilizar la interfaz avanzada de usuarios (AdvancedUserInterfaz), el proceso de autentificación realizará comprobaciones sobre la activación del usuario utilizando los

Exigiendo canal seguro

180

Page 191: CursoSymfony2

métodos anterioriores. Si utilizamos la interfaz más sencilla UserInterfaz, talescomprobaciones no se llevarán a cabo.Utilizaremos la interfaz avanzada de usuarios para construir nuestra entidad de usuarios. Porun poquito más de trabajo tendremos habilitado un completo sistema de control de nuestrosusuarios.En realidad, la entidad usuario ya la tenemos casi lista. En efecto, podemos utilizar nuestraentidad JAMNotasFrontendBundle:Usuario para conectar al proveedor de usuarios connuestra base de datos. Simplemente debemos hacer que dicha clase implemente la interfazAdvancedUserInterface. Fíjate que muchos de los métodos exigidos por esta última ya lostenemos construidos. Así que solo tendremos que añadir y definir los que nos falten, esdecir:

• getRoles()

• eraseCredentials()

• isAccountNonExpired()

• isAccountNonLocked()

• isCredentialsNonExpired()

• isEnabled()A continuación mostramos como quedaría nuestra entidadJAMNotasFrontendBundle:Usuario tras la implementación de la interfaz.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Entity/Usuario.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Entity; 4 5 use Doctrine\ORM\Mapping as ORM; 6 use Doctrine\Common\Collections\ArrayCollection; 7 use Symfony\Component\Validator\Constraints as Assert; 8 use Symfony\Component\Security\Core\User\UserInterface; 9 use Symfony\Component\Security\Core\User\AdvancedUserInterface;10 11 /**12 * Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario13 *14 * @ORM\Table()15 * @ORM\Entity16 */17 class Usuario implements AdvancedUserInterface18 {19 20 // Aquí todos los métodos ya implementados ...21 22 //// Métodos de la interfaz AdvancedUserInterface que faltan ////23 24 public function eraseCredentials()25 {26 27 }28

Exigiendo canal seguro

181

Page 192: CursoSymfony2

29 function equals(UserInterface $user)30 {31 return $user->getUsername() === $this->username;32 }33 34 public function getRoles()35 {36 $roles = array();37 foreach ($this->grupos as $g)38 {39 $roles[] = $g->getRol();40 }41 42 return $roles;43 }44 45 public function isAccountNonExpired()46 {47 return true;48 }49 50 public function isAccountNonLocked()51 {52 return true;53 }54 55 public function isCredentialsNonExpired()56 {57 return true;58 }59 60 public function isEnabled()61 {62 return $this->getIsActive();63 }64 }

En nuestro modelo cada grupo tiene definido un rol, así que la construcción del array deroles en el método getRoles() es muy sencilla.

Nota

Un modelo más potente y flexible sería aquel en el que los roles están en otra tabla yse asocian con los grupos mediante una tabla intermedia.

Por otro lado, como nuestro modelo no tiene en cuenta ni la caducidad ni el bloqueo decuentas de usuarios, los métodos isAccountNonExpired(), isAccountNonLocked() yisCredentialsNonExpired(), devuelven directamente un valor true. Por último, paranosotroso un usuario habilitado es un usuario activo, así que el método isEnabled()devuelve precisamente el valor del atributo isActive.

Exigiendo canal seguro

182

Page 193: CursoSymfony2

El próximo paso es añadir al fichero security.yml un nuevo proveedor de usuariosasociado a la entidad que acabamos de crear, e indicar en el cortafuegos de nuestraaplicación que deseamos utilizar este nuevo proveedor de usuarios. El fichero security.ymlqueda finalmente así:app/config/security.yml

1 security: 2 encoders: 3 Symfony\Component\Security\Core\User\User: plaintext 4 Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario: plaintext 5 6 role_hierarchy: 7 ROLE_ADMIN: ROLE_USER 8 ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] 9 ROLE_PREMIUM: [ ROLE_REGISTRADO ]10 11 providers:12 in_memory:13 users:14 alberto: { password: pruebas, roles: [ 'ROLE_REGISTRADO' ] }15 maria: { password: pruebas, roles: [ 'ROLE_PREMIUM' ] }16 miguel: { password: pruebas, roles: [ 'ROLE_ADMIN' ] }17 18 base_datos:19 entity: { class: JAMNotasFrontendBundle:Usuario, property: username }20 21 firewalls:22 dev:23 pattern: ^/(_(profiler|wdt)|css|images|js)/24 security: false25 26 login:27 pattern: ^/demo/secured/login$28 security: false29 30 secured_area:31 pattern: ^/demo/secured/32 form_login:33 check_path: /demo/secured/login_check34 login_path: /demo/secured/login35 logout:36 path: /demo/secured/logout37 target: /demo/38 #anonymous: ~39 #http_basic:40 # realm: "Secured Demo Area"41 jamn_area_publica:42 pattern: ^/notas/(demo|registro|activar|tarifas|contratar|login$)43 security: false44 45 jamn_area_protegida:46 pattern: ^/notas47 provider: base_datos48 form_login:

49 login_path: /notas/login50 check_path: /notas/login_check51

Exigiendo canal seguro

183

Page 194: CursoSymfony2

52 logout:53 path: /notas/logout54 target: /notas55 56 access_control:57 - { path: ^/notas/miespacio, roles: ROLE_PREMIUM }58 - { path: ^/notas/administracion, roles: ROLE_ADMIN, requires_channel: https }59 - { path: ^/notas, roles: ROLE_REGISTRADO }

La definición del nuevo proveedor de usuarios basado en la entidad de Doctrine2 se realizaen las líneas 18-19. Como ves simplemente hay que indicar el nombre de la entidad queimplementa la interfaz de usuario. Luego, en la línea 47, se exige al cortafuegosjamn_area_protegida que use como proveedor de usuarios el que acabamos de crear.Observa también que hemos añadido un encoder en la línea 4 para la entidadJAMNotasFrontendBundle:Usuario, ya que es obligatorio definir un encoder para las clasesUser asociadas a los proveedores de identidad. Por lo pronto hemos utilizado el más sencillo,que deja el password sin codificar. En la próxima sección lo cambiaremos por uno másrobusto y explicaremos como utilizarlo.Para que puedas probar el nuevo proveedor de usuarios, tienes que introducir algunosusuarios en la base de datos rellenando, al menos, el campo username y password. Ademásdebes añadir en la tabla grupos los tres registros siguientes:Tabla Grupo

nombre rolregistrado ROLE_REGISTRADOpremium ROLE_PREMIUMadmin ROLE_ADMIN

Y asociar grupos a los usuarios, a través de la tabla usuario_grupo.

Nota

Si no estás utilizando el mecanismos de Fixtures que hemos explicado en los ejercicios de la unidad 7, puedes utilizar el siguiente script SQL parainsertar usuario y grupos.INSERT INTO `Grupo` (`id`, `nombre`, `rol`) VALUES(1, 'registrado', 'ROLE_REGISTRADO'),(2, 'premium', 'ROLE_PREMIUM'),(3, 'administrador', 'ROLE_ADMIN');

INSERT INTO `Usuario` (`id`, `nombre`, `apellidos`, `salt`, `username`, `password`, `email`, `isActive`, `tokenRegistro`) VALUES(1, 'Alberto', 'Einstein', '', 'alberto', 'pruebas', '[email protected]', 1, ''),(2, 'Maria', 'Curie', '', 'maria', 'pruebas', '[email protected]', 1, ''),(3, 'Miguel', 'Faraday', '', 'miguel', 'pruebas', '[email protected]', 1, '');

INSERT INTO `usuario_grupo` (`usuario_id`, `grupo_id`) VALUES(1, 1),(2, 2),(3, 3);

El servicio de codificación. Los encodersLa función de los encoders quedó clara al principio de la unidad. En esta sección veremoscomo utilizarlos en la práctica.Por lo general, los passwords son almacenados en los sistemas de información codificados mediante algún algoritmo hash. Por eso cada proveedor de usuarios debe conocer qué

El servicio de codificación. Los encoders

184

Page 195: CursoSymfony2

algoritmo aplicar al password facilitado por el mecanismo de autentificación para contrastarcon el valor almacenado en el sistema de información. Y esa es precisamente la función delservicio de codificación: especificar el algoritmo que se debe aplicar al password.En la sección encoders del fichero security.yml se debe especificar un algoritmo por cadaproveedor de usuarios utilizado. O más bien dicho, por cada entidad User asociada a losproveedores de usuarios que estemos utilizando.Symfony2 proporciona los algoritmos sha1 y sha512, y se aplican de la siguiente manera:

...encoders: Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario: algorithm: sha1 iterations: 1 encode_as_base64: false...

Nota

El algoritmo puede ser sha1 o sha512.

Ahora, para que los usuarios que definiste en la sección anterior puedan entrar en laaplicación, debes cambiar el campo password por la cadena que resulta de aplicar estealgoritmo a dicho pasword. Por ejemplo, si el password de un usuario era pruebas, ahoradebes cambiarlo por 620a7de82763527406a413ca7ee267816d332811, que es la cadenaresultante de aplicar sha1 a pruebas.Por otro lado, en algún momento habrá que implementar un proceso de registro de usuariosde la aplicación. En ese proceso se le pedirá al usuario que introduzca su password ydebemos codificarlo con este algoritmo. Para realizar tal operación podemos (y debemos)utilizar el propio servicio de codificación en la acción que implemente el registro:

<?php///... en algún lugar de la acción de registro

$factory = $this->get('security.encoder_factory');$user = new Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario();

$encoder = $factory->getEncoder($user);$password = $encoder->encodePassword('pruebas', $user->getSalt());$user->setPassword($password);

En la próxima unidad utilizaremos este snnipet para construir el proceso de registro.

Recuperando el objeto UsuarioUna vez que la authentificación se ha llevado a cabo, podemos acceder a los datos delusuario mediante el objeto User asociado. Desde una acción se puede hacer así:

Recuperando el objeto Usuario

185

Page 196: CursoSymfony2

public function indexAction(){ $user = $this->get('security.context')->getToken()->getUser();}

O incluso de manera más corta:

public function indexAction(){ $user = $this->getUser();}

En una plantilla twig este objeto puede se accedido a través de la clave app.use:

<p>Username: {{ app.user.username }}</p>

Recuerda que hasta ahora siempre mostrábamos las notas del usuario alberto. Ya es horade arreglar esto y recuperar las notas del usuario que inicia sesión. Para lo cual basta concambiar el método dameEtiquetasYNotas() del controlador NotasController.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/NotasController.php

<?php ... protected function dameEtiquetasYNotas() { $session = $this->get('session'); $em = $this->getDoctrine()->getEntityManager();

// Sustituimos esta líneas:

//$usuario = $em->getRepository('JAMNotasFrontendBundle:Usuario') //->findOneByUsername('alberto');

// por esta: $usuario = $this->get('security.context')->getToken()->getUser();

...

La unidad en chuletas

Users providers

security: ... providers: in_memory: users: alberto: { password: pruebas, roles: [ 'ROLE_REGISTRADO' ] } maria: { password: pruebas, roles: [ 'ROLE_PREMIUM' ] } miguel: { password: pruebas, roles: [ 'ROLE_ADMIN' ] }

La unidad en chuletas

186

Page 197: CursoSymfony2

base_datos: entity: { class: JAMNotasFrontendBundle:Usuario, property: username } ...

CortafuegosMecanismo de autentificación HTTP

firewalls: ...

jamn_area_protegida: pattern: ^/notas/ http_basic: realm: "Area protegida" ...

Mecanismo de autentificación con Formulario login

jamn_area_protegida: pattern: ^/notas/ form_login: login_path: /notas/login check_path: /notas/login_check logout: path: /notas/logout target: /notas

Controlador y plantilla para formulario de login

<?php

namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Security\Core\SecurityContext;

class LoginController extends Controller {

public function loginAction() { if ($this->get('request')->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { $error = $this->get('request')->attributes->get(SecurityContext::AUTHENTICATION_ERROR); } else { $error = $this->get('session')->get(SecurityContext::AUTHENTICATION_ERROR); }

return $this->render('JAMNotasFrontendBundle:Login:login.html.twig', array( 'last_username' => $this->get('request')->getSession()->get(SecurityContext::LAST_USERNAME), 'error' => $error,

)); } }

{% extends '::base.html.twig' %}

{% block body %}

Cortafuegos

187

Page 198: CursoSymfony2

<h1>Formulario de Login</h1>

{% if error %} <div class="error">{{ error.message }}</div> {% endif %}

<form action="{{ path("jamn_login_check") }}" method="post" id="login"> <div> <label for="username">Username</label> <input type="text" id="username" name="_username" value="{{ last_username }}" /> </div>

<div> <label for="password">Password</label> <input type="password" id="password" name="_password" /> </div>

<input type="submit" class="symfony-button-grey" value="LOGIN" /> </form> {% endblock %}

Anticortafuego:

jamn_area_publica: pattern: ^/notas/(demo|registro|activar|tarifas|contratar|login$) security: false

AutorizaciónDefinición de roles

role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] ROLE_PREMIUM: [ ROLE_REGISTRADO ]

Protección de recursos

access_control: - { path: ^/notas/miespacio, roles: ROLE_PREMIUM } - { path: ^/notas/administracion, roles: ROLE_ADMIN } - { path: ^/notas, roles: ROLE_REGISTRADO }

Encoders

encoders: Symfony\Component\Security\Core\User\User: plaintext Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario: algorithm: sha1 iterations: 1 encode_as_base64: false

Usar el servicio encoder en una acción

Autorización

188

Page 199: CursoSymfony2

<?php///... en algún lugar de la acción de registro

$factory = $this->get('security.encoder_factory');$user = new Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario();

$encoder = $factory->getEncoder($user);$password = $encoder->encodePassword('pruebas', $user->getSalt());$user->setPassword($password);

referencia oficial del componente securiy

ComentariosAquí puedes enviar comentarios, dudas y sugerencias. Utiliza la barra de scroll para recorrertodos los mensajes. El formulario de envío se encuentra al final.

Autor del código: Juan David Rodríguez García <[email protected]>

Comentarios

189

Page 200: CursoSymfony2

Unidad 10: Desarrollo de la aplicación MentorNotas(VI). Esamblando todo el frontendLlegó la hora de rematar la aplicación que venimos desarrollando desde la unidad 5. Vamosa comenzar haciendo un repaso de lo que ya llevamos hecho y así sabremos lo que aún nosfalta.Por lo pronto la aplicación:

• tiene operativo el inicio de sesión• muestra el listado de etiquetas del usuario que ha iniciado la sesión• muestra el título de las notas de dicho usuario,• cuando se pica en una etiqueta se muestran las notas asociadas a esa etiqueta• tiene operativo el buscador de notas (por título y/o texto)• muestra como detalle de la nota una tabla con el ID y el título de la nota.• muestra unos enlaces para crear, editar y borrar una nota, pero no son completamente

funcionales.Así que aún nos queda:

• hacer funcional la creación, edición y borrado de notas, así como la subida de archivosasociados a notas cuando el usuario sea premium.

• presentar toda la información en el espacio reservado para el detalle de la nota.• implementar el proceso de registro de usuarios,• implementar el proceso de contrato de una cuenta premium,• mostrar publicidad cuando el usuario no sea premium,• adecentar el aspecto gráfico de la aplicación,• crear la parte de administración,

En esta unidad completaremos todos estos puntos a excepción del último, que será resueltoen la siguiente unidad.Podemos atacar cada uno de estos puntos en el orden que queramos. En realidad no haydependencia entre ellos. Así que comenzaremos por poner bonita la aplicación. Eso nos daráanimo para seguir adelante.

La eclosión de la crisálidaEl objetivo de este apartado es cambiar las plantillas twig de la aplicación para adaptarlas ala interfaz gráfica HTML + javascript que se propuso en la unidad 5. Recuerda:

• inicio.html• inicio-editar_nota.html• login.html• registro.html

Para ello fragmentaremos en plantillas twig dichos layouts, identificando sus partes comunesy las que deben pintarse dinámicamente. Utilizando los mecanismos de herencia e inclusiónde las plantillas organizaremos todos estos fragmentos cuidando de que no hayarepeticiones de código innecesarias.

Unidad 10: Desarrollo de la aplicación MentorNotas (VI). Esamblando todo el frontend

190

Page 201: CursoSymfony2

Primero los activos (assets)En primer lugar vamos a colocar los activos en el lugar que le corresponde. Si te acuerdas loque se dijo en la unidad 3, estos deben colocarse en el directorio Resources/public. Losorganizaremos de la siguiente manera:

archivos ui archivos en el pluginui/src/css Resources/public/css

ui/src/js Resources/public/js

ui/src/images Resources/public/images

ui/vendors Resources/public/vendors

Ahora, para tenerlos accesibles directamente desde el servidor web, ejecutamos la siguientetarea:

app/console assets:install --symlink web

Que crea enlaces simbólicos en el directorio web hacia los assets que acabamos de añadir anuestro plugin. Compruébalo.Y ya tenemos todas las librerías de activos que necesitamos para construir la interfazgráfica.

Después las plantillasPara crear un conjunto de plantillas bien organizado que implemente la interfaz propuestaen el tema 5, lo primero que tenemos que hacer es examinar bien la estructuras de dichainterfaz para conocerla lo mejor que podamos. Sólo así seremos capaces de realizar dichaadaptación. Por ello te animamos a que antes de seguir leyendo realices dicho estudio. Esdecir examina el código HTML y procura extraer su estructura. Para realizar esta tarearesultan muy útiles algunas herramientas de desarrollo web como Firebug y Web Developer,ambas plugins muy conocidos de Firefox. Otros navegadores, como Chrome, incorporan deserie algunas herramientas de desarrollo que también ayudan a conocer la estructura de undocumento HTML.¿Termináste el estudio de la estructura HTML?. Muy bien, entonces te habrás dado cuenta deque:

• En la sección head se añaden las CSS's y las librerías javascript que acabamos deincorporar al proyecto como assets. Aquí aparece un primer problema para laadaptación. Resulta que nuestra plantilla base incorpora las CSS's en la sección head,pero coloca los javascripts al final de la sección body. La solución es sencilla; modificarla plantilla base para que los javascripts estén en la sección head. Sin embargo no esnecesario hacer esto. Aunque las librerías se carguen al final de la sección body, lainterfaz seguirá funcionando perfectamente, ya que las funciones javascript de jQueryno comenzarán a ejecutarse hasta que el documento esté completamente cargado (esees el sentido de la sentencia $(document).ready() con la que se inician nuestrosjavascript). Así que dejamos tal cual a la plantilla base.

• Existen dos tipos de interfaces, es decir, pantallas con dos tipos de estructura distintas;las encargadas de mostrar la información sobre las notas,

Primero los activos (assets)

191

Page 202: CursoSymfony2

y las encargadas de mostrar formularios de login y registro, que también servirán paramostrar la pantalla de contrato premium y alguna otra.

Esto nos sugiere que tendremos que componer dos layouts distintos, o dicho de otraforma, dos plantillas twig de nivel 2 en el modelos de herencia de 3 niveles que estamos

aplicando.

Primero los activos (assets)

192

Page 203: CursoSymfony2

• Las pantallas que muestran las notas están estructuradas en 5 zonas: la cabecera (capaui-layout-north), el listado de etiquetas (capa ui-layout-west), el listado de notas(capa ui-layout-center), el detalle de la nota, que también se usa para la edición delas notas (capa ui-layout-east) y el pie (capa ui-layout-south). Todas estas capasson interpretadas por la librería javascript jquery.layout.min-1.2.0.js la cualenriquece el aspecto de la página colocando el contenido ordenadamente en trescolumnas con cabecera y pie.

• De todas las zonas anteriores, la única que cambia de aspecto es la que muestra eldetalle de la nota, que también se utiliza para mostrar el formulario de creación y/oedición de una nota.

Comenzaremos por la adaptación de las pantallas que muestran notas, más complejas einteresantes y que representan la parte nuclear de la aplicación. Llamaremos a esta parte elpanel de notas.

Adaptación de las pantallas del panel de notasComo todas las acciones relacionadas con el panel de notas siempre pintan 4 de las 5 zonasexactamente igual (cuidado, con igual nos referimos a estructuralmente igual, obviamentelos datos dinámicos cambiarán en función de la interacción del usuario), podemos colocar enla plantilla layout-etiquetas-notas.twig.html todas estas zonas comunes más losjavascripts y CSS's.

El layout del panel de notassrc/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/layout-etiquetas-notas.twig.html: 1 {% extends '::base.html.twig' %} 2 3 {% block stylesheets%} 4 <link href="{{ asset('bundles/jamnotasfrontend/vendors/jquery/css/ui-lightness/jquery-ui-1.8.17.custom.css') }}" type="text/css" rel="stylesheet" /> 5 <link href="{{ asset('bundles/jamnotasfrontend/vendors/jquery-layout/css/layout-default-latest.css') }}" type="text/css" rel="stylesheet" /> 6 <link href="{{ asset('bundles/jamnotasfrontend/css/ui.css') }}" type="text/css" rel="stylesheet" /> 7 {% endblock%} 8 9 {% block javascripts %}10 <script src="{{ asset('bundles/jamnotasfrontend/vendors/jquery/js/jquery-1.7.1.min.js') }}" type="text/javascript"></script>11 <script src="{{ asset('bundles/jamnotasfrontend/vendors/jquery/js/jquery-ui-1.8.17.custom.min.js') }}" type="text/javascript"></script>12 <script src="{{ asset('bundles/jamnotasfrontend/vendors/jquery-layout/js/jquery.layout.min-1.2.0.js') }}" type="text/javascript"></script>13 <script src="{{ asset('bundles/jamnotasfrontend/js/ui.js') }}" type="text/javascript"></script>14 {% endblock %}15 16 {% block body %}17 18 {% include 'JAMNotasFrontendBundle:Notas:cabecera.html.twig' %}19 20 {% include 'JAMNotasFrontendBundle:Notas:etiquetas.html.twig' %}21 22 {% include 'JAMNotasFrontendBundle:Notas:notas.html.twig' %}23 24 {% include 'JAMNotasFrontendBundle:Notas:pie.html.twig' %}25 26 {% block detalle_y_edicion %} {% endblock %}27 28 {% endblock %}

En esta plantilla hemos redefinido los bloques styleheets y javascripts de la plantillapadre (::base.html.twig) añadiendo los assets del proyecto, y también se ha redefinido elbloque body. Dentro de este último bloque deben ir las cinco capas de las que venimoshablando, cuatro de ellas fijas y una que cambia según estemos viendo una nota oeditándola. Por tanto se trataría de copiar aquí el código de las capas ui-layout-north,ui-layout-west, ui-layout-center y ui-layout-south y adaptar el contenido dinámicoque viene de las acciones. Esto es lo que hemos hecho en las líneas 18-24 mediante elmecanismo de inclusión. De esta manera la plantilla muestra más claramente la estructura ycompondremos el código concreto de cada zona en cada una de las plantillas:

• JAMNotasFrontendBundle:Notas:cabecera.html.twig,• JAMNotasFrontendBundle:Notas:etiquetas.html.twig,• JAMNotasFrontendBundle:Notas:notas.html.twig,

Adaptación de las pantallas del panel de notas

193

Page 204: CursoSymfony2

• JAMNotasFrontendBundle:Notas:pie.html.twigLas acciones del panel de notas de nuestra aplicación pintarán sus datos utilizando plantillasque hereden de esta última y que implementen el bloque detalle_y_edicion. Esta solucióncumple el principio DRY (Don't Repeat Yourself), además de mostrar con claridad laestructura de las pantallas.A continuación mostramos el código de cada una de las plantillas incluidas porlayout-etiquetas-notas.twig.html.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/cabecera.html.twig 1 <div class="ui-layout-north ui-widget-content" > 2 3 <div class="banner"> 4 {% if not is_granted('ROLE_PREMIUM') %} 5 <span style="font-size: xx-large"> 6 PUBLICIDAD 7 </span> 8 <a class="ui-button usuario-tool premium ui-corner-all" href="{{ path('jamn_contratar') }}"> 9 contrata premium, elimina la publicidad y obten numerosas ventajas!10 </a>11 {% else %}12 <span style="font-size: xx-large">13 BOTONERA PREMIUM14 </span>15 <a class="ui-button usuario-tool premium ui-corner-all" href="{{ path('jamn_espacio_premium') }}">16 Mi espacio premium17 </a>18 {% endif %}19 </div>20 21 <div class="logo">22 <span>MentorNotas</span>23 <img style="float:left;" height="60" src="{{ asset('bundles/jamnotasfrontend/images/mentor.png')}}"></img>24 25 </div>26 27 <div class="signout">28 <span class="usuario-nombre">{{ app.user.nombre }} {{app.user.apellidos }}</span>29 <a id="btn_salir" class="ui-button ui-button-text-icon-primary usuario-tool salir ui-corner-all" href="{{path('jamn_logout')}}">30 <span class="ui-button-icon-primary ui-icon ui-icon-power"></span>31 <span class="ui-button-text">salir</span></a>32 </div>33 34 </div>

Esta plantilla muestra la publicidad o las carácteristicas premium y un enlace para contrataruna tarifa premium o acceder al espacio premium, según el tipo de usuario, el logo deMentor, el nombre del usuario que ha iniciado la sesión y el botón de salir. Observa laincorporación de estos elementos dinámicos en las líneas 4-18, 15, 23, 28 y 29. Prestaespecial atención al uso que hemos hecho de la función twig is_granted() para decidir sipintamos publicidad o características premium.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/etiquetas.html.twig

1 <div class="ui-layout-west etiquetas"> 2 <h3 class="ui-widget-header">Etiquetas</h3> 3 <div class="ui-layout-content ui-widget-content"> 4 <ul> 5 <li> 6 <span class="ui-icon ui-icon-star" style="float:left;"></span> 7 <a href="{{ path("jamn_conetiqueta", {"etiqueta": 'todas'}) }}"> todas las notas</a> 8 </li> 9 {% for etiqueta in etiquetas %}10 <li>11 <span class="ui-icon ui-icon-tag" style="float:left;"></span>12 <a href="{{ path("jamn_conetiqueta", {"etiqueta": etiqueta.id}) }}">{{ etiqueta.texto }}</a>13 </li>14 {% endfor%}15 </ul>16 </div>17 </div>

Esta plantilla muestra un listado de enlaces de etiquetas (líneas 9-14). También pinta unenlace especial (línea 7) que se utiliza para mostrar todas las notas.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/notas.html.twig

Adaptación de las pantallas del panel de notas

194

Page 205: CursoSymfony2

1 <div class="ui-layout-center"> 2 3 <h3 class="ui-widget-header">Listado de notas</h3> 4 5 6 <div class="ui-widget my-widget"> 7 <div class="ui-widget-content my-widget-content"> 8 9 <form id="form_buscar" action="{{ path("jamn_buscar") }}" method="post">10 <input class="buscar ui-corner-all" size="20" name="termino" id="termino"/>11 <button type=submit" class="btn_buscar"> Buscar</button>12 </form>13 </div>14 </div>15 16 <div class="separador"></div>17 18 19 <div class="ui-layout-content ui-widget-content">20 <div class="ui-widget-content my-widget-content">21 22 {% for nota in notas %}23 {% if nota.id == nota_seleccionada.id %}24 <div class="nota seleccionada">25 {% else %}26 <div class="nota">27 {% endif %}28 <a href="{{ path('jamn_nota',{"id": nota.id}) }}">29 <div class="nota-container">30 <div class="nota-titulo">31 {{ nota.titulo }}32 </div>33 <div class="nota-content">34 <span class="nota-fecha"> {{ nota.fecha.date }}</span>35 {{ nota.texto|raw }}36 37 </div>38 </div>39 </a>40 </div>41 42 {% else %}43 <div class="ui-state-highlight">44 <span class="ui-icon ui-icon-alert" style="float:left;"></span>45 <span style="padding: 0.5em;"> No se han encontrado notas</span></div>46 {% endfor %}47 48 </div>49 </div>50 51 </div>52 53 </div>

Esta pantalla pinta un formulario para buscar notas y un listado de notas. El primero se pintaen las líneas 9-12 y el segundo en las líneas 22-46. Dos detalles importantes a tener encuenta en la implementación del listado de notas. En primer lugar, para poder resaltar lanota seleccionada, se hace una comparación del id de la nota que se está pintando con el idde la nota seleccionada. Si coincide se usa la clase CSS nota_seleccionada en lugar denota. En segundo lugar, fíjate en el {% else %} de la línea 42, ¡no tiene su {% if %}correspondiente!. Esto, aunque lo parezca, no es un error, es una característica del lenguajetwig. El tema es que ese {% else %} forma parte del bucle {% for nota in notas %}, ysignifica que si no hay ninguna nota en la colección notas se ejecute lo que viene despuésdel {% else %}, es decir, que se muestre un mensaje informando de ello.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/pie.html.twig

1 <div class="ui-layout-south ui-widget-content"> 2 <div class="pie-de-pagina">

Adaptación de las pantallas del panel de notas

195

Page 206: CursoSymfony2

3 <div class="cr"> 4 © 2012, Juan David Rodríguez 5 </div> 6 <div class="powered"> 7 <span style="font-style: italic">powered by</span> 8 <img height="30" src="{{ asset("bundles/jamnotasfrontend/images/php-logo.jpg") }}"/> 9 <img height="30" src="{{ asset("bundles/jamnotasfrontend/images/logo.png") }} "/>10 <img height="30" src="{{ asset("bundles/jamnotasfrontend/images/jquery_logo.png") }} "/>11 12 </div>13 </div>14 </div>

Por último esta plantilla muestra información acerca del copyright y de las tecnologías quese han utilizado en la confección de la aplicación, es decir, lo típico de un pie de página.

Las acciones del panel de notasY ahora vamos a por las acciones. Ya sabemos la filosofía: las acciones del panel de notaspintarán sus datos sobre plantillas que hereden de layout-etiquetas-notas.html.twig, yque redefinirán el bloque detalle_y_edicion. Comencemos por la acción indexAction()que pintará sus datos sobre la plantilla index.html.twig.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/NotasController.php

1 <?php 2 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Controller; 3 4 use Symfony\Bundle\FrameworkBundle\Controller\Controller; 5 use Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Nota; 6 7 class NotasController extends Controller 8 { 9 10 public function indexAction()11 {12 $request = $this->getRequest(); // equivalente a $this->get('request');13 $session = $this->get('session');14 15 $ruta = $request->get('_route');16 17 switch ($ruta)18 {19 case 'jamn_homepage':20 21 break;22 23 case 'jamn_conetiqueta':24 $session->set('busqueda.tipo', 'por_etiqueta');25 $session->set('busqueda.valor', $request->get('etiqueta'));26 $session->set('nota.seleccionada.id', '');27 28 break;29 30 case 'jamn_buscar':31 $session->set('busqueda.tipo', 'por_termino');32 $session->set('busqueda.valor', $request->get('termino'));33 $session->set('nota.seleccionada.id', '');

Las acciones del panel de notas

196

Page 207: CursoSymfony2

34 35 break;36 case 'jamn_nota':37 $session->set('nota.seleccionada.id', $request->get('id'));38 break;39 }40 41 list($etiquetas, $notas, $notaSeleccionada) = $this->dameEtiquetasYNotas();42 43 // creamos un formulario para borrar la nota44 if ($notaSeleccionada instanceof Nota) {45 $deleteForm = $this->createDeleteForm($notaSeleccionada->getId())->createView();46 } else {47 $deleteForm = null;48 }49 50 return $this->render('JAMNotasFrontendBundle:Notas:index.html.twig', array(51 'etiquetas' => $etiquetas,52 'notas' => $notas,53 'nota_seleccionada' => $notaSeleccionada,54 'delete_form' => $deleteForm,55 ));56 }57 58 ...

En esencia es la misma acción que ya habíamos implementado a lo largo del curso. Ladiferencia es que hemos añadido un nuevo elemento que pasamos a la plantilla: unformulario para borrar la nota seleccionada (líneas 44-48). Dicho formulario es creado poruna función auxiliar que debes añadir al código del NotasController:

1 <?php 2 ... 3 protected function createDeleteForm($id) 4 { 5 return $this->createFormBuilder(array('id' => $id)) 6 ->add('id', 'hidden') 7 ->getForm() 8 ; 9 }10 ...

Por último tenemos que reescribir la acción borrarAction. Recuerda que el código quehabíamos escrito para dicha acción no borraba nada, tan solo avisaba de que en un futurotendría que arreglarse. Pues el futuro ya está aquí:

1 <?php 2 ... 3 public function borrarAction() 4 { 5 $request = $this->getRequest(); 6 $session = $this->get('session'); 7 $form = $this->createDeleteForm($request->get('id')); 8 9 $form->bindRequest($request);10 11 if ($form->isValid()) {12 $em = $this->getDoctrine()->getEntityManager();

Las acciones del panel de notas

197

Page 208: CursoSymfony2

13 $entity = $em->getRepository('JAMNotasFrontendBundle:Nota')->find($request->get('id'));14 15 if (!$entity) {16 throw $this->createNotFoundException('Esa nota no existe.');17 }18 19 $em->remove($entity);20 $em->flush();21 22 $session->set('nota.seleccionada.id','');23 }24 ...25 26 27 28 return $this->redirect($this->generateUrl('jamn_homepage'));29 }

Además, para que la plantilla JAMNotasFrontendBundle:Notas:index.html.twig funcionecorrectamente hay que modificar ligeramente la función del NotasControllerdameEtiquetasYNotas la cual queda así:

1 <?php 2 ... 3 protected function dameEtiquetasYNotas() 4 { 5 $session = $this->get('session'); 6 $em = $this->getDoctrine()->getEntityManager(); 7 8 $usuario = $this->get('security.context')->getToken()->getUser(); 9 10 11 $busqueda_tipo = $session->get('busqueda.tipo');12 13 $busqueda_valor = $session->get('busqueda.valor');14 15 // Etiquetas. Se pillan todas16 $etiquetas = $em->getRepository('JAMNotasFrontendBundle:Etiqueta')->17 findByUsuarioOrderedByTexto($usuario);18 19 // Notas. Se pillan según el filtro almacenado en la sesión20 if ($busqueda_tipo == 'por_etiqueta' && $busqueda_valor != 'todas') {21 $notas = $em->getRepository('JAMNotasFrontendBundle:Nota')->22 findByUsuarioAndEtiqueta($usuario, $busqueda_valor);23 } elseif ($busqueda_tipo == 'por_termino') {24 $notas = $em->getRepository('JAMNotasFrontendBundle:Nota')->25 findByUsuarioAndTermino($usuario, $busqueda_valor);26 } else {27 $notas = $em->getRepository('JAMNotasFrontendBundle:Nota')->28 findByUsuarioOrderedByFecha($usuario);29 }30 31 $nota_seleccionada = null;32 if (count($notas) > 0) {33 $nota_selecionada_id = $session->get('nota.seleccionada.id');34 if (!is_null($nota_selecionada_id) && $nota_selecionada_id != '') {35 $nota_seleccionada = $em->getRepository('JAMNotasFrontendBundle:Nota')->

36 findOneById($nota_selecionada_id);37 } else {38 $nota_seleccionada = $notas[0];39 }

Las acciones del panel de notas

198

Page 209: CursoSymfony2

40 }41 42 return array($etiquetas, $notas, $nota_seleccionada);43 }

La modificación consiste en las líneas 32-40. En ellas se elige como nota seleccionada la quevenga en la sesión, y la primera de la lista en el caso de que en la sesión no esté disponibleeste dato. Finalmente si no hay ninguna nota en la colección de notas, se devuelve un valornulo.Finalmente, el código de la plantilla queda:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/index.html.twig

1 {% extends 'JAMNotasFrontendBundle:Notas:layout-etiquetas-notas.html.twig' %} 2 3 {% block detalle_y_edicion %} 4 5 <div class="ui-layout-east"> 6 <h3 class="ui-widget-header">Nota</h3> 7 8 {% if nota_seleccionada %} 9 10 <div id="confirma-borrado"></div>11 12 <div class="ui-widget my-widget">13 <div class="ui-widget-content my-widget-content">14 <span class="my-buttonset-left">15 <a id="btn_crear" href="{{ path('jamn_nueva')}}">crear</a>16 </span>17 18 <span class="my-buttonset-right">19 <a id="btn_editar" href="{{ path('jamn_editar', {'id': nota_seleccionada.id }) }}">editar</a>20 <button id="btn_borrar" type="submit">Borrar</button>21 <form id="form_borrar" action="{{ path('jamn_borrar', { 'id': nota_seleccionada.id }) }}" method="post">22 {{ form_widget(delete_form) }}23 </form>24 </span>25 </div>26 </div>27 28 <div class="separador"></div>29 30 <div class="ui-layout-content ui-widget-content">31 <div class="ui-widget-content my-widget-content">32 <div id="nota-detalle">33 34 <div class="nota-detalle-titulo">35 {{ nota_seleccionada.titulo }}36 </div>37 38 {% if nota_seleccionada.path %}39 <div class="nota-detalle-adjunto">40 <span class="ui-icon ui-icon-disk" style="float: left;"></span>

41 {% set urlFile = asset('uploads') ~ '/' ~ app.user.getUsername ~ '-' ~ nota_seleccionada.path %}42 <a href="{{ urlFile }}">{{ nota_seleccionada.nombreFichero}}</a>43 44 </div>45 {% endif %}46 47 <div class="nota-detalle-etiquetas">48 {% for etiqueta in nota_seleccionada.etiquetas %}49 <span class="nota-detalle-etiqueta ui-corner-all">{{ etiqueta.texto }}</span>50 {% endfor %}51 52 </div>53 <div class="nota-detalle-content">54 {{ nota_seleccionada.texto|raw }}

55 </div>56 </div>57 </div>58 </div>

Las acciones del panel de notas

199

Page 210: CursoSymfony2

59 {% else %}60 <div class="ui-widget my-widget">61 <div class="ui-widget-content my-widget-content">62 <span class="my-buttonset-left">63 <a id="btn_crear" href="{{ path('jamn_nueva')}}">crear</a>64 </span>65 </div>66 </div>67 {% endif %}68 </div>69 70 {% endblock %}

En esta plantilla, si existe la nota seleccionada, se pinta una barra de botones con lasacciones crear, editar y borrar (líneas 12-26). También se pinta un formulario paraborrar la nota que es activado mediante javascript por el botón borrar (líneas 21-23). Acontinuación se pintan los detalles de la nota: el título (línea 35), el archivo adjunto, si lotiene (líneas 38-45), las etiquetas de la nota (líneas 47-52), el texto (líneas 53-55). Si la notaseleccionada no existe, simplemente se pinta un botón para crear una nota nueva (líneas59-67).Ya puedes probar la acción indexAction es decir

http://localhost/Symfony/web/app_dev.php/notas

Comprueba que ya funciona la búsqueda por etiquetas y términos, las selección de notas enel listado de notas y el borrado de la nota seleccionada.Ahora vamos a por la creación y la edición de notas. Recuerda que las accionesnuevaAction() y editarAction() ya estaban implementadas, pero al igual que sucedíacon la acción borrarAction(), no funcionaban adecuadamente. Ahora vamos a darles suforma definitiva.Pero antes vamos a componer las plantillas correspondientes a estas acciones. Tanto en elcaso de la edición como de la creación, el único cambio se producirá en la zona del detallede la nota, que será sustituida por un formulario de creación y/o edición. Por tanto lasplantillas para estas dos acciones heredarán de la plantillalayout-etiquetas-notas.html.twig. Esa es la idea que hemos desarrollado más arriba.Nada nuevo hasta ahora.Si has analizado con detalle la interfaz de usuario propuesta en la unidad 5, el formulario deedición/creación contiene:

• Un editor enriquecido• Un etiquetador• Una botonera con los botones hecho, cancelar y borrar, para el formulario de edición

y hecho y cancelar para el formulario de creación.Estos elementos son enriquecidos mediantes nuevas librerías javascript. Así que lasplantillas que hagamos tendrán que redefinir los bloques javascripts y stylesheets paraañadir dichas librerías y sus CSS's correspondientes.Por otro lado, el formulario de edición y de creación, son muy similares pero presentandiferencias suficientes como para plantearse crear dos plantillas distintas para cada uno deellos. Lo que haremos, aludiendo al principio DRY, es componer una plantilla con loselementos comunes que será utilizada por ambas acciones. A dicha plantilla añadiremos lalógica necesario para que incluya el formulario de creación o edición según el caso.Veámoslo en la práctica.

Las acciones del panel de notas

200

Page 211: CursoSymfony2

La plantilla crearOEditar.html.twig será la plantilla común:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/index.html.twig 1 {% extends 'JAMNotasFrontendBundle:Notas:layout-etiquetas-notas.html.twig' %} 2 3 {% block javascripts%} 4 5 {{ parent() }} 6 <script src="{{ asset('bundles/jamnotasfrontend/vendors/CLEditor1_3_0/jquery.cleditor.min.js') }}" type="text/javascript"></script> 7 <script src="{{ asset('bundles/jamnotasfrontend/vendors/tagit/js/tag-it.js') }}" type="text/javascript"></script> 8 9 <script>10 $(document).ready( function() {11 var tituloBox = $("#nota_titulo");12 13 $( "#btn_hecho_crear" ).button({14 icons: {15 primary: "ui-icon-disk"16 }17 });18 19 $("#btn_hecho_crear").click(function(){20 $("#form_crear_nota").submit();21 });22 23 $( "#btn_hecho_editar" ).button({24 icons: {25 primary: "ui-icon-disk"26 }27 });28 29 $("#btn_hecho_editar").click(function(){30 $("#form_editar_nota").submit();31 });32 33 $( "#btn_cancelar" ).button({34 icons: {35 primary: "ui-icon-cancel"36 }37 });38 39 40 41 $("#nota_etiquetas").tagit({42 availableTags: [{%for etiqueta in etiquetas%} "{{ etiqueta.texto}}", {% endfor %}],43 44 });45 46 47 tituloBox.focus(function(){48 if($(this).attr("value") == tituloBox1Default) $(this).attr("value", "");49 $(this).addClass("nota-titulo-activo");50 });51 52 tituloBox.blur(function(){53 $(this).removeClass("nota-titulo-activo");54 55 56 });57 $("#nota_texto").cleditor({58 controls: "bold italic underline strikethrough subscript superscript | font size " +59 "style | color highlight removeformat | bullets numbering | outdent " +60 "indent | alignleft center alignright justify | undo redo | " +61 "rule image link unlink | print source"62 });63 })64 65 </script>66 {% endblock%}67 68 {% block stylesheets %}69 {{ parent() }}70 <link rel="stylesheet" type="text/css" href="{{ asset('bundles/jamnotasfrontend/vendors/CLEditor1_3_0/jquery.cleditor.css') }}" />71 <link rel="stylesheet" type="text/css" href="{{ asset('bundles/jamnotasfrontend/vendors/tagit/css/jquery.ui.autocomplete.custom.css') }}" />72 <link rel="stylesheet" type="text/css" href="{{ asset('bundles/jamnotasfrontend/vendors/tagit/css/jquery.tagit.css') }}" />73 74 {% endblock %}75 76 77 {% block detalle_y_edicion %}78 79 {% if edita %}80 {% include 'JAMNotasFrontendBundle:Notas:editar.html.twig' %}81 {% else %}82 {% include 'JAMNotasFrontendBundle:Notas:crear.html.twig' %}83 {% endif %}84 85 86 87 {% endblock %}

Observa la inclusión de las nuevas librerías javascript (líneas 6-7), del script utilizado paraenriquecer el formulario (líneas 9-64) y la inclusión de las nuevas CSS's (líneas 69-71). Porúltimo observa la lógica introducida en el bloque detalle_y_edicion, en función de laexistencia o no de una variable denominada edita, se incluirá la plantilla para el formulariode edición o de creación. Veámos el código de estos últimos.

Las acciones del panel de notas

201

Page 212: CursoSymfony2

src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/crear.html.twig 1 <div class="ui-layout-east"> 2 <h3 class="ui-widget-header">Crear Nota</h3> 3 4 <div id="confirma-borrado"></div> 5 6 <div class="ui-widget my-widget"> 7 <div class="ui-widget-content my-widget-content"> 8 <span class="my-buttonset-left"> 9 <button id="btn_hecho_crear">hecho</button>10 </span>11 12 <span class="my-buttonset-right">13 <a id="btn_cancelar" href="{{ path('jamn_homepage') }}">cancelar</a>14 </span>15 </div>16 </div>17 18 <div class="separador"></div>19 20 <div class="ui-layout-content ui-widget-content">21 <div class="ui-widget-content my-widget-content">22 23 <form id="form_crear_nota" action="{{ path('jamn_nueva') }}" method="post" {{ form_enctype(new_form) }}>24 25 {% if form_errors(new_form) %}26 <div class="ui-state-error">{{ form_errors(new_form) }}</div>27 {% endif %}28 29 <div class="row">30 <div style="font-size: small">Título:</div>31 {% if form_errors(new_form.titulo) %}32 <div style="width: 300px;" class="ui-state-error">{{ form_errors(new_form.titulo) }}</div>33 {% endif %}34 35 {{ form_widget(new_form.titulo, {'attr': {'class': 'nota-titulo ui-corner-all', 'style': 'width:300px;'} }) }}36 </div>37 38 <div class="row">39 <span style="font-size: small">Etiquetas:</span>40 41 42 <ul id="nota_etiquetas">43 </ul>44 </div>45 46 <div class="row">47 {% if form_errors(new_form.texto) %}48 <div class="ui-state-error">{{ form_errors(new_form.texto) }}</div>49 {% endif %}50 {{ form_widget(new_form.texto) }}51 52 </div>53 54 {% if is_granted('ROLE_PREMIUM') %}55 <div class="row">56 <div style="font-size: small">Cambiar Fichero:</div>57 {% if form_errors(new_form.file) %}58 <div class="ui-state-error">{{ form_errors(new_form.file) }}</div>59 {% endif %}60 {{ form_widget(new_form.file) }}61 {% endif %}62 </div>63 64 {{ form_widget(new_form._token) }}65 66 </form>67 </div>68 </div>69 </div>

Y el de edición:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Notas/editar.html.twig

1 <div class="ui-layout-east"> 2 <h3 class="ui-widget-header">Editar Nota</h3> 3 4 <div id="confirma-borrado"></div> 5 6 <div class="ui-widget my-widget"> 7 <div class="ui-widget-content my-widget-content"> 8 <span class="my-buttonset-left"> 9 <button id="btn_hecho_editar">hecho</button>

Las acciones del panel de notas

202

Page 213: CursoSymfony2

10 </span>11 12 <span class="my-buttonset-right">13 <a id="btn_cancelar" href="{{ path('jamn_nota', {'id': nota_seleccionada.id}) }}">cancelar</a>14 <button id="btn_borrar" type="submit">Borrar</button>15 <form id="form_borrar" action="{{ path('jamn_borrar', { 'id': nota_seleccionada.id }) }}" method="post">16 {{ form_widget(delete_form) }}17 </form>18 </span>19 </div>20 </div>21 22 <div class="separador"></div>23 24 <div class="ui-layout-content ui-widget-content">25 <div class="ui-widget-content my-widget-content">26 27 <form id="form_editar_nota" action="{{ path('jamn_editar', { 'id': nota_seleccionada.id }) }}" method="post" {{ form_enctype(edit_form) }}>28 29 {% if form_errors(edit_form) %}30 <div class="ui-state-error">{{ form_errors(edit_form) }}</div>31 {% endif %}32 33 <div class="row">34 <div style="font-size: small">Título:</div>35 {% if form_errors(edit_form.titulo) %}36 <div style="width: 300px;" class="ui-state-error">{{ form_errors(edit_form.titulo) }}</div>37 {% endif %}38 39 {{ form_widget(edit_form.titulo, {'attr': {'class': 'nota-titulo ui-corner-all', 'style': 'width:300px;'} }) }}40 </div>41 42 <div class="row">43 <span style="font-size: small">Etiquetas:</span>44 45 46 <ul id="nota_etiquetas">47 {%for etiqueta in nota_seleccionada.etiquetas %}48 <li>{{ etiqueta.texto }}</li>49 {% endfor %}50 </ul>51 </div>52 53 <div class="row">54 {% if form_errors(edit_form.texto) %}55 <div class="ui-state-error">{{ form_errors(edit_form.texto) }}</div>56 {% endif %}57 {{ form_widget(edit_form.texto) }}58 59 </div>60 61 <div class="row">62 <div>63 <div style="font-size: small">Fichero:</div>64 65 {% if nota_seleccionada.path %}66 {% set urlFile = asset( nota_seleccionada.webPath(app.user.getUsername) ) %}67 <a href="{{ urlFile }}">{{ nota_seleccionada.path}}</a>68 {% endif %}69 </div>70 </div>71 72 {% if is_granted('ROLE_PREMIUM') %}73 <div class="row">74 <div style="font-size: small">Cambiar Fichero:</div>75 {% if form_errors(edit_form.file) %}76 <div class="ui-state-error">{{ form_errors(edit_form.file) }}</div>77 {% endif %}78 {{ form_widget(edit_form.file) }}79 {% endif %}80 </div>81 82 {{ form_widget(edit_form._token) }}83 84 </form>85 </div>86 </div>87 </div>

En ambas plantillas se utiliza un formulario que tiene los siguientes campos:

• título• texto• file

Este formulario se corresponde con el tipo NotaType, que ya hemos implementado en losejercicios de la unidad 8. Bueno, en realidad hay que añadirle el campo texto:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Form/Type/NotaType

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Form\Type; 4

Las acciones del panel de notas

203

Page 214: CursoSymfony2

5 use Symfony\Component\Form\AbstractType; 6 use Symfony\Component\Form\FormBuilder; 7 8 class NotaType extends AbstractType 9 {10 11 public function buildForm(FormBuilder $builder, array $options)12 {13 $builder->add('titulo', 'text')14 ->add('texto', 'textarea')15 ->add('file', 'file');16 }17 18 public function getName()19 {20 return 'nota';21 }22 23 // Esto no es siempre necesario, pero para construir formularios embebidos24 // es imprescindibles, así que no cuesta nada acostumbrarse a ponerlo25 public function getDefaultOptions(array $options)26 {27 return array(28 'data_class' => 'Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Nota',29 );30 }31 32 }

Otro elemento a destacar es el formulario de borrado de notas que aparece en las líneas14-17 de la plantilla de edición. Fíjate que es la misma cosa que ya hicimos en la plantillaindex.html.twig.Veámos ahora como se tratan en ambos casos la gestión de etiquetas para cada nota que secrea o edita con estos formularios. En las líneas 46-50 de la plantilla de edición (42-43 de laplantilla de creación), se añade una lista desordenada (<ul></ul>) con las etiquetas de cadanota. Esta lista es enriquecida mediante la librería jQuery tag-it.js, la cual se encarga deconvertir el listado en un elegante etiquetador. Cuando se envía el formulario al servidor, lapropia librería se encarga de pasar la colección de etiquetas que se hayan creado en unavariable de la request denominada item. De esa manera, la acción correspondiente podrárecuperarla y asociar adecuadamente dichas etiquetas a la nota que se está creando oeditando.Por último observa como la subidad de ficheros (al final de ambas plantillas), que se hace taly como hemos estudiado en los ejercicios de la unidad 8, se habilita únicamente cuando elusuario es de tipo premium.Ahora vamos a por las acciones correspondientes a estas plantillas. Primero mostraremos elcódigo y después lo comentaremos.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/NotasController

1 <?php 2 ... 3 public function nuevaAction() { 4 $request = $this->getRequest(); 5 6 list($etiquetas, $notas, $nota_seleccionada) = $this->dameEtiquetasYNotas(); 7

Las acciones del panel de notas

204

Page 215: CursoSymfony2

8 $em = $this->getDoctrine()->getEntityManager(); 9 10 $nota=new Nota();11 $newForm = $this->createForm(new NotaType(), $nota);12 13 if ($request->getMethod() == "POST") {14 15 $newForm->bindRequest($request);16 17 if ($newForm->isValid()) {18 $usuario = $this->get('security.context')->getToken()->getUser();19 20 $item = $request->get('item');21 $this->actualizaEtiquetas($nota, $item['tags'], $usuario);22 23 $nota->setUsuario($usuario);24 $nota->setFecha(new \DateTime());25 26 if ($newForm['file']->getData() != '')27 $nota->upload($usuario->getUsername());28 29 $em->persist($nota);30 31 $em->flush();32 33 return $this->redirect($this->generateUrl('jamn_homepage'));34 }35 }36 37 return $this->render('JAMNotasFrontendBundle:Notas:crearOEditar.html.twig', array(38 'etiquetas' => $etiquetas,39 'notas' => $notas,40 'nota_seleccionada' => $nota,41 'new_form' => $newForm->createView(),42 'edita' => false,43 ));44 }45 46 public function editarAction() {47 $request = $this->getRequest();48 $id = $request->get('id');49 list($etiquetas, $notas, $nota_seleccionada) = $this->dameEtiquetasYNotas();50 51 $em = $this->getDoctrine()->getEntityManager();52 53 $nota = $em->getRepository('JAMNotasFrontendBundle:Nota')->find($id);54 55 if (!$nota) {56 throw $this->createNotFoundException('No se ha podido encontrar esa nota');57 }58 59 $editForm = $this->createForm(new NotaType(), $nota);60 $deleteForm = $this->createDeleteForm($id);61

62 if ($this->getRequest()->getMethod() == "POST") {63 64 $editForm->bindRequest($request);65 66 if ($editForm->isValid()) {67 $usuario = $this->get('security.context')->getToken()->getUser();

Las acciones del panel de notas

205

Page 216: CursoSymfony2

68 69 $item = $request->get('item');70 $this->actualizaEtiquetas($nota, $item['tags'], $usuario);71 72 $nota->setFecha(new \DateTime());73 74 if ($editForm['file']->getData() != '')75 $nota->upload($usuario->getUsername());76 77 $em->persist($nota);78 79 $em->flush();80 81 return $this->redirect($this->generateUrl('jamn_homepage'));82 }83 }84 85 return $this->render('JAMNotasFrontendBundle:Notas:crearOEditar.html.twig', array(86 'etiquetas' => $etiquetas,87 'notas' => $notas,88 'nota_seleccionada' => $nota,89 'edit_form' => $editForm->createView(),90 'delete_form' => $deleteForm->createView(),91 'edita' => true,92 ));93 }

Ambas acciones comienzan igual que la acción indexAction(), recuperando el listado deetiquetas y notas del usuario y la nota seleccionada (esta última no se necesita para lacreación de notas). A continuación se crea un objeto de tipo Nota, que se inicializa en el casode edición con la nota seleccionada, y que se utiliza para construir el formulario a partir deltipo NotaType. Entonces se discrimina entre el caso de que la petición sea por GET o POST.En el primer caso solamente hay que pintar el formularioy en el segundo hay que procesarlos datos para modificar o crear la nota.Tanto en la creación como en la edición de notas se utiliza una función auxiliar denominadaactualizaEtiquetas(). Su cometido es asociar a la nota las etiquetas que vienen delformulario a través de la varible de la petición item. El código de esta función es:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/NotasController

1 protected function actualizaEtiquetas($nota, $tags, $usuario) { 2 3 if (count($tags) == 0) { 4 $tags = array(); 5 } 6 $em = $this->getDoctrine()->getEntityManager(); 7 8 $nota->getEtiquetas()->clear(); 9 10 11 foreach ($tags as $tag) {12 $etiqueta = $em->getRepository('JAMNotasFrontendBundle:Etiqueta')->findOneByTextoAndUsuario($tag, $usuario);13 14 if (!$etiqueta instanceof Etiqueta) {15 $etiqueta = new Etiqueta();16 $etiqueta->setTexto($tag);17 $etiqueta->setUsuario($usuario);18 $em->persist($etiqueta);19 }20 21 $nota->addEtiqueta($etiqueta);22 }23 24 $em->flush();25 }

Las acciones del panel de notas

206

Page 217: CursoSymfony2

Lo que hace este código es borrar todas las etiquetas de la nota y, a continuación asociar denuevo las etiquetas que se han pasado por el argumento. Es una manera sencilla de resolverel problema de la actualización de atributos de una entidad.Por último se pasan los elementos necesarios para ser pintados por la plantillacrearOEditar.html.twig. Observa el valor de la variable edita en cada una de lasacciones y como se corresponde con lo que hemos dicho más arriba cuando planteábamoslas plantillas.Y fin de la historia. Con esto ya tenemos el panel de notas completamente funcional.

El proceso de registroEn este apartado vamos a desarrollar el proceso de registro de nuevos usuarios en laaplicación. Comenzamos explicando el proceso:En la pantalla de login de la aplicación se añadirá un enlace bien claro para que los usuariosque aún no dispongan de una cuenta en la aplicación puedan crear una. Cuando el usuariopique en este enlace se le presentará un formulario de registro en el que introducirá sunombre, apellidos, nombre de usuario, email y el password por duplicado.Cuando envíe el formulario al servidor, la aplicación creará en la base de datos un registropara el usuario con el campo isActive definido como false y con el campotokenRegistro relleno con una cadena alfanumérica larga y única. Además se asociará elusuario al grupo registrado.Acto seguido se enviará un e-mail al usuario notificándole que ha solicitado crear una cuentaen la aplicación y que si desea activarla debe picar en un enlace que se le ofrece en elpropio email. Este enlace es una URL de la aplicación que lleva como parámetro GET eltoken que se ha generado anteriormente. Cuando el usuario pique en el enlace se ejecutarála acción de activación del usuario. Dicha acción comprobará si existe un registro usuariocon el token que se le ha enviado, y en caso afirmativo lo activará (pondrá el campoisActive a true). De esa manera el usuario ya podrá hacer login en la aplicación y utilizarlacomo usuario registrado.Y esa es la esencia de cualquier proceso de (auto)registro de usuarios. Veámos ahora comohemos resuelto el problema.En primer lugar creamos un tipo nuevo para crear el formulario de registro, lodenominaremos RegistroType, y contendrá todos los campos que hemos mencionado másarriba:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Form/Type/RegistroType.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Form\Type; 4 5 use Symfony\Component\Form\AbstractType; 6 use Symfony\Component\Form\FormBuilder; 7 8 class RegistroType extends AbstractType 9 {10 public function buildForm(FormBuilder $builder, array $options)11 {12 $builder->add('nombre', 'text')13 ->add('apellidos', 'text')14 ->add('username', 'text')

El proceso de registro

207

Page 218: CursoSymfony2

15 ->add('email', 'text')16 ->add('password', 'password')17 ->add('password_again', 'password');18 }19 20 public function getName()21 {22 return 'registro';23 }24 25 public function getDefaultOptions(array $options)26 {27 return array(28 'data_class' => 'Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario',29 );30 }31 }

Hemos asociado este tipo a la entidad Usuario, pero para que esta asociación sea correcta,hay que añadir el campo password_again a dicha entidad:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Entity/Usuario.php

1 <?php 2 3 ... 4 /** 5 * @var string $password_again 6 * 7 * @Assert\NotBlank() 8 * @Assert\MaxLength(255) 9 * @Assert\Regex(10 * pattern="/^[\w-]+$/",11 * message="El password no puede contener más que caracteres alfanuméricos y guiones")12 */13 private $password_again;14 15 /**16 * Set password_again17 *18 * @param string $password_again19 */20 public function setPasswordAgain($password) {21 $this->password_again = $password;22 }23 24 /**25 * Get password_again26 *27 * @return string28 */29 public function getPasswordAgain() {30 return $this->password_again;31 }32 ...*

Fíjate que este nuevo atributo no se persiste en la base de datos, ya que no tiene ningún tipo de metadato de mapeo asociado. Se trata de un campo que será utilizado por el formulario de registro. Además, es importante que cuando se validen los campos del formulario, nos aseguremos de que los campos username y email no estén repetidos en la base de datos, y de que los campos password y password_again sean iguales. Para conseguir esto simplemente tenemos que añadir las reglas de validación adecuadas a esta

El proceso de registro

208

Page 219: CursoSymfony2

misma entidad. Mirando en la referencia de Symfony2 vemos que para exigir que losatributos username y email sean únicos hay que añadir estas líneas al principio del archivoUsuario.php:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Entity/Usuario.php

1 <?php 2 ... 3 /** 4 * Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario 5 * 6 * @ORM\Table() 7 * @ORM\Entity(repositoryClass="Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\UsuarioRepository") 8 * 9 * @UniqueEntity("email")10 * @UniqueEntity("username")11 */12 class Usuario implements AdvancedUserInterface {13 ...*

Y para que los atributos password y password_again sean idénticos recurrimos a un tipo devalidador más sofisticado basados en métodos (funciones) que deben satisfacer alguna reglade validación y que no se corresponden con un atributo del objeto. El nombre de estasfunciones debe comenzar por "get" o "is". Añade el siguiente trozo de código al ficheroUsuario.php:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Entity/Usuario.php

1 ...2 /**3 * @Assert\True(message = "Has escrito dos password distintos")4 */5 public function isPasswordOK() {6 return ($this->password === $this->password_again);7 }8 ...*

La idea es sencilla, te creas una función que comience por "is" o "get", con la lógica quequieras, y le indicas mediante una aserción (nosotros lo estamos haciendo con anotaciones)el tipo de validación que quieres hacer sobre esa función. En este caso simplementepedimos que el resultado sea true. Este tipo de validadores son muy útiles cuandonecesitamos comprobar que se cumplen ciertas relaciones entre atributos del objeto.Con esto ya tenemos disponible un tipo para crear el formulario de registro, y un objeto consus validadores definidos que podemos asociar a dicho tipo.A continuación vamos a implementar la acción de registro. Recuerda que esta acción ya latenemos enlazada en el routing del bundle (ruta jamn_registro).src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/LoginController.php

1 <?php 2 ... 3 use Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario; 4 use Jazzyweb\AulasMentor\NotasFrontendBundle\Form\Type\RegistroType; 5 ... 6 7 public function registroAction() { 8

El proceso de registro

209

Page 220: CursoSymfony2

9 $request = $this->getRequest();10 $usuario = new Usuario();11 12 $form = $this->createForm(new RegistroType(), $usuario);13 14 if ($request->getMethod() == "POST") {15 $form->bindRequest($request);16 if ($form->isValid()) {17 $serviceRegistro = $this->get('jam_notas_frontend.registro');18 $serviceRegistro->registra($usuario, $form->get('password')->getData());19 20 return $this->render('JAMNotasFrontendBundle:Login:registro_success.html.twig', array('usuario' => $usuario));21 }22 }23 24 return $this->render('JAMNotasFrontendBundle:Login:registro.html.twig', array('form' => $form->createView()));25 }

Observa los use que hemos añadido para usar las clase Usuario y RegistroType en laacción. Como ves hemos utilizado para el tratamiento de formularios el procedimientogeneral que explicamos en la unidad 8. Lo interesante de este código es lo que ocurrecuando se envían los datos del formulario por POST y se pasa el proceso de validación:simplemente se lleva a cabo el registro. Mediante el contenedor de dependenciasinstanciamos un servicio llamado jam_notas_frontend.registro que se encargará de

• insertar al usuario en la base de datos, con el campo isActive a false y con un tokende registro.

• enviar un correo electrónico al usuario indicándole que ha sido dado de alta en laaplicación y que puede activar su cuenta picando en un enlace que se proporciona en elpropio correo.

Obviamente este magnífico servicio hay que implementarlo. Pues vamos a ello. Observa queel servicio debe ser capaz de acceder a la base de datos y de enviar e-mails. Para ello puedehacer uso de los servicios existentes en el framework doctrine y mailer. Además, cuandovaya a insertar el password facilitado por el usuario debe hacerlo respetando el tipo decodificación que se haya definido en el fichero security.yml. Lo mejor es utilizar el serviciode codificación de Symfony2, que ya se ocupa de todos estos detalles. Por lo pronto hemosidentificado 3 dependencias de nuestro servicio de registro. Vamos a añadir una menosobvia; utilizaremos también el servicio de templating para componer el texto del e-mail quese enviará al usuario. Pues con esto en mente proponemos la siguiente clase como serviciode registro:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Services/Registro.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Services; 4 5 class Registro { 6 7 public function __construct($doctrine, $mailer, $templating, $factory_encoder) { 8 $this->doctrine = $doctrine; 9 $this->mailer = $mailer;10 $this->templating = $templating;11 $this->factory_encoder = $factory_encoder;12 }13 14 public function registra($usuario, $password) {15 $usuario->setIsActive(false);16 $usuario->setTokenRegistro(substr(md5(uniqid(rand(), true)), 0, 32));17

El proceso de registro

210

Page 221: CursoSymfony2

18 $em = $this->doctrine->getEntityManager();19 20 $grupo = $em->getRepository('JAMNotasFrontendBundle:Grupo')21 ->findOneByRol('ROLE_REGISTRADO');22 23 $usuario->addGrupo($grupo);24 25 $encoder = $this->factory_encoder->getEncoder($usuario);26 27 $salt = substr(md5(uniqid(rand(), true)), 0, 10);28 $usuario->setSalt($salt);29 $password = $encoder->encodePassword($password, $usuario->getSalt());30 31 $usuario->setPassword($password);32 33 $em->persist($usuario);34 35 $em->flush();36 37 $message = \Swift_Message::newInstance()38 ->setSubject('Alta en la aplicación MentorNotas')39 ->setFrom('[email protected]')40 ->setTo($usuario->getEmail())41 ->setBody($this->templating->render('JAMNotasFrontendBundle:Login:email_registro.html.twig', array('usuario' => $usuario)))42 ;43 $this->mailer->send($message);44 }45 46 }

Como ya estudiamos en la unidad 4, los servicios se crean inyectando las dependencias enel constructor y añadiendo los métodos que requieran. En nuestro caso, el métodoregistra() se encarga de llevar a cabo todas las acciones que hemos descrito más arriba:crear el usuario, hacerlo inactivo, añadirlo al grupo registrado, codificar adecuadamente supassword, crear su token, componer el mail y enviarlo. Todo ello apoyandose en los serviciosdependientes. Analiza el código anterior identificando donde se hace cada una de estasoperaciones. Fíjate que en la composición del e-mail, se utiliza el renderizado de unaplantilla para rellenar el cuerpo del mensaje. El código de esta plantilla se presenta acontinuación:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Login/email_registro.html.twig

Hola {{ usuario.nombre }} {{ usuario.apellidos }},

Tú o alguien ha creado una cuenta en la aplicación MentorNotas.

Si quieres activarla sigue este enlace:

{{ url("jamn_activar_cuenta", {"token": usuario.tokenRegistro}) }}

Un saludo!

Es por eso que el servicio de registro depende del servicio de templating.Ahora, para que el contenedor de dependencias sepa instanciar el servicio que acabamos decrear, hay que registrarlo en el bundle indicando sus dependencias:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/config/services.yml

parameters: jam_notas_frontend.registro.class: Jazzyweb\AulasMentor\NotasFrontendBundle\Services\Registro

services: jam_notas_frontend.registro: class: %jam_notas_frontend.registro.class% arguments: - "@doctrine"

El proceso de registro

211

Page 222: CursoSymfony2

- "@mailer" - "@templating" - "@security.encoder_factory"

Ya sólo nos queda crear las plantillas registro.html.twig yregistro_success.html.twig usadas en el controladorJAMNotasFrontendBundle:Login:registro.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Login/registro.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block body %} 4 <div class="mentornotas-content-login"> 5 <h1>Regístrate</h1> 6 <div> 7 <form novalidate="true" action="{{ path("jamn_registro") }}" method="post" id="login"> 8 9 {% if form_errors(form) %}10 <div class="error">{{ form_errors(form) }}</div>11 {% endif %}12 13 <div>14 {% if form_errors(form.nombre ) %}15 <div class="error">{{ form_errors(form.nombre ) }}</div>16 {%endif%}17 <label for="nombre">Tu nombre:</label>18 {{ form_widget(form.nombre) }}19 </div>20 21 <div>22 {% if form_errors(form.apellidos ) %}23 <div class="error">{{ form_errors(form.apellidos ) }}</div>24 {%endif%}25 <label for="apellidos">Apellidos:</label>26 {{ form_widget(form.apellidos) }}27 </div>28 29 <div>30 {% if form_errors(form.username ) %}31 <div class="error">{{ form_errors(form.username ) }}</div>32 {%endif%}33 <label for="username">Username:</label>34 {{ form_widget(form.username) }}35 </div>36 37 <div>38 {% if form_errors(form.email ) %}39 <div class="error">{{ form_errors(form.email ) }}</div>40 {%endif%}41 <label for="email">Email:</label>42 {{ form_widget(form.email) }}43 44 </div>

45 46 <div>47 {% if form_errors(form.password ) %}48 <div class="error">{{ form_errors(form.password ) }}</div>49 {%endif%}50 <label for="password">Password:</label>51 {{ form_widget(form.password) }}

El proceso de registro

212

Page 223: CursoSymfony2

52 53 </div>54 55 <div>56 {% if form_errors(form.password_again ) %}57 <div class="error">{{ form_errors(form.password_again ) }}</div>58 {%endif%}59 <label for="password_again">Password(otra vez):</label>60 {{ form_widget(form.password_again) }}61 62 </div>63 64 {{ form_rest(form) }}65 <input type="submit" class="mentornotas-button-grey" value="Adelante" />66 </form>67 </div>68 </div>69 {% endblock %}

Esta plantilla pinta el formulario de registro, y la que transcribimos a continuación informade que el proceso de registro se ha llevado a cabo con éxito.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Login/registro_success.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block body %} 4 <div class="mentornotas-content-login"> 5 6 <h1>Hola {{ usuario.nombre }}</h1> 7 8 <p> Gracias por registrarte en <i>MentorNotas</i></p> 9 10 <p>En breve recibirás un correo electrónico con un enlace para que definas tu11 password y actives tu cuenta.</p>12 13 <p><a href="{{ path('jamn_homepage') }}">Ir a página de login</a></p>14 15 </div>16 {% endblock %}

Y ya tenemos lista una parte del proceso de registro. Tan solo nos falta la activación de lacuenta cuando el usuario pique en el enlace que se le envía por e-mail. Para que funcione elenvío de e-mails, asegúrate de que tienes adecuadamente configurado el servicio mailing(en la unidad 4 se cuenta como hacer esto).Recuerda que la ruta para la activación de la cuenta también la tenemos ya registrada, sellama jamn_activar_cuenta. La acción mapeada en dicha ruta se llamaJAMNotasFrontendBundle:Login:activar, y lo que debe hacer es comprobar que el tokenque le llega por GET se corresponde con alguno de los que tenemos en la base de datos y,en su caso, activar el usuario asociado a dicho token. Y eso es exactamente lo que hace elsiguiente código:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller/LoginController.php

1 <?php 2 ... 3 4 public function activarAction() 5 {

El proceso de registro

213

Page 224: CursoSymfony2

6 $request = $this->getRequest(); 7 8 $em = $this->get('doctrine')->getEntityManager(); 9 10 $usuario = $em->getRepository('JAMNotasFrontendBundle:Usuario')11 ->findOneByTokenRegistro($request->get('token'));12 13 if (!$usuario) {14 throw $this->createNotFoundException('Usuario no registrado');15 }16 17 $usuario->setIsActive(true);18 $em->persist($usuario);19 20 $em->flush();21 22 return $this->render('JAMNotasFrontendBundle:Login:activar_success.html.twig', array('usuario' => $usuario));23 }

Para completar la funcionalidad debemos implementar la plantillaJAMNotasFrontendBundle:Login:activar_success.html.twig.src/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Login/activar_success.html.twig

1 {% extends '::base.html.twig' %} 2 3 {% block body %} 4 <div class="mentornotas-content-login"> 5 6 <h1>Hola {{ usuario.nombre }}</h1> 7 8 <p> 9 Tu cuenta ha sido activada. Ya puedes entrar en la aplicación a través de10 este enlace:11 </p>12 13 <p><a href="{{ path('jamn_homepage') }}">Ir a página de login</a></p>14 15 </div>16 {% endblock %}

Y ahora ya tenemos completo el proceso de registro.

El resto de la aplicación.Aunque lo más importante de la aplicación ya está terminado, aún nos faltan algunas cosaspor hacer:

• Hay que modificar las plantillas para adaptarlas a la interfaz de usuario propuesta en launidad 5.

• También hay que implementar la contratación de cuentas premium.• Y hay que desarrollar la parte de administración.

Pero esta unidad finaliza aquí. Los dos primeros puntos los acabarás tú en los ejercicios, y eltercero será el tema que trataremos en la siguiente unidad.Autor del código: Juan David Rodríguez García <[email protected]>

El resto de la aplicación.

214

Page 225: CursoSymfony2

Unidad 11: Desarrollo de la aplicación MentorNotas(VII). Desarrollo del backendLlegamos al final del curso. Con lo que has estudiado hasta ahora ya dispones de suficientesconocimientos para desarrollar lo que vamos a explicar en esta unidad: la parte deadministración, conocida universalmente como backend de la aplicación.Prácticamente cualquier aplicación web precisa de cierto trabajo de administración para quefuncione adecuadamente. Por ejemplo, la gestión de usuarios es una tarea de administraciónque suele ser común a todas ellas. Por lo general, la administración de cualquier aplicaciónconsiste en la gestión de cada una de las entidades que la componen, es decir en crear(Create), recuperar (Retrieve), actualizar (Update) y borrar (Delete) elementos de dichasentidades. En el argot de las aplicaciones web: en desarrollar módulos CRUD sobre lasentidades. Une las primeras letras de las operaciones que hemos enumerado antes (eninglés) y te darás cuenta de lo que significa módulo CRUD.Por ello una herramienta versátil y absolutamente eficaz para la administración es un clientedel tipo phpMyAdmin (http://www.phpmyadmin.net). A falta de una herramienta más precisa,este tipo de software puede servirnos. El problema que presentan es que es demasiadogenérico y da demasiado poder al administrador de la aplicación, de manera que un fallo enla administración podría dejar en la estacada a la aplicación completa. Además, en ocasionesexisten importantes relaciones entre las entidades que deberían ser controladas por laaplicación para que funcione correctamente . Por eso, aunque estas herramientas genéricaspuedan servirnos en una primera etapa, si queremos que nuestra aplicación quedecompleta, debemos construir una herramienta de gestión a medida. Y eso es lo que haremosen esta unidad.

Estrategias de desarrollo para la parte de administraciónProponemos tres estrategias para el desarrollo de la administración de la aplicación:

1. Completamente a medida,2. Con ayuda del generador de módulos CRUD de Symfony23. Utilizando algún bundle de terceros para el desarrollo de backends.

La primera de ellas es la más obvia y directa; ya sabes programar con Symfony2, así quepuedes crear para cada entidad que debas administrar controladores (acciones) y plantillasmediante las que se muestren listados y formularios para crear, actualizar y borrarelementos.Si decides seguir esta estrategia, te darás cuenta de que el código que has picado para cadaentidad es prácticamente idéntico, y que la diferencia esencial es que cambia el nombre dela entidad. Por ello, Symfony2 incorpora una herramienta (asistente) que generaautomáticamente el código mínimo para realizar las operaciones CRUD. Posteriormente, si laaplicación lo requiere tendrás que tunear ese código para adaptarlo a las necesidadesconcretas.El generador de módulos de administración de Symfony2 está bien, pero en honor a laverdad, el de symfony 1.4 está mucho mejor. En realidad symfony 1.4 proporciona dosherramientas para generar módulos de administración. Una más sencilla, que es similar a lade Symfony2, mediante la que se genera un código mínimo, que incluye:

• listado de objetos,• formularios para crear y editar objetos,

y otra mucho más elaborada mediante la que se generan módulos de administración con

• listado de objetos con paginado,

Unidad 11: Desarrollo de la aplicación MentorNotas (VII). Desarrollo del backend

215

Page 226: CursoSymfony2

• filtro de búsqueda de objetos,• formularios para crear y editar objetos,• CSS's poco intrusivas que pueden combinarse fácilmente en el diseño de cualquier

aplicación,• configuración de las funcionalidades mediante un fichero yaml,• código y plantillas fácilmente modificables y adaptables mediante el mecanismo de

extensión (herencia).Symfony2, por lo pronto, y no creo que vaya a cambiar la situación, proporciona ungenerador de módulos sencillo al estilo del primero que hemos descrito. No obstante existenvarios bundles de terceros con los que se construyen aplicaciones de administración quesuperan en funcionalidades y estilismo al generador de administración de symfony 1.4. Esosí, no son oficiales de Symfony2.En esta unidad mostraremos las dos últimas estrategias: el uso del generador de módulosCRUD de Symfony2 y el del SonataAdminBundle, uno de los bundles más populares para eldesarrollo de módulos de administración.

El generador de módulos CRUD de Symfony2Generar un módulo de administración (CRUD) con el generador de módulos CRUD deSymfony2 es extremadamente sencillo. Vamos a comprobarlo generando un módulo para laadministración de la entidad Tarifa.

app/console generate:doctrine:crud

Como ves se trata de un asistente interactivo, al estilo del generador de bundles, así quetoca a responder preguntas. En primer lugar nos pide el nombre corto de la entidad quedeseamos administrar. Introduce JAMNotasFrontendBundle:Tarifa. A continuación dile quesi quieres generar la opción de escribir, y después usa yaml como formato de configuración.Esto último se utiliza para las rutas, y como en nuestro bundle estamos usando ficheros yamlpara su configuración, debemos continuar usando este formato. Puedes mantener comoprefijo de la ruta el que te propone. Finalmente contesta que si a la generación de código y ala importación de las rutas. Si, al finalizar el proceso, te indica que hagas manualmentealgún cambio, hazle caso.Ahora puedes probar lo que acabamos de hacer a través de la URL

http://localhost/Symfony/web/app_dev.php/notas/tarifa

Verás una rudimentaria pantalla con un listado de tarifas. Puedes editarlas, borrarlas y creartarifas nuevas. Pruébalo. A nivel de código a ocurrido lo siguiente:

• En el directorio src/Jazzyweb/AulasMentor/NotasFrontendBundle/Controller se hagenerado un controlador denominado TarifaController con el código queimplementa las operaciones de listar, editar, borrar y crear tarifas.

• Se ha generado el directoriosrc/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/views/Tarifa con lasplantillas twig utilizadas en el módulo.

• Se ha generado el archivosrc/Jazzyweb/AulasMentor/NotasFrontendBundle/Resources/config/routing/tarifa.ymlcon las rutas del módulo.

El generador de módulos CRUD de Symfony2

216

Page 227: CursoSymfony2

Examina todo este código y procura entender todo lo que hace. No te debe resultarcomplicado con lo que has estudiado hasta el momento. Lo mismo que acabamos de hacercon la entidad Tarifa podemos hacer con el resto de las entidade que requieranadministración. Una vez generados todos los módulos es el momento del tunning, es decir,de modificar el código para adaptarlo a nuestras necesidades. Por ejemplo:

• Podemos cambiar las plantillas para que utilicen las CSS's de nuestra aplicación.• Podemos añadir un paginado a los listados. Puedes currartelo tú, o mejor, puedes

utilizar bundles de terceros que realizan esta tarea.• Podemos añadir un filtro de búsqueda.• Y todo lo que se te ocurra.

A partir de aquí la cosa va de indagar, probar, investigar, equivocarse, aprender de loserrores, asimilar y registrar todo aquello que consigamos hacer funcionar y que creamos útilpara otros desarrollo. En fin, literalmente romperse la cabeza, que es de lo que va esto deldesarrollo.

El generador de módulos CRUD SonataAdminBundleSonata Project es un proyecto de una empresa francesa (Ekino) que pretende desarrollar unasolución de comercio electrónico sobre Symfony2. Una de las partes de este proyecto es eldesarrollo de una herramienta para generar aplicaciones de administración (backends). Estaparte, denominada SonataAdminBundle, está en un estado de desarrollo muy avanzado y escompletamente funcional, aunque la documentación no está, en mi humilde opinión,completamente afinada y hay que partirse la cabeza (de nuevo), un poco más de lo normalpara ponerlo en funcionamiento. En lo que queda de tema, y de curso :-), daremos lasinstrucciones para instalar el bundle y ponerlo a funcionar. Exploraremos sus característicasbásicas, pero ten en cuenta que con este bundle se puede hacer mucho más de lo que aquíexplicaremos. Como siempre, se trata de explorar la documentación y "jugar" con el bundlehasta exprimirlo.

Instalación del SonataAdminBundleEl SonataAdminBundle depende de otros bundles de Symfony2, concretamente de:

• SonataCacheBundle

• SonataBlockBundle

• SonatajQueryBundle

• KnpMenuBundle (Version 1.1.*)• Exporter

Para instalar todo este código debes añadir a tu fichero deps las siguientes líneas:

...[SonataAdminBundle] git=http://github.com/sonata-project/SonataAdminBundle.git target=/bundles/Sonata/AdminBundle version=origin/2.0

[SonataBlockBundle] git=http://github.com/sonata-project/SonataBlockBundle.git target=/bundles/Sonata/BlockBundle version=origin/2.0

El generador de módulos CRUD SonataAdminBundle

217

Page 228: CursoSymfony2

[SonataCacheBundle] git=http://github.com/sonata-project/SonataCacheBundle.git target=/bundles/Sonata/CacheBundle version=origin/2.0

[SonatajQueryBundle] git=http://github.com/sonata-project/SonatajQueryBundle.git target=/bundles/Sonata/jQueryBundle

[SonataDoctrineORMAdminBundle] git=http://github.com/sonata-project/SonataDoctrineORMAdminBundle.git target=/bundles/Sonata/DoctrineORMAdminBundle version=origin/2.0

[KnpMenuBundle] git=http://github.com/KnpLabs/KnpMenuBundle.git target=/bundles/Knp/Bundle/MenuBundle version=origin/1.1.0

[KnpMenu] git=http://github.com/KnpLabs/KnpMenu.git target=/knp/menu

[Exporter] git=http://github.com/sonata-project/exporter.git target=/exporter

Antes de proseguir borra la cache:

sudo rm -rf app/cache/*sudo chmod -R 777 app/cache

o

sudo php app/console cache:clear

Nota

En linux puedes que tengas problemas con los permisos. Asegurate de hacer:

sudo chmod -R 777 app/cache

Ahora para descargar y añadir todo este código al proyecto, debes ejecutar por línea decomando la instrucción:

./bin/vendors install

El generador de módulos CRUD SonataAdminBundle

218

Page 229: CursoSymfony2

Si te bajaste la distribución estándard de Symfony2 con vendors, tendrás que reinstalarlotodo:

./bin/vendors install --reinstall

A continuación debemos registrar en el autoload.php los espacios de nombres de losbundles que acabamos de instalar:app/autoload.php

<?php $loader->registerNamespaces(array( // ... 'Sonata' => __DIR__.'/../vendor/bundles', 'Exporter' => __DIR__.'/../vendor/exporter/lib', 'Knp\Bundle' => __DIR__.'/../vendor/bundles', 'Knp\Menu' => __DIR__.'/../vendor/knp/menu/src', // ... ));

Y a continuación registramos los bundles:app/AppKernel.php

<?php...public function registerBundles() { $bundles = array( ... new Sonata\AdminBundle\SonataAdminBundle(), new Sonata\BlockBundle\SonataBlockBundle(), new Sonata\CacheBundle\SonataCacheBundle(), new Sonata\jQueryBundle\SonatajQueryBundle(), new Knp\Bundle\MenuBundle\KnpMenuBundle(), new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),

Ahora instalamos los assets que vienen con los bundles:

php app/console assets:install --symlink web

Nota

En windows no se puede utilizar la opción --symlink

Y ahora configuramos los bundles. Para ello añadimos al archivoapp/config/config.yml

...sonata_admin:

El generador de módulos CRUD SonataAdminBundle

219

Page 230: CursoSymfony2

title: Administración de la aplicación MentorNotas title_logo: bundles/sonataadmin/logo_title.png templates: # default global templates layout: SonataAdminBundle::standard_layout.html.twig ajax: SonataAdminBundle::ajax_layout.html.twig dashboard: SonataAdminBundle:Core:dashboard.html.twig

# default actions templates, should extend a global templates list: SonataAdminBundle:CRUD:list.html.twig show: SonataAdminBundle:CRUD:show.html.twig edit: SonataAdminBundle:CRUD:edit.html.twig

dashboard: blocks: # display a dashboard block - { position: left, type: sonata.admin.block.admin_list }

sonata_block: default_contexts: [cms] blocks: sonata.admin.block.admin_list: contexts: [admin]

Asegurate de que en este mismo fichero (app/config/config.yml) la opción translatorestá activada.

...translator: { fallback: %locale% }...

Vamos a por las rutas. Añade al ficheroapp/config/routing.yml

... admin: resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml' prefix: /admin

_sonata_admin: resource: . type: sonata_admin prefix: /admin

Con esto ya deberías poder ver algo. Borra la cache y prueba la siguiente ruta:

http://localhost/Symfony/web/app_dev.php/admin/dashboard

Si todo ha ido bien deberías ver el panel de control (dashboard) vacío, únicamente con eltítulo.Ahora ya estamos en condiciones de añadir módulos de administración a nuestro proyecto através del SonataAdminBundle.

El generador de módulos CRUD SonataAdminBundle

220

Page 231: CursoSymfony2

Inyección de los módulos de administraciónAntes de hacer nada vamos a describir la filosofía de uso de este bundle. El bundleproporciona un controlador que implementa todas las operaciones propias de un móduloCRUD:

• crear nuevos objetos• editar objetos existentes• listar los objetos existentes• filtrar (buscar) objetos• borrar objetos

y algunas más. Échale un vistazo al archivovendor/bundles/Sonata/AdminBundle/Controller/CRUDController, y podrás ver todaslas acciones que incorpora el controlador base del bundle. La idea es que cada entidad quevaya a ser administrada con este bundle, utilice como controlador una clase que derive deeste que acabamos de ver, y si las circunstancias lo requieren, se realicen las modificacionesque sean necesaria usando el mecanismo de herencia. En muchos casos nos servirá (y nossobrará) el controlador base tal y como está.Por otro lado, cada entidad administrada tendrá asociada una clase cuya finalidad esconfigurar el aspecto que presentarán los formularios asociados (campos que se mostraránen el listado, campos que se mostrarán en los filtros, campos que se mostrarán en elformulario de edición/creación, validadores extras, y algunas cosas más). Esta clase debeextender a Sonata\AdminBundle\Admin\Admin, y representa el vínculo con elSonataAdminBundle.Y ahora viene la gracia del bundle. Esta última clase asociada a la entidad que define comodeben ser los formularios de administración, es registrada como un servicio de Symfony2 enel archivo Resources/config/services.yml del bundle donde estemos construyendo laparte de administración, en nuestro caso en el NotasBackendBundle. Los argumentos que sele inyectan como dependencias de este nuevo servicio son la entidad que va a seradministrada y el controlador que se utilizará para administrarla. Y ya está, el servicio queacabamos de crear, que conoce al SonataAdminBundle, pues deriva de una de sus clases, seencarga de añadir al dashboard la administración de la entidad en cuestión.Como ves, el SonataAdminBundle utiliza el mecanismo de inyección de dependencias parasu funcionamiento. Por eso hemos llamado a este apartado inyección de los módulos deadministración.Aunque todo esto te puede resultar un poco abstracto hasta que no lo veas enfuncionamiento, échale un par de lecturas intensas antes de continuar con los siguientespárrafos.Y ahora que ya has leido varias veces la filosofía de uso del SonataAdminBundlde, vamos aponerlo en marcha. Comenzaremos por añadir un modulo de administración para la entidadTarifa.Lo primero que haremos será crear en el NotasBackendBundle (que en su momento creamospensando en la parte de administración), una clase, que denominaremos

AdminController (el nombre puede ser cualquiera), que derive deCRUDController.src/Jazzyweb/AulasMentor/NotasBackendBundle/Controller/AdminController

Inyección de los módulos de administración

221

Page 232: CursoSymfony2

<?php

namespace Jazzyweb\AulasMentor\NotasBackendBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;

class AdminController extends Controller{

}

En principio, está clase servirá para todas las entidades que vayamos a administrar, ya quecontiene la implementación de las acciones propias y comunes de cualquier módulo CRUD.Ahora vamos a crear una clase de administración asociada a la entidad Tarifa, dondedefiniremos el aspecto de los formularios CRUD.src/Jazzyweb/AulasMentor/NotasBackendBundle/Admin/TarifaAdmin.php

<?php

namespace Jazzyweb\AulasMentor\NotasBackendBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;use Sonata\AdminBundle\Datagrid\ListMapper;use Sonata\AdminBundle\Datagrid\DatagridMapper;use Sonata\AdminBundle\Validator\ErrorElement;use Sonata\AdminBundle\Form\FormMapper;

class TarifaAdmin extends Admin {

protected function configureFormFields(FormMapper $formMapper) { $formMapper ->add('nombre') ->add('periodo') ->add('precio') ->add('validoDesde') ->add('validoHasta') ; }

protected function configureDatagridFilters(DatagridMapper $datagridMapper) { $datagridMapper ->add('nombre') ->add('periodo') ->add('precio') ; }

protected function configureListFields(ListMapper $listMapper) { $listMapper ->addIdentifier('nombre') ->add('periodo') ->add('precio')

Inyección de los módulos de administración

222

Page 233: CursoSymfony2

->add('validoDesde') ->add('validoHasta') ; }

public function validate(ErrorElement $errorElement, $object) {

}

}

En el método configureFormFields, se definen los campos que tendrán los formularios deedición y creación de objetos Tarifa. En el método configureDatagridFilters se definenlos campos que incluirá el formulario usado para filtrar registros. En el métodoconfigureListFields, se definen los campos que aparecerán en el listado de registros. Porúltimo, en el método validate, se añaden validadores extras que puedan ser necesarios.Por lo general, las reglas de validación que hemos definido en las entidades son suficientes.Así que ya tenemos la clase de administración asociada a la entidad Tarifa, y un controladorgenérico. El próximo paso es inyectar esta clase como nuevo servicio de nuestro proyecto.Ello, como ya sabes, se hace en el archivo services.yml del bundle donde estamosllevando a cabo la implementación.src/Jazzyweb/AulasMentor/NotasBackendBundle/Resources/config/services.yml

parameters:# jam_notas_backend.example.class: Jazzyweb\AulasMentor\NotasBackendBundle\Example

services: sonata.notas_backend.admin.tarifa: class: Jazzyweb\AulasMentor\NotasBackendBundle\Admin\TarifaAdmin tags: - { name: sonata.admin, manager_type: orm, group: 'Contratos', label: 'Tarifas' } arguments: - null - Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Tarifa - JAMNotasBackendBundle:Admin

Hemos llamado al servicio sonata.notas_backend.admin.tarifa, la clase que define alservicio es Jazzyweb\AulasMentor\NotasBackendBundle\Admin\TarifaAdmin, y losargumentos que se le pasan son:Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Tarifa yJAMNotasBackendBundle:Admin,es decir; la entidad que queremos administrar y el controlador básico que hemos creado alprincipio.En el apartado tags, se definen la etiqueta que se mostrará en el dashboard y el grupo alque pertenecerá el módulo.Y ya está, fíjate que no tenemos que preocuparnos del routing, el SonataAdminBundle seencarga de todo. Si vuelves a acceder al dashboard verás ahora que se ha añadido un itemde menú y un bloque para el acceso a la administración de la entidad Tarifa.Y lo mismo podemos hacer con el resto de entidades adminstradas.

Entidad Contratosrc/Jazzyweb/AulasMentor/NotasBackendBundle/Admin/ContratoAdmin.php

Entidad Contrato

223

Page 234: CursoSymfony2

<?php

namespace Jazzyweb\AulasMentor\NotasBackendBundle\Admin;

use Sonata\AdminBundle\Admin\Admin; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Validator\ErrorElement; use Sonata\AdminBundle\Form\FormMapper;

class ContratoAdmin extends Admin {

protected function configureFormFields(FormMapper $formMapper) { $formMapper ->add('referencia') ->add('usuario') ->add('fecha') ->add('tarifa')

; }

protected function configureDatagridFilters(DatagridMapper $datagridMapper) { $datagridMapper ->add('referencia') ->add('tarifa') ->add('usuario') ; }

protected function configureListFields(ListMapper $listMapper) { $listMapper ->addIdentifier('referencia') ->add('fecha') ->add('tarifa') ->add('usuario') ->add('_action', 'actions', array( 'actions' => array( 'edit' => array(), 'delete' => array(), ))) ; }

public function validate(ErrorElement $errorElement, $object) {

}

}

src/Jazzyweb/AulasMentor/NotasBackendBundle/Resources/config/services.yml

services.yml:...sonata.notas_backend.admin.contratos:

Entidad Contrato

224

Page 235: CursoSymfony2

class: Jazzyweb\AulasMentor\NotasBackendBundle\Admin\ContratoAdmin tags: - { name: sonata.admin, manager_type: orm, group: 'Contratos', label: Contratos } arguments: - null - Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Contrato - JAMNotasBackendBundle:Admin

Entidad Gruposrc/Jazzyweb/AulasMentor/NotasBackendBundle/Admin/GrupoAdmin.php

<?php

namespace Jazzyweb\AulasMentor\NotasBackendBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;use Sonata\AdminBundle\Datagrid\ListMapper;use Sonata\AdminBundle\Datagrid\DatagridMapper;use Sonata\AdminBundle\Validator\ErrorElement;use Sonata\AdminBundle\Form\FormMapper;

class GrupoAdmin extends Admin {

protected function configureFormFields(FormMapper $formMapper) { $formMapper ->add('nombre') ->add('rol') ; }

protected function configureDatagridFilters(DatagridMapper $datagridMapper) { $datagridMapper ->add('nombre') ->add('rol') ; }

protected function configureListFields(ListMapper $listMapper) { $listMapper ->addIdentifier('nombre') ->add('rol') ; }

public function validate(ErrorElement $errorElement, $object) {

}}

src/Jazzyweb/AulasMentor/NotasBackendBundle/Resources/config/services.yml

services:... sonata.notas_backend.admin.grupo:

Entidad Grupo

225

Page 236: CursoSymfony2

class: Jazzyweb\AulasMentor\NotasBackendBundle\Admin\GrupoAdmin tags: - { name: sonata.admin, manager_type: orm, group: 'Administración', label: Grupos } arguments: - null - Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Grupo - JAMNotasBackendBundle:Admin

Entidad PublicidadEn la administración de la entidad Publicidad tenemos que poder subir las imágenes de losbanners correspondientes. Estaría bien, además, que en el listado de los registros depublicidad se visualizase una miniatura de la imagen. Esta característica obliga a realizaralgunas cosillas extras. Veamos como podemos conseguir esta funcionalidad. Por lo prontoprocedemos como en las anteriores entidades: Creamos su clase de administración y laregistramos como servicio que utiliza la clase JAMNotasBackendBundle:Admin comocontrolador:src/Jazzyweb/AulasMentor/NotasBackendBundle/Admin/PublicidadAdmin.php

<?php

namespace Jazzyweb\AulasMentor\NotasBackendBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;use Sonata\AdminBundle\Datagrid\ListMapper;use Sonata\AdminBundle\Datagrid\DatagridMapper;use Sonata\AdminBundle\Validator\ErrorElement;use Sonata\AdminBundle\Form\FormMapper;

class PublicidadAdmin extends Admin {

protected function configureFormFields(FormMapper $formMapper) { $formMapper ->add('nombre') ->add('texto') ->add('path', null, array("read_only" => true)) ->add('file', 'file', array('required' => false)) ; }

protected function configureDatagridFilters(DatagridMapper $datagridMapper) { $datagridMapper ->add('nombre') ->add('texto') ; }

protected function configureListFields(ListMapper $listMapper) { $listMapper ->add('imagen', null, array('template' => 'JAMNotasBackendBundle:Sonata:imagen.html.twig')) ->addIdentifier('nombre') ->add('texto') ->add('path') ;

}

public function validate(ErrorElement $errorElement, $object) {

}

public function prePersist($object) {

Entidad Publicidad

226

Page 237: CursoSymfony2

$object->upload(); }

public function preUpdate($object) { $object->upload(); }

public function postRemove($object) { $object->removeUpload(); }

}

Fíjate que en la configuración de los campos del formulario de edición/creación(configureFormFields), se ha añadido un campo de tipo file, es decir un campo desubidad de archivos. Para que todo vaya bien es necesario que la entidad Publicidadimplemente los métodos para la subida y gestión de archivos asociados que ya hemosutilizado en unidades anteriore para la entidad Nota. La entidad Publicidad quedaría así:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Entity/Publicidad.php

<?php

namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Entity;

use Doctrine\ORM\Mapping as ORM;use Symfony\Component\Validator\Constraints as Assert;

/** * Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Publicidad * * @ORM\Table() * @ORM\Entity(repositoryClass="Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\PublicidadRepository") */class Publicidad {

/** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id;

/** * @var string $nombre * * @ORM\Column(name="nombre", type="string", length=255) */ private $nombre;

/** * @var string $texto * * @ORM\Column(name="texto", type="string", length=255) */ private $texto;

Entidad Publicidad

227

Page 238: CursoSymfony2

/** * @var string $path * * @ORM\Column(name="path", type="string", length=255, nullable="true") */ private $path;

/** * @Assert\File(maxSize="6000000") */ public $file;

/** * Get id * * @return integer */ public function getId() { return $this->id; }

/** * Set nombre * * @param string $nombre */ public function setNombre($nombre) { $this->nombre = $nombre; }

/** * Get nombre * * @return string */ public function getNombre() { return $this->nombre; }

/** * Set texto * * @param string $texto */ public function setTexto($texto) { $this->texto = $texto; }

/** * Get texto * * @return string */

Entidad Publicidad

228

Page 239: CursoSymfony2

public function getTexto() { return $this->texto; }

/** * Set path * * @param string $path */ public function setPath($path) { $this->path = $path; }

/** * Get path * * @return string */ public function getPath() { return $this->path; }

public function getAbsolutePath() { return null === $this->path ? null : $this->getUploadRootDir() . '/' . $this->path; }

public function getWebPath() { return null === $this->path ? null : $this->getUploadDir() . '/' . $this->path; }

protected function getUploadRootDir() { // the absolute directory path where uploaded documents should be saved return __DIR__ . '/../../../../../web/' . $this->getUploadDir(); }

protected function getUploadDir() { // get rid of the __DIR__ so it doesn't screw when displaying uploaded doc/image in the view. return 'uploads/publicidad'; }

public function upload() { // the file property can be empty if the field is not required if (null === $this->file) { return; }

// we use the original file name here but you should // sanitize it at least to avoid any security issues // move takes the target directory and then the target filename to move to $this->file->move($this->getUploadRootDir(), $this->file->getClientOriginalName());

// set the path property to the filename where you'ved saved the file $this->path = $this->file->getClientOriginalName();

// clean up the file property as you won't need it anymore $this->file = null; }

public function __toString() { return $this->getNombre(); }

} //*

Y registramos el servicio:``src/Jazzyweb/AulasMentor/NotasBackendBundle/Resources/config/services.yml`

Entidad Publicidad

229

Page 240: CursoSymfony2

services:... sonata.notas_backend.admin.publicidad: class: Jazzyweb\AulasMentor\NotasBackendBundle\Admin\PublicidadAdmin tags: - { name: sonata.admin, manager_type: orm, group: 'Publicidad', label: Publicidad } arguments: - null - Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Publicidad - JAMNotasBackendBundle:Admin

Con esto ya podemos añadir registros de publicidad con su imagen. Fíjate que en realidad nohemos hecho más que adaptar la entidad Publicidad para que sepa tratar la subidad deficheros, y añadir en la configuración del formulario de administración un campo del tipofile. Ahora vamos a ver como añadir una miniatura de las imágenes que subimos en ellistado de registros de publicidad.Fíjate que en el método configureListFields de la clase de configuración de la entidadPublicidad (PublicidadAdmin), hemos añadido un campo denominado imagen. Este campolleva como listado de opciones una plantilla que indica como debe pintarse el campo. Bastacon definir esta plantilla en el lugar que le corresponde en función de su nombre:src/Jazzyweb/AulasMentor/NotasBackendBundle/Resources/views/Sonata/imagen.html.twig

{% extends 'SonataAdminBundle:CRUD:base_list_field.html.twig' %}

{% block field%}<img height="42" src="{{ asset(object.getWebPath) }}" />{% endblock %}

Y ahora ya tenemos tuneado y listo nuestro módulo de administración de la entidadPublicidad.

Entidad UsuarioY vamos a por la última entidad que vamos a administrar. En esta ocasión tendremos quehacer un cambio en el controlador, ya que cuando se añade un usuario no solo hay quegrabar sus datos en la base de datos, sino que además hay que encriptar el password yenviar un mail al usuario para avisarle de que se le ha abierto una cuenta en el sistema. Esdecir, lo mismo que se hizo en el proceso de registro.Así que lo primero que hacemos es crear la clase asociada a la entidad que define susformularios.src/Jazzyweb/AulasMentor/NotasBackendBundle/Admin/UsuarioAdmin.php

<?php

namespace Jazzyweb\AulasMentor\NotasBackendBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;use Sonata\AdminBundle\Datagrid\ListMapper;use Sonata\AdminBundle\Datagrid\DatagridMapper;use Sonata\AdminBundle\Validator\ErrorElement;use Sonata\AdminBundle\Form\FormMapper;

class UsuarioAdmin extends Admin {

Entidad Usuario

230

Page 241: CursoSymfony2

protected function configureFormFields(FormMapper $formMapper) {

$formMapper ->add('nombre') ->add('apellidos') ->add('username') ->add('email') ->add('password', 'password') ->add('password_again', 'password') ->add('grupos', null, array('required' => false)) ; }

protected function configureDatagridFilters(DatagridMapper $datagridMapper) { $datagridMapper ->add('nombre') ->add('apellidos') ->add('username') ; }

protected function configureListFields(ListMapper $listMapper) { $listMapper ->addIdentifier('username') ->add('nombre') ->add('apellidos') ->add('isActive', null, array('editable' => true, 'label' => 'Activo')) ; }

public function validate(ErrorElement $errorElement, $object) {

}

}

Fíjate en el campo isActive que se añade en el método configureListFields, se le haañadido la opción editable. Esto significa que en el listado de usuarios, se mostrará elcampo isActive como un checkButton, y se podrá cambiar desde el propio listado. Esta esuna de las virguerías de este bundle, y tiene muchas más. Explorarlo a fondo, aunque en unprincipio puede ser duro, te dará muchas sorpresas. Además aprenderás a utilizar unaherramienta potentísima para la creación de aplicaciones de administración.Ahora vamos a crear un controlador específico para esta entidad. Le llamaremosUsuarioAdminController, y tiene esta pinta:src/Jazzyweb/AulasMentor/NotasBackendBundle/Controller/UsuarioAdminController.php

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasBackendBundle\Controller; 4 5 use Sonata\AdminBundle\Controller\CRUDController as Controller; 6 7 class UsuarioAdminController extends Controller 8 {

Entidad Usuario

231

Page 242: CursoSymfony2

9 10 public function createAction()11 {12 if (false === $this->admin->isGranted('CREATE')) {13 throw new AccessDeniedException();14 }15 16 $object = $this->admin->getNewInstance();17 18 $this->admin->setSubject($object);19 20 $form = $this->admin->getForm();21 $form->setData($object);22 23 if ($this->get('request')->getMethod() == 'POST') {24 $form->bindRequest($this->get('request'));25 26 if ($form->isValid()) {27 28 $serviceRegistro = $this->get('jam_notas_frontend.registro');29 $serviceRegistro-> registra($object, $form->get('password')->getData());30 $object->setIsActive(true);31 32 $this->admin->create($object);33 34 if ($this->isXmlHttpRequest()) {35 return $this->renderJson(array(36 'result' => 'ok',37 'objectId' => $this->admin->getNormalizedIdentifier($object)38 ));39 }40 41 $this->get('session')->setFlash('sonata_flash_success', 'flash_create_success');42 // redirect to edit mode43 return $this->redirectTo($object);44 }45 46 $this->get('session')->setFlash('sonata_flash_error', 'flash_create_error');47 }48 49 $view = $form->createView();50 51 // set the theme for the current Admin Form52 $this->get('twig')->getExtension('form')->setTheme($view, $this->admin->getFormTheme());53 54 return $this->render($this->admin->getEditTemplate(), array(55 'action' => 'create',56 'form' => $view,57 'object' => $object,58 ));59 }60 61 }

¿Qué es lo que hemos hecho aquí?. Igual que hicimos con la clase AdminController, hemos extendido la clase UsuarioAdminController de Sonata\AdminBundle\Controller\CRUDController. A continuación hemos copiado el código de la acción createAction() de la clase padre Sonata\AdminBundle\Controller\CRUDController en la clase UsuarioAdminController, y lo hemos modificado para que el proceso de creación de un usuario incluya la encriptación del password y el envío del mail de bienvenida. Fíjate que esto se hace en las líneas 28-30, donde se instancia el servicio jam_notas_frontend.registro que hemos construido en la unidad 10. Es decir, siempre se trata de modificar aquello que el bundle no te proporciona. Obviamente, el SonataAdminBundle no puede saber que la creación de tu entidad Usuario requiere el envío de un email y la encriptación de un password, pero te ofrece la oportunidad

Entidad Usuario

232

Page 243: CursoSymfony2

de que cambies el código de las acciones para adaptarlo a tu modelo de negocio.Ahora registramos la clase UsuarioAdmin como servicio, con la precaución de pasar comocontrolador la clase UsuarioAdminController que acabamos de crear, en lugar de la claseAdminController que hemos estado utilizando hasta ahora.src/Jazzyweb/AulasMentor/NotasBackendBundle/Resources/config/services.yml

services:... sonata.notas_backend.admin.usuario: class: Jazzyweb\AulasMentor\NotasBackendBundle\Admin\UsuarioAdmin tags: - { name: sonata.admin, manager_type: orm, group: 'Administración', label: Usuarios } arguments: - null - Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario - JAMNotasBackendBundle:UsuarioAdmin

¡Y ya está!, ya tenemos una completa aplicación de administración para nuestro proyecto.En realidad es bastante mejorable. Por ejemplo, encontrarás que la administración de laentidad Usuario la hemos esbozado y encaminado pero presenta algunos fallos. Porejemplo: el password no debería ser introducido por el administrador, si no que en el procesode activación de la cuenta que se inicia cuando el usuario pica en el enlace enviado en elemail, la aplicación debería presentarle al usuario un formulario para que definiese supassword.Por otro lado, hemos mostrado una pequeña parte de las capacidades de este magníficobundle. Aunque no es obligatorio en el curso, es un buen ejercicio para consolidar loaprendido, estudiar con detalle la documentación del bundle y comprobar la cantidad decosas que se pueden hacer con él. La documentación la puedes consultar enhttp://sonata-project.org/bundles/admin.

8888888888 8888888 888b 888 888 888 8888b 888 888 888 88888b 888 8888888 888 888Y88b 888 888 888 888 Y88b888 888 888 888 Y88888 888 888 888 Y8888 888 8888888 888 Y888

oooo$$$$$$$$$$$$oooo oo$$$$$$$$$$$$$$$$$$$$$$$$o oo$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o o$ $$ o$ o $ oo o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o $$ $$ $$o$oo $ $ "$ o$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$o $$$o$$o$"$$$$$$o$ o$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$o $$$$$$$$ $$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$$$$$$ """$$$ "$$$""""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$ $$$ o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$o o$$" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$o $$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" "$$$$$$ooooo$$$$o o$$$oooo$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o$$$$$$$$$$$$$$$$$

Entidad Usuario

233

Page 244: CursoSymfony2

$$$$$$$$"$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$"""""""" """" $$$$ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" o$$$ "$$$o """$$$$$$$$$$$$$$$$$$"$$" $$$ $$$o "$$""$$$$$$"""" o$$$ $$$$o o$$$" "$$$$o o$$$$$$o"$$$$o o$$$$ "$$$$$oo ""$$$$o$$$$$o o$$$$"" ""$$$$$oooo "$$$o$$$$$$$$$""" ""$$$$$$$oo $$$$$$$$$$ """"$$$$$$$$$$$ $$$$$$$$$$$$ $$$$$$$$$$" "$$$""""

Entidad Usuario

234

Page 245: CursoSymfony2

Ejercicios del Curso: Desarrollo de Aplicaciones Webcon Symfony2Contenidos:

Ejercicios de la unidad 2

Nota

Enlace a la Unidad 2: Desarrollo de una aplicación web siguiendo el patrón MVC

Ejercicio 1¿Cuáles son las razones que argumenta Fabien Potencier para decir que Symfony2 no es unframework MVC?

Ejercicio 2Explica al menos dos razones por las que es importante separar el código de la aplicación enuna parte accesible directamente al servidor web y otra no accesible.

Ejercicio 3¿Cuál es la función principal del controlador frontal en una aplicación web?

Ejercicio 4¿Qué debes tener en cuenta cuando utilizas PHP como lenguaje de plantillas? ¿Qué ventajasencuentras al utilizar PHP de esta manera?

Ejercicio 5Observa que las acciones del menú buscar por energía y búsqueda combinada arrojanun error 404, ya que no están implementadas. Corrige este error implementándolas.

Ejercicio 6Fíjate en las plantillas buscarPorNombre.php y mostrarAlimentos.php. En las dos apareceel mismo código para mostrar en una tabla un array de alimentos. Esto, desde el punto devista de las buenas prácticas, es algo incorrecto. ¿Por qué crees que no está bien estasolución? Propón una solución más en la línea de las buenas prácticas de programación.

Sugerencia

La cosa tiene que ver con el principio DRY (Don't Repeat Yourself)

Ejercicios de la unidad 3

Ejercicios del Curso: Desarrollo de Aplicaciones Web con Symfony2

235

Page 246: CursoSymfony2

Nota

Enlace a la Unidad 3: Symfony2 a vista de pájaro

Ejercicio 1Crea un nuevo entorno de ejecución que se comporte exactamente igual que el entorno dedesarrollo (dev), pero que no muestre la barra de depuración. Llama a este nuevo entornopru. Describe los pasos que has dado para resolver el problema.

Sugerencia

Debes crear un nuevo controlador frontal para indicarle al kernel que quieres cargar laconfiguración pru.

Sugerencia

Observa el apartado web_profiler del archivo config_dev.yml. Ahí se indica si sequiere mostrar o no la barra de depuración.

Sugerencia

En el fichero AppKernel.php se carga el bundle AcmeDemoBundle() de formaselectiva en función del entorno de ejecución que se esté utilizando.

Ejercicio 2Crea un nuevo bundle (llámalo como quieras) con el generador automático de bundles.Colócalo en el directorio fuentes (no te asustes, este último es nuevo, no viene en ladistribución estándar), en lugar de en src y define el prefijo fuentes para todas las rutasdel bundle. Describe los pasos que has dado para que todo funcione.

Ejercicio 3Construye una nueva acción que devuelva este sencillo documento HTML:

<html><body><h1>Hola caracola!</h1></body></html>

sin utilizar el método render y, por lo tanto, sin utilizar ninguna plantilla.

Ejercicio 1

236

Page 247: CursoSymfony2

Sugerencia

Recuerda que las acciones deben devolver un objeto Response.

Describe los pasos que has dado para resolver el problema. Transcribe el código que hasañadido al proyecto.

Ejercicio 4Construye el resto de los enlaces del menú. transcribe el código que has añadido alproyecto.

Ejercicio 5Añade las funcionalidades buscar por energía y búsqueda combinada. Describe los pasosque has dado y transcribe el código que has añadido al proyecto.

Ejercicios de la unidad 4

Nota

Enlace a la Unidad 4: Inyección de Dependencias

Ejercicio 1Desarrolla e incorpora al bundle JazzywebAlulasMentorAlimentosBundle un serviciomediante el que se pueda obtener información de los alimentos mediante consulta a lawikipedia. Describe como has resuelto el problema y transcribe el código que has tenido queañadir.

Sugerencia

En la wikipedia, la URL que le correponde a un artículo es del tipo:

http://es.wikipedia.org/wiki/Nombre_Articulo

Ejercicio 2Añade una funcionalidad a la aplicación de gestión de alimentos que permita mostrar lainformación que la wikipedia tiene de los alimentos del sistema. Describe como has resueltoel problema y transcribe el código que has tenido que añadir.

Ejercicio 4

237

Page 248: CursoSymfony2

Sugerencia

Puedes añadir a la plantilla verAlimento.php la información obtenida desde lawikipedia.

Ejercicios de la unidad 5

Nota

Enlace a la Unidad 5: Desarrollo de la aplicación MentorNotas (I). Análisis

Como has podido comprobar, esta unidad no tiene mucho que ver con el frameworkSymfony2. Es necesaria en tanto que para desarrollar una aplicación con cierto "cuerpo", esnecesario disponer de un análisis previo que nos ponga en situación y nos sirva comoreferencia y "planos" de nuestra construcción software.Así que nos vamos a olvidar por lo pronto de Symfony2 y vamos a dedicar los ejercicios deesta unidad a explorar un poco más a fondo como esta organizado el código HTML + CSS +javascript que utilizaremos como interfaz gráfica de usuario de nuestra aplicación.

Ejercicio 1Comencemos por el layout. Hemos utilizado un plugin de JQuery llamado jquery ui layout(http://layout.jquery-dev.net/). Por ello hemos de utilizar en todas las páginas la libreríajavascript JQuery (http://jquery.com/). También hemos utilizado, para enriquecer el aspectode los botones y otros elementos, la librería jquery-ui (http://jqueryui.com/).Localiza en el archivo inicio.html donde se cargan dichas librerías.

Ejercicio 2La librería jquery ui layout utiliza unas CSS's para dar estilos a los layout que con ella secrean. También la librería jquery-ui utiliza sus CSS's. Localízalas en el código del archivoinicio.html.

Ejercicio 3Fíjate en el aspecto de la página inicio.html. Esta organizada en cinco zonas claramenteidentificables: Un encabezado arriba, un pie de página abajo, y tres zonas en el centro; unapara las etiquetas (izquierda), otra para el listado de notas (centro) y la última para el detallede la nota (derecha). Es la librería jquery ui layout la encargada de enriquecer el códigoHTML de la página. Pero para que pueda hacer esto, el HTML debe tener una estructuradeterminada que entiende jquery ui layout para construir el layout, es decir, para colocarcada una de las zonas que hemos mencionado antes en su sitio.Identifica en el código del archivo inicio.html cuales son las capas que "informan" ajquery ui layout sobre las zonas que debe crear.

Ejercicio 4

Ejercicios de la unidad 5

238

Page 249: CursoSymfony2

Ahora fijate en la página inicio-editar_nota.html. Hemos colocado el formulario deedición en el lugar que estaba el detalle de la nota. Este formulario de edición tiene un editormuy majo en el que puedes dar formato al texto que escribes. También tiene un"etiquetador" enriquecido con el que puedes añadir/borrar etiquetas sin más que escribirlasy confirmarlas con la tecla enter. Pruébalo. En realidad, el editor de texto no es más que untextarea de HTML enriquecido por un plugin de JQuery, y el "etiquetador" es una lista HTMLenriquecida por otro plugin de JQuery.Localiza el códio HTML correspondientes a estos dos elementos, las librerías javascript(plugins de JQuery) y las llamadas a las funciones de dichas librerías que provocan elenriquecimiento de los elementos HTML. Localiza también las CSS's que se utilizan.

Ejercicios de la unidad 6

Nota

Enlace a la Unidad 6: Desarrollo de la aplicación MentorNotas (II). Rutas yControladores

Ejercicio 1Explora la información que contiene el objeto Request haciendo un volcado del mismomediante el siguiente código:

<?php ... $request = $this->getRequest(); // equivalente a $this->get('request');

echo '<pre>'; print_r($request); echo '</pre>'; exit;

Examínala detenidamente. ¿Cuál es el parámetro mediante el que se mantiene la sesión?Prueba a realizar el volcado de la Request utilizando el método __toString() de la misma.Observa que ofrece una versión simplificada con los datos más relevantes.Utiliza la documentación de la API de Symfonuy sobre el objeto Request para averiguar:

• El nombre del HOST• El método utilizado para realizar la petición• El puerto donde se ejecuta el servidor

Transcribe el código que has escrito para realizar el ejercicio.

Ejercicio 2En este ejercicio vamos a explorar el uso del parámetro _format de la Request. En muchas aplicaciones web, especialmente en los servicios web, el servidor debe devolver una respuesta que no es un fichero HTML, sino un JSON, un XML, etcétera. A veces puede ser necesario ofrecer la misma información en distintos formatos, para que el cliente la trate como mejor le venga. Con el parámetro _format del objeto Request es precisamente lo que

Ejercicios de la unidad 6

239

Page 250: CursoSymfony2

conseguimos.En este ejercicio vas a implementar una funcionalidad que consiste en devolver el listado denotas de un usuario en un archivo XML y/o en un archivo JSON, según lo que se pida en larequest.El XML tendrá el siguiente aspecto:

<notas> <nota> <id>1</id> <titulo>el titulo 1</titulo> <texto>el texto 1</texto> </nota>

<nota> <id>2</id> <titulo>el titulo 2</titulo> <texto>el texto 2</texto> </nota></notas>

Y el JSON:

{"notas": [ { "id": 1, "titulo": "el titulo 1", "texto": "el texto 1" }, { "id": 2, "titulo": "el titulo 2", "texto": "el texto 2" } ]}

Como, por lo pronto no tenemos las notas almacenadas en ningún sitio, puedesinventártelas. De hecho puedes utilizar exactamente los mismos ejemplos anteriores paraimplementar las plantillas.Comienza por crear una ruta nueva para la acción que vas a crear. Esta ruta debe incluir ensu patrón (pattern) el atributo _format. Haz que el _format por defecto sea json. Ademásestablece como requisito de la ruta que el atributo _format deba ser json o xml.

Sugerencia

Posibles patrones:/dameNotas.{_format}, dameNotas/{_format}, dameNotasEnFormato{_format}.

Ejercicios de la unidad 6

240

Page 251: CursoSymfony2

Crea la acción correspondiente. Debes recoger el parámetro _format de la Request yrenderizarla con la plantilla que le corresponda (JSON o XML).

Sugerencia

Esta línea de código te puede ayudar:return$this->render('JAMNotasFrontendBundle:Notas:dameNotas.'.$format.'.twig');

Crea las dos plantillas correspondientes, una para el JSON y otra para el XML.Transcribe el código que has escrito para resolver el ejercicio.

Ejercicio 3En este ejercicio vamos a realizar un estudio práctico de la sesión de Symfony2. Para elloharemos uso de la técnica de depuración que hemos empleado más arriba:

<?phpecho '<pre>';print_r($ObjetoAInspeccionar);echo '</pre>';exit;

Nota

Debes enviar como respuesta a los ejercicios de esta unidad, pantallazos de losresultados que vas obteniendo. Puedes volcar en un pdf todas las imágenescorrespondientes a los pantallazos o directamente comprimir dichas imágenes en unzip.

1. Primero vamos a comprobar la creación de la sesión en el servidor. Busca el directoriodonde tu servidor web crea los archivos de sesiones y, si en dicho directorio hubieraalgún o algunos archivos, elimínalos. Entonces ejecuta tu navegador (si lo tienesabierto, ciérralo previamente) y realiza una petición a la aplicación. Observa comoinmediatamente se ha creado un archivo en el directorio del servidor donde php guardalas sesiones.

2. Comprueba que en tu navegador existe una cookie asociada al servidor (localhost ennuestro caso ya que el servidor de desarrollo se encuentra en la misma máquina que elnavegador web) denominada PHPSESSID y cuyo valor coincide con el sufijo del archivode sesión del punto anterior. Ya tienes la relación entre el cliente web y el servidor. Abredicho fichero y obsérvalo con detalle.

3. Ahora vamos a ver la representación que PHP hace de dicho fichero. Para ello crea unaacción nueva y realiza una inspección a la variable $_SESSION de PHP mediante latécnica de depuración que hemos indicado al principio del ejercicio. Ahora puedesconfrontar el contenido del fichero de sesión con los datos de este array asociativo quees la forma nativa que tienen PHP para representar la sesión.

Ejercicio 3

241

Page 252: CursoSymfony2

4. A continuación realiza una inspección del objeto con el que Symfony2 representa lasesión (``$this->get('session')``). Compáralo con el resultado del punto anterior. Tedarás cuenta de que la representación de la sesión de *Symfony2 es mucho máscompleta que la que hace PHP de forma nativa.

Ejercicios de la unidad 7

Nota

Enlace a la Unidad 7: Desarrollo de la aplicación MentorNotas (III). El modelo y lapersistencia de datos.

Ejercicio 1. FixturesCuando las aplicación que estamos desarrollando lleva asociada una base de datos, lo cuales muy frecuente, resulta útil contar con una serie de datos mínimos en la base de datos. Enocasiones, incluso se requieren ciertos datos iniciales para que la aplicación funcione. Porello es preciso distribuir junto con la aplicación un script SQL con el que se pueda cargar labase de datos con dicha serie de datos mínimos. Por otro lado, en el proceso de construcciónde la aplicación vamos contaminando la base de datos con datos inservibles a medida que laprobamos. De tanto en tanto, especialmente a la hora de ejecutar pruebas funcionales,tenemos que limpiar la base de datos borrando todos los datos y volviendo a ejecutar elscript.Pues bien, aunque la estrategia del script SQL es totalmente válida para resolver losproblemas que acabamos de plantear, el uso de fixtures proporciona una manera muchomás cómoda de poblar e inicializar con datos de prueba la base de datos. Los fixtures sondatos de pruebas que son cargados en la base de datos utilizando un comando de Symfony2a través de la terminal de comandos. Las ventajas que presenta esta estrategia son:

• Al ser cargados directamente por Symfony2 (más concretamente por Doctrine2) elscript es agnóstico a la base de datos, y por tanto puede poblar cualquier tipo de basede datos compatible con Doctrine2 (en la solución propuesta al principio debemos crearun script por cada tipo de base de datos para asegurarnos que la carga se harácorrectamente).

• No tenemos que "salirnos" del entorno de Symfony2 para cargar los datos cada vez quedeseemos reinicializar la base de datos. Es decir no tenemos que cambiar al cliente SQL(*phpMyAdmin, mysql client, etcétera) y cargar el script correspondiente. Basta conejecutar el comando en cuestión. Esto agiliza mucho el proceso de reinicio de la base dedatos, y en la fase de desarrollo se agradece muchísimo.

En realidad ni Symfony2 ni el bundle de Doctrine2 que viene de serie con la distribuciónstándard de Symfony2 incorporan ninguna utilidad (comando) para la carga automática defixtures. Pero existe un bundle que sí lo hace. En este ejercicio aprenderemos a:

• incorporar un bundle externo a nuestro proyecto• utilizar el sistema de fixtures para agilizar la reinicialización de la base de datos de

nuestro proyecto con datos de pruebas.

Instalación del bundle DoctrineFixturesBundleEn primer lugar actualizamos el fichero de dependencias deps con las referencias a los repositorios donde se encuentra el código que vamos a añadir al proyecto. Añade a este

Ejercicios de la unidad 7

242

Page 253: CursoSymfony2

fichero las siguientes líneas:

...

[doctrine-fixtures] git=http://github.com/doctrine/data-fixtures.git

[DoctrineFixturesBundle] git=http://github.com/symfony/DoctrineFixturesBundle.git target=/bundles/Symfony/Bundle/DoctrineFixturesBundle

Échale un vistazo a este fichero. Observa que en él se encuentra, entre otras dependencias,el núcleo de Symfony2.¿Qué líneas referencian a dicho núcleo?Ahora vamos a bajar el código cuyas referencias acabamos de añadir. Esto se hace con laherramienta bin/vendors:Si hubieramos partido de una distribución estándard de Symfony2 sin vendors utilizariamosla herramienta de la siguiente forma:

./bin/vendors update

Pero como nuestra distribución ya venía con los vendors instalados, debemos volver ainstalarlos para que se sincronicen con los respectivos repositorios remotos:

./bin/vendors install --reinstall

Nota

Obviamente, para instalar los vendors hay que disponer de conexión a internet. Tardaun ratito, así que paciencia. Si tienes problemas al realizar la operación lee el apartadoInstalación manual del bundle (sin utilizar bin/vendor). Puedo asegurarte que es másrápido y menos propenso a fallos la instalación manual que describimos al final deesta actividad. Pero hay que decir que la manera oficial de instalar librerías deterceros (vendors) es la que estamos contando aquí.

Esta operación borra las librerías del directorio vendors, y vuelve a colocarlasdescargándolas de los repositorios y sincronizándolas con ellos.Una vez finalizada la operación puedes comprobar que la librería doctrine-fixtures hasido instalada en el directorio vendor/doctrine-fixtures, y el bundleDoctrineFixturesBundle se ha instalado en el directoriovendor/bundles/Doctrine/Bundle/FixturesBundle.¿Por qué crees que se han instalado en dichos directorios?Ahora tenemos que registrar manualmente el bundle DoctrineFixturesBundle en el ficheroAppKernel.php:

Ejercicios de la unidad 7

243

Page 254: CursoSymfony2

<?php

public function registerBundles(){ $bundles = array( // ... new Symfony\Bundle\DoctrineFixturesBundle\DoctrineFixturesBundle(), // ... ); // ...}

Y registrar el espacio de nombres de las clases del bundle en el fichero autoload.php:

<?php...$loader->registerNamespaces(array( // ... 'Doctrine\\Common\\DataFixtures' => __DIR__.'/../vendor/doctrine-fixtures/lib', 'Doctrine\\Common' => __DIR__.'/../vendor/doctrine-common/lib', // ...));

Importante

Asegurate de que registras estos nombres de espacio antes de 'Doctrine\\Common.

Y ya podemos añadir nuestros fixtures.

Creación de los fixturesLos fixtures son clases PHP que definen los datos de prueba que deseamos incorporarautomáticamente a la base de datos. Deben ubicarse en el directorio Fixtures/ORM dealgún bundle. En nuestro caso las colocaremos en el JAMNotasFrontendBundle, es decir enel directorio src/Jazzyweb/AulasMentor/NotasFrontendBundle/Fixtures/ORM.Comenzamos por añadir algunos usuarios de prueba. Creamos la clase LoadGrupoData en elficherosrc/Jazzyweb/AulasMentor/NotasFrontendBundle/Fixtures/ORM/LoadGrupoData.php.

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Fixtures\ORM; 4 5 use Doctrine\Common\Persistence\ObjectManager; 6 use Doctrine\Common\DataFixtures\AbstractFixture; 7 use Doctrine\Common\DataFixtures\OrderedFixtureInterface; 8 use Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Grupo; 9 10 class LoadGrupoData extends AbstractFixture implements OrderedFixtureInterface

Creación de los fixtures

244

Page 255: CursoSymfony2

11 {12 public function load(ObjectManager $manager)13 {14 // Grupos15 $grupo1 = new Grupo();16 $grupo1->setNombre('registrado');17 $grupo1->setRol('ROLE_REGISTRADO');18 $this->addReference('grupo-registrado', $grupo1);19 $manager->persist($grupo1);20 21 $grupo2 = new Grupo();22 $grupo2->setNombre('premium');23 $grupo2->setRol('ROLE_PREMIUM');24 $this->addReference('grupo-premium', $grupo2);25 $manager->persist($grupo2);26 27 $grupo3 = new Grupo();28 $grupo3->setNombre('administrador');29 $grupo3->setRol('ROLE_ADMIN');30 $this->addReference('grupo-admin', $grupo3);31 $manager->persist($grupo3);32 33 34 $manager->flush();35 }36 37 public function getOrder()38 {39 return 1;40 }41 }

Los fixtures son clases que extienden deDoctrine\Common\DataFixtures\AbstractFixture e implementan la interfazDoctrine\Common\DataFixtures\OrderedFixtureInterface. Deben implementar unafunción load(), donde se crean los objetos que se desean persisitir en la base de datos. Lamanera de hacer esto es como ya hemos estudiado en la unidad 7: se crea el objeto, sedefinen sus atributos y se inyecta en el servicio de persistencia mediante el métodopersist(). Finalmente, para hacer la escritura en la base de datos se usa el métodoflush().La novedad en el código anterior es el uso del método addReference() de la claseLoadGrupoData. Dicho método crea una referencia al objeto indicado en su segundoargumento con el nombre indicado en el primer argumento. Esta referencia podrá serutilizada en otras clases fixtures para realizar asociaciones entre objetos. Por ejemplo,podemos crear una clase LoadUsuarioData y en ella podemos crear y persistir objetosUsuario's asociados a los grupos que hemos creados en LoadGrupoData a través de susreferencias.Por último, cuando existen asociaciones entre objetos, el orden de creación de los mismospuede ser importante. Por ejemplo, para añadir una Nota a un Usuario, hay que crearprimero la Nota, después el Usuario y por último podemos añadir la Nota al Usuario. Porello la ejecución de las clases fixtures no siempre puede llevarse a cabo en cualquier orden.El método getOrder() de las mismas, indica al framework en qué orden debe ejecutarlas.

Creación de los fixtures

245

Page 256: CursoSymfony2

Una vez que tenemos alguna clase fixture ya podemos ejecutar el comando que cargará labase de datos con los datos especificados en ellas:

php app/console doctrine:fixtures:load --fixtures=src/Jazzyweb/AulasMentor/NotasFrontendBundle/Fixtures

Comprueba que se hayan cargado los grupos en la base de datos.Ahora vamos a crear unos cuantos de usuarios y le asociaremos algunos de los grupos quehemos creado (y referenciado) en la clase LoadGrupoData. Creamos la claseLoadUsuarioData:src/Jazzyweb/AulasMentor/NotasFrontendBundle/Fixtures/ORM/LoadUsuarioData.php.

1 <?php 2 3 namespace Jazzyweb\AulasMentor\NotasFrontendBundle\Fixtures\ORM; 4 5 use Doctrine\Common\Persistence\ObjectManager; 6 use Doctrine\Common\DataFixtures\AbstractFixture; 7 use Doctrine\Common\DataFixtures\OrderedFixtureInterface; 8 use Jazzyweb\AulasMentor\NotasFrontendBundle\Entity\Usuario; 9 10 class LoadUsuarioData extends AbstractFixture implements OrderedFixtureInterface11 {12 public function load(ObjectManager $manager)13 {14 // Usuarios15 $usuario1 = new Usuario();16 $usuario1->setNombre('Alberto');17 $usuario1->setApellidos('Einstein');18 $usuario1->setUsername('alberto');19 $usuario1->setPassword('620a7de82763527406a413ca7ee267816d332811');20 $usuario1->setEmail('[email protected]');21 $usuario1->setSalt('');22 $usuario1->setIsActive(1);23 $usuario1->setTokenRegistro('');24 25 $usuario1->addGrupo($this->getReference('grupo-registrado'));26 27 $manager->persist($usuario1);28 $this->addReference('alberto', $usuario1);29 30 $usuario2 = new Usuario();31 $usuario2->setNombre('Máximo');32 $usuario2->setApellidos('Planck');33 $usuario2->setUsername('maximo');34 $usuario2->setPassword('620a7de82763527406a413ca7ee267816d332811');35 $usuario2->setEmail('[email protected]');36 $usuario2->setSalt('');37 $usuario2->setIsActive(1);38 $usuario2->setTokenRegistro('');39 40 $usuario2->addGrupo($this->getReference('grupo-premium'));41 42 $manager->persist($usuario2);

43 $this->addReference('maximo', $usuario2);44

Creación de los fixtures

246

Page 257: CursoSymfony2

45 $usuario3 = new Usuario();46 $usuario3->setNombre('María');47 $usuario3->setApellidos('Curie');48 $usuario3->setUsername('maria');49 $usuario3->setPassword('620a7de82763527406a413ca7ee267816d332811');50 $usuario3->setEmail('[email protected]');51 $usuario3->setSalt('');52 $usuario3->setIsActive(1);53 $usuario3->setTokenRegistro('');54 55 $usuario3->addGrupo($this->getReference('grupo-admin'));56 57 $manager->persist($usuario3);58 $this->addReference('maria', $usuario3);59 60 $usuario4 = new Usuario();61 $usuario4->setNombre('Isaac');62 $usuario4->setApellidos('Newton');63 $usuario4->setUsername('isaac');64 $usuario4->setPassword('620a7de82763527406a413ca7ee267816d332811');65 $usuario4->setEmail('[email protected]');66 $usuario4->setSalt('');67 $usuario4->setIsActive(1);68 $usuario4->setTokenRegistro('');69 70 $usuario4->addGrupo($this->getReference('grupo-premium'));71 $usuario4->addGrupo($this->getReference('grupo-admin'));72 73 $manager->persist($usuario4);74 $this->addReference('isaac', $usuario4);75 76 $manager->flush();77 }78 79 public function getOrder()80 {81 return 2;82 }83 }

Observa como podemos obtener los objetos Grupo que hemos creado en la claseLoadGrupoData mediante el método getReference('nombre_de_la_referencia'). Una vezrecuperado dichos objetos podemos asociarlos a los Usuario's sin más que utilizar el métodoaddGrupo() de estos últimos. Fíjate también que hemos creado referencias de los objetosUsuario's para poder utilizarlas en el resto de las fixtures de las entidades que aún tenemosque implementar.Vuelve a ejecutar el comando:

php app/console doctrine:fixtures:load --fixtures=src/Jazzyweb/AulasMentor/NotasFrontendBundle/Fixtures

Se reinicializará la base de datos con los datos especificados en las fixtures, es decir con losgrupos de antes y los usuarios que acabamos de especificar.Y eso es todo. Ahora te toca a ti seguir el ejercicio. Implementa clases fixtures para rellenar las tablas que quedan. Introduce al menos tres notas y y tres etiquetas por cada usuario, y haz que haya usuarios en todos los grupos para poder probar bien la aplicación en las

Creación de los fixtures

247

Page 258: CursoSymfony2

siguientes unidades.Transcribe el código que has añadido para completar el ejercicio.

Instalación manual del bundle (sin utilizar bin/vendor)La reinstalación de todos los vendors puede llevar bastante tiempo y está expuesta aposibles fallos en la red. Además, como se bajan las últimas versiones de las librerías, podríaocurrir que algo dejase de funcionar. Si has tenido alguno de estos problemas te indicamosaquí como instalar únicamente las librerías necesarias para las fixtures sobre el código queya tienes de los vendors. Es decir, sin reinstalarlos todos de nuevo.Ejecuta los siguientes comandos:git clone http://github.com/doctrine/data-fixtures.git vendor/doctrine-fixturesgit clone -b 2.0 http://github.com/doctrine/DoctrineFixturesBundle.git vendor/bundles/Symfony/Bundle/DoctrineFixturesBundle

Y ya está. El registro del bundle y los espacios de nombres en el autoload.php se hacen dela misma forma.

Ejercicio 2Cambia la base de datos MySQL por una base de datos sqlite. Estas últimas son muysencillas y son muy apropiadas cuando no tenemos necesidades muy exigentes con elalmancenamiento de datos, ya que consisten en un simple fichero, es decir, no se necesitaun servidor para la base de datos.

Sugerencia

Los parámetros de conexión necesarios para este tipo de base de datos son: driver(pdo_sqlite), dbname y path. Una vez que tengas correctamente definidos dichosparámetros, debes volver a crear la base de datos y el esquema (conjunto de tablas)con los comandos pertinentes.

Ejercicio 3Modifica el bundle Jazzyweb/AulasMentor/AlimentosBundle para que se utilice el servicio depersistencia Doctrine2 en lugar del modelo que se propuso en la unidad 3. Explicaclaramente los pasos que has dado.

Ejercicios de la unidad 9

Nota

Enlace a la Unidad 9: Desarrollo de la aplicación MentorNotas (V). Seguridad -Autentificación y Autorización

Instalación manual del bundle (sin utilizar bin/vendor)

248

Page 259: CursoSymfony2

Nota

Cuando se trabaja con el sistema de seguridad de Symfony hay que tener siemprepresente si el navegador mantiene la cookie o el token de autentificación para estarseguro de que lo que estamos probando se comporta realmente como pensamos. Porello, en ocasiones, tendrás que borrar la caché del navegador o simplementereiniciarlo. También te servirá de ayuda mirar la barra de depuración del entorno dedesarrollo, pues ahí se muestra claramente si existe un usuario autentificado o no.

Ejercicio 1Cambia el cortafuegos jamn_area_protegida para que utilice autentificación basic HTTP yel proveedor de usuarios por base de datos. Prueba a cerrar la sesión mediante el enlacecerrar sesión. ¿Se ha cerrado realmente la sesión? ¿Por qué ocurre esto?

Ejercicio 2Añade al layout de la aplicación una capa que muestre el nombre del usuario y el perfil conel que ha iniciado la sesión.

Ejercicio 3El sistema de seguridad de Symfony2 permite crear cortafuegos que pueden ser"atravesados" por lo que se denomina un usuario anónimo, es decir, un usuario que no estáregistrado en ningun sistema de información pero al que se le asigna un token deautentificación y, por tanto, tiene un objeto User asociado. Para activar dicho usuarioanónimo basta con añadir la directiva anonynous: ~ al cortafuegos que queramos. Añadedicha directiva al cortafuegos jamn_area_publica y elimina de dicho cortafuegos ladirectiva security: false. Prueba ahora la aplicación. ¿En que ha cambiado elcomportamiento? ¿Le encuentras alguna ventaja? Describe la/s ventajas que hasencontrado.

Nota

Los usuarios anónimos son técnicamente usuarios autenticados. Lo que quiere decirque el método isAuthenticated() de un usuario anónimo devolverá un valor true.Para chequear si el usuario está realmente autenticado, hay que comporbar si tiene elrol IS_AUTHENTICATED_FULLY

Ejercicio 4Como se trata de un ejercicio vamos a ser un poquito caprichosos y bloquearemos lascuentas de aquellos usuarios cuyo apellido comience con la letra E o cuyo nombre terminacon la letra A. ¿Cómo implementarías esta funcionalidad?

Ejercicios de la unidad 10

Ejercicio 1

249

Page 260: CursoSymfony2

Nota

Enlace a la Unidad 10: Desarrollo de la aplicación MentorNotas (VI). Esamblando todoel frontend

Nota

En la entrega de estos ejercicios, además de las habituales explicaciones, debesenviar la versión final de tu JAMNotasFrontendBundle, con estos ejercicios realizados(se entiende)

Ejercicio 1Adapta las plantillas de la pantalla de login y del proceso de registro al diseño de lainterfazde usuario propuesto en la unidad 5 y que hemos comenzado a aplicar en esta unidad.

Ejercicio 2Implementa el proceso de contratación de una cuenta premium. Describe como lo hashecho.

Nota

En el proceso de contratación debería haber un momento en el que se hiciese uso dealgún servicio de pago electrónico, lo que se conoce como pasarela del pagos, quenormalmente es la de un banco determinado o la de paypal. En la implementación deesta funcionalidad no tienes por qué disponer de un servicio de pago electrónico, seríademasiado jaleo (aunque paypal pone a disposición de los desarrolladores un entornoy una API para simular transferencias que es muy interesante y sencillo de usar). Loque debes hacer aquí es recurrir a la idea de servicio mock, es decir de simular elservicio, haciendo que las transferencias que se hagan sean, por ejemplo, siempreválidas.

Ejercicio 3Implementa alguna funcionalidad nueva y útil que le de más valor a la aplicación.Sugerencias:

• posibilidad de intercambiar notas entre usuarios• posibilidad de enviar notas por correo electrónico• posibilidad de compartir las notas por twitter, facebook u otras redes sociales• lo que se te ocurra.

Ejercicio 1

250

Page 261: CursoSymfony2

Indices y tablas• genindex

• search

Indices y tablas

251

Page 262: CursoSymfony2

Indices y tablas• genindex

• search

1 "Patrones de Diseño" de los autores Erich Gamma, Richard Helm, RalphJohnson y John Vlissides (conocidos como The Gun of Four) es un clásico enla literatura sobre este tema.

2 http://en.wikipedia.org/wiki/Separation_of_concerns3 En el caso de Apache con PHP, que es el que nos interesa en este curso, el

servidor debe estar configurado adecuadamente para que se puedanincluir archivos PHP que están fuera del Document root*. Esto se hace conla directiva open_basedir.

4 http://es.wikipedia.org/wiki/Decorator_%28patr%C3%B3n_de_dise%C3%B1o%295 http://twig.sensiolabs.org/6 Drupal 8, phpBB4, Silex, son el nombre de algunos de los proyectos que

han optado por utilizar los componentes de Symfony2.7 Algunos de los componentes ya cuentan con documentación:

http://symfony.com/doc/current/components8 https://github.com/symfony9 Atención, activar los mensajes de depuración no significa mostrar la barra

de depuración. Se trata de arrojar al fichero de registro (directorioapp/log) mensajes que ayudan a depurar la aplicación.

10 http://martinfowler.com/11 http://www.evernote.com12 http://jquery.com13 http://layout.jquery-dev.net14 http://es.wikipedia.org/wiki/Expresi%C3%B3n_regular15 Si no tienes muy claro en que consiste el mecanismo de sesión de PHP

puedes mirar http://www.php.net/manual/es/features.sessions.php. En elcapítulo 6 del curso Desarrollo de aplicaciones web con symfony de AulasMentor también se explica con detalle el mecanismo de sesión. Puedesverlo en http://el.curso.de.symfony14

16 http://en.wikipedia.org/wiki/PHPDoc#DocBlock17 Propel es otro gran ORM que se integra fácilmente con Symfony218 Hibernate Query Language (HQL) y Java Persistence Query Language

(JPQL) son lenguajes del mismo tipo.19 Más adelante veremos que también se pueden crear formularios que no

están asociados a ningún objeto.

Indices y tablas

252