├── .gitignore ├── 5-formularios ├── data-transformers.md ├── form.png ├── form-recipe.png ├── embedded-forms.png ├── receta-dificil.png ├── embedded-forms-ii.png ├── ejercicios.md ├── validacion.md ├── form-events.md ├── field-types.md ├── formularios-embebidos.md └── conceptos-basicos.md ├── 1-introduccion ├── mvc.png ├── tree.png ├── bienvenida.png ├── bundle_tree.png ├── web-profiler.png ├── builtin-server.png ├── symfony_install.png ├── composer_install.png ├── web-debug-toolbar.png ├── lenguajes-mas-usados.png ├── symfony2_http_framework.jpg ├── ejercicios.md ├── directorios.md ├── bundles.md ├── que-es-symfony.md ├── la-evolucion-de-php-y-los-frameworks-mvc.md ├── profiler-y-consola.md ├── instalacion.md ├── ventajas-e-inconvenientes-de-los-frameworks.md └── composer.md ├── compiled ├── ppt │ ├── Tema 1.ppt │ ├── Tema 2.ppt │ ├── Tema 3.ppt │ └── Plantilla TEMAS.ppt └── processed │ ├── tema1.docx │ └── tema2.docx ├── 3-doctrine ├── recipe_show.png ├── cascade_persist_exception.png ├── ejercicios.md ├── doctrine.md ├── configuracion.md ├── lazy-eager.md ├── repositorios.md ├── dql.md ├── entidades.md └── relaciones.md ├── 4-twig ├── twig-inheritance.png ├── ejercicios.md ├── twig.md ├── extensiones.md ├── layouts-herencia.md ├── assets.md ├── include-render.md └── conceptos-basicos.md ├── 2-symfony-a-vista-de-pajaro ├── uml-Request.png ├── uml-Response.png ├── ejercicios.md ├── model.md ├── templating.md ├── request-response.md ├── fundamentos-http.md ├── routing.md └── controller.md ├── 8-seguridad ├── conceptos-avanzados.md ├── ejercicios.md ├── conceptos-basicos.md └── usuarios.md ├── 6-inyeccion ├── ejercicios.md ├── symfony2.md └── conceptos-teoricos.md ├── composer.json ├── 7-eventos ├── ejercicios.md ├── introduccion.md └── event-dispatcher.md ├── compile.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | compiled/docx/* 2 | -------------------------------------------------------------------------------- /5-formularios/data-transformers.md: -------------------------------------------------------------------------------- 1 | # Data Transformers 2 | -------------------------------------------------------------------------------- /1-introduccion/mvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/mvc.png -------------------------------------------------------------------------------- /5-formularios/form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/5-formularios/form.png -------------------------------------------------------------------------------- /1-introduccion/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/tree.png -------------------------------------------------------------------------------- /compiled/ppt/Tema 1.ppt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/compiled/ppt/Tema 1.ppt -------------------------------------------------------------------------------- /compiled/ppt/Tema 2.ppt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/compiled/ppt/Tema 2.ppt -------------------------------------------------------------------------------- /compiled/ppt/Tema 3.ppt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/compiled/ppt/Tema 3.ppt -------------------------------------------------------------------------------- /3-doctrine/recipe_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/3-doctrine/recipe_show.png -------------------------------------------------------------------------------- /4-twig/twig-inheritance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/4-twig/twig-inheritance.png -------------------------------------------------------------------------------- /1-introduccion/bienvenida.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/bienvenida.png -------------------------------------------------------------------------------- /5-formularios/form-recipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/5-formularios/form-recipe.png -------------------------------------------------------------------------------- /compiled/processed/tema1.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/compiled/processed/tema1.docx -------------------------------------------------------------------------------- /compiled/processed/tema2.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/compiled/processed/tema2.docx -------------------------------------------------------------------------------- /1-introduccion/bundle_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/bundle_tree.png -------------------------------------------------------------------------------- /1-introduccion/web-profiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/web-profiler.png -------------------------------------------------------------------------------- /5-formularios/embedded-forms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/5-formularios/embedded-forms.png -------------------------------------------------------------------------------- /5-formularios/receta-dificil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/5-formularios/receta-dificil.png -------------------------------------------------------------------------------- /compiled/ppt/Plantilla TEMAS.ppt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/compiled/ppt/Plantilla TEMAS.ppt -------------------------------------------------------------------------------- /1-introduccion/builtin-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/builtin-server.png -------------------------------------------------------------------------------- /1-introduccion/symfony_install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/symfony_install.png -------------------------------------------------------------------------------- /1-introduccion/composer_install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/composer_install.png -------------------------------------------------------------------------------- /1-introduccion/web-debug-toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/web-debug-toolbar.png -------------------------------------------------------------------------------- /5-formularios/embedded-forms-ii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/5-formularios/embedded-forms-ii.png -------------------------------------------------------------------------------- /1-introduccion/lenguajes-mas-usados.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/lenguajes-mas-usados.png -------------------------------------------------------------------------------- /1-introduccion/symfony2_http_framework.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/1-introduccion/symfony2_http_framework.jpg -------------------------------------------------------------------------------- /3-doctrine/cascade_persist_exception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/3-doctrine/cascade_persist_exception.png -------------------------------------------------------------------------------- /2-symfony-a-vista-de-pajaro/uml-Request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/2-symfony-a-vista-de-pajaro/uml-Request.png -------------------------------------------------------------------------------- /2-symfony-a-vista-de-pajaro/uml-Response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlescliment/curso-symfony2/HEAD/2-symfony-a-vista-de-pajaro/uml-Response.png -------------------------------------------------------------------------------- /8-seguridad/conceptos-avanzados.md: -------------------------------------------------------------------------------- 1 | ## Avanzado 2 | 3 | Seguridad en controladores 4 | Seguridad en plantillas 5 | Personalizar el directorio donde se almacenan las sesiones 6 | HTTPS 7 | ACL 8 | -------------------------------------------------------------------------------- /6-inyeccion/ejercicios.md: -------------------------------------------------------------------------------- 1 | # Ejercicios 2 | 3 | **Ejercicio 1:** 4 | 5 | Además de la inyección de dependencias, ¿de qué otros modos puede conseguirse la inversión de control?. ¿Podrías escribir ejemplos en PHP o pseudocódigo? 6 | 7 | 8 | **Ejercicio 2:** 9 | 10 | Examina los controladores de tu aplicación. Modifica el código de tu controlador más complejo extrayendo la lógica a un servicio. 11 | -------------------------------------------------------------------------------- /5-formularios/ejercicios.md: -------------------------------------------------------------------------------- 1 | # Ejercicios 2 | 3 | **Ejercicio 1:** 4 | Crea las vistas necesarias para crear y editar las entidades de tu aplicación web. 5 | 6 | **Ejercicio 2:** 7 | Añade reglas de validación a al menos una de las entidades. 8 | 9 | **Ejercicio 3:** 10 | Busca en la documentación oficial de Symfony 2 qué son los _Data Transformers_ y trata de explicarlo con tus propias palabras. 11 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carlescliment/curso-symfony2", 3 | "description": "Curso de Symfony 2.", 4 | "keywords": ["curso", "symfony2"], 5 | "license": "GPL-2.0", 6 | "authors": [ 7 | { 8 | "name": "Carles Climent Granell", 9 | "email": "carlescliment@gmail.com", 10 | "homepage": "http://www.carlescliment.com", 11 | "role": "Developer" 12 | } 13 | ], 14 | } 15 | 16 | -------------------------------------------------------------------------------- /3-doctrine/ejercicios.md: -------------------------------------------------------------------------------- 1 | # Ejercicios 2 | 3 | **Ejercicio 1:** 4 | 5 | Tu web empieza a ser bastante grande y piensas que tienes problemas de rendimiento con la base de datos. ¿Qué problemas piensas que pueden darse? ¿Qué pasos darías para encontrar estos problemas? ¿Cómo los resolverías? 6 | 7 | 8 | **Ejercicio 2:** 9 | 10 | Investiga cómo resuelve Doctrine el problema de las migraciones. Crea un _data fixture_ con datos iniciales para tu proyecto. ¿Con qué comando lo cargas desde el terminal? 11 | 12 | -------------------------------------------------------------------------------- /7-eventos/ejercicios.md: -------------------------------------------------------------------------------- 1 | # Ejercicios 2 | 3 | **Ejercicio 1:** 4 | 5 | ¿Sabrías explicar qué es el patrón Observer? ¿Y el patrón Mediator? ¿Qué ventajas y/o desventajas plantean? ¿Cuál de estos patrones es el implementado por el Componente de Inyección de Dependencias de Symfony 2? 6 | 7 | **Ejercicio 2:** 8 | 9 | Averigua la diferencia entre un `Event Listener` y un `Event Subscriber` en Symfony 2. 10 | 11 | 12 | **Ejercicio 3:** 13 | 14 | Utilizando [Monolog](http://symfony.com/doc/current/cookbook/logging/monolog.html) y el sistema de eventos de Symfony 2, implementa una clase que registre en un archivo las acciones principales de tu aplicación. 15 | -------------------------------------------------------------------------------- /2-symfony-a-vista-de-pajaro/ejercicios.md: -------------------------------------------------------------------------------- 1 | # Ejercicios 2 | 3 | **Ejercicio 1:** 4 | 5 | Investiga sobre REST. ¿Cuáles son los principios básicos de una aplicación RESTFful? ¿En qué tipo de aplicaciones crees que resulta más interesante, y en cuáles menos? 6 | 7 | 8 | **Ejercicio 2:** 9 | 10 | Imagina que en tu aplicación tienes una entidad "Receta" de manera que accedes a ella según id: 11 | 12 | recipes/{id} 13 | 14 | Para mejorar el SEO de tu web, quieres utilizar urls amigables: 15 | 16 | recipes/pollo-al-pil-pil 17 | 18 | ¿Cómo podrías extender el sistema de enrutado de Symfony para lograrlo? 19 | 20 | Si es viable, implementa la funcionalidad en tu proyecto. -------------------------------------------------------------------------------- /1-introduccion/ejercicios.md: -------------------------------------------------------------------------------- 1 | # Ejercicios 2 | 3 | **Ejercicio 1:** 4 | 5 | Escoge dos web frameworks de cualquier lenguaje. Tómate un tiempo leyendo sus características principales. 6 | ¿Cuáles aparecen más destacadas? ¿Qué puntos encuentras en común, y en cuales crees se diferencian más ambos frameworks? 7 | 8 | 9 | **Ejercicio 2:** 10 | 11 | Piensa una web que quieras hacer a lo largo de este curso. ¡Será tu proyecto!. Una vez lo tengas claro, créate un repositorio en Github con nombre apropiado. Sube ahí una versión estándar de Symfony. Modifica el archivo composer.json de acuerdo al nombre de tu proyecto y añádete como autor/a. 12 | Publica tu proyecto en Packagist. 13 | 14 | -------------------------------------------------------------------------------- /2-symfony-a-vista-de-pajaro/model.md: -------------------------------------------------------------------------------- 1 | # El Modelo 2 | 3 | El modelo de nuestra aplicación es todo aquello que representa a las necesidades de negocio. La mayoría de las aplicaciones disponen de una capa de persistencia para almacenar las distintas entidades en la base de datos. En una aplicación típica de Symfony 2, la interacción con la base de datos se abstrae utilizando [Object Relational Mappers](http://en.wikipedia.org/wiki/Object-relational_mapping) como [Doctrine 2](http://www.doctrine-project.org/). Estas herramientas, así como cualquier otra con la que interactúe la aplicación, son de libre elección. 4 | 5 | Por lo tanto Symfony no ofrece guías respecto al modelado del negocio, la organización de las clases que la conforman y las decisiones en cuanto a la arquitectura y diseño del negocio. 6 | -------------------------------------------------------------------------------- /4-twig/ejercicios.md: -------------------------------------------------------------------------------- 1 | # Ejercicios 2 | 3 | **Ejercicio 1:** 4 | Añade algunas vistas a tu aplicación siguiendo los ejemplos expuestos en el tema. Puedes utilizar tu propio diseño o descargarte un framework CSS como Foundation, Twitter Bootstrap o HTML5 Boilerplate, entre otros. 5 | 6 | 7 | **Ejercicio 2:** 8 | En el footer queremos mostrar nuestra dirección e-mail de contacto, pero para facilitar que la web sea utilizada por otros programadores queremos que la dirección sea configurable. 9 | 10 | Añade un parámetro `contact_email` a la configuración de tu aplicación Symfony. Averigua cómo añadir ese parámetro a las variables globales de Twig para que cualquier plantilla pueda acceder a su valor. Al final, tu plantilla debería contener un fragmento similar al siguiente: 11 | 12 | ``` 13 | 16 | ``` 17 | -------------------------------------------------------------------------------- /3-doctrine/doctrine.md: -------------------------------------------------------------------------------- 1 | # ¿Qué es Doctrine? 2 | 3 | Doctrine (en la actualidad en su versión 2) es un [Object-Relational-Mapper](http://en.wikipedia.org/wiki/Object-relational_mapping) en PHP. Los ORMs proporcionan una capa de abstracción orientada a objetos sobre la base de datos. 4 | 5 | Al ocultar la implementación subyacente, los ORMs facilitan la portabilidad en el caso de cambios en el [SGDB](http://es.wikipedia.org/wiki/Sistema_de_gesti%C3%B3n_de_bases_de_datos). Además, proporcionan los métodos básicos para la carga, manipulación y persistencia de los datos. 6 | 7 | Los ORMs pueden suponer un problema de rendimiento si no se utilizan con cuidado, por lo que conviene conocerlos en detalle antes de optar por su implantación en una aplicación. 8 | 9 | Aunque **Doctrine no es un componente de Symfony**, la versión estandar del framework incluye esta biblioteca entre sus vendors. 10 | 11 | -------------------------------------------------------------------------------- /8-seguridad/ejercicios.md: -------------------------------------------------------------------------------- 1 | # Ejercicios 2 | 3 | **Ejercicio 1:** 4 | 5 | Crea una zona segura de administración en tu aplicación que requiera un usuario autenticado. Puede ser una url o la web completa. Utiliza el provider `in_memory` especificando la lista de usuarios y passwords disponibles en tu archivo `security.yml`. Utiliza autenticación básica HTTP. 6 | 7 | 8 | **Ejercicio 2:** 9 | 10 | Modifica tu aplicación para usar un formulario de acceso en lugar de autenticación HTTP. 11 | 12 | **Ejercicio 3:** 13 | 14 | Modifica tu aplicación para usar usuarios de la base de datos en lugar del provider `in_memory`. Crea algunos usuarios directamente en la base de datos (puedes usar conversores online sha1) para comprobar que funciona. 15 | 16 | 17 | **Ejercicio 4:** 18 | 19 | Crea un formulario de registro en el que los usuarios puedan registrarse en tu aplicación. Los usuarios creados tendrán el rol `ROLE_USER`. 20 | 21 | **Ejercicio 5:** 22 | 23 | Crea una sección para administrar usuarios. Esta sección solo estará disponible para el rol `ROLE ADMIN` y permitirá modificar los roles de cualquier usuario y marcarlos como inactivos. 24 | -------------------------------------------------------------------------------- /4-twig/twig.md: -------------------------------------------------------------------------------- 1 | # ¿Qué es Twig? 2 | 3 | [Twig](http://twig.sensiolabs.org/) es un motor de plantillas para PHP desarrollado por la empresa que creó Symfony, SensioLabs. 4 | 5 | Los motores de plantillas proporcionan un lenguaje simplificado para las vistas y permiten un código más elegante. Además, facilitan la manipulación por parte de diseñadores y maquetadores sin conocimientos específicos del lenguaje. 6 | 7 | Twig reúne las siguientes características: 8 | 9 | - Uso de variables 10 | - Uso de funciones y métodos 11 | - Inclusión de vistas parciales 12 | - Condicionales 13 | - Bucles 14 | - Asignaciones 15 | - Manejo de errores y excepciones 16 | - Herencia 17 | 18 | 19 | Existen multitud de motores en el mercado para PHP y otros lenguajes. En el artículo de wikipedia [Comparison of web template engines](http://en.wikipedia.org/wiki/Comparison_of_web_template_engines) se describen los más representativos. 20 | 21 | Plantilla en PHP: 22 | 23 | ```php 24 | 33 | ``` 34 | 35 | Plantilla en Twig: 36 | 37 | ```twig 38 | 43 | ``` 44 | 45 | Plantilla en HAML: 46 | 47 | ```haml 48 | %ul#navigation 49 | - navigation.each do |item| 50 | %li 51 | %a{ :href => item['href'] }= item['caption'] 52 | ``` 53 | -------------------------------------------------------------------------------- /3-doctrine/configuracion.md: -------------------------------------------------------------------------------- 1 | # Configuración 2 | 3 | Doctrine necesita conocer algunos datos sobre la base de datos. Dónde está, cómo acceder a ella, qué driver utilizar y el juego de caracteres elegido. Todos estos parámetros se configuran en los archivos `config.yml` y `parameters.yml`. 4 | 5 | ```config.yml 6 | // app/config/config.yml 7 | # Doctrine Configuration 8 | doctrine: 9 | dbal: 10 | driver: %database_driver% 11 | host: %database_host% 12 | port: %database_port% 13 | dbname: %database_name% 14 | user: %database_user% 15 | password: %database_password% 16 | charset: UTF8 17 | 18 | orm: 19 | auto_generate_proxy_classes: %kernel.debug% 20 | auto_mapping: true 21 | ``` 22 | 23 | ```parameters.yml 24 | // app/config/parameters.yml 25 | parameters: 26 | database_driver: pdo_mysql 27 | database_host: 127.0.0.1 28 | database_port: null 29 | database_name: symfony 30 | database_user: root 31 | database_password: mypassword 32 | ``` 33 | 34 | El driver elegido determinará qué base de datos estamos utilizando. Las opciones son: 35 | - `pdo_mysql`: MySQL 36 | - `pdo_sqlite`: SQLite 37 | - `pdo_pgsql`: PostgreSQL 38 | - `pdo_oci`: Oracle 39 | - `pdo_sqlsrv`: Microsoft SQL Server 40 | - `oci8`: Oracle con la extensión oci8 de PHP. 41 | 42 | Una vez establecidos los parámetros de nuestra base de datos, disponemos de algunos comandos de consola para administrar la base de datos. 43 | 44 | - Crear la base de datos: 45 | `php app/console doctrine:database:create` 46 | 47 | - Eliminar la base de datos: 48 | `php app/console doctrine:database:drop --force` 49 | 50 | -------------------------------------------------------------------------------- /1-introduccion/directorios.md: -------------------------------------------------------------------------------- 1 | # Organización de directorios 2 | 3 | Una instalación de Symfony tiene una estructura similar a la siguiente: 4 | 5 | ![Directorios](tree.png "Directorios") 6 | 7 | En `/app` se encuentran los archivos correspondientes a la aplicación: 8 | 9 | * `AppKernel.php` define qué _bundles_ hay instalados en nuestra instalación. Cada vez que queramos instalar un nuevo bundle deberemos incluirlo en el método `registerBundles()` de esta clase. 10 | * `config` almacena los distintos archivos de configuración de la aplicación. Esto incluye los **parámetros** de la aplicación según entorno, los **servicios** incluídos y los **enrutadores** y **firewalls** instalados. 11 | * `cache` es el directorio por defecto en el que Symfony almacena algunos datos para optimizar el rendimiento de la caché. 12 | * `logs` contiene los registros de actividad para cada entorno. 13 | * `console` es un binario que contiene la consola de Symfony, útil para realizar algunas operaciones. La veremos en próximos capítulos. 14 | * `Resources` almacena recursos de distinta índole, ya sean **plantillas**, **fixtures** o librerías de diversa índole. 15 | 16 | 17 | En `/bin` se almacenan ejecutables destinados a ser invocados desde terminal. 18 | 19 | `/src` contiene nuestros propios bundles. Es decir, los componentes (controladores, rutas, entidades, modelos, vistas...) escritos por nosotros. 20 | 21 | Por último, en `/web` se deposita la parte pública de la aplicación web. Hojas de estilo, Javascripts y elementos estáticos como imágenes o vídeos. 22 | 23 | 24 | Casi todos los elementos de esta estructura de directorios pueden configurarse, tal y como se explica en la [documentación oficial](http://symfony.com/doc/current/cookbook/configuration/override_dir_structure.html). -------------------------------------------------------------------------------- /2-symfony-a-vista-de-pajaro/templating.md: -------------------------------------------------------------------------------- 1 | # La vista 2 | 3 | En capítulos anteriores hemos visto que el enrutado se encarga de distribuir las peticiones entre los distintos controladores. Los controladores, a su vez, extraen la información necesaria de la petición y construyen con ellos una respuesta. Aunque sería posible devolver HTML directamente desde el controlador, es recomendable delegar esta función al motor de plantillas. 4 | 5 | 6 | El motor por defecto en Symfony 2 es **twig**. 7 | 8 | 9 | ```base.html.twig 10 | 11 | 12 | 13 | Welcome to Symfony! 14 | 15 | 16 |

{{ page_title }}

17 | 18 | 23 | 24 | 25 | ``` 26 | 27 | En twig, las llaves dobles `{{ ... }}` se utilizan para mostrar una variable, mientras que la combinación de llave y símbolo de porcentaje `{% ... %}` simboliza el uso de una expresión. 28 | 29 | 30 | Tal y como vimos en el capítulo de controladores, para renderizar una plantilla desde un controlador utilizamos el método `render()`. 31 | 32 | ``` 33 | public function showAction($id) 34 | { 35 | // ... 36 | return $this->render('MyRecipesBundle:Recipe:show.html.twig', array( 37 | 'recipe' => $recipe, 38 | )); 39 | } 40 | ``` 41 | 42 | Symfony buscará la plantilla show.html.twig indicada en la ruta `src/My/RecipesBundle/Resources/views/Recipe/show.html.twig`. 43 | 44 | Baste con esta pequeña introducción a las vistas por ahora, más adelante dedicaremos un tema completo a Twig y el renderizado de plantillas. 45 | -------------------------------------------------------------------------------- /3-doctrine/lazy-eager.md: -------------------------------------------------------------------------------- 1 | # Lazy y Eager 2 | 3 | La forma en la que Doctrine gestiona la carga de una entidad y las entidades con las que se relaciona tiene un gran impacto en el rendimiento de la aplicación. Por ello conviene estudiar detenidamente cuál es el comportamiento más conveniente. 4 | 5 | ## EAGER 6 | 7 | ```yml 8 | My\RecipesBundle\Entity\Recipe: 9 | manyToOne: 10 | author: 11 | fetch: EAGER 12 | # ... 13 | # ... 14 | ``` 15 | 16 | La carga EAGER implica que cuando se recupera una entidad de la base de datos, automáticamente se cargarán todas las entidades relacionadas con él. De este modo, al cargar un `Recipe` se cargará automáticamente el objeto `Author` asociado, realizándose dos consultas a la base de datos. 17 | 18 | ## LAZY 19 | 20 | ```yml 21 | My\RecipesBundle\Entity\Recipe: 22 | manyToOne: 23 | author: 24 | fetch: LAZY 25 | # ... 26 | # ... 27 | ``` 28 | 29 | En la carga LAZY - por defecto - los objetos se recuperan de la base de datos en tiempo de ejecución. El objeto `Author` asociado a `Recipe` no será cargado hasta el momento en que éste sea necesario, por ejemplo cuando ejecutemos `$recipe->getAuthor()`. 30 | 31 | Si la relación de `Author` a `Recipe` se define como `LAZY`, entonces se cargará la colección completa la primera vez que sea accedida. 32 | 33 | ``` 34 | // Carga la colección completa de recetas. 35 | $author->getRecipes(); 36 | ``` 37 | 38 | ## EXTRA_LAZY 39 | 40 | La carga EXTRA_LAZY fue introducida en la versión 2.1 de Doctrine. Presenta algunos cambios sobre la carga LAZY, dado que evita la carga de la colección completa en llamadas a los siguientes métodos: 41 | 42 | ``` 43 | // No cargan la colección completa 44 | 45 | Collection#contains($entity); 46 | Collection#count(); 47 | Collection#slice($offset, $length); 48 | Collection#add($entity); 49 | Collection#offsetSet($key, $entity); 50 | ``` 51 | 52 | -------------------------------------------------------------------------------- /3-doctrine/repositorios.md: -------------------------------------------------------------------------------- 1 | # Repositorios 2 | 3 | En [el capítulo anterior](dql.md) se explica cómo realizar consultas complejas utilizando **DQL**. Pero, ¿dónde y cómo organizar consultas?. 4 | 5 | En general, toda las consultas a la base de datos deberían organizarse en repositorios de Doctrine. 6 | 7 | ## Qué es un repositorio 8 | 9 | Un repositorio es una clase que agrupa un conjunto de métodos para realizar consultas sobre una determinada entidad. Cuando se ejecuta el método `getRepository('MyRecipesBundle:Author')`, Doctrine comprobará en primer lugar si se ha definido un repositorio para la entidad `Author`. De no ser así, construye un repositorio base de clase `Doctrine\ORM\EntityRepository`. 10 | 11 | ## Crear un repositorio 12 | 13 | Para crear un repositorio sobre la clase `Author` escribiremos una clase `AuthorRepository` en el directorio `src/My/RecipesBundle/Repository`. 14 | 15 | ``` 16 | namespace My\RecipesBundle\Repository; 17 | 18 | use Doctrine\ORM\EntityRepository; 19 | 20 | class AuthorRepository extends EntityRepository { 21 | 22 | public function findTopChefs() { 23 | return $this->getEntityManager() 24 | ->createQuery('SELECT a 25 | FROM MyRecipesBundle:Author a 26 | JOIN a.recipes r 27 | WHERE r.difficulty = :difficulty') 28 | ->setParameter('difficulty', 'difícil') 29 | ->getResult(); 30 | } 31 | } 32 | ``` 33 | 34 | El siguiente paso será indicar a Doctrine que la entidad `Author` estará gestionada por nuestro propio repositorio. 35 | 36 | ``` 37 | My\RecipesBundle\Entity\Author: 38 | type: entity 39 | repositoryClass: My\RecipesBundle\Repository\AuthorRepository 40 | # ... 41 | ``` 42 | 43 | 44 | Y ya sólo queda utilizar el nuevo método implementado. 45 | 46 | 47 | ``` 48 | /** 49 | * @Template() 50 | */ 51 | public function topChefsAction() 52 | { 53 | $repository = $this->getDoctrine()->getRepository('MyRecipesBundle:Author'); 54 | $chefs = $repository->findTopChefs(); 55 | return array('chefs' => $chefs); 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /compile.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | themes = { "1-introduccion" : 4 | ["la-evolucion-de-php-y-los-frameworks-mvc", 5 | "ventajas-e-inconvenientes-de-los-frameworks", 6 | "que-es-symfony", 7 | "instalacion", 8 | "directorios", 9 | "bundles", 10 | "composer", 11 | "profiler-y-consola", 12 | "ejercicios"], 13 | "2-symfony-a-vista-de-pajaro" : 14 | ["fundamentos-http", 15 | "request-response", 16 | "routing", 17 | "controller", 18 | "templating", 19 | "model", 20 | "ejercicios"], 21 | "3-doctrine" : 22 | ["doctrine", 23 | "configuracion", 24 | "entidades", 25 | "relaciones", 26 | "lazy-eager", 27 | "dql", 28 | "repositorios", 29 | "ejercicios"], 30 | "4-twig" : 31 | ["twig", 32 | "conceptos-basicos", 33 | "layouts-herencia", 34 | "include-render", 35 | "assets", 36 | "extensiones"], 37 | "5-formularios" : 38 | ["conceptos-basicos", 39 | "validacion", 40 | "field-types", 41 | "formularios-embebidos", 42 | "form-events"], 43 | "6-inyeccion" : 44 | ["conceptos-teoricos", 45 | "symfony2", 46 | "ejercicios"], 47 | "7-eventos" : 48 | ["introduccion", 49 | "event-dispatcher", 50 | "ejercicios"], 51 | "8-seguridad" : 52 | ["conceptos-basicos", 53 | "usuarios", 54 | "ejercicios"], 55 | } 56 | 57 | def convert(theme, files): 58 | full_path_files = [full_path(theme, file) for file in files] 59 | print "Compiling", theme 60 | execute_conversion("docx", theme, full_path_files); 61 | 62 | def full_path(theme, file): 63 | return theme + "/" + file + ".md" 64 | 65 | def execute_conversion(doctype, theme, files): 66 | joined = ' '.join(files) 67 | command = "pandoc -o ./compiled/%s/%s.%s %s -t %s"%(doctype, theme, doctype, joined, doctype); 68 | os.system(command) 69 | 70 | 71 | for theme, files in themes.items() : 72 | convert(theme, files) 73 | -------------------------------------------------------------------------------- /4-twig/extensiones.md: -------------------------------------------------------------------------------- 1 | # Extensiones 2 | 3 | Las extensiones de Twig permiten encapsular porciones de código en clases reusables y bien organizadas. Por ejemplo, imaginemos el siguiente código: 4 | 5 | ```html 6 |

7 | {{ ... }} 8 |

9 | ``` 10 | 11 | El código anterior asigna una clase CSS en función de la dificultad de la receta. Esta operación es bastante común y es posible que tengamos que repetirla en otras construcciones HTML, como elementos de un listado. 12 | 13 | ```html 14 | 19 | ``` 20 | 21 | Para encapsular el código en clases reusables, una opción es crear una extensión. 22 | 23 | 24 | ## Extension Class 25 | 26 | Empezaremos creando la extensión de nuestro bundle. 27 | 28 | ```php 29 | // src/My/RecipesBundle/Twig/RecipesExtension.php 30 | 31 | namespace My\RecipesBundle\Twig; 32 | 33 | use My\RecipesBundle\Entity\Recipe; 34 | 35 | class RecipesExtension extends \Twig_Extension 36 | { 37 | public function getFilters() 38 | { 39 | return array( 40 | new \Twig_SimpleFilter('cssClass', array($this, 'cssClass')), 41 | ); 42 | } 43 | 44 | public function cssClass($recipe) 45 | { 46 | if ($recipe->isEasy()) { 47 | return 'easy'; 48 | } 49 | if ($recipe->isNormal()) { 50 | return 'normal'; 51 | } 52 | if ($recipe->isHard()) { 53 | return 'hard'; 54 | } 55 | return 'unknown'; 56 | } 57 | 58 | public function getName() 59 | { 60 | return 'my_recipes_extension'; 61 | } 62 | } 63 | 64 | ## Registrar la extensión 65 | 66 | Para registrar una extensión basta con exponerla como servicio en el archivo `services.yml` del bundle y añadirle el tag `twig.extension` tal y como se muestra a continuación: 67 | 68 | ```yaml 69 | # src/My/RecipesBundle/Resources/config/services.yml 70 | services: 71 | my.twig.recipes_extension: 72 | class: My\RecipesBundle\Twig\RecipesExtension 73 | tags: 74 | - { name: twig.extension } 75 | ``` 76 | 77 | ## Usar la extensión 78 | 79 | Ya podemos utilizar el filtro `cssClass` de nuestra extensión y limpiar las plantillas del viejo código replicado. 80 | 81 | ```html 82 |

83 | {{ ... }} 84 |

85 | ``` 86 | 87 | ```html 88 | 93 | ``` 94 | -------------------------------------------------------------------------------- /1-introduccion/bundles.md: -------------------------------------------------------------------------------- 1 | # Bundles 2 | 3 | En Symfony2, un bundle es un conjunto de archivos y directorios cuyo objetivo es proporcionar funcionalidad al sistema. Entre estos archivos podemos encontrar modelos, entidades, archivos de configuración, plantillas, javascripts y hojas de estilo, entre otros. 4 | 5 | ## Generación automática de bundles 6 | 7 | El primer paso necesario para extender la funcionalidad de nuestra instalación Symfony será crear un bundle personalizado. Aunque podemos crearlo manualmente, la consola dispone de un práctico comando para ello. 8 | 9 | ``` 10 | $ php app/console generate:bundle --namespace=My/RecipesBundle --format=yml 11 | ``` 12 | 13 | En el comando estamos pasando dos parámetros. El primero definirá el espacio de nombres en el que se alojarán las clases y funciones del bundle. El parámetro format especifica el formato de los archivos de configuración. Además de `yml` podemos elegir `xml` y `php`. 14 | 15 | Cuando lo ejecutemos iniciaremos un diálogo via terminal donde se nos permitirán una serie de personalizaciones. De momento elegiremos las opciones por defecto. Al finalizar, se nos mostrará un texto similar al siguiente: 16 | 17 | `You can now start using the generated code!` 18 | 19 | El comando habrá realizado las siguientes acciones: 20 | - Crear un nuevo directorio `src/My/RecipesBundle` que contendrá algunas clases autogeneradas. 21 | 22 | - Actualizar `app/config/routing.yml` añadiendo las rutas del bundle al sistema de enrutado. 23 | ``` 24 | my_recipes: 25 | resource: "@MyRecipesBundle/Resources/config/routing.yml" 26 | prefix: / 27 | ``` 28 | 29 | - Activar el nuevo bundle en `app/AppKernel.php` 30 | ``` 31 | $bundles = array( 32 | //... 33 | new My\RecipesBundle\MyRecipesBundle(), 34 | ); 35 | ``` 36 | 37 | 38 | ## Organización de los bundles 39 | Un bundle autogenerado por la consola presenta la siguiente estructura: 40 | 41 | ![Árbol de directorios de un bundle](bundle_tree.png "Árbol de directorios de un bundle") 42 | 43 | 44 | * `MyRecipesBundle.php` define el bundle extendiendo la clase `Bundle` de Symfony 2. Esta es la clase que se instancia en AppKernel al registar el bundle en la instalación Symfony2. En ella podemos personalizar algunos parámetros como el espacio de nombres o la extensión a utilizar en el contenedor de inyección de dependencias, que veremos más adelante. 45 | * `DependencyInjection` configurar el bundle en más detalle en el contenedor de inyección y realizar algunas operaciones previas al compilado del mismo. 46 | * `Controller` aloja a los controladores de la aplicación. 47 | * `Resources` almacena información de enrutado, servicios proporcionados por el bundle y plantillas. También puede contener archivos de traducción, documentación o archivos estáticos css y js. 48 | * `Tests` contendrá los tests automáticos que escribamos para el bundle. 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /5-formularios/validacion.md: -------------------------------------------------------------------------------- 1 | # Validación 2 | Aunque la documentación oficial sobre [validación de formularios](http://symfony.com/doc/current/book/forms.html#form-validation) proporciona una extensísima información, aquí resumiremos los puntos más importantes. 3 | 4 | 5 | ## Constraints 6 | 7 | El servicio de validación de symfony permite validar cualquier clase a la que se haya sometido a reglas. Estas reglas reciben el nombre de **constraints**. 8 | 9 | Podemos añadir constraints utilizando anotaciones, archivos xml o archivos yml. Por mantener la coherencia con el resto del material utilizaremos el tercer formato. Implementar un par de constraints de ejemplo sobre la clase `Author`: 10 | 11 | ```yaml 12 | # src/My/RecipesBundle/Resources/config/validation.yml 13 | My\RecipesBundle\Entity\Author: 14 | properties: 15 | name: 16 | - NotBlank: ~ 17 | surname: 18 | - NotBlank: ~ 19 | ``` 20 | 21 | Una vez establecidas las constraints, desde cualquier controlador o clase con acceso a la capa de servicios podemos utilizar el validador. 22 | 23 | ```php 24 | $author = new Author('Iñaki', ''); 25 | $validator = $this->get('validator'); 26 | $errors = $validator->validate($author); 27 | ``` 28 | 29 | En el caso anterior estamos construyendo un objeto `Author` con el atributo `surname` vacío, por lo que `$validator>validate($author)` devolverá un array indicando el error encontrado. En caso contrario, devolverá un array vacío. 30 | 31 | Cuando invocamos el método `isValid()` de un formulario, internamente se está utilizando el servicio de validación para validar la clase subyacente. Además, el componente de formularios es capaz de interpretar el valor de retorno y contextualizarlo en el campo del formulario que corresponda. 32 | 33 | Los constraints pueden aplicarse a atributos del objeto y a métodos. Los métodos proporcionan mayor potencia en la validación al introducir nuestra propia lógica. Por ejemplo, para vincular un constraint a un método `isValid()` deberemos añadir una sección `getters` al archivo de validación. 34 | 35 | ```yaml 36 | # src/My/RecipesBundle/Resources/config/validation.yml 37 | My\RecipesBundle\Entity\Author: 38 | getters: 39 | valid: 40 | - "True": { message: "No aceptamos recetas del autor" } 41 | # ... 42 | ``` 43 | 44 | ```php 45 | // src/My/RecipesBundle/Entity/Author.php 46 | 47 | class Author 48 | { 49 | // ... 50 | public function isValid() 51 | { 52 | return $this->__toString() != 'Karlos Arguiñano'; 53 | } 54 | } 55 | ``` 56 | 57 | 58 | El servicio de validación proporciona una larga lista de constraints con distintos propósitos. Para mayor información consultad la [documentación oficial](http://symfony.com/doc/current/book/validation.html#constraints). Si los constraints proporcionados no fuesen suficiente, el componente de validación permite [definir constraints personalizados](http://symfony.com/doc/current/cookbook/validation/custom_constraint.html). 59 | -------------------------------------------------------------------------------- /4-twig/layouts-herencia.md: -------------------------------------------------------------------------------- 1 | # Layouts y herencia 2 | 3 | Todas las aplicaciones web disponen de al menos un layout. El layout define la estructura fundamental de la web, los estilos, bloques, menús y otros elementos que serán compartidos por las distintas vistas de la aplicación. En Twig, las plantillas pueden compartir un mismo layout a través de la herencia. La herencia en twig no se limita a un nivel, y es bastante común utilizar hasta tres niveles de herencia. 4 | 5 | ![Herencia de tres niveles](twig-inheritance.png "Herencia de tres niveles") 6 | 7 | 8 | ## Layouts 9 | Los layouts de las aplicaciones Symfony se almacenan en `app/Resources/views`. Si disponemos de un solo layout, por convenio suele denominarse `base.html.twig`, aunque no estamos obligados a ello. La instalación estándar de Symfony proporciona un layout básico. 10 | 11 | 12 | ```html 13 | 14 | 15 | 16 | 17 | 18 | {% block title %}Welcome!{% endblock %} 19 | {% block stylesheets %}{% endblock %} 20 | 21 | 22 | 23 | {% block body %}{% endblock %} 24 | {% block javascripts %}{% endblock %} 25 | 26 | 27 | ``` 28 | 29 | El layout no extiende a ninguna otra plantilla. Al contrario, su cometido es facilitar una estructura común a la que puedan adherirse el resto. El elemento más importante de un layout es el tag `block`. 30 | 31 | 32 | ## Herencia 33 | 34 | En el layout anterior podemos ver cuatro bloques. Las plantillas que extiendan el layout pueden personalizar el contenido de cada uno de ellos, aunque no están obligadas a hacerlo. Si no lo hacen, se mostrará el contenido de la plantilla padre. A continuación se muestra un ejemplo de una plantilla extendiendo el layout. 35 | 36 | ```html 37 | 38 | {% extends '::base.html.twig' %} 39 | 40 | {% block title %}{{ recipe.name }}{% endblock %} 41 | 42 | {% block body %} 43 |

{{ recipe.name }}

44 |

Por {{ recipe.author }}

45 | 46 |

{{ recipe.description }}

47 | 48 |

Ingredientes

49 | 56 | {% endblock %} 57 | ``` 58 | 59 | El tag `extends` debe ocupar siempre la primera línea de una plantilla hija. La sintaxis que utiliza se basa en el siguiente patrón: `{bundle}:{controlador}:{plantilla}`. En este caso no se ha definido un bundle ni un controlador, por lo que la plantilla se buscará en el directorio de la aplicación: `app/Resources/views/`. 60 | 61 | El orden en el que se definan los bloques sobreescritos no es importante. En este caso, la plantilla ha sobreescrito los bloques `title` y `body`, pero ha dejado los bloques `stylesheets` y `javascripts` intactos. 62 | 63 | Es posible añadir información a un bloque sin sobreescribir completamente su contenido. Para ello podemos usar la función `parent()`. 64 | 65 | ```html 66 | {% block mibloque %} 67 | {{ parent() }} 68 |

Contenido a añadir

69 | {% endblock %} 70 | ``` -------------------------------------------------------------------------------- /1-introduccion/que-es-symfony.md: -------------------------------------------------------------------------------- 1 | # ¿Qué es Symfony? 2 | 3 | ## Qué es Symfony 4 | 5 | De acuerdo a la [definición de Symfony en su propia web](http://symfony.com/what-is-symfony), Symfony es **un framework PHP, una filosofía y una comunidad**. En su artículo [What is Symfony2?](http://fabien.potencier.org/article/49/what-is-symfony2), Fabien Potencier se extiende un poco más en la definición de Symfony. Según Fabien, leemos que, por una parte... 6 | 7 | ` 8 | Symfony2 is a reusable set of standalone, decoupled, and cohesive PHP components that solve common web development problems. 9 | ` 10 | 11 | ... y por otra ... 12 | 13 | ` 14 | Based on these components, Symfony2 is also a full-stack web framework. 15 | ` 16 | 17 | Es decir, hay varias maneras de utilizar Symfony en los proyectos PHP. La más obvia consiste en construir nuestra aplicacion sobre el framework Symfony 2 al completo, pero si lo deseamos también podemos utilizar únicamente algunos de sus componentes. 18 | 19 | 20 | ## HTTP Framework 21 | A menudo, Symfony 2 es definido como un **framework MVC**. El [patrón MVC](es.wikipedia.org/wiki/Modelo_Vista_Controlador) consiste en separar en capas distintas los componentes encargados de manejar la vista, el modelo y el controlador. 22 | 23 | ![Arquitectura MVC](mvc.png "Arquitectura MVC") 24 | 25 | Aunque Symfony 2 comparte algunos de los conceptos del patrón MVC (separación por capas), su objetivo es otro; atender peticiones HTTP de una manera organizada y eficaz. Por ello, Symfony 2 se define como un **framework HTTP**. 26 | 27 | Symfony abstrae la petición HTTP en un objeto Request que es procesado por el framework. Para ello intervienen varios componentes; el enrutado, el controlador responsable de dicha petición y el Event Dispatcher. La forma en que esté organizado el modelo depende completamente de nosotros. Podemos devolver contenido HTML o respuestas en JSON, XML, o cualquier otro formato. Por lo tanto, ni el modelo ni la vista dependen en absoluto del framework. 28 | 29 | ![Symfony2 HTTP Framework](symfony2_http_framework.jpg "Symfony2 HTTP Framework") 30 | 31 | Las ventajas de esta arquitectura son innumerables. Al abstraer la petición PHP en un objeto response, el framework ya no depende de las históricas variables PHP como `$_SESSION`, `$_SERVER`, `$_POST` o `$_GET`. Esto permite crear peticiones programáticamente y pasárselas al kernel sin necesidad de emplear peticiones **reales**. De esta manera es posible utilizar aplicaciones Symfony desde distintos entornos, como programas externos o tests automáticos. 32 | 33 | Por otra parte y gracias a su sistema de eventos, el framework Symfony permite a los desarrolladores intervenir en cualquier punto de la petición para transformar los datos o realizar operaciones en paralelo. 34 | 35 | 36 | ## Comunidad 37 | Ninguna plataforma open-source sería nada sin su comunidad. Las comunidades de software libre son lugares excelentes donde aprender de los demás, recibir y aportar nuevos puntos de vista y, en definitiva, pasar un buen rato. 38 | 39 | Además de los eventos internacionales, en España se celebra anualmente la conferencia [deSymfony](http://desymfony.com/) donde se citan los mejores desarrolladores del framework. También se han organizado otros grupos a nivel local, entre los que tenemos el grupo local de Valencia, [SymfonyVLC](http://www.symfony-valencia.es/). 40 | 41 | -------------------------------------------------------------------------------- /7-eventos/introduccion.md: -------------------------------------------------------------------------------- 1 | # Introducción 2 | 3 | Volvamos a la aplicación de recetas, concretamente a la acción del controlador en la que se creaba una nueva receta: 4 | 5 | ``` 6 | // src/My/RecipesBundle/Controller/RecipeController.php 7 | class RecipeController extends Controller 8 | { 9 | public function createAction(Request $request) 10 | { 11 | $recipe = new Recipe(); 12 | $form = $this->createForm(new RecipeType, $recipe); 13 | $form->handleRequest($request); 14 | 15 | if ($form->isValid()) { 16 | $this->persistAndFlush($recipe); 17 | return $this->redirect($this->generateUrl('my_recipes_recipe_show', array('id' => $recipe->getId()))); 18 | } 19 | return array('form' => $form->createView()); 20 | } 21 | } 22 | ``` 23 | 24 | El servicio encargado de crear la receta es el siguiente: 25 | 26 | ``` 27 | // src/My/RecipesBundle/Model/RecipeCreator.php 28 | namespace My\RecipesBundle\Model; 29 | 30 | use Doctrine\Common\Persistence\ObjectManager; 31 | use My\RecipesBundle\Entity\Recipe; 32 | 33 | class RecipeCreator 34 | { 35 | 36 | private $om; 37 | 38 | public function __construct(ObjectManager $om) { 39 | $this->om = $om; 40 | } 41 | 42 | public function create(Recipe $recipe) 43 | { 44 | $this->om->persist($recipe); 45 | $this->om->flush(); 46 | } 47 | } 48 | ``` 49 | 50 | 51 | Imaginemos que uno de los requisitos de negocio es enviar un aviso por email al administrador de la web cada vez que se publica la receta. Podríamos decidir añadir este comportamiento a la clase `RecipeCreator`. 52 | 53 | 54 | ``` 55 | class RecipeCreator 56 | { 57 | // ... 58 | public function create(Recipe $recipe) 59 | { 60 | $this->om->persist($recipe); 61 | $this->om->flush(); 62 | $this->systemMailer->sendRecipeInfo($recipe); 63 | } 64 | } 65 | ``` 66 | 67 | Posteriormente se nos solicita que registremos la operación en un log, por lo que de nuevo añadimos la nueva funcionalidad al `RecipeCreator`. 68 | 69 | ``` 70 | class RecipeCreator 71 | { 72 | // ... 73 | public function create(Recipe $recipe) 74 | { 75 | $this->om->persist($recipe); 76 | $this->om->flush(); 77 | $this->systemMailer->sendRecipeInfo($recipe); 78 | $this->systemLogger->log('info', sprintf('New recipe created with name %s', $recipe->getName())); 79 | } 80 | } 81 | ``` 82 | 83 | Ahora nuestro creador tiene tres responsabilidades: 84 | 85 | - Guarda la receta en la base de datos. 86 | - Envía un email. 87 | - Registra la operación en un log. 88 | 89 | 90 | Aún así, dado que lo hemos encapsulado todo en otras clases, el código parece bastante sencillo, ¿verdad? En realidad hay varios aspectos en este diseño que son mejorables. En primer lugar hemos roto el llamado [Principio de una sola responsabilidad](https://docs.google.com/file/d/0ByOwmqah_nuGNHEtcU5OekdDMkk/edit) o SRP. Pero además también hemos introducido acoplamiento temporal. 91 | 92 | El acoplamiento temporal significa que dos o más acciones son llevadas a cabo por el mismo componente sólo porque ocurren en el mismo instante. 93 | 94 | Ya hemos visto principios que facilitan el desacoplamiento del código y la extensibilidad, como la inyección de dependencias y el uso de servicios. En este tema vamos a aprender a resolver el acoplamiento temporal con el uso de eventos. 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Indice 2 | 3 | 1. Introducción 4 | 1. [La evolución de PHP y los frameworks MVC](/1-introduccion/la-evolucion-de-php-y-los-frameworks-mvc.md) 5 | 1. [Ventajas e inconvenientes de los frameworks](/1-introduccion/ventajas-e-inconvenientes-de-los-frameworks.md) 6 | 1. [¿Qué es Symfony?](/1-introduccion/que-es-symfony.md) 7 | 1. [Instalación](/1-introduccion/instalacion.md) 8 | 1. [Organización de directorios](/1-introduccion/directorios.md) 9 | 1. [Bundles](/1-introduccion/bundles.md) 10 | 1. [Gestión de dependencias con composer](/1-introduccion/composer.md) 11 | 1. [El profiler y la consola](/1-introduccion/profiler-y-consola.md) 12 | 1. [Ejercicios](/1-introduccion/ejercicios.md) 13 | 14 | 2. El Framework Symfony 2 15 | 2. [Fundamentos HTTP](/2-symfony-a-vista-de-pajaro/fundamentos-http.md) 16 | 2. [Request y Response en Symfony 2](/2-symfony-a-vista-de-pajaro/request-response.md) 17 | 2. [Routing](/2-symfony-a-vista-de-pajaro/routing.md) 18 | 2. [Controlador](/2-symfony-a-vista-de-pajaro/controller.md) 19 | 2. [Vista](/2-symfony-a-vista-de-pajaro/templating.md) 20 | 2. [Modelo](/2-symfony-a-vista-de-pajaro/model.md) 21 | 2. [Ejercicios](/2-symfony-a-vista-de-pajaro/ejercicios.md) 22 | 23 | 3. Gestión de la persistencia con Doctrine 24 | 3. [¿Qué es Doctrine?](/3-doctrine/doctrine.md) 25 | 3. [Configuración](/3-doctrine/configuracion.md) 26 | 3. [Entidades](/3-doctrine/entidades.md) 27 | 3. [Relaciones](/3-doctrine/relaciones.md) 28 | 3. [Lazy y eager](/3-doctrine/lazy-eager.md) 29 | 3. [Doctrine Query Language](/3-doctrine/dql.md) 30 | 3. [Repositorios](/3-doctrine/repositorios.md) 31 | 3. [Ejercicios](/3-doctrine/ejercicios.md) 32 | 33 | 4. El motor de plantillas Twig 34 | 4. [¿Qué es Twig?](/4-twig/twig.md) 35 | 4. [Conceptos básicos](/4-twig/conceptos-basicos.md) 36 | 4. [Layouts y herencia](/4-twig/layouts-herencia.md) 37 | 4. [Vistas parciales](/4-twig/include-render.md) 38 | 4. [Generación de assets](/4-twig/assets.md) 39 | 4. [Extensiones](/4-twig/extensiones.md) 40 | 4. [Ejercicios](/4-twig/ejercicios.md) 41 | 42 | 5. Formularios y validaciones 43 | 5. [Conceptos básicos](/5-formularios/conceptos-basicos.md) 44 | 5. [Validación](/5-formularios/validacion.md) 45 | 5. [Field Types](/5-formularios/field-types.md) 46 | 5. [Formularios embebidos](/5-formularios/formularios-embebidos.md) 47 | 5. [Form events](/5-formularios/form-events.md) 48 | 5. [Ejercicios](/5-formularios/ejercicios.md) 49 | 50 | 6. Inyección de dependencias 51 | 6. [Conceptos teóricos](/6-inyeccion/conceptos-teoricos.md) 52 | 6. [El componente de inyección de dependencias de Symfony 2](/6-inyeccion/symfony2.md) 53 | 6. [Ejercicios](/6-inyeccion/ejercicios.md) 54 | 55 | 7. Eventos 56 | 7. [Introducción](/7-eventos/introduccion.md) 57 | 7. [El EventDispatcher Component](/7-eventos/event-dispatcher.md) 58 | 7. [Ejercicios](/7-eventos/ejercicios.md) 59 | 60 | 8. Seguridad 61 | 8. [Conceptos básicos](/8-seguridad/conceptos-basicos.md) 62 | 8. [Usuarios y roles](/8-seguridad/usuarios.md) 63 | 8. [Ejercicios](/8-seguridad/ejercicios.md) 64 | 65 | 9. Internacionalización 66 | 9. Ficheros de traducción 67 | 9. [Translate constraint messages](http://symfony.com/doc/current/book/translation.html#book-translation-constraint-messages) 68 | 69 | 10. Testing con PHPUnit 70 | 10. Testing funcional 71 | 10. Testing unitario 72 | 10. Mocks, Stubs y Fake Objects 73 | 10. Test Driven Development con Symfony 2 74 | 75 | 76 | -------------------------------------------------------------------------------- /3-doctrine/dql.md: -------------------------------------------------------------------------------- 1 | # Doctrine Query Language 2 | 3 | Symfony proporciona algunos métodos para realizar operaciones básicas sobre entidades tales como la creación, borrado, carga, filtrado y ordenación. En ocasiones, sin embargo, es necesario realizar consultas más complejas que no pueden resolverse con estos métodos. Por ello, Doctrine proporciona la herramienta **Doctrine Query Builder** basada en el **Doctrine Query Language** (DQL). 4 | 5 | DQL es similar a SQL en su sintaxis, pero se centra en las clases que representan a las entidades, y no en las tablas subyacentes. 6 | 7 | 8 | ## Crear consultas con DQL 9 | 10 | El Entity Manager de Doctrine proporciona un acceso a DQL a través del método `createQuery()`. 11 | 12 | ``` 13 | $em = $this->getDoctrine()->getManager(); 14 | $query = $em->createQuery( 15 | 'SELECT a 16 | FROM MyRecipesBundle:Author a 17 | JOIN a.recipes r 18 | WHERE r.difficulty = :difficulty 19 | ORDER BY a.surname DESC' 20 | )->setParameter('difficulty', 'difícil'); 21 | 22 | $hardcore_authors = $query->getResult(); 23 | ``` 24 | 25 | 26 | Otra forma de realizar la consulta es a través del `QueryBuilder`. 27 | 28 | 29 | ``` 30 | $em = $this->getDoctrine()->getManager(); 31 | $repository = $em->getRepository('MyRecipesBundle:author'); 32 | $query = $repository->createQueryBuilder('a') 33 | ->innerJoin('a.recipes', 'r') 34 | ->where('r.difficulty = :difficulty') 35 | ->orderBy('a.surname', 'DESC') 36 | ->setParameter('difficulty', 'difícil') 37 | ->getQuery(); 38 | 39 | $hardcore_authors = $query->getResult(); 40 | ``` 41 | 42 | `getResult()` devolverá una colección de entidades, mientras que `getSingleResult()` espera una sola entidad. En el caso de encontrar varias entidades o no encontrar ninguna, `getSingleResult()` levantará una excepción. 43 | 44 | 45 | ## Limit y offset 46 | 47 | Los equivalentes `LIMIT` y `OFFSET` de DQL se consiguen a través de los métodos `setMaxResults($limit)` y `setFirstResult($offset)`. 48 | 49 | ``` 50 | $query = $em->createQuery( 51 | 'SELECT r 52 | FROM MyRecipesBundle:Recipes r 53 | JOIN r.author a 54 | JOIN r.ingredients i' 55 | )->setFirstResult(100) 56 | ->setMaxResults(10) 57 | ->getQuery(); 58 | ``` 59 | 60 | ## Valores escalares 61 | 62 | En DQL también es posible recuperar valores escalares en lugar de objetos. 63 | 64 | ``` 65 | $query = $em->createQuery( 66 | 'SELECT MAX(a.id) 67 | FROM MyRecipesBundle:Author a' 68 | )->getQuery(); 69 | $last_id = $query->getSingleScalarResult(); 70 | ``` 71 | 72 | ## Optimización básica 73 | 74 | Podemos optimizar las consultas facilitando información al `hydrator`, que se encarga de construir los objetos cargados. 75 | 76 | ``` 77 | $query = $em->createQuery( 78 | 'SELECT r, a, i 79 | FROM MyRecipesBundle:Recipes r 80 | JOIN r.author a 81 | JOIN r.ingredients i' 82 | ); 83 | $full_built_recipes = $query->getResult(); 84 | ``` 85 | 86 | Obsérvese la cláusula SELECT de la consulta anterior, que contiene `r, a, i`. De este modo, Doctrine cargará todos los objetos `Ingredient` y `Author` en `Recipe` en el menor número de consultas posible. Si bien supone mayor consumo de memoria, permite optimizar enormemente las transferencias con la base de datos. 87 | 88 | 89 | Otra forma de optimizar recursos es utilizar arrays en lugar de entidades completas. 90 | 91 | ``` 92 | $query = $em->createQuery( 93 | 'SELECT i.id, i.name 94 | FROM MyRecipesBundle:Ingredient i' 95 | )->getQuery(); 96 | $ingredients = $query->getArrayResult(); 97 | ``` 98 | -------------------------------------------------------------------------------- /1-introduccion/la-evolucion-de-php-y-los-frameworks-mvc.md: -------------------------------------------------------------------------------- 1 | # La evolución de PHP y los frameworks MVC 2 | 3 | PHP nace en 1995 de la mano de Rasmus Lerdorf, en una época donde emergen otros lenguajes de tipado dinámico como Python o Ruby. Fue concebido para el **desarrollo de webs dinámicas** y a día de hoy es el lenguaje más extendido entre los servidores web, según [w3techs.com](http://w3techs.com). 4 | 5 | ![Lenguajes más usados por servidores web](lenguajes-mas-usados.png "Lenguajes más usados por servidores web") 6 | 7 | Entre los motivos que explican la expansión de PHP podemos encontrar su suave curva de aprendizaje, la facilidad de instalación y configuración en cualquier entorno y la actual presencia de programadores en el mercado. Al ser un lenguaje interpretado, el despliegue de actualizaciones es tan sencillo como sobreescribir los ficheros existentes, por lo que el proceso de desarrollo se acelera con respecto a los lenguajes compilados como Java. 8 | 9 | Pese a todos estos logros, PHP sigue estando muy mal considerado por amplios sectores de la comunidad de desarrolladores. ¿Por qué? Para responder a esta pregunta podemos partir de las decisiones en el diseño del lenguaje (un ejemplo, la cantidad y desorden de funciones para manejar arrays), pero también por su comunidad. Las mismas ventajas del lenguaje han atraído a multitud de programadores semi-profesionales o amateurs, y esto ha ayudado a generalizar las malas prácticas entre los proyectos PHP. Plataformas como Drupal o Wordpress [han extendido en gran medida esta imagen](https://api.drupal.org/api/drupal/modules%21user%21user.module/function/user_save/7). 10 | 11 | Podemos decir, sin embargo, que la comunidad PHP ya ha superado el punto de inflexión y empieza a remontar. Las versiones 5.0 y posteriores han intentado enmendar los errores del pasado con algunos cambios importantes, como el soporte para orientación a objetos y closures, entre otros. 12 | 13 | Uno de las mayores transformaciones ha sido la aparición de diversos frameworks de desarrollo que ha impulsado la estandarización y difusión de buenas prácticas entre la comunidad de desarrolladores. Entre esos frameworks, Symfony en su segunda versión es tal vez el más influyente en la actualidad. 14 | 15 | 16 | | Framework | Lenguaje | Año de lanzamiento | Versión actual | 17 | |-------------|-----------|--------------------|----------------| 18 | | CodeIgniter | PHP | 2002 | 2.1.3 | 19 | | Ruby on Rails | Ruby | 2004 | 4.0.0 | 20 | | CakePHP | PHP | 2005 | 2.4.1 | 21 | | Symfony | PHP | 2005 | 2.3.3 | 22 | | Django | Python | 2005 | 1.5.5 | 23 | | Turbogears | Python | 2005 | 1.0 | 24 | | Zend | PHP | 2006 | 2.2.4 | 25 | | web2py | Python | 2007 | 2.6.3 | 26 | | Sinatra | Ruby | 2007 | 1.4.2 | 27 | | Yii | PHP | 2008 | 1.1.14 | 28 | | Tornado | Python | 2009 | 3.0 | 29 | | Padrino | Ruby | 2010 | 0.11.10 | 30 | | Laravel | PHP | 2013 | 4.0.7 | 31 | | Silex | PHP | 2010 | 1.1 | 32 | 33 | 34 | Fuente: [Comparison of web application frameworks, Wikipedia](http://en.wikipedia.org/wiki/Comparison_of_web_application_frameworks) 35 | 36 | De entre los frameworks, podemos distinguir los monolíticos y los basados en componentes. Symfony 2 pertenece al segundo grupo. 37 | -------------------------------------------------------------------------------- /4-twig/assets.md: -------------------------------------------------------------------------------- 1 | # Generación de assets 2 | 3 | Los assets son aquellos contenidos estáticos no HTML que se cargan durante el renderizado de la página web. Entre ellos se encuentran las hojas de estilo (CSS), los scripts de cliente (JavaScript), las imágenes, vídeos y otros contenidos multimedia. 4 | 5 | ## JavaScript y CSS 6 | 7 | Para enlazar a contenidos estáticos se utiliza la función `asset`. 8 | 9 | ```html 10 | Mis recetas 11 | 12 | 13 | ``` 14 | 15 | Los assets se almacenan en el directorio `web/`, por lo que la imagen del ejemplo se encontraría en `web/images/logo.png`. 16 | 17 | ## Versiones 18 | 19 | En un navegador moderno, el contenido estático se almacena en una caché la primera vez que se accede a una aplicación web. De este modo se mejora el rendimiento evitando nuevas peticiones al servidor cada vez que se actualiza una página. 20 | 21 | El problema ocurre cuando se despliega una nueva versión del archivo estático en el servidor. Si el cliente (navegador) ha cacheado una imagen en una visita anterior, los cambios en la imagen no se verán reflejados. Para evitar este problema, assetic permite proporcionar un sufijo de versión: 22 | 23 | ``` 24 | # app/config/config.yml 25 | framework: 26 | # ... 27 | templating: { engines: ['twig'], assets_version: v2 } 28 | ``` 29 | 30 | Internamente, Symfony añade un _query parameter_ en la función `asset()` que invalida la caché del cliente. Es decir, el enlace en la imagen `images/logo.png` se convertirá en `images/logo.png?v2. 31 | 32 | ## Assetic Bundle 33 | 34 | El versionado de imagenes obliga a cambiar manualmente la configuración de `assets_version` cada vez que desplegamos cambios en el contenido estático. El bundle Assetic facilita la gestión de los assets automatizando esta tarea, además de proporcionar otras interesantes ventajas. 35 | 36 | 37 | 38 | ### Tags javascripts y stylesheets 39 | 40 | Los bloques `javascripts` y `stylesheets` permiten añadir varios archivos a la vez y referenciar bundles. 41 | 42 | ```html 43 | {% javascripts '@MyRecipesBundle/Resources/public/js/*' 44 | 'js/*' 45 | 'vendor/fundation/fundation.js' %} 46 | 47 | {% endjavascripts %} 48 | ``` 49 | 50 | ### Filtros 51 | 52 | Assetic bundle también permite añadir filtros para procesar los archivos estáticos. Entre otras utilidades permite comprimir, ofuscar y procesar las hojas de estilo con SAAS o LESS. 53 | 54 | 55 | El ejemplo más común de filtro es el `yui compressor`, que reúne todos los scripts en uno solo reduciendo el número de peticiones en la carga de una página. 56 | 57 | ```yaml 58 | # app/config/config.yml 59 | assetic: 60 | filters: 61 | yui_js: 62 | jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar" 63 | ``` 64 | 65 | ```html 66 | {% javascripts '@MyRecipesBundle/Resources/public/js/*' filter='yui_js' %} 67 | 68 | {% endjavascripts %} 69 | ``` 70 | 71 | ### Generación de assets 72 | 73 | Una vez configurado el bundle assetic, podremos regenerar los contenidos estáticos (incluyendo el procesamiento con los filtros indicados) ejecutando el siguiente comando de consola: 74 | 75 | ```bash 76 | $ app/console assetic:dump 77 | ``` 78 | 79 | 80 | Para una información más extensa, consulta la [documentación oficial](http://symfony.com/doc/current/cookbook/assetic/asset_management.html) -------------------------------------------------------------------------------- /5-formularios/form-events.md: -------------------------------------------------------------------------------- 1 | # Form Events 2 | 3 | Los eventos de formulario son una manera de manipular formularios existentes de acuerdo a ciertas reglas de negocio. El ejemplo más común es el de mostrar formularios diferentes en función del estado del objeto subyacente. 4 | 5 | Imaginemos, por ejemplo, que queremos añadir un textarea en el formulario de recetas, pero solo en las recetas difíciles. 6 | 7 | En primer lugar vamos a crear una vista de edición de recetas: 8 | 9 | ```yaml 10 | # src/My/RecipesBundle/Resources/config/routing.yml 11 | 12 | #... 13 | my_recipes_recipe_edit: 14 | pattern: /recipes/{id}/edit 15 | defaults: { _controller: MyRecipesBundle:Recipe:edit } 16 | ``` 17 | 18 | 19 | ```php 20 | // src/My/RecipesBundle/Controller/RecipeController.php 21 | 22 | class RecipeController extends Controller 23 | { 24 | 25 | /** 26 | * @Template() 27 | */ 28 | public function editAction(Recipe $recipe, Request $request) 29 | { 30 | $form = $this->createForm(new RecipeType, $recipe); 31 | $form->handleRequest($request); 32 | 33 | if ($form->isValid()) { 34 | $this->getDoctrine()->getManager()->flush(); 35 | return $this->redirect($this->generateUrl('my_recipes_recipe_show', array('id' => $recipe->getId()))); 36 | } 37 | return array( 38 | 'form' => $form->createView(), 39 | 'recipe' => $recipe); 40 | } 41 | } 42 | ``` 43 | 44 | ```html 45 | {# src/My/RecipesBundle/Resources/views/Recipe/edit.html.twig #} 46 | 47 | {% extends '::base.html.twig' %} 48 | 49 | {% block title %}Edit {{ recipe.name }}{% endblock %} 50 | 51 | {% block body %} 52 | {{ form(form) }} 53 | {% endblock %} 54 | 55 | ``` 56 | 57 | 58 | 59 | ## Crear y añadir un event subscriber 60 | 61 | Para realizar el cambio en tiempo de ejecución vamos a añadir un _Event Subscriber_. Trataremos con detalle los distintos tipos de evento más adelante. 62 | 63 | ```php 64 | // src/My/RecipesBundle/Form/EventListener/AddNotesFieldSubscriber.php 65 | namespace My\RecipesBundle\Form\EventListener; 66 | 67 | use Symfony\Component\Form\FormEvent; 68 | use Symfony\Component\Form\FormEvents; 69 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 70 | 71 | class AddNotesFieldSubscriber implements EventSubscriberInterface 72 | { 73 | public static function getSubscribedEvents() 74 | { 75 | return array(FormEvents::PRE_SET_DATA => 'preSetData'); 76 | } 77 | 78 | public function preSetData(FormEvent $event) 79 | { 80 | $recipe = $event->getData(); 81 | $form = $event->getForm(); 82 | 83 | if ($recipe && $recipe->isHard()) { 84 | $form->add('notes', 'textarea', array('required' => false)); 85 | } 86 | } 87 | } 88 | ``` 89 | 90 | Utilizaremos el _Event Subscriber_ en el formulario mediante el método `addEventSubscriber()`. 91 | 92 | ```php 93 | // src/My/RecipesBundle/Form/Type/RecipeType.php 94 | 95 | use My\RecipesBundle\Form\EventListener\AddNotesFieldSubscriber; 96 | 97 | class RecipeType extends AbstractType 98 | { 99 | 100 | public function buildForm(FormBuilderInterface $builder, array $options) 101 | { 102 | $builder 103 | // ... 104 | ->add('save', 'submit'); 105 | $builder->addEventSubscriber(new AddNotesFieldSubscriber()); 106 | } 107 | } 108 | ``` 109 | 110 | En la clase `Recipe` añadiremos el campo `notes` y actualizaremos la base de datos. Una vez realizados los cambios pertinentes podremos comprobar cómo se añade un textarea de manera condicional. 111 | 112 | ![Form events](receta-dificil.png "Form events") 113 | 114 | 115 | Para una información más extensa sobre eventos en formularios consultad el [capítulo correspondiente](http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html) en la documentación oficial. -------------------------------------------------------------------------------- /1-introduccion/profiler-y-consola.md: -------------------------------------------------------------------------------- 1 | # El profiler y la consola 2 | 3 | La instalación Symfony proporciona dos útiles herramientas que todo desarrollador utiliza tarde o temprano. El profiler servirá para proporcionar diversa información sobre lo que ocurre en cualquier petición. La consola, además de para extraer alguna información, permitirá realizar acciones desde el terminal, facilitando la automatización de procesos. 4 | 5 | ## Profiler 6 | 7 | El profiler es un componente de Symfony que recoge información de cada petición que recibe la aplicación y la almacena para su análisis posterior. En la edición estándar de Symfony2, el profiler y las herramientas incorporadas Web Debug Toolbar y Web Profiler están activadas para el entorno de desarrollo. 8 | 9 | Podemos (des)activar y personalizar el profiler a través del archivo de configuración `config.yml`. 10 | 11 | 12 | ```config.yml 13 | web_profiler: 14 | toolbar: true 15 | position: bottom 16 | 17 | # gives you the opportunity to look at the collected data before following the redirect 18 | intercept_redirects: false 19 | ``` 20 | 21 | Una vez activado en un entorno, ante cualquier petición se nos mostrará una útil barra de herramientas con información diversa. 22 | 23 | ![Web Debug Toolbar](web-debug-toolbar.png "Web Debug Toolbar") 24 | 25 | Los distintos iconos se expandirán tras seleccionarlos y mostrarán información más detallada. A continuación se muestra el detalle de consultas realizadas a la base de datos en una petición. 26 | 27 | ![Web Profiler](web-profiler.png "Web Profiler") 28 | 29 | 30 | ### El profiler en tests funcionales 31 | 32 | El profiler de Symfony puede utilizarse para automatizar pruebas de rendimiento. Por ejemplo, se podría implementar una batería de tests que recorriese una aplicación y se asegurase de que no se sobrepasa un número de consultas determinado. Para obtener más información sobre cómo implementar estas pruebas, consultad la receta [How to use the Profiler in a Functional Test](http://symfony.com/doc/current/cookbook/testing/profiling.html) en la documentación oficial. 33 | 34 | 35 | ### El profiler y el rendimiento 36 | 37 | La activación del profiler genera un impacto profundo en el rendimiento de la aplicación. La recolección de los datos, procesamiento y posterior almacenamiento en ficheros temporales hacen del profiler una herramienta peligrosa en entornos de producción. Lo mejor, por ello, es activarlo únicamente en el entorno de desarrollo. 38 | 39 | Del mismo modo, conviene desactivar el profiler en todos los tests funcionales en los que no sea estrictamente necesario, permitiendo baterías de tests más rápidas que agilicen los procesos de integración. 40 | 41 | 42 | ## Consola 43 | 44 | La consola de Symfony 2 proporciona una interfaz de terminal para operar con nuestra aplicación Symfony. Para comprobar los comandos disponibles debemos ejecutar `app/console` sin parámetros. El número de comandos disponibles variará en función de los bundles que hayamos instalados, puesto que estos pueden registrar acciones en la consola. 45 | 46 | Los comandos de la sección `generate` permiten automatizar la creación de bundles, controladores o entidades de doctrine. Es el llamado `scaffolding` o andamiaje. 47 | 48 | Otros comandos imprescindibles de la consola son aquellos que permiten gestionar la caché: 49 | 50 | `app/console cache:clear` eliminará los archivos de caché. 51 | `app/console cache:warmup` efectuará el calentamiento de una caché vacía, mejorando el rendimiento de la web antes de que lleguen peticiones. 52 | 53 | 54 | 55 | Casi todos los comandos permiten especificar el entorno en el que van a ser utilizados con el parámetro -e. El siguiente comando se ejecutará únicamente en el entorno de test: 56 | 57 | `app/console doctrine:schema:create -e test` 58 | 59 | En una instalación estándar de Symfony 2 hay demasiados comandos para ser tratados aquí individualmente. La mejor referencia de cada uno de ellos se encuentra en la propia interfaz de ayuda de la consola. Para una información más extensa y actualizada [consulta la documentación oficial](http://symfony.com/doc/current/components/console/introduction.html). -------------------------------------------------------------------------------- /2-symfony-a-vista-de-pajaro/request-response.md: -------------------------------------------------------------------------------- 1 | # Request y Response 2 | 3 | En el anteriores capítulos hemos visto algunos fundamentos de HTTP y definido Symfony como un **framework HTTP**. Symfony construye una capa de abstracción sobre HTTP, que se sustenta en dos clases importantes del componente HTTPFoundation: Request y Response. 4 | 5 | ## La clase Request 6 | 7 | ![Request](uml-Request.png "Request") 8 | 9 | La clase Request contiene todo lo necesario para realizar una petición web, incluídas las cookies que permitirán gestionar las sesiones, archivos transferidos, etcétera. Podríamos crear una request utilizando las viariables globales PHP: 10 | 11 | ```php 12 | $request = Request::createFromGlobals(); 13 | ``` 14 | Así es como se procesa una petición en `web/app.php`: 15 | 16 | ``` 17 | $kernel = new AppKernel('prod'); 18 | $request = Request::createFromGlobals(); 19 | $response = $kernel->handle($request); 20 | ``` 21 | 22 | Pero también es posible crear una petición programáticamente sin necesidad de realizar una request real, pasarla a nuestra aplicación Symfony y procesar la respuesta: 23 | 24 | ```php 25 | $kernel = new AppKernel('prod'); 26 | $request = Request::create('/recetas/pollo-al-pil-pil', 'GET'); 27 | $response = $kernel->handle($request); 28 | ``` 29 | 30 | 31 | ## La clase Response 32 | 33 | ![Response](uml-Response.png "Response") 34 | 35 | Response encapsula una respuesta HTTP. Toda petición a `AppKernel` debe devolver un objeto response. Como se observa en `web/app.php`, una vez recuperado el objeto Response se invoca al método `send()` encargado de mostrar la respuesta: 36 | 37 | 38 | ``` 39 | $response = $kernel->handle($request); 40 | $response->send(); 41 | ``` 42 | 43 | ## Tipos especiales de respuesta 44 | 45 | Un caso específico de respuesta es la clase [JsonResponse](http://api.symfony.com/2.2/Symfony/Component/HttpFoundation/JsonResponse.html) para respuestas cuyo contenido se devuelve en [JSON](http://en.wikipedia.org/wiki/JSON). 46 | 47 | Otro ejemplo lo tenemos en la clase [RedirectResponse](http://api.symfony.com/master/Symfony/Component/HttpFoundation/RedirectResponse.html) que indica a los clientes la dirección a la que deben dirigirse. 48 | 49 | La clase [StreamedResponse](http://api.symfony.com/master/Symfony/Component/HttpFoundation/StreamedResponse.html) ofrece respuestas en streaming y es utilizada fundamentalmente en medios pesados como vídeo o grandes volúmenes de datos. 50 | 51 | 52 | ## Eventos en el ciclo de una petición 53 | 54 | A lo largo del ciclo de vida de una petición, Symfony dispara distintos eventos que permiten a sus componentes reaccionar para modificar la respuesta o realizar acciones diversas. Cualquier bundle de nuestra aplicación puede asímismo adherirse a estos eventos. 55 | 56 | Antes de procesar la petición, en el método `handle()` de la clase `HttpKernel`, se dispara un evento `KernelEvents::REQUEST`: 57 | 58 | 59 | ``` 60 | $event = new GetResponseEvent($this, $request, $type); 61 | $this->dispatcher->dispatch(KernelEvents::REQUEST, $event); 62 | ``` 63 | 64 | Para obtener el controlador que atenderá la petición se utiliza un evento `KernelEvents::CONTROLLER`. El componente de routing recibe este evento, examina el objeto Request y la configuración de enrutado y devuelve el controlador correspondiente. 65 | 66 | ``` 67 | $event = new FilterControllerEvent($this, $controller, $request, $type); 68 | $this->dispatcher->dispatch(KernelEvents::CONTROLLER, $event); 69 | $controller = $event->getController(); 70 | ``` 71 | 72 | En algunas ocasiones el controlador no devuelve una instancia de `Response`. Por ejemplo, la etiqueta [Template()](http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/view.html) permite aligerar las acciones de un controlador devolviendo un array y siguiendo cierto convenio. En estos casos, `AppKernel` dispara un evento `KernelEvents::VIEW`, y serán otros componentes los que se encarguen de construir el objeto `Response`. 73 | 74 | 75 | Cualquier excepción que ocurra durante el procesamiento de una petición tendrá como consecuencia que se dispare un evento `KernelEvents::EXCEPTION`. Symfony utiliza este evento para enmascarar la excepción en una vista más amigable y ofrecer herramientas de debugging. 76 | 77 | 78 | Una vez enviada la respuesta, en `web/app.php` se dispara un evento `KernelEvents::TERMINATE`. 79 | 80 | ``` 81 | $kernel->terminate($request, $response); 82 | ``` 83 | 84 | 85 | -------------------------------------------------------------------------------- /6-inyeccion/symfony2.md: -------------------------------------------------------------------------------- 1 | # El componente de inyección de dependencias en Symfony 2 2 | 3 | ## Entornos 4 | 5 | Una aplicación Symfony puede ejecutarse en distintos entornos. Los entornos más típicos son `prod`, `dev` y `test`, que corresponden a producción, desarrollo y pruebas. Adicionalmente podemos configurar tantos entornos distintos como sean necesarios. 6 | 7 | Si observamos el archivo `index.php` de nuestra instalación Symfony podremos ver la siguiente línea: 8 | 9 | ``` 10 | $kernel = new AppKernel('prod', false); 11 | ``` 12 | 13 | El primer argumento `prod` indica el entorno que se va a aplicar a la instancia de la aplicación. Con esta configuración, Symfony buscará el fichero `app/config/config_prod.yml`. Básicamente Symfony añade el nombre del entorno como sufijo del archivo de configuración. 14 | 15 | Esta separación en entornos permite configurar los parámetros de forma independiente. Así, es posible separar las bases de datos a las que la aplicación ataca en función de si el entorno es `test` o `dev`. Pero además de configurar parámetros, también permitirá _inyectar_ servicios distintos de acuerdo a las necesidades de cada entorno. 16 | 17 | 18 | ## Cómo crear servicios 19 | 20 | En nuestra aplicación de recetas teníamos una acción en el controlador `RecipeController` que permitía mostrar las últimas recetas publicadas. 21 | 22 | // src/My/RecipesBundle/Controller/RecipeController.php 23 | 24 | class RecipeController extends Controller 25 | { 26 | 27 | public function lastRecipesAction() 28 | { 29 | $date = new \DateTime('-10 days'); 30 | $repository = $this->getDoctrine()->getRepository('MyRecipesBundle:Recipe'); 31 | $recipes = $repository->findPublishedAfter($date); 32 | return array('recipes' => $recipes); 33 | } 34 | } 35 | 36 | 37 | Vamos a extraer este fragmento de código a un servicio independiente. En primer lugar, moveremos el código a una nueva clase. 38 | 39 | // src/My/RecipesBundle/Model/LastRecipes.php 40 | namespace My\RecipesBundle\Model; 41 | 42 | use Doctrine\Common\Persistence\ObjectManager; 43 | 44 | class LastRecipes 45 | { 46 | private $repository; 47 | 48 | public function __construct(ObjectManager $om) { 49 | $this->repository = $om->getRepository('MyRecipesBundle:Recipe'); 50 | } 51 | 52 | public function findFrom(\DateTime $from_date) 53 | { 54 | return $this->repository->findPublishedAfter($from_date); 55 | } 56 | } 57 | 58 | 59 | Publicaremos el servicio en el archivo `services.yml`. 60 | 61 | services: 62 | #... 63 | my_recipes.last_recipes: 64 | class: My\RecipesBundle\Model\LastRecipes 65 | arguments: ["@doctrine.orm.entity_manager"] 66 | 67 | 68 | Y modificaremos el controlador: 69 | 70 | // src/My/RecipesBundle/Controller/RecipeController.php 71 | 72 | class RecipeController extends Controller 73 | { 74 | 75 | public function lastRecipesAction() 76 | { 77 | $date = new \DateTime('-10 days'); 78 | return array( 79 | 'recipes' => $this->get('my_recipes.last_recipes')->findFrom($date), 80 | ); 81 | } 82 | } 83 | 84 | 85 | La extracción del código de controladores a servicios tiene diversas ventajas. Los controladores son la capa de la aplicación más cercana al framework, y la única que debería estar acoplada a él. Moviendo el código a servicios independientes conseguimos un diseño de la aplicación más portable. Por otra parte, ahora podemos sobreescribir el servicio proporcionado en cualquier entorno sin tener que modificar el controlador: 86 | 87 | # app/config/config_otherenvironment.yml 88 | services: 89 | #... 90 | my_recipes.last_recipes: 91 | class: My\RecipesBundle\Model\OtherClass 92 | 93 | 94 | La inyección de dependencias resulta especialmente útil en los casos en los que nuestra aplicación conecta con otras aplicaciones del exterior. Por ejemplo, es posible que nuestra aplicación utilice una clase determinada para publicar mensajes en Twitter. Gracias a la inyección y a la configuración por entornos, podríamos evitar sencillamente que desde el entorno `dev` y `test` se publicasen mensajes reales. 95 | 96 | Para una descripción más exhaustiva del funcionamiento del componente de inyección de dependencias, consultad la [sección correspondiente](http://symfony.com/doc/current/components/dependency_injection/index.html) en la documentación oficial. 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /1-introduccion/instalacion.md: -------------------------------------------------------------------------------- 1 | # Instalación 2 | 3 | Aunque Symfony puede ser descargado y descomprimido directamente allí donde lo queramos dejar, en este curso vamos a utilizar desde el principio el gestor de dependencias **composer**. Esta guía describe el proceso de instalación en una máquina basada en Debian. Para otros sistemas operativos, consultad la [guía de instalación](http://symfony.com/doc/current/book/installation.html) en la web del framework. 4 | 5 | ## Cómo descargar composer 6 | 7 | Situaos en el directorio desde el que pendan los distintos sites del servidor. Por ejemplo, `/var/www`. 8 | 9 | ```bash 10 | $ sudo apt-get install curl 11 | $ curl -sS https://getcomposer.org/installer | php 12 | ``` 13 | 14 | ![Instalación de Composer](composer_install.png "Instalación de Composer") 15 | 16 | ## Cómo instalar Symfony 2 17 | 18 | En el mismo directorio donde hayáis descargado composer, ejecutad la siguiente instrucción: 19 | 20 | ``` 21 | $ php composer.phar create-project symfony/framework-standard-edition nombre-de-mi-proyecto/ 22 | ``` 23 | 24 | Tras descargar los componentes necesarios, el terminal nos pedirá interactivamente que le proporcionemos cierta información: 25 | 26 | ![Configuración de Symfony](symfony_install.png "Configuración de Symfony") 27 | 28 | 29 | - **database_driver**: Configura el motor de base de datos a utilizar para la instalación. Algunas opciones son `pdo_mysql` o `pdo_sqlite`. Recuerda que necesitarás tener instaladas en tu equipo las extensiones correspondientes. 30 | - **database_host**: La máquina donde se aloja la base de datos. Por defecto la máquina local (127.0.0.1). 31 | - **database_port**: El puerto mediante el cual se accede a la base de datos. Dejándolo a `null` se utilizará el puerto por defecto del motor elegido. 32 | - **database_name**: Nombre de la base de datos de la instalación. 33 | - **database_user**: Usuario con el que se accederá a la base de datos. 34 | - **database_password**: Contraseña del usuario de la base de datos. 35 | - **mailer_transport**: Protocolo de transporte a utilizar en el envío de emails. Algunas opciones son `sendmail` o `smtp`. 36 | - **mailer_user**: Si aplica, usuario a utilizar para el envío de emails. 37 | - **mailer_password**: Si aplica, contraseña del usuario para el envío de emails. 38 | - **local**: Localización idiomática del sitio. Afecta, por ejemplo, al modo en el que se formatean las fechas y a los ficheros de traducción cargados. 39 | - **secret**: Esta clave es utilizada por Symfony en sus mecanismos de encriptación. ¡No te olvides de cambiarla!. 40 | 41 | 42 | 43 | Una vez proporcionados los parámetros necesarios daremos permisos de escritura a los directorios `app/cache` y `app/logs` [tal y como se describe en la web oficial](http://symfony.com/doc/current/book/installation.html#configuration-and-setup). 44 | 45 | 46 | ``` 47 | $ APACHEUSER=`ps aux | grep -E '[a]pache|[h]ttpd' | grep -v root | head -1 | cut -d\ -f1` 48 | $ sudo setfacl -R -m u:$APACHEUSER:rwX -m u:`whoami`:rwX app/cache/ app/logs/ 49 | $ sudo setfacl -dR -m u:$APACHEUSER:rwX -m u:`whoami`:rwX app/cache/ app/logs/ 50 | ``` 51 | 52 | 53 | ## Ejemplo de configuración en Apache web server 54 | 55 | 56 | ``` 57 | 58 | ServerAdmin mi@mail.es 59 | ServerName local.symfony.com 60 | 61 | DocumentRoot /var/www/vhosts/symfony/web 62 | 63 | AllowOverride None 64 | 65 | RewriteEngine On 66 | RewriteCond %{REQUEST_FILENAME} !-f 67 | RewriteRule ^(.*) app.php [QSA,L] 68 | 69 | 70 | ErrorLog ${APACHE_LOG_DIR}/symfony-error.log 71 | 72 | # Possible values include: debug, info, notice, warn, error, crit, 73 | # alert, emerg. 74 | LogLevel warn 75 | 76 | CustomLog ${APACHE_LOG_DIR}/access.log combined 77 | 78 | ``` 79 | 80 | 81 | ## Servidor embebido 82 | 83 | Si no deseamos perder tiempo con un servidor de desarrollo y disponemos de una versión de PHP 5.4 o superior podemos utilizar el [servidor embebido de PHP](http://www.php.net/manual/en/features.commandline.webserver.php). Este servidor está pensado para entornos de desarrollo y nunca debería usarse en entornos de producción. 84 | 85 | La consola de Symfony 2 proporciona un comando para arrancar nuestra aplicación Symfony; `app/console server:run`. 86 | 87 | ![Builtin server](builtin-server.png "Builtin server") 88 | 89 | 90 | ## ¡Bienvenido! 91 | 92 | Tras configurar el servidor web para que dirija correctamente las peticiones al sitio podremos acceder a él desde nuestro navegador en `http://tu-site-symfony.com/app_dev.php/` o `localhost:8000` si hemos arrancado el servidor embebido. 93 | 94 | ![Pantalla de bienvenida](bienvenida.png "Pantalla de bienvenida") 95 | 96 | Tómate tu tiempo curioseando tu instalación. ¡Enhorabuena!. 97 | 98 | -------------------------------------------------------------------------------- /6-inyeccion/conceptos-teoricos.md: -------------------------------------------------------------------------------- 1 | # Conceptos teóricos 2 | 3 | 4 | ## El problema del acoplamiento 5 | 6 | De entre los tres paradigmas más conocidos, el funcional, el orientado a objetos y el procedural, seguramente sea este último es el más extendido y el que causa más problemas en cuanto a escalabilidad, portabilidad y _testabilidad_ del código. En una aplicación procedural típica, las funciones o procedimientos de más bajo nivel invocan a otras funciones o procedimientos del sistema. 7 | 8 | ``` 9 | function save_account($account_data) { 10 | // ... 11 | $success = write_database_row('accounts', $account_data); 12 | if (!$success) { 13 | $message = sprintf('An error occurred saving the account for %s', $account_data['name']); 14 | throw new Exception($message); 15 | } 16 | } 17 | ``` 18 | 19 | En el código anterior, la función `save_account()` invoca a otro procedimiento `write_database_row`. Como consecuencia se producen dos inconvenientes: 20 | 21 | - Es imposible testear la función de manera unitaria, puesto que se invocará el procedimiento que ejecuta la escritura en la base de datos. 22 | - La función `save_account` depende de una función concreta de la aplicación, por lo que para portar este código a otra aplicación necesitaremos arrastrar con él a cada una de sus dependencias y subdependencias. 23 | 24 | Aunque este tipo de problemas se asocian a la programación procedural, podemos encontrarlos también en código supuestamente orientado a objetos. 25 | 26 | ``` 27 | class Account { 28 | 29 | public method save() { 30 | $db = \App::get('DB'); 31 | $success = $db->insertRow('accounts', $this->toArray()); 32 | if (!$success) { 33 | $message = sprintf('An error occurred saving the account for %s', $this->name); 34 | throw new \Exception($message); 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | 41 | ## Inversión de control 42 | 43 | La [inversión de control](http://en.wikipedia.org/wiki/Inversion_of_control) se basa en el [Principio de Hollywood](http://en.wikipedia.org/wiki/Hollywood_principle), llamado así en referencia al slogan utilizado por los empleadores en Hollywood: _No nos llames, nosotros te llamaremos_. 44 | 45 | Es un **principio de diseño** en el cual los componentes de mayor nivel son responsables de proporcionar abstracciones a los de menor nivel. Las instancias concretas de estas abstracciones son reemplazables y necesitan una interfaz común. El flujo por lo tanto se invierte, ahora son las capas superiores las que _controlan_ a las inferiores y no al revés. 46 | 47 | Las clases de menor nivel ignoran la implementación del resto del sistema y por lo tanto están desacopladas del mismo. No importa cuál sea el ecosistema en el que se encuentren, funcionarán igualmente siempre que se respeten las interfaces proporcionadas por las abstracciones que se les proporcionan. 48 | 49 | 50 | 51 | 52 | ## Inyección de dependencias 53 | 54 | La [inyección de dependencias](http://en.wikipedia.org/wiki/Dependency_injection) es una **técnica** que facilita la inversión de control, y podemos encontrarla tanto en en el paradigma funcional como en el orientado a objetos. Expuesto en pocas palabras, se basa en pasar instancias como argumentos en lugar de construir estas instancias en el interior del método, realizar invocaciones estáticas o utilizar variables globales. Así, podemos inyectar una dependencia directamente en el método invocado, o bien en el constructor de la clase. 55 | 56 | 57 | ``` 58 | function save_account($account_data, $save_callback) { 59 | // ... 60 | $success = $save_callback('accounts', $account_data); 61 | if (!$success) { 62 | $message = sprintf('An error occurred saving the account for %s', $account_data['name']); 63 | throw new Exception($message); 64 | } 65 | } 66 | ``` 67 | 68 | ``` 69 | class Account { 70 | 71 | public method save(DatabaseInterface $db) { 72 | $success = $db->insertRow('accounts', $this->toArray()); 73 | if (!$success) { 74 | $message = sprintf('An error occurred saving the account for %s', $this->name); 75 | throw new Exception($message); 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | 82 | ## Contenedores de inyección de dependencias 83 | 84 | Los contenedores de inyección de dependencias son **herramientas** concretas, a modo de frameworks, que facilitan la inyección automática de instancias en nuestros objetos. Cada implementación utiliza su propia estrategia, ya sea a través de metaprogramación y anotaciones, configuración en ficheros yaml y otros. 85 | 86 | Symfony tiene su propio contenedor proporcionado por el [Componente de Inyección de Dependencias](http://symfony.com/doc/current/components/dependency_injection/introduction.html). En Silex se utiliza el contenedor [Pimple](http://pimple.sensiolabs.org/). El contenedor [Twittee](http://twittee.org/) es tan sencillo que su implementación cabe en un tweet. 87 | -------------------------------------------------------------------------------- /1-introduccion/ventajas-e-inconvenientes-de-los-frameworks.md: -------------------------------------------------------------------------------- 1 | # Ventajas e inconvenientes de los frameworks 2 | 3 | Aunque los frameworks de desarrollo suponen por lo general una gran ventaja en la mayoría de proyectos, también tienen algunos inconvenientes. 4 | 5 | ## Ventajas 6 | 7 | ### Productividad 8 | Los frameworks proporcionan soluciones prefabricadas para los problemas más comunes. Atención de peticiones, formularios e interacción con base de datos son ejemplos de soluciones que casi todos los frameworks ofrecen. Esto permite a los desarrolladores centrarse en las necesidades de negocio sin tener que resolver los detalles técnicos de más bajo nivel. 9 | 10 | ### Organización 11 | Un framework, normalmente, ofrece una estructura clara y organizada a varios niveles, como la estructura de directorios y la separación por capas. En consecuencia es más fácil saber dónde encontrar cualquier recurso cuando sea necesario cambiarlo. 12 | 13 | ### Convención 14 | Relacionado con el punto anterior, alrededor de un framework se construye una _manera de resolver las cosas_, un estilo. Empezando por los coding styles hasta patrones de diseño concretos, los desarrolladores pueden y deben acogerse a esas convenciones. Así, cualquier desarrollador habituado al framework pueda integrarse en cualquier proyecto con mayor facilidad. 15 | 16 | ### Documentación 17 | Todos los frameworks disponen de un sitio web con documentación más o menos completa, bloggers que comparten sus soluciones, vídeos, charlas y conferencias. 18 | 19 | ### Seguridad 20 | Los problemas de seguridad suelen estar resueltos por el mismo framework. En el caso de descubrir amenazas que puedan comprometer la seguridad, es la misma comunidad la que los soluciona de manera más rápida y eficaz que si lo hicieras tú o tu departamento. 21 | 22 | ### Rendimiento 23 | Cuantos más desarrolladores utilicen el framework, más ojos hay pendientes del rendimiento que les ofrece. Por ello, los frameworks suelen ofrecer implementaciones más rápidas de las que pueda implementar un desarrollador individual. 24 | 25 | ### Comunidad 26 | Muchas de las ventajas anteriores serían imposibles sin la existencia de las comunidades. Éstas proveen, además, multitud de módulos, plugins, bundles o gemas de manera libre y gratuíta. Antes de enfrentarse a cualquier problema conviene realizar una búsqueda en internet. Seguramente alguien ya lo haya resuelto. 27 | 28 | Por otra parte, las comunidades suelen organizarse en grupos locales y reunirse en eventos nacionales e internacionales. ¡Son una buena oportunidad de conocer gente interesante y apasionada! 29 | 30 | 31 | ## Desventajas 32 | 33 | ### Rendimiento (recursos de proceso y memoria) 34 | Los frameworks consumen, en general, más recursos que una aplicación ad-hoc orientada al rendimiento. En aplicaciones muy exigentes, un framework puede resultar poco apropiado. 35 | 36 | ### Curva inicial de aprendizaje 37 | Cada framework tiene su ecosistema de componentes que el desarrollador debe aprender, no basta con conocer el lenguaje sobre el que está escrito. Por ello, los frameworks son islas de conocimiento. 38 | 39 | ### Convención 40 | Aunque normalmente las convenciones constituyen una ventaja, en ocasiones también pueden resultar un impedimento. Algunas veces, ante problemas muy concretos, el establecimiento de convenios obliga a los desarrolladores a _esquivar_ al framework. Algunos desarrolladores sienten también cierta falta de libertad y creatividad al utilizar frameworks muy orientados a los convenios, como Ruby on Rails. 41 | 42 | ### Sensación de bala de plata 43 | A medida que un desarrollador conoce el framework, se introduce en una _zona de confort_. A la larga es posible que el desarrollador piense que su framework es la mejor solución para todo, sin estudiar otras alternativas. Por ello es muy recomendable actualizarse constantemente y conocer otros frameworks y plataformas que enriquezcan nuestra _caja de herramientas_. 44 | 45 | ### ¿De verdad necesito un framework? 46 | Ante el impulso inicial de los grandes frameworks monolíticos han surgido alternativas que proporcionan capas más finas de funcionalidad y ofrecen una mayor flexibilidad al desarrollador; son los llamados _microframeworks_. Entre ellos tenemos Sinatra (Ruby), Flask (Python) o Silex (PHP). 47 | 48 | Gracias a los gestores de componentes, como Composer en PHP, es sencillo construirse aplicaciones a medida. Por ejemplo, una aplicación PHP podría tomar el componente de inyección de dependencias Pimple, el Event Dispatcher de Symfony 2 y cualquier otro componente que se proporcione aislado. Esta solución puede ser más apropiada para necesidades de negocio complejas o desarrolladores exigentes. 49 | 50 | Como ejemplo de esta segregación en paquetes, es interesante estudiar el caso de [The Aura Project](http://auraphp.com/). 51 | 52 | 53 | Otros recursos sobre ventajas y desventajas de los frameworks: 54 | - [Software Framework Advantages and Disadvantages](http://nagbhushan.wordpress.com/2010/10/03/framework-advantages-and-disadvantages/) 55 | - [Pros and cons of using frameworks](http://www.1stwebdesigner.com/design/pros-cons-frameworks/) 56 | -------------------------------------------------------------------------------- /4-twig/include-render.md: -------------------------------------------------------------------------------- 1 | # Vistas parciales 2 | 3 | A medida que las aplicaciones crecen, las plantillas se van haciendo más y más complejas. Para garantizar su mantenimiento es conveniente limpiar de forma constante esas plantillas y controlar que se mantengan organizadas y en buena forma. Una buena forma de conseguirlo es a través de las vistas parciales. 4 | 5 | En Symfony hay dos formas de utilizar vistas parciales; el tag `include` de Twig, y la función `render()` de la extensión Twig de Symfony. 6 | 7 | ## Include 8 | 9 | El tag `include` carga y muestra el contenido de la plantilla que está siendo incluída. Permite organizar plantillas demasiado grandes y aislar fragmentos que pueden ser utilizados desde otras plantillas. 10 | 11 | ```html 12 | 13 | 14 | 15 | 16 | {% include '::header.html.twig' %} 17 | 18 | 19 | {% block body %}{% endblock %} 20 | {% block javascripts %}{% endblock %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% block title %}Welcome!{% endblock %} 28 | {% block stylesheets %}{% endblock %} 29 | 30 | ``` 31 | 32 | 33 | ## Render 34 | 35 | La función `render` permite incrustar peticiones a un controlador. Con ello conseguimos un menor acoplamiento entre los controladores y la vista. 36 | 37 | Imaginemos que queremos mostrar las últimas recetas publicadas en un bloque inferior en todas las vistas de la aplicación. Podríamos empezar modificando el layout. 38 | 39 | ```html 40 | 41 | 42 | 43 | 44 | {% include '::header.html.twig' %} 45 | 46 | 47 | {% block body %}{% endblock %} 48 | 49 |

Últimas recetas

50 | 55 | 56 | {% block javascripts %}{% endblock %} 57 | 58 | 59 | ``` 60 | 61 | Con esta construcción será necesario proporcionar la variable `last_recipes` a cualquier plantilla que extienda el layout. 62 | 63 | 64 | ```php 65 | // src/My/RecipesBundle/Controller/DefaultController.php 66 | class DefaultController extends Controller 67 | { 68 | 69 | /** 70 | * @Template() 71 | */ 72 | public function showAction(Recipe $recipe) 73 | { 74 | return array( 75 | 'last_recipes' => $this->getLastRecipes(), 76 | 'recipe' => $recipe); 77 | } 78 | 79 | /** 80 | * @Template() 81 | */ 82 | public function topChefsAction() 83 | { 84 | // ... 85 | return array( 86 | 'last_recipes' => $this->getLastRecipes(), 87 | 'chefs' => $chefs); 88 | } 89 | 90 | 91 | // ... 92 | 93 | private function getLastRecipes() 94 | { 95 | $date = new \DateTime('-10 days'); 96 | $repository = $this->getDoctrine()->getRepository('MyRecipesBundle:Recipe'); 97 | return $repository->findPublishedAfter($date); 98 | } 99 | } 100 | ``` 101 | 102 | 103 | En cada acción que añadamos tendremos que recordar añadir las últimas recetas. A medida que añadamos nuevos bloques en la aplicación la situación puede volverse más y más inmanejable. Para corregir esta situación utilizaremos un controlador _embebido_. 104 | 105 | En primer lugar, cambiaremos el layout sustituyendo el bloque por una instrucción `render()`. 106 | 107 | ```html 108 | 109 | 110 | 111 | 112 | {% include '::header.html.twig' %} 113 | 114 | 115 | {% block body %}{% endblock %} 116 | 117 | {{ render(controller('MyRecipesBundle:Default:lastRecipes')) }} 118 | 119 | {% block javascripts %}{% endblock %} 120 | 121 | 122 | ``` 123 | 124 | Escribiremos la acción `lastRecipesAction` en el controlador `Default`. 125 | 126 | ```php 127 | /** 128 | * @Template() 129 | */ 130 | public function lastRecipesAction() 131 | { 132 | $date = new \DateTime('-10 days'); 133 | $repository = $this->getDoctrine()->getRepository('MyRecipesBundle:Recipe'); 134 | $recipes = $repository->findPublishedAfter($date);; 135 | return array('recipes' => $recipes); 136 | } 137 | ``` 138 | 139 | 140 | Y moveremos el código HTML a la plantilla correspondiente. 141 | 142 | ```html 143 | 144 |

Últimas recetas

145 | 150 | ``` 151 | 152 | El último paso será limpiar las referencias a `last_recipes` del resto de acciones del controlador, obteniendo un código mucho más limpio y organizado. 153 | 154 | -------------------------------------------------------------------------------- /7-eventos/event-dispatcher.md: -------------------------------------------------------------------------------- 1 | # El EventDispatcher Component 2 | 3 | Como vimos [a principios de este curso](/2-symfony-a-vista-de-pajaro/request-response.md), el kernel de Symfony 2 utiliza el sistema de eventos durante el procesamiento de una petición para permitir puntos de extensión. En esta sección veremos cómo utilizar el componente de eventos para nuestros propios propósitos. 4 | 5 | 6 | 7 | ## El Dispatcher 8 | 9 | La clase `EventDispatcher` es el eje sobre el que gira el sistema de eventos. El `EventDispatcher` mantiene una lista de _escuchantes_ (en adelante listeners). Cuando una instancia notifica un evento al `EventDispatcher`, éste es el encargado de notificar a los listeners adecuados. 10 | 11 | Un listener es cualquier objeto o función capaz de recibir y procesar un evento determinado cuando éste se produce. 12 | 13 | ```php 14 | use Symfony\Component\EventDispatcher\EventDispatcher; 15 | 16 | $dispatcher = new EventDispatcher(); 17 | $listener = new RecipesListener(); 18 | $dispatcher->addListener('recipe.create', array($listener, 'onRecipeCreate')); 19 | ``` 20 | 21 | 22 | En el ejemplo anterior hemos utilizado el método `addListener` de `EventDispatcher` para pasarle un objeto de `RecipesListener`. El dispatcher invocará al método `onRecipeCreate` cada vez que se le notifique un evento `recipe.create`. 23 | 24 | ## El Listener 25 | 26 | ```php 27 | 28 | namespace My\RecipesBundle\Event; 29 | 30 | use My\RecipesBundle\Entity\Recipe; 31 | 32 | class RecipesListener 33 | { 34 | 35 | private $mailer; 36 | 37 | public function __construct(\SwiftMailer $mailer) 38 | { 39 | $this->mailer = $mailer; 40 | } 41 | 42 | public function onRecipeCreate(RecipeEvent $event) 43 | { 44 | $recipe = $event->getRecipe(); 45 | $this->notifyToAdmins($recipe); 46 | } 47 | 48 | 49 | private function notifyToAdmins(Recipe $recipe) 50 | { 51 | // ... 52 | $this->mailer->send($email); 53 | } 54 | } 55 | 56 | ``` 57 | 58 | En esta implementación concreta, el listener `RecipesListener` recibe el evento y lo procesa para generar un email. Aunque podemos utilizar la clase genérica `Symfony\Component\EventDispatcher\Event` para algunos casos, siempre que necesitemos pasar información a los listeners tendremos que crear nuestra propia clase. 59 | 60 | 61 | ## El Evento 62 | 63 | ``` 64 | namespace My\RecipesBundle\Event; 65 | 66 | use My\RecipesBundle\Entity\Recipe; 67 | 68 | class RecipeEvent 69 | { 70 | 71 | private $recipe; 72 | 73 | public function __construct(Recipe $recipe) 74 | { 75 | $this->recipe = $recipe; 76 | } 77 | } 78 | ``` 79 | 80 | ## Lanzar eventos en aplicaciones Symfony 81 | 82 | Para registrar un listener, en Symfony utilizaremos el contenedor escribiendo una entrada en el archivo `services.yml` de nuestro bundle. 83 | 84 | ```yaml 85 | 86 | services: 87 | #... 88 | 89 | my_recipes.recipes_listener: 90 | class: My\RecipesBundle\Event\RecipesListener 91 | arguments: ["@mailer"] 92 | tags: 93 | - { name: kernel.event_listener, event: recipe.create, method: onRecipeCreate } 94 | ``` 95 | 96 | Dado que los controladores tienen acceso al contenedor de inyección de dependencias, y que en éste está registrado el propio dispatcher, resulta bastante natural que sea el controlador el encargado de disparar el evento. 97 | 98 | 99 | ```php 100 | class RecipeController extends Controller 101 | { 102 | 103 | /** 104 | * @Template() 105 | */ 106 | public function createAction(Request $request) 107 | { 108 | $recipe = new Recipe(); 109 | // ... 110 | 111 | if ($form->isValid()) { 112 | // ... 113 | $this->get('event_dispatcher')->dispatch('recipe.create', new RecipeEvent($recipe)); 114 | return $this->redirect($this->generateUrl('my_recipes_recipe_show', array('id' => $recipe->getId()))); 115 | 116 | } 117 | return array('form' => $form->createView()); 118 | } 119 | ``` 120 | 121 | Además del controlador, cualquier objeto con acceso al `EventDispatcher` puede lanzar un evento. Aun así, dado que que el uso del `EventDispatcher` es una decisión de diseño del sistema, lo recomendable es mantener las clases del modelo agnósticas al funcionamiento del sistema. Es decir, así como en el patrón Observer decíamos que una clase no debería saber que está siendo observada, tampoco debería saber que hay un EventDispatcher por encima de ella. 122 | 123 | 124 | 125 | 126 | ## Eventos en cadena 127 | 128 | Todo objeto `Event` contiene el propio `EventDispatcher`. Esto permite encadenar unos eventos con otros. Así, el listener presentado como ejemplo podría a su vez disparar eventos en cadena. 129 | 130 | 131 | ```php 132 | 133 | class RecipesListener 134 | { 135 | 136 | // ... 137 | 138 | private function notifyToAdmins(Recipe $recipe) 139 | { 140 | // ... 141 | $this->mailer->send($email); 142 | $event->getDispatcher->dispatch('email.sent', new EmailEvent($email)); 143 | } 144 | } 145 | ``` 146 | 147 | 148 | -------------------------------------------------------------------------------- /5-formularios/field-types.md: -------------------------------------------------------------------------------- 1 | # Field types 2 | 3 | Un formulario consiste en un conjunto de campos. Los _field types_ identifican los tipos de campo que tenemos a nuestra disposición. Además de los [field types proporcionados por Symfony](http://symfony.com/doc/current/reference/forms/types.html) podremos añadir nuestros propios tipos. En Symfony, además, un formulario puede ser definido como field type, flexibilizando enormemente la construcción de formularios complejos. 4 | 5 | 6 | ## Field types en formularios 7 | 8 | Como hemos visto en anteriores capítulos, para añadir campos a un formulario utilizamos el método `add()`. 9 | 10 | ```php 11 | class AuthorType extends AbstractType 12 | { 13 | public function buildForm(FormBuilderInterface $builder, array $options) 14 | { 15 | $builder 16 | ->add('name', 'text') 17 | ->add('surname', 'text', array('required' => false, 'attr' => array('class' => 'surname'))) 18 | //...; 19 | } 20 | ``` 21 | 22 | - El primer parámetro de `add` se refiere al atributo relacionado en la clase subyacente y es obligatorio. 23 | - El segundo parámetro indica el field type. `text` es un field type básico que se transformará en un `` durante el renderizado. Este atributo es opcional, y en su defecto, el componente de formularios tratará de _adivinar_ el tipo adecuado en función del valor del atributo. 24 | - El tercer parámetro es un array opcional que permite especificar propiedades del campo. Las más comunes, `label` y `required`, son aplicables a todos los form types. `attr` permite especificar atributos que se aplicarán posteriormente a la etiqueta HTML. Cada field type puede tener otras propiedades configurables. 25 | 26 | 27 | 28 | ## Crear field types propios 29 | 30 | Vamos a crear un formulario para crear recetas. De momento el formulario va a ser muy sencillo y solo va a permitir proporcionar el nombre y la dificultad de la receta. Hemos creado un modelo `Difficulties` para tener el código más organizado: 31 | 32 | ```php 33 | namespace My\RecipesBundle\Model; 34 | 35 | class Difficulties 36 | { 37 | const UNKNOWN = 'unknown'; 38 | const EASY = 'easy'; 39 | const NORMAL = 'normal'; 40 | const HARD = 'hard'; 41 | 42 | public static function toArray() 43 | { 44 | return array( 45 | self::UNKNOWN => self::UNKNOWN, 46 | self::EASY => self::EASY, 47 | self::NORMAL => self::NORMAL, 48 | self::HARD => self::HARD, 49 | ); 50 | } 51 | } 52 | ``` 53 | 54 | Escribimos el formulario utilizando el field type `choice` proporcionado por Symfony. 55 | 56 | ```php 57 | // src/My/RecipesBundle/Form/Type/RecipeType.php 58 | 59 | class RecipeType extends AbstractType 60 | { 61 | public function buildForm(FormBuilderInterface $builder, array $options) 62 | { 63 | $builder 64 | ->add('name', 'text') 65 | ->add('difficulty', 'choice', array('choices' => Difficulties::toArray())) 66 | ->add('save', 'submit'); 67 | } 68 | // ... 69 | } 70 | ``` 71 | 72 | ![Formulario de Recipe](form-recipe.png "Formulario de Recipe") 73 | 74 | Para aumentar la reusabilidad del código vamos a crear un form type propio para la dificultad. 75 | 76 | ```php 77 | // src/My/RecipesBundle/Form/Type/DifficultyType.php 78 | namespace My\RecipesBundle\Form\Type; 79 | 80 | use Symfony\Component\Form\AbstractType; 81 | use Symfony\Component\OptionsResolver\OptionsResolverInterface; 82 | use My\RecipesBundle\Model\Difficulties; 83 | 84 | class DifficultyType extends AbstractType 85 | { 86 | public function setDefaultOptions(OptionsResolverInterface $resolver) 87 | { 88 | $resolver->setDefaults(array( 89 | 'choices' => Difficulties::toArray() 90 | )); 91 | } 92 | 93 | public function getParent() 94 | { 95 | return 'choice'; 96 | } 97 | 98 | public function getName() 99 | { 100 | return 'difficulty'; 101 | } 102 | } 103 | ``` 104 | 105 | Y lo usaremos en el formulario de recetas. 106 | 107 | ```php 108 | // src/My/RecipesBundle/Form/Type/RecipeType.php 109 | 110 | class RecipeType extends AbstractType 111 | { 112 | public function buildForm(FormBuilderInterface $builder, array $options) 113 | { 114 | $builder 115 | ->add('name', 'text') 116 | ->add('difficulty', new DifficultyType) 117 | ->add('save', 'submit'); 118 | } 119 | } 120 | ``` 121 | 122 | 123 | ## Ofrecer field types como servicios 124 | El siguiente paso para ofrecer código extensible, reusable y mantenible es exponer nuestro `DifficultyType` en la capa de servicios. 125 | 126 | ```php 127 | # src/My/RecipesBundle/Resources/config/services.yml 128 | services: 129 | my_recipes.form.type.difficulty: 130 | class: My\RecipesBundle\Form\Type\DifficultyType 131 | tags: 132 | - { name: form.type, alias: difficulty } 133 | ``` 134 | 135 | A partir de entonces el desplegable estará disponible como cualquier otro field type. 136 | 137 | ```php 138 | $builder 139 | ->add('difficulty', 'difficulty'); 140 | ``` 141 | -------------------------------------------------------------------------------- /5-formularios/formularios-embebidos.md: -------------------------------------------------------------------------------- 1 | # Formularios embebidos 2 | 3 | Hasta ahora hemos visto formularios sobre una única entidad. A menudo, sin embargo, necesitamos representar varias entidades relacionadas en un solo formulario. Symfony facilita la tarea a través de los formularios embebidos. 4 | 5 | Nota: en castellano no existe la palabra embebido. Es un barbarismo que utilizaremos en este material como sinónimo de _incrustado_ o _empotrado_. 6 | 7 | 8 | ## Embeber un formulario 9 | Hasta ahora hemos escrito un formulario para recetas y otro para autores. Vamos a crear un único formulario que permita insertar de un solo golpe una receta y su autor. 10 | 11 | Nos aseguraremos de que la entidad `Recipe` dispone de los setters y getters necesarios: 12 | 13 | ```php 14 | // src/My/RecipesBundle/Entity/Recipe.php 15 | 16 | class Recipe 17 | { 18 | // ... 19 | 20 | public function getAuthor() 21 | { 22 | return $this->author; 23 | } 24 | 25 | public function setAuthor(Author $author) 26 | { 27 | $this->author = $author; 28 | return $this; 29 | } 30 | } 31 | ``` 32 | 33 | En el formulario `AuthorType` añadiremos un poco de información para que el componente de formularios sepa cómo debe tratar al objeto subyacente. 34 | 35 | ```php 36 | // src/My/RecipesBundle/Form/Type/AuthorType.php 37 | 38 | class AuthorType extends AbstractType 39 | { 40 | // ... 41 | 42 | public function setDefaultOptions(OptionsResolverInterface $resolver) 43 | { 44 | $resolver->setDefaults(array( 45 | 'data_class' => 'My\RecipesBundle\Entity\Author', 46 | // ... 47 | )); 48 | } 49 | } 50 | ``` 51 | 52 | 53 | Y finalmente modificaremos el formulario `RecipeType` para reutilizar el formulario `AuthorType`. Previamente habremos definido el formulario de Author como servicio. 54 | 55 | ```yaml 56 | services: 57 | # ... 58 | 59 | my_recipes.form.type.author: 60 | class: My\RecipesBundle\Form\Type\AuthorType 61 | tags: 62 | - { name: form.type, alias: author } 63 | ``` 64 | 65 | 66 | ```php 67 | // src/My/RecipesBundle/Form/Type/RecipeType.php 68 | class RecipeType extends AbstractType 69 | { 70 | public function buildForm(FormBuilderInterface $builder, array $options) 71 | { 72 | $builder 73 | ->add('name', 'text') 74 | ->add('difficulty', 'difficulty') 75 | ->add('author', 'author') 76 | ->add('save', 'submit'); 77 | } 78 | 79 | // ... 80 | } 81 | ``` 82 | 83 | Con esta configuración deberíamos ver el siguiente formulario en el navegador. 84 | 85 | ![Formulario embebido](embedded-forms.png "Formulario embebido") 86 | 87 | Como se aprecia en la imagen, se están renderizando dos botones de _submit_. Esto es así porque tanto en la clase `AuthorType` como en `RecipeType` se está añadiendo el correspondiente submit. Para evitarlo eliminaremos el componente submit del form `AuthorType`. 88 | 89 | ```php 90 | // src/My/RecipesBundle/Form/Type/AuthorType.php 91 | 92 | class AuthorType extends AbstractType 93 | { 94 | public function buildForm(FormBuilderInterface $builder, array $options) 95 | { 96 | $builder 97 | ->add('name', 'text') 98 | ->add('surname', 'text'); 99 | } 100 | 101 | // ... 102 | } 103 | ``` 104 | 105 | 106 | ![Formulario embebido corregido](embedded-forms-ii.png "Formulario embebido corregido") 107 | 108 | Deberemos incluir explicitamente el botón en la vista que renderiza por separado el formulario de autor. 109 | 110 | ```html 111 | {# src/My/RecipesBundle/Resources/views/Author/create.html.twig #} 112 | {% block body %} 113 | 119 | {{ form_end(form) }} 120 | {% endblock %} 121 | ``` 122 | 123 | Ahora sí, cuando creemos una receta se creará automáticamente el autor seleccionado. El último paso es propagar la validación a los formularios embebidos. Para ello modificaremos `RecipeType`. 124 | 125 | ```php 126 | // src/My/RecipesBundle/Form/Type/RecipeType.php 127 | 128 | class RecipeType extends AbstractType 129 | { 130 | // ... 131 | 132 | public function setDefaultOptions(OptionsResolverInterface $resolver) 133 | { 134 | $resolver->setDefaults(array( 135 | 'data_class' => 'My\RecipesBundle\Entity\Recipe', 136 | 'cascade_validation' => true, 137 | )); 138 | } 139 | } 140 | ``` 141 | 142 | ## Embeber colecciones de formulario 143 | 144 | Las colecciones de formularios permiten resolver las relaciones uno a muchos y muchos a muchos. Por ejemplo, en el formulario de recetas podríamos incluir una colección de formularios que permita añadir múltiples ingredientes. 145 | 146 | Las colecciones de formularios requieren de un poco de código en JavaScript, y merecen ser estudiadas en un capítulo aparte. Toda la información al respecto está disponible en el capítulo [How to Embed a Collection of Forms](http://symfony.com/doc/current/cookbook/form/form_collections.html) de la documentación oficial. 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /3-doctrine/entidades.md: -------------------------------------------------------------------------------- 1 | # Entidades 2 | 3 | 4 | ## Definir entidades 5 | 6 | Las entidades en Doctrine son aquellos actores del sistema que necesitan ser persistidos. Para crear una entidad, en primer lugar empezaremos por escribir una clase en el directorio `Entity` de nuestro bundle. 7 | 8 | ```Recipe.php 9 | // src/My/RecipesBundle/Entity/Recipe.php 10 | namespace My\RecipesBundle\Entity; 11 | 12 | 13 | class Recipe 14 | { 15 | private $id; 16 | 17 | protected $name; 18 | 19 | protected $difficulty; 20 | 21 | protected $description; 22 | } 23 | ``` 24 | 25 | Posteriormente necesitaremos proporcionar la información de mapeo de los campos de esta clase con una tabla de la base de datos. Disponemos de varias maneras de hacerlo: anotaciones, xml e yml. En esta guía optaremos por la configuración en yml. 26 | 27 | 28 | ```Recipe.orm.yml 29 | # src/My/RecipesBundle/Resources/config/doctrine/Recipe.orm.yml 30 | My\RecipesBundle\Entity\Recipe: 31 | type: entity 32 | table: recipes 33 | id: 34 | id: 35 | type: integer 36 | generator: { strategy: AUTO } 37 | fields: 38 | name: 39 | type: string 40 | length: 255 41 | difficulty: 42 | type: string 43 | length: 40 44 | description: 45 | type: text 46 | ``` 47 | 48 | ## Creación de tablas en la base de datos 49 | 50 | Estos dos archivos son ya suficientes para generar la entidad en la base de datos. Para comprobarlo, ejecuta la siguiente instrucción en el terminal. 51 | 52 | ``` 53 | $ php app/console doctrine:schema:create --dump-sql 54 | CREATE TABLE recipes (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(40) NOT NULL, description LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; 55 | ``` 56 | 57 | Confirma la creación eliminando los parámetros opcionales. 58 | 59 | ``` 60 | $ php app/console doctrine:schema:create 61 | ATTENTION: This operation should not be executed in a production environment. 62 | 63 | Creating database schema... 64 | Database schema created successfully! 65 | ``` 66 | 67 | Para poder acceder a los atributos de la clase necesitaremos algunos getters. El componente de formularios de symfony, por otra parte, necesitará algunos setters. Si queremos añadir estos métodos automáticamente podemos usar la consola: 68 | 69 | 70 | ``` 71 | $ app/console doctrine:generate:entities My/RecipesBundle/Entity/Recipe 72 | Generating entity "My\RecipesBundle\Entity\Recipe" 73 | > backing up Recipe.php to Recipe.php~ 74 | > generating My\RecipesBundle\Entity\Recipe 75 | ``` 76 | 77 | Si ahora visitamos la clase Recipe, veremos que se han creado accesores para todos los atributos. 78 | 79 | 80 | Una forma más directa de crear una entidad es el comando `app/console doctrine:generate:entity`, que interactivamente permite la generación automática de la entidad. 81 | 82 | 83 | ## Persistir entidades 84 | 85 | Para manipular entidades de Doctrine necesitaremos el `EntityManager`. Esta clase es la encargada de gestionar la persistencia. 86 | 87 | Los controladores de Doctrine que extiendan la clase `Controller` pueden acceder al `EntityManager` con `$this->getDoctrine()->getManager()`. 88 | 89 | 90 | ```DefaultController.php 91 | // src/My/RecipesBundle/Controller/DefaultController.php 92 | 93 | use My\RecipesBundle\Entity\Recipe; 94 | use Symfony\Component\HttpFoundation\Response; 95 | 96 | public function createAction() 97 | { 98 | $recipe = new Recipe(); 99 | $recipe->setName('Pollo al pil-pil'); 100 | $recipe->setDificulty('fácil'); 101 | $recipe->setDescription('...'); 102 | 103 | $em = $this->getDoctrine()->getManager(); 104 | $em->persist($recipe); 105 | $em->flush(); 106 | 107 | return new Response('Creada receta con id ' . $recipe->getId()); 108 | } 109 | ``` 110 | 111 | La instrucción `persist` no desencadena la operación `INSERT` hasta que se ejecuta la instrucción `flush()`. En ese momento, Doctrine persiste la entidad y actualiza su id. 112 | 113 | 114 | ## Recuperar entidades 115 | 116 | La forma más sencilla de recuperar una entidad es a través de su `id` mediante el método `find()`. Para recuperar entidades necesitaremos un **Repositorio**. Doctrine proporciona repositorios por defecto para todas las entidades que definamos. 117 | 118 | 119 | ```DefaultController.php 120 | // src/My/RecipesBundle/Controller/DefaultController.php 121 | 122 | public function showAction($id) 123 | { 124 | $repository = $this->getDoctrine() 125 | ->getRepository('MyRecipesBundle:Recipe'); 126 | $recipe = $repository->find($id); 127 | // ... 128 | } 129 | ``` 130 | 131 | Para recuperar todos los elementos utilizaremos `findAll`. 132 | 133 | ``` 134 | $repository->findAll(); // todas las instancias de Recipe. 135 | ``` 136 | 137 | También podemos especificar algunos criterios de filtrado y ordenación con findBy() 138 | ``` 139 | $repository->findBy(array('difficulty' => 'easy')); // Instancias filtradas por dificultad 140 | $repository->findBy(array(), array('name' => 'DESC')); // Ordenación 141 | ``` 142 | 143 | Doctrine utiliza metaprogramación para permitir algunos métodos más legibles. 144 | ``` 145 | $repository->findByDifficulty('easy'); 146 | $repository->findOneByName('Pollo al pil-pil'); 147 | ``` 148 | 149 | 150 | ## Actualizar entidades 151 | 152 | Para actualizar una entidad, basta con modificar sus atributos e invocar al método `flush()` del `EntityManager`. 153 | 154 | ``` 155 | $recipe = $repository->findOneByName('Pollo al pil-pil'); 156 | $recipe->setName('Pollo al chilindrón'); 157 | $this->getDoctrine()->getManager()->flush(); 158 | ``` 159 | 160 | 161 | ## Eliminar entidades 162 | El borrado de entidades se realiza mediante el método `remove()` del `EntityManager`. 163 | 164 | ``` 165 | $recipe = $repository->find($id); 166 | $em = $this->getDoctrine()->getManager(); 167 | $em->remove($recipe); 168 | $em->flush(); 169 | ``` 170 | -------------------------------------------------------------------------------- /1-introduccion/composer.md: -------------------------------------------------------------------------------- 1 | # Gestión de paquetes con Composer 2 | 3 | ## Qué es Composer 4 | [Composer](http://getcomposer.org/) es una herramienta para la gestión de dependencias en PHP. A diferencia de un gestor de paquetes, la función de Composer no es instalar un paquete en un sistema operativo, sino gestionarlas dentro de una aplicación concreta. 5 | 6 | Liberado en 2012, Composer ha supuesto una auténtica revolución en el mundo PHP, muy necesitado de esta herramienta. Supone la estandarización en la definición de las dependencias tal y como ya hicieron en NodeJS [npm](https://npmjs.org/) o Ruby [bundler](http://bundler.io/). Esta estandarización ha permitido el florecimiento de todo un ecosistema de bibliotecas base y para distintos frameworks que la han adoptado. 7 | 8 | En Symfony, Composer fue introducido en su versión 2.1. En la versión anterior se utilizaba un gestor de dependencias propio. 9 | 10 | ## Cómo definir un proyecto con Composer 11 | Toda aplicación que utilice Composer debe contener en su raíz un archivo `composer.json` que defina sus atributos básicos. Por ejemplo: 12 | 13 | ```composer.json 14 | { 15 | "name": "carlescliment/html2pdf-service", 16 | "description": "A REST microservice that converts html input into pdf files. Written in Silex.", 17 | "version": "0.0.2", 18 | "type": "project", 19 | "keywords": ["printing", "pdf"], 20 | "license": "GPL-2.0", 21 | "authors": [ 22 | { 23 | "name": "Carles Climent Granell", 24 | "email": "carlescliment@gmail.com", 25 | "homepage": "http://www.carlescliment.com", 26 | "role": "Developer" 27 | } 28 | ], 29 | "require": { 30 | "php": ">=5.3.2", 31 | # ... 32 | }, 33 | "require-dev": { 34 | "symfony/browser-kit": ">=2.3,<2.4-dev", 35 | "phpunit/phpunit": "3.7.*" 36 | }, 37 | "autoload": { 38 | "psr-0": { "carlescliment\\Html2Pdf": "src" } 39 | }, 40 | "minimum-stability" : "stable" 41 | } 42 | ``` 43 | 44 | De este ejemplo podemos extraer que: 45 | * El nombre del paquete es `html2pdf-service`, y el vendor (autor) es `carlescliment`. 46 | * Se encuentra en la versión `0.0.2`. 47 | * Es de tipo "project". Los tipos válidos estándar son; `library` (por defecto), `project`, `metapackage` y `composer-plugin`. Otros tipos personalizados también son admitidos, como `wordpress-plugin`. 48 | * Se han añadido dos palabras clave `printing` y `pdf` para facilitar su búsqueda. 49 | * Está liberado bajo licencia GPL 2. 50 | * Su autor es Carles Climent Granell. 51 | * Tiene algunas dependencias necesarias para su puesta en producción (require) y otras para entornos de desarrollo (require-dev). 52 | * Utiliza una estrategia de autocarga definida por el estándar PSR-0. 53 | * Sólo admite dependencias estables. 54 | 55 | 56 | Veamos cómo funciona con mayor detenimiento. 57 | 58 | ## Cómo establecer dependencias 59 | Para establecer una dependencia introduciremos el nombre del paquete o biblioteca en una clave `require`. 60 | 61 | ```composer.json 62 | { 63 | "require": { 64 | "php": ">=5.3.2", 65 | "monolog/monolog": "1.2.*" 66 | } 67 | } 68 | ``` 69 | 70 | En la parte izquierda especificaremos el nombre del paquete, mientras que en la derecha especificamos la versión. 71 | 72 | Las versiones pueden especificarse de distintas maneras: 73 | 74 | | Match | Ejemplo | Descripción | 75 | |---------|-------|----| 76 | | Exacto | `1.0.2` | Descarga la versión especificada y solo esa. | 77 | | Rango | `>=1.0` | Versión igual o mayor que 1.0 | 78 | | Rango | `>=1.0,!=1.4` | Versión igual o mayor que 1.0, excepto la 1.4. La coma se interpreta como un AND. | 79 | | Rango | `>=1.0,<2.0` | Versiones entre 1.0 y 2.0 | 80 | | Rango | >=1.0,<1.1 | >=1.2 | Versiones entre 1.0 y 1.1, o mayor que la 1.2. La tubería se interpreta como un OR | 81 | | Comodín | `1.0.*` | Equivale a >=1.0,<1.1 | 82 | | Tilde | `~1.2` | Equivale a >=1.2,<2.0 | 83 | 84 | Por defecto, solo se tendrán en cuenta las versiones estables de cada paquete. Este comportamiento puede ser modificado mediante la clave `minimum-stability`. Las opciones son `dev`, `apha`, `beta`, `RC` y `stable`. 85 | 86 | ```composer.json 87 | { 88 | # ... 89 | "minimum-stability": "dev" 90 | } 91 | ``` 92 | Una vez especificadas las dependencias, basta con instalarlas ejecutando `php composer.phar update`. Tras ejecutar el comando se iniciará la descarga de las dependencias, se generará un autoloader y se creará un archivo `composer.lock`. 93 | 94 | ## Autoloading 95 | Composer crea un archivo `vendor/autoload.php` que contiene la carga automática de clases y espacios de nombres de aquellos paquetes que lo necesiten. Esto te permitirá incluir fácilmente estos paquetes en tu proyecto añadiendo simplemente un `require` en tu aplicación: 96 | 97 | `require 'vendor/autoload.php';` 98 | 99 | Para cargar tu propio código en el autoloader puedes añadir la etiqueta `autoload` en `composer.json`. 100 | 101 | ```composer.json 102 | { 103 | # ... 104 | "autoload": { 105 | "psr-0": { "carlescliment\\Html2Pdf": "src" } 106 | } 107 | } 108 | ``` 109 | 110 | ## El Lock File 111 | El archivo `composer.lock` contiene las versiones instaladas actualmente en la aplicación. Si composer encuentra este archivo, descargará exactamente las versiones definidas en el archivo. Es recomendable añadir el archivos composer.lock al repositorio de versiones. De esta manera, cualquiera que se descargue el código instalará las mismas versiones que quien añadió el archivo. ¡Pensad por ejemplo en los sistemas de integración continua!. 112 | 113 | En el caso de que Composer no encuentre un `composer.lock`, generará uno nuevo a partir del `composer.json`. 114 | 115 | ## Packagist 116 | Si queremos publicar un proyecto para que otros puedan descargarlo via Composer, debemos dotarlo de un archivo `composer.json` válido y darlo de alta en la base de datos de la web [Packagist](https://packagist.org/). La mayoría de los gestores de versiones del mercado permiten disparar de manera automática acciones (hooks) que informarán a Packagist de nuevas versiones. 117 | 118 | Es posible añadir otras fuentes, además de Packagist. [Satis](https://github.com/composer/satis), por ejemplo, es una herramienta que permite crearnos un _Packagist privado_. Resulta muy útil cuando tenemos dependencias de código que no deseamos publicar. Para más información sobre cómo gestionar repositorios privados con composer ver el siguiente enlace: 119 | 120 | [Handling private packages with Satis](https://github.com/composer/composer/blob/master/doc/articles/handling-private-packages-with-satis.md) 121 | -------------------------------------------------------------------------------- /2-symfony-a-vista-de-pajaro/fundamentos-http.md: -------------------------------------------------------------------------------- 1 | # Fundamentos HTTP 2 | 3 | ## Qué es HTTP 4 | 5 | HTTP es un **protocolo de aplicación** usado en **la web** (www) para comunicaciones entre cliente y servidor. El servidor puede ser un ordenador que aloja una aplicación web, y el cliente web más común que conocemos es el navegador o **web browser**. Pero con el tiempo han salido muchos otros modelos de cliente servidor. De hecho, en la actualidad es muy común que un servidor ejerza de cliente de otros servidores. 6 | 7 | ![La Pila OSI](http://www.washington.edu/lst/help/computing_fundamentals/networking/img/osi_model.jpg "La Pila OSI") 8 | 9 | HTTP define cómo debe construirse una **petición** y cómo debe devolverse una **respuesta**. 10 | 11 | ## La petición 12 | 13 | En una petición se especifica: 14 | * Una línea con el método, el recurso y la versión de HTTP 15 | * Cabeceras opcionales 16 | * La dirección del servidor 17 | * El cuerpo de la petición, separado por una línea en blanco. 18 | 19 | 20 | ```http-request 21 | PUT /recetas/pollo-al-pil-pil HTTP/1.1 22 | Host: cocinando.com 23 | Content-Type: application/json 24 | 25 | {nombre:"Pollo al Pil-Pil", dificultad:2, ingredientes:["1 chorrito de aceite de oliva", "2 pechugas de pollo fileteadas", "Una cabeza de ajos", "Guindillas", "Perejil", "1 limón", "Colorante", "1 vaso de cerveza", "Sal"]} 26 | 27 | ``` 28 | 29 | 30 | ### Métodos 31 | 32 | La versión HTTP/1.1 define [ocho posibles métodos](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html) para una petición. Estos métodos pueden tener algunas propiedades, como _seguro_ e _idempotente_. 33 | 34 | #### Métodos seguros 35 | 36 | Son métodos que no desencadenan acciones en el servidor, aparte de recoger información. Entre ellos tenemos GET y HEAD. Aunque estos métodos estén definidos como seguros, depende de nosotros, los implementadores, que lo sean efectivamente. Es decir, debemos procurar que las llamadas GET y HEAD no desencadenen acciones que alteren el estado del sistema. 37 | 38 | #### Métodos idempotentes 39 | 40 | Son métodos que podemos repetir varias veces obteniendo el mismo resultado en el servidor. Entre ellos tenemos GET, HEAD, PUT, DELETE, OPTIONS y TRACE. Por el contrario, el método POST no es idemponente, ya que repitiendo la misma operación alteramos el estado del sistema. Por definición, todos los métodos seguros son idempotentes. 41 | 42 | 43 | #### Listado de métodos 44 | 45 | | Método | Seguro | Idempotente | Descripción | 46 | |-----------|-------------|--------------|---------------| 47 | | OPTIONS | Sí | Sí | Solicita información sobre las distintas opciones de comunicación disponibles. | 48 | | GET | Sí | Sí | Recupera cualquier información en forma de recurso. | 49 | | HEAD | Sí | Sí | Idéntico a GET, salvo que en la respuesta no se devuelve contenido. Usado para recibir algunos datos del recurso optimizando la transferencia. | 50 | | POST | No | No | Almacena la entidad enviada. El URN representa a otra entidad que se encargará de gestionar ese almacenamiento. | 51 | | PUT | No | Sí | Almacena la entidad enviada. El URN representa a la propia entidad. | 52 | | DELETE | No | Sí | Elimina la entidad representada por el URN. | 53 | | TRACE | Sí | Sí | Utiliza para debug, el servidor devuelve el propio contenido de la petición. | 54 | | CONNECT | | | Reservado para establecer conexiones permanentes (tunneling) | 55 | | PATCH (*) | No | Sí | Realiza modificaciones parciales en entidades existentes. | 56 | 57 | (*) El método PATCH es bastante novedoso y no se incluye en el protocolo, aunque está siendo utilizado cada vez más. 58 | 59 | 60 | 61 | ### Recursos 62 | 63 | Los recursos se identifican por URNs (Uniform Resource Names). Los recursos deben estar identificados de manera unívoca, esto es, sólo debería haber un URN por cada recurso. Ejemplos de recursos válidos son: 64 | 65 | - `/` 66 | - `/clientes/23` 67 | - `/clientes/juan-martinez` 68 | 69 | 70 | Los query parameters enviados no definen recursos. Por ejemplo los siguientes recursos serían equivalentes y no se ajustarían al estándar HTTP. Ambos apuntarían a la colección "clientes": 71 | 72 | - `/clientes?nombre=Juan` 73 | - `/clientes?nombre=Antonio` 74 | 75 | 76 | ### Cabeceras opcionales 77 | 78 | El protocolo HTTP define [una serie de cabeceras](http://www.w3.org/Protocols/HTTP/HTRQ_Headers.html) que pueden enviarse adicionalmente en una petición. Adicionalmente se han extendido otras cabeceras no estándar que muchos servidores aceptan igualmente. Entre las cabeceras más comunes tenemos: 79 | 80 | | Nombre | Descripción | Ejemplo | 81 | |----------|------------------|---------| 82 | | From | Identifica al autor de la petición. | From: my@email.com | 83 | | Accept | Formatos que el cliente acepta | Accept: text/plain, text/html | 84 | | Accept-Encoding | Similar a Accept, especifica los formatos de codificación aceptados. | Accept-Encoding: x-zip | 85 | | Referer | Especifica la dirección desde la que se ha accedido al recurso | Referer : http://misrecetas.com/pollo-al-pil-pil | 86 | 87 | 88 | ## La respuesta 89 | 90 | Una respuesta HTTP consta de: 91 | 92 | - Una linea de status con la versión, el código de respuesta y el nombre del código. 93 | - Cabeceras opcionales 94 | - El cuerpo de la respuesta 95 | 96 | ```http-response 97 | HTTP/1.1 200 OK 98 | Date: Mon, 23 May 2005 22:38:34 GMT 99 | Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux) 100 | Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT 101 | ETag: "3f80f-1b6-3e1cb03b" 102 | Content-Type: text/html; charset=UTF-8 103 | Content-Length: 131 104 | Connection: close 105 | 106 | 107 | 108 | An Example Page 109 | 110 | 111 | Hello World, this is a very simple HTML document. 112 | 113 | 114 | ``` 115 | 116 | ### Códigos de respuesta 117 | 118 | A continuación se describen algunos códigos de respuesta y su significado. Para una información más completa, consulta la [documentación oficial](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html). 119 | 120 | | Código | Nombre | Descripción | 121 | | -------|--------|-------------| 122 | | 100 | Continue | El cliente debe continuar enviando más informacion de la petición. | 123 | | 200 | OK | La petición ha tenido éxito | 124 | | 201 | Created | Un nuevo recurso ha sido creado | 125 | | 202 | Accepted | La petición se ha aceptado y será procesada. | 126 | | 204 | No Content | La petición se ha llevado a cabo con éxito, pero no se devuelve contenido | 127 | | 300 | Multiple choices | Indica al cliente distintas opciones donde encontrar el recurso | 128 | | 301 | Moved permanently | El recurso ya no existe. La nueva ruta debería proporcionarse en el cuerpo de la respuesta | 129 | | 400 | Bad Request | El servidor no pudo procesar la petición porque esta no estaba bien formada | 130 | | 403 | Forbidden | La petición ha sido rechazada | 131 | | 404 | Not Found | El recurso no se ha encontrado | 132 | | 500 | Internal Server Error | Ha ocurrido un error en el servidor | 133 | | 501 | Not Implemented | La funcionalidad aún no ha sido implementada | 134 | | 505 | HTTP Version Not Supported | La versión indicada en la petición es compatible con el servidor | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /4-twig/conceptos-basicos.md: -------------------------------------------------------------------------------- 1 | # Conceptos básicos 2 | 3 | ## Sintaxis 4 | 5 | Twig proporciona tres tipos de marcas: 6 | 7 | | Marca | Ejemplo | Propósito | 8 | |-----------|------------------------------------------|--------------------------------------| 9 | | `{{ ... }}` | `{{ recipe.name }}` | Muestra contenido | 10 | | `{% ... %}` | `{% if expression %} ... {% endif %}` | Estructuras de control, evaluaciones | 11 | | `{# ... #}` | `{# Some comment here #}` | Inserta un comentario HTML | 12 | | var|filter | {{ recipe.created|date }} | Aplica un filtro a la variable | 13 | 14 | 15 | ## Renderizado 16 | 17 | Una de las mayores ventajas de Twig es la seguridad que proporciona ante ataques de [Cross Site Scripting](http://en.wikipedia.org/wiki/Cross-site_scripting) (XSS). Imaginemos que, en la aplicación de recetas, un usuario malintencionado creara una receta con nombre ``. Imaginemos ahora una plantilla en PHP como la siguiente: 18 | 19 | ```html 20 |

getName(); ?>

21 | 22 | ``` 23 | 24 | El HTML en consecuencia sería: 25 | 26 | ```html 27 |

28 | 29 | ``` 30 | 31 | Cualquier usuario que accediese a esa página ejecutaría un código JavaScript indeseado. ¡Las consecuencias podrían ser gravísimas!. 32 | 33 | Afortunadamente, en Twig, todas las variables que mostremos a través de `{{ ... }}` serán escapadas automáticamente. 34 | ```html 35 |

<script>alert('You have been hacked')</script>

36 | 37 | ``` 38 | 39 | Twig también proporciona atajos al acceder a objetos. En twig, las llamadas `recipe.name` y `recipe.getName()` son equivalentes. Cuando utilizamos la forma abreviada, twig buscará en el objeto `recipe` un atributo público `name`. Si no lo encuentra, buscará los métodos `name()`, `getName()` e `isName()`. Debido a que existe una compilación y guardado en caché esta funcionalidad no tiene impacto en el rendimiento. 40 | 41 | ## Variables 42 | 43 | En una plantilla Twig podemos utilizar variables locales o globales. 44 | 45 | ### Variables locales 46 | Las variables locales son aquellas que se han proporcionado a la plantilla a través del controlador: 47 | 48 | ``` 49 | $this->render('MyRecipesBundle:Default:show', array('recipe' => $recipe)); 50 | ``` 51 | 52 | También son variables locales las definidas dentro de la propia plantilla: 53 | 54 | ``` 55 | {% set system_messages = ['error', 'warning', 'notice', 'success'] %} 56 | {% for type in system_messages %} 57 | {{ ... }} 58 | {% endfor %} 59 | ``` 60 | 61 | ### Variables globales 62 | 63 | Symfony define la variable global `app` que permite acceder a otras variables de la aplicación: 64 | 65 | - `app.security`: Contexto de seguridad. 66 | - `app.user`: Usuario actual. 67 | - `app.request`: Objeto request. 68 | - `app.session`: Objeto sesión. 69 | - `app.environment`: Entorno actual (dev, prod, test...). 70 | - `app.debug`: Modo debug (true, false). 71 | 72 | Podemos definir nuestras propias variables globales en twig modificando `config.yml`. 73 | 74 | ```yaml 75 | # app/config/parameters.yml 76 | parameters: 77 | ga_tracking: UA-xxxxx-x 78 | 79 | 80 | # app/config/config.yml 81 | twig: 82 | globals: 83 | ga_tracking: "%ga_tracking%" 84 | ``` 85 | 86 | Para una mayor información sobre la definición de variables globales, consultad el capítulo [Global Variables](http://symfony.com/doc/current/cookbook/templating/global_variables.html) de la documentación oficial. 87 | 88 | 89 | ## Tags 90 | 91 | ### Estructuras de control 92 | 93 | En Twig existen dos estructuras de control; bucles y condicionales. Los condicionales se representan con el *tag* `if`. 94 | 95 | ```twig 96 | {% if recipe.difficulty == 'fácil' %} 97 | No tendrás problemas para concinar esta receta. 98 | {% elseif recipe.difficulty == 'media' %} 99 | Esta receta requiere conocimientos avanzados de cocina. 100 | {% else %} 101 | ¡Para dominar esta receta necesitas ser un Top Chef! 102 | {% endif %} 103 | ``` 104 | 105 | Podremos recorrer arrays y colecciones con el tag `for`. 106 | 107 | ```twig 108 |

Recetas del autor

109 | 114 | ``` 115 | 116 | En los bucles podemos recuperar el número de la iteración con `loop.index` y `loop.index0`: 117 | 118 | ```twig 119 | {% for recipe in author.recipes %} 120 |
  • {{ recipe.name }}
  • 121 | {% endfor %} 122 | ``` 123 | 124 | 125 | 126 | ### Macros 127 | Las macros equivales a funciones de un lenguaje de programación. Permite reusar componentes en varias plantillas. 128 | 129 | ```twig 130 | {% macro list_recipes(recipes) %} 131 | 136 | {% endmacro %} 137 | ``` 138 | 139 | De este modo, el ejemplo del tag `for` podría ser reescrito: 140 | 141 | ```twig 142 | {% import "recipe_helpers.html" as helpers %} 143 | 144 |

    Recetas del autor

    145 | {{ helpers.list_recipes(author.recipes) }} 146 | ``` 147 | 148 | ### Otros tags 149 | 150 | El tag `{% spaceless %}` eliminará los espacios en blanco, ofreciendo documentos HTML más limpios. 151 | 152 | ```twig 153 | {% spaceless %} 154 |
    155 |

    Aquí una linea de texto

    156 |
    157 | {% endspaceless %} 158 | 159 | 160 |

    Aquí una linea de texto

    161 | ``` 162 | 163 | 164 | El tag `verbatim` permite que el texto en su interior se muestre tal cual en el cliente. Útil cuando queremos representar código. 165 | 166 | ```html 167 | {% verbatim %} 168 |
    Esto se mostrará tal cual, con los tags HTML visbiles.
    169 | {% endverbatim %} 170 | ``` 171 | 172 | Twig ofrece una amplia variedad de tags documentada en [la web oficial](http://twig.sensiolabs.org/doc/tags/index.html). 173 | 174 | 175 | 176 | ## Filtros 177 | Los filtros son funciones aplicamos sobre el contenido. Existe una [larga lista de filtros](http://twig.sensiolabs.org/doc/filters/index.html) proporcionados por Twig, a la que podemos añadir nuestros propios filtros mediante extensiones. 178 | 179 | El elemento sobre el que apliquemos el filtro será tomado como input, y a partir de él se devolverá un output. Este output será el que finalmente se muestre en pantalla. Por ejemplo, el filtro `upper` convierte un texto a mayúsculas. 180 | 181 | ```html 182 |

    {{ recipe.name|upper }}

    183 | 184 |

    POLLO AL PIL-PIL

    185 | ``` 186 | 187 | ## Funciones 188 | 189 | En Twig pueden añadirse funciones que extiendan las capacidades de nuestras plantillas. Symfony añade algunas funciones al motor, como las relativas a la [gestión de formularios](http://symfony.com/doc/current/reference/forms/twig_reference.html#reference-form-twig-functions) o a la creación dinámica de enlaces. Para la generación de enlaces disponemos de las funciones `url` y `path`. Mientras que la primera genera una url completa, la segunda sólo añade una URI relativa. 190 | 191 | ```html 192 | Ver 193 | Ver 194 | 195 | Ver 196 | Ver 197 | ``` 198 | 199 | 200 | ## Caché 201 | 202 | La primera vez que se renderiza una plantilla se genera un código equivalente en PHP. A este proceso se le denomina _compilado_. Las plantillas compiladas se almacenan por defecto en el directorio `app/cache/{environment}/twig`, donde `{environment}` es el nombre del entorno. 203 | 204 | Para que los efectos se manifiesten cuando modifiquemos una plantilla, deberemos limpiar la caché. Esto no ocurre en los entornos de `test` y `dev`, donde la caché está desactivada. 205 | 206 | -------------------------------------------------------------------------------- /8-seguridad/conceptos-basicos.md: -------------------------------------------------------------------------------- 1 | # Conceptos básicos 2 | 3 | Symfony 2 dispone de su propio [componente de seguridad](http://symfony.com/doc/current/components/security/introduction.html) que puede ser utilizado de forma independiente en cualquier aplicación PHP. 4 | 5 | La seguridad en Symfony 2 se realiza en dos pasos: la *autenticación*, donde el sistema reconoce quién realiza la petición, y la *autorización*, donde se aplicarán las reglas que decidirán si el usuario puede o no acceder al recurso especificado. 6 | 7 | 8 | ## Autenticación y autorización 9 | 10 | ### Firewalls (autenticación) 11 | 12 | Los firewalls son reglas que se aplican a las rutas de la aplicación y definen áreas seguras. 13 | 14 | ``` 15 | # app/config/security.yml 16 | security: 17 | firewalls: 18 | secured_area: 19 | pattern: ^/ 20 | anonymous: ~ 21 | http_basic: 22 | realm: "Mis recetas" 23 | 24 | # ... 25 | ``` 26 | 27 | El firewall del ejemplo anterior de security.yml se está aplicando a toda la web, por lo que cualquier petición será gestinada por dicho firewall. Existen diversas formas de autenticar a un usuario. La más sencilla de ellas es la autenticación básica del protocolo HTTP (usuario/contraseña), que es la utilizada en el firewall del ejemplo 28 | 29 | 30 | ### Control de acceso (autorización) 31 | 32 | Cuando se accede a una ruta bajo un firewall, se aplican las reglas de control de acceso que se hayan definido en el mismo. 33 | 34 | ``` 35 | # app/config/security.yml 36 | security: 37 | firewalls: 38 | secured_area: 39 | pattern: ^/ 40 | anonymous: ~ 41 | http_basic: 42 | realm: "Mis recetas" 43 | 44 | access_control: 45 | - { path: ^/secured, roles: ROLE_ADMIN } 46 | 47 | # ... 48 | ``` 49 | 50 | En el anterior ejemplo hemos definido una zona segura en `/secured`. Cualquier ruta que siga el patrón `/secured/*` se puede acceder solo por aquellos usuarios con el rol `ROLE_ADMIN`. Pero ¿dónde se definen los usuarios?. 51 | 52 | 53 | ### Usuarios 54 | 55 | El componente de seguridad de Symfony utiliza *user providers* para obtener los usuarios del sistema. El user provider más sencillo se denomina `in_memory` y permite especificar usuarios que se almacenan directamente en memoria. 56 | 57 | ``` 58 | # app/config/security.yml 59 | security: 60 | firewalls: 61 | secured_area: 62 | pattern: ^/ 63 | anonymous: ~ 64 | http_basic: 65 | realm: "Mis recetas" 66 | 67 | access_control: 68 | - { path: ^/secured, roles: ROLE_ADMIN } 69 | 70 | providers: 71 | in_memory: 72 | memory: 73 | users: 74 | user: { password: user, roles: 'ROLE_USER' } 75 | admin: { password: admin, roles: 'ROLE_ADMIN' } 76 | 77 | encoders: 78 | Symfony\Component\Security\Core\User\User: plaintext 79 | ``` 80 | 81 | En el ejemplo anterior se definen dos usuarios; por una parte el usuario `user` identificado por el password `user`, que tiene el rol `ROLE_USER`, y por la otra el usuario `admin` identificado por `admin` con el rol `ROLE_ADMIN`. 82 | 83 | La última linea bajo `encoders` define la clase a utilizar para nuestros usuarios y la forma en que se codifica su password. Está definida como `plaintext`, por lo que no hay ninguna codificación. 84 | 85 | Con esta configuración ya tendremos definida una zona segura. Accediendo a ella veremos que el navegador solicita autenticación mediante la comentada autenticación básica HTTP. 86 | 87 | 88 | ### Autenticación con un formulario 89 | 90 | Aunque la autenticación HTTP puede bastar en algunos casos, lo habitual en los sitios web es utilizar un formulario de login. 91 | 92 | Para utilizar un formulario, en primer lugar necesitaremos establecer dos rutas; `login` y `check`. La primera se encargará del renderizado del formulario y la segunda de la validación del mismo. Sustituiremos la anterior clave `http_basic` por una nueva `form_login` tal y como se muestra en el ejemplo siguiente. 93 | 94 | ``` 95 | # app/config/security.yml 96 | security: 97 | firewalls: 98 | secured_area: 99 | pattern: ^/ 100 | anonymous: ~ 101 | form_login: 102 | login_path: login 103 | check_path: login_check 104 | ``` 105 | 106 | 107 | Una vez establecidas estas rutas en el firewall, las añadiremos a nuestro router. Es importante que los nombres de las rutas (`login` y `login_check`) coincidan con los valores introducidos en `security.yml`. 108 | 109 | ``` 110 | # app/config/routing.yml 111 | login: 112 | path: /login 113 | defaults: { _controller: MyRecipesBundle:Security:login } 114 | login_check: 115 | path: /login_check 116 | ``` 117 | 118 | El siguiente paso es completar el controlador `MyRecipesBundle:Security:login`. 119 | 120 | ``` 121 | // src/My/RecipesBundle/Controller/SecurityController.php; 122 | namespace My\RecipesBundle\Controller; 123 | 124 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; 125 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 126 | use Symfony\Component\HttpFoundation\Request; 127 | use Symfony\Component\Security\Core\SecurityContext; 128 | 129 | class SecurityController extends Controller 130 | { 131 | /** 132 | * @Template() 133 | */ 134 | public function loginAction(Request $request) 135 | { 136 | $session = $request->getSession(); 137 | 138 | // get the login error if there is one 139 | if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { 140 | $error = $request->attributes->get( 141 | SecurityContext::AUTHENTICATION_ERROR 142 | ); 143 | } else { 144 | $error = $session->get(SecurityContext::AUTHENTICATION_ERROR); 145 | $session->remove(SecurityContext::AUTHENTICATION_ERROR); 146 | } 147 | 148 | return array( 149 | // last username entered by the user 150 | 'last_username' => $session->get(SecurityContext::LAST_USERNAME), 151 | 'error' => $error, 152 | ); 153 | } 154 | } 155 | ``` 156 | 157 | Y finalmente crearemos la plantilla correspondiente. 158 | 159 | ```twig 160 | {# src/My/RecipesBundle/Resources/views/Security/login.html.twig #} 161 | {% if error %} 162 |
    {{ error.message }}
    163 | {% endif %} 164 | 165 |
    166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 |
    174 | ``` 175 | 176 | 177 | ¡Y ya está! Por supuesto podemos personalizar nuestro formulario tanto como queramos, siempre que se mantengan los nombres de los input fields. Tras estos cambios, cuando intentemos acceder a una ruta segura, el framework redirigirá la petición al formulario de entrada. 178 | 179 | 180 | ### Cierre de sesión 181 | 182 | Los cambios necesarios para ofrecer logout a los usuarios del sistema son bastante más sencillos, dado que el componente de seguridad ya tiene un comportamiento por defecto que se encarga de ello, basta con activarlo añadiedo la siguiente linea al archivo `security.yml`. 183 | 184 | ``` 185 | # app/config/security.yml 186 | security: 187 | firewalls: 188 | secured_area: 189 | # ... 190 | logout: ~ 191 | # ... 192 | ``` 193 | 194 | De esta manera, cualquier usuario autenticado que acceda a la ruta `/logout` dará por cerrada su sesión y será redirigido a la ruta índice `/`. 195 | 196 | Para personalizar las rutas de salida y redirección posterior podemos añadir los parámetros de configuración `path` y `target` inmediatamente bajo `logout`. 197 | 198 | ``` 199 | # app/config/security.yml 200 | security: 201 | firewalls: 202 | secured_area: 203 | # ... 204 | logout: 205 | path: /quit 206 | target: /recipes 207 | # ... 208 | ``` 209 | 210 | 211 | En ambos casos será necesario que añadamos esta ruta de salida a nuestro archivo `routing.yml`. 212 | 213 | ``` 214 | # app/config/routing.yml 215 | logout: 216 | path: /logout 217 | ``` 218 | -------------------------------------------------------------------------------- /2-symfony-a-vista-de-pajaro/routing.md: -------------------------------------------------------------------------------- 1 | # Routing 2 | 3 | Para una información más completa y actualizada, consultad la [documentación oficial](http://symfony.com/doc/current/book/routing.html). 4 | 5 | El sistema de enrutado relaciona los recursos indicados en las peticiones con los controladores encargados de gestionarlos. 6 | 7 | Symfony carga todas las rutas del archivo de enrutado de la aplicación, normalmente en `app/config/routing.yml`. En este archivo podemos incluir referencias a otras fuentes junto con otras opciones de configuración. 8 | 9 | ```app/config/routing.yml 10 | my_recipes: 11 | resource: "@MyRecipesBundle/Resources/config/routing.yml" 12 | prefix: / 13 | ``` 14 | 15 | Los archivos de configuración pueden escribirse en `yml`, `xml` o `php`. En este material didáctico veremos todos los ejemplos en yml. 16 | 17 | 18 | ## Crear una ruta 19 | 20 | Es una buena práctica almacenar las rutas proporcionadas por nuestros bundles en sus propios archivos `routing.yml`. De esta manera garantizaremos su portabilidad y mantendremos el enrutado más organizado. Una ruta está formada por dos atributos básicos: el patrón de la ruta o `path` y un diccionario que define el controlador a utilizar: 21 | 22 | ``` 23 | recipes_list: 24 | path: /recipes/ 25 | defaults: { _controller: MyRecipesBundle:Recipe:list } 26 | 27 | recipes_show: 28 | path: /recipes/{recipe_name} 29 | defaults: { _controller: MyRecipesBundle:Recipe:show } 30 | ``` 31 | 32 | Estas dos acciones están capturando las peticiones a los recursos especificados en `path` y enviándolas a sendos controladores. Los valores entre llaves simbolizan parámetros que son transferidos al controlador. 33 | 34 | La clase controladora concreta y el método a utilizar se especifican según el siguiente convenio: 35 | 36 | `Nombre de bundle : Nombre del controlador : Nombre de la acción` 37 | 38 | De acuerdo con los anteriores ejemplos, se invocaría finalmente a los siguientes métodos: 39 | 40 | ``` 41 | My\RecipesBundle\Controller\RecipeController::list(); 42 | My\RecipesBundle\Controller\RecipeController::show($recipe_name); 43 | ``` 44 | 45 | 46 | ## Parámetros opcionales 47 | 48 | Si tenemos rutas que comparten una misma acción del controlador, podemos reorganizarlas utilizando parámetros por defecto. Por ejemplo, en la siguiente configuración: 49 | 50 | ``` 51 | recipes_list: 52 | path: /recipes/ 53 | defaults: { _controller: MyRecipesBundle:Recipe:list } 54 | 55 | recipes_list_page: 56 | path: /recipes/{page} 57 | defaults: { _controller: MyRecipesBundle:Recipe:list } 58 | ``` 59 | 60 | Ambas dirigen la petición al controlador `Acme\BlogBundle\Controller\BlogController::indexAction()`, con la diferencia de que en la ruta inferior se está especificando el número de página. Estas dos rutas podrían refactorizarse en una única ruta con parámetro por defecto: 61 | 62 | ``` 63 | recipes_list: 64 | path: /recipes/{page} 65 | defaults: { _controller: MyRecipesBundle:Recipe:list, page: 1 } 66 | ``` 67 | 68 | ## Requisitos 69 | 70 | 71 | ### Validación de parámetros 72 | 73 | Si volvemos a los ejemplos anteriores, el resultado en nuestro fichero de enrutado sería el siguiente: 74 | 75 | 76 | ``` 77 | recipes_list: 78 | path: /recipes/{page} 79 | defaults: { _controller: MyRecipesBundle:Recipe:list, page: 1 } 80 | 81 | recipes_show: 82 | path: /recipes/{recipe_name} 83 | defaults: { _controller: MyRecipesBundle:Recipe:show } 84 | ``` 85 | 86 | ¿Cómo debería reaccionar Symfony ante la petición `/recipes/5`? ¿Debería responder con la página 5 del listado de recetas? ¿O, al contrario, devería responder con la receta de nombre "5"? 87 | 88 | En este caso Symfony respondería con la primera opción, ya que **las rutas que se definen primero tienen prioridad**. Pero la segunda ruta se vería por tanto enmascarada por la primera. 89 | 90 | En Symfony es posible asegurar que las rutas cumplen algunos requisitos mediante la clave requirements. El conflicto anterior podría resolverse añadiéndose la siguiente validación: 91 | 92 | ``` 93 | recipes_list: 94 | path: /recipes/{page} 95 | defaults: { _controller: MyRecipesBundle:Recipe:list, page: 1 } 96 | requirements: 97 | page: \d+ 98 | 99 | recipes_show: 100 | path: /recipes/{recipe_name} 101 | defaults: { _controller: MyRecipesBundle:Recipe:show } 102 | ``` 103 | 104 | Nótese la adición del parámetro `requirements` en la ruta `recipes_list`, donde se especifica que el parámetro `page` debe ser un número entero (uno o más dígitos). Ante una petición al recurso `/recipes/pollo-al-pil-pil`, el componente de enrutado comprobaría en primer lugar que se cumplieran todos los requisitos de la ruta `recipes_list`. Al no cumplirse el requisito, repetiría la operación con `recipes_show`, vinculando la ruta a la acción `My\RecipesBundle\Controller\RecipeController::show()`. 105 | 106 | ---------------------------------------------------------------------- 107 | > Nota del Autor: Aunque este caso se expone en la documentación oficial, en mi opinión es una mala práctica resolver estos conflictos mediante la etiqueta requirements. Los recursos deberían ser unívocos, no dar lugar a la ambigüedad. Imagínese el caso de la novela "1984", que en una aplicación para bibliotecas podría responder al recurso `/books/1984`. ¡Seguríamos teniendo colisiones con la página 1984 del listado de libros!. Una solución mejor para resolver el problema de la paginación es utilizar query arguments: `/books?page=1984`. 108 | ---------------------------------------------------------------------- 109 | 110 | Un mejor uso de la etiqueta requirements es validar las rutas anticipadamente, antes de que las peticiones lleguen al controlador. Si validamos en el enrutado que un parámetro numérico {id} es efectivamente numérico, evitaremos realizar futuras comprobaciones o consultas innecesarias a la base de datos. 111 | 112 | ``` 113 | recipes_show: 114 | path: /recipes/{recipe_id} 115 | defaults: { _controller: MyRecipesBundle:Recipe:show } 116 | requirements: 117 | recipe_id: \d+ 118 | ``` 119 | 120 | ### Validación de método 121 | 122 | En Symfony es posible enviar recursos a controladores distintos en función del método utilizado. 123 | 124 | ``` 125 | recipes_list: 126 | path: /recipes/ 127 | defaults: { _controller: MyRecipesBundle:Recipe:list } 128 | methods: [GET] 129 | 130 | recipes_add: 131 | path: /recipes/ 132 | defaults: { _controller: MyRecipesBundle:Recipe:create } 133 | methods: [POST] 134 | ``` 135 | 136 | Según esta configuración, se utilizará la acción RecipeController::list() para las peticiones a '/recipes/' con el verbo 'GET', y RecipeController::create() para la misma petición con el verbo 'POST'. 137 | 138 | ### Validación de host 139 | 140 | Symfony permite validar la dirección del host al que se envía la petición. Puede ser útil, por ejemplo, cuando queremos separar la web de un API REST utilizando subdominios: 141 | 142 | 143 | ``` 144 | recipes_api_list: 145 | path: /recipes/ 146 | host: api.recipes.com 147 | defaults: { _controller: MyRecipesBundle:API:list } 148 | 149 | recipes_list: 150 | path: /recipes/ 151 | defaults: { _controller: MyRecipesBundle:Recipe:list } 152 | ``` 153 | 154 | ### Validación de formato 155 | 156 | Con el filtro `_format` podemos cribar las peticiones según la cabecera HTTP `Content-Type`. 157 | 158 | ``` 159 | recipes_list: 160 | path: /recipes/ 161 | defaults: { _controller: MyRecipesBundle:Recipe:list } 162 | requirements: 163 | _format: html|rss 164 | 165 | recipes_list_json: 166 | path: /recipes/ 167 | defaults: { _controller: MyRecipesBundle:API:list } 168 | requirements: 169 | _format: json 170 | ``` 171 | 172 | De este modo podemos obtener representaciones distintas del mismo recurso según el formato recibido. 173 | 174 | 175 | ## Generar rutas con el componente de enrutado 176 | 177 | Cualquier clase con acceso al contenedor de inyección de dependencias de Symfony puede generar URIs. 178 | 179 | ``` 180 | $router = $this->get('router'); 181 | $uri = $router->generate('recipes_show', array('recipe_id' => 55)); 182 | 183 | // $uri == '/recipes/55'; 184 | ``` 185 | 186 | A continación se describen otros modos de uso: 187 | 188 | ``` 189 | // Rutas absolutas: http://www.misrecetas.com/recipes/55 190 | $router = $this->get('router'); 191 | $uri = $router->generate('recipes_show', array('recipe_id' => 55), true); 192 | 193 | // Query strings: /recipes/55?param1=foo 194 | $router = $this->get('router'); 195 | $uri = $router->generate('recipes_show', array('recipe_id' => 55, 'param1' => 'foo')); 196 | 197 | ``` 198 | 199 | 200 | 201 | Los controladores disponen de un método auxiliar `generateUrl()`. 202 | 203 | ``` 204 | $uri = $this->generateUrl('recipes_show', array('recipe_id' => 55)); 205 | // $uri == '/recipes/55'; 206 | ``` 207 | 208 | -------------------------------------------------------------------------------- /8-seguridad/usuarios.md: -------------------------------------------------------------------------------- 1 | # Usuarios y roles 2 | 3 | La mayoría de las aplicaciones web utilizan mecanismos más flexibles para gestionar usuarios que el almacenamiento `in_memory` que hemos visto en el ejemplo del punto anterior. En este capítulo veremos cómo crear una entidad en la base de datos que representará a los usuarios de nuestro sistema. 4 | 5 | ### Crear la entidad User 6 | 7 | En primer lugar generaremos un nuevo bundle encargado de gestionar usuarios. Por consistencia con los ejemplos mostrados hasta ahora lo llamaremos MyUserBundle. 8 | 9 | ``` 10 | app/console generate:bundle --namespace=My/UserBundle 11 | ``` 12 | 13 | Posteriormente crearemos la entidad User. 14 | 15 | ``` 16 | app/console doctrine:generate:entity --entity=MyUserBundle:User --fields="email:string(255) username:string(255) password:string(64) salt:string(64)" --format=yml 17 | ``` 18 | 19 | Como resultado obtendremos la siguiente entidad: 20 | 21 | ```php 22 | // src/My/UserBundle/Entity/User.php 23 | 24 | namespace My\UserBundle\Entity; 25 | 26 | use Doctrine\ORM\Mapping as ORM; 27 | 28 | /** 29 | * User 30 | */ 31 | class User 32 | { 33 | /** 34 | * @var integer 35 | */ 36 | private $id; 37 | 38 | /** 39 | * @var string 40 | */ 41 | private $email; 42 | 43 | /** 44 | * @var string 45 | */ 46 | private $username; 47 | 48 | /** 49 | * @var string 50 | */ 51 | private $password; 52 | 53 | /** 54 | * @var string 55 | */ 56 | private $salt; 57 | 58 | 59 | /** 60 | * Get id 61 | * 62 | * @return integer 63 | */ 64 | public function getId() 65 | { 66 | return $this->id; 67 | } 68 | 69 | /** 70 | * Set email 71 | * 72 | * @param string $email 73 | * @return User 74 | */ 75 | public function setEmail($email) 76 | { 77 | $this->email = $email; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Get email 84 | * 85 | * @return string 86 | */ 87 | public function getEmail() 88 | { 89 | return $this->email; 90 | } 91 | 92 | /** 93 | * Set username 94 | * 95 | * @param string $username 96 | * @return User 97 | */ 98 | public function setUsername($username) 99 | { 100 | $this->username = $username; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * Get username 107 | * 108 | * @return string 109 | */ 110 | public function getUsername() 111 | { 112 | return $this->username; 113 | } 114 | 115 | /** 116 | * Set password 117 | * 118 | * @param string $password 119 | * @return User 120 | */ 121 | public function setPassword($password) 122 | { 123 | $this->password = $password; 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * Get password 130 | * 131 | * @return string 132 | */ 133 | public function getPassword() 134 | { 135 | return $this->password; 136 | } 137 | 138 | /** 139 | * Set salt 140 | * 141 | * @param string $salt 142 | * @return User 143 | */ 144 | public function setSalt($salt) 145 | { 146 | $this->salt = $salt; 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * Get salt 153 | * 154 | * @return string 155 | */ 156 | public function getSalt() 157 | { 158 | return $this->salt; 159 | } 160 | } 161 | ``` 162 | 163 | Y un fichero `yml` del ORM Doctrine. 164 | 165 | ```yml 166 | #src/My/UserBundle/Resources/config/doctrine/User.orm.yml 167 | My\UserBundle\Entity\User: 168 | type: entity 169 | table: null 170 | fields: 171 | id: 172 | type: integer 173 | id: true 174 | generator: 175 | strategy: AUTO 176 | email: 177 | type: string 178 | length: '255' 179 | username: 180 | type: string 181 | length: '255' 182 | password: 183 | type: string 184 | length: '64' 185 | salt: 186 | type: string 187 | length: '64' 188 | lifecycleCallbacks: { } 189 | ``` 190 | 191 | Modificaremos el email para que sea único: 192 | 193 | ```yml 194 | #src/My/UserBundle/Resources/config/doctrine/User.orm.yml 195 | My\UserBundle\Entity\User: 196 | type: entity 197 | table: users 198 | fields: 199 | # ... 200 | email: 201 | type: string 202 | length: '255' 203 | unique: true 204 | # ... 205 | ``` 206 | 207 | 208 | Y actualizamos la base de datos, creando la tabla correspondiente: 209 | 210 | ``` 211 | $ app/console doctrine:schema:update --force 212 | 213 | Updating database schema... 214 | Database schema updated successfully! "1" queries were executed 215 | ``` 216 | 217 | 218 | ### Implementar la interfaz UserInterface 219 | 220 | Symfony necesita que nuestra clase usuario implemente ciertos métodos, que están definidos en la interfaz `UserInterface`. Estos métodos son: 221 | 222 | - `getRoles()` 223 | - `getPassword()` 224 | - `getSalt()` 225 | - `getUsername()` 226 | - `eraseCredentials()` 227 | 228 | Haremos que nuestra clase implemente la interfaz y añadiremos los métodos pendientes. 229 | 230 | 231 | ```php 232 | // src/My/UserBundle/Entity/User.php 233 | 234 | namespace My\UserBundle\Entity; 235 | 236 | use Doctrine\ORM\Mapping as ORM; 237 | use Symfony\Component\Security\Core\User\UserInterface; 238 | 239 | /** 240 | * User 241 | */ 242 | class User implements UserInterface 243 | { 244 | // ... 245 | 246 | public function getRoles() 247 | { 248 | return array('ROLE_USER'); 249 | } 250 | 251 | 252 | public function eraseCredentials() 253 | { 254 | } 255 | 256 | } 257 | ``` 258 | 259 | 260 | ### Configurar el user provider 261 | 262 | El último paso consiste en configurar el componente de seguridad de Symfony para que acceda a la base de datos durante la autenticación de los usuarios: 263 | 264 | ``` 265 | #app/config/security.yml 266 | security: 267 | encoders: 268 | My\UserBundle\Entity\User: 269 | algorithm: sha1 270 | encode_as_base64: false 271 | iterations: 1 272 | 273 | role_hierarchy: 274 | ROLE_ADMIN: ROLE_USER 275 | ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] 276 | 277 | providers: 278 | users: 279 | entity: { class: MyUserBundle:User, property: username } 280 | 281 | firewalls: 282 | secured_area: 283 | pattern: ^/ 284 | anonymous: ~ 285 | form_login: 286 | login_path: login 287 | check_path: login_check 288 | logout: ~ 289 | 290 | access_control: 291 | - { path: ^/secured, roles: ROLE_ADMIN } 292 | ``` 293 | 294 | 295 | Insertaremos un usuario de prueba en la base de datos para comprobar su funcionamiento: 296 | 297 | ``` 298 | INSERT INTO users (username, email, password, salt) VALUES ('curso', 'curso@email.es', '5bc4c37a302f3a672c69516df20c6ba644e68356', ''); 299 | ``` 300 | 301 | Si accedemos al formulario de login, podemos introducir el usuario `curso` y la contraseña `curso` que corresponden al usuario insertado. 302 | 303 | ### Roles 304 | 305 | En el ejemplo anterior hemos implementado un método getRoles que siempre devuelve el mismo array con el rol `ROLE_USER`. Modificaremos nuestra entidad `User` y el archivo de mapeo del ORM para permitir una gestión de roles más potente. 306 | 307 | 308 | 309 | ```php 310 | // src/My/UserBundle/Entity/User.php 311 | 312 | /** 313 | * User 314 | */ 315 | class User implements UserInterface 316 | { 317 | 318 | // ... 319 | 320 | /** 321 | * @var string 322 | */ 323 | private $roles = ''; 324 | 325 | 326 | // ... 327 | 328 | public function getRoles() 329 | { 330 | return explode(' ', $this->roles); 331 | } 332 | 333 | 334 | } 335 | ``` 336 | 337 | ```yml 338 | # src/My/UserBundle/Resources/config/doctrine/User.orm.yml 339 | My\UserBundle\Entity\User: 340 | type: entity 341 | table: users 342 | fields: 343 | # ... 344 | roles: 345 | type: string 346 | length: '255' 347 | lifecycleCallbacks: { } 348 | ``` 349 | 350 | 351 | Actualizaremos la base de datos. 352 | 353 | ``` 354 | app/console doctrine:schema:update --force 355 | ``` 356 | 357 | Ahora ya podemos añadir roles dinámicamente y gestionar el control de acceso a zonas de administración. 358 | 359 | 360 | Con estas bases podemos implementar sistemas de autenticación tan complejos como necesitemos. Para una mayor información sobre el Componente de Seguridad de Symfony 2, consultad la [documentación oficial](http://symfony.com/doc/current/book/security.html). -------------------------------------------------------------------------------- /2-symfony-a-vista-de-pajaro/controller.md: -------------------------------------------------------------------------------- 1 | # Controlador 2 | 3 | Para una información más completa y actualizada, consultad la [documentación oficial](http://symfony.com/doc/current/book/controller.html). 4 | 5 | Un controlador es una función o método que recoge la información de una petición HTTP y devuelve una respuesta HTTP. Tal y como se ha descrito en el capítulo de [enrutado](2-symfony-a-vista-de-pajaro/routing.md), el enrutador es el encargado de buscar qué controlador se utiliza en cada petición y proporcionarle los parámetros necesarios. 6 | 7 | ```routing.yml 8 | recipes_list: 9 | path: /recipes/ 10 | defaults: { _controller: MyRecipesBundle:Recipe:list } 11 | 12 | recipes_show: 13 | path: /recipes/{recipe_id} 14 | defaults: { _controller: MyRecipesBundle:Recipe:show } 15 | ``` 16 | 17 | ```RecipeController.php 18 | // src/My/RecipesBundle/Controller/RecipeController.php 19 | namespace My\RecipesBundle\Controller; 20 | 21 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 22 | use Symfony\Component\HttpFoundation\Response; 23 | 24 | class RecipeController extends Controller 25 | { 26 | public function listAction() 27 | { 28 | return new Response('

    No hay recetas

    '); 29 | } 30 | 31 | public function showAction($recipe_id) 32 | { 33 | return new Response('...'); 34 | } 35 | } 36 | 37 | ``` 38 | 39 | ## Paso de parámetros 40 | 41 | Además de los parámetros proporcionados por el enrutador, en Symfony podemos inyectar el propio objeto Request. 42 | 43 | ```RecipeController.php 44 | public function showAction($recipe_id, Request $request) 45 | { 46 | return new Response('...'); 47 | } 48 | ``` 49 | 50 | Gracias al uso de la [reflexión](http://en.wikipedia.org/wiki/Reflection_(computer_programming%29) no importa el orden en el que especifiquemos estos parámetros. 51 | 52 | ## El controlador base 53 | Symfony proporciona una clase base `Controller` que podemos extender en nuestros controladores y que proporciona algunos métodos útiles.: 54 | 55 | ### Redirigir 56 | Crea una respuesta de tipo RedirectResponse. 57 | 58 | ``` 59 | public function showAction() 60 | { 61 | return $this->redirect($this->generateUrl('redirect_path')); 62 | } 63 | ``` 64 | 65 | ### Reenviar 66 | En lugar de redirigir al cliente indicándole una nueva ruta, pasa la petición a otro controlador. 67 | 68 | ``` 69 | public function showAction($id) 70 | { 71 | return $this->forward('MyForwardingBundle:Forwarded:show', array( 72 | 'id' => $id, 73 | )); 74 | } 75 | ``` 76 | 77 | Internamente se realiza una [sub-request](http://symfony.com/doc/current/components/http_kernel/introduction.html#http-kernel-sub-requests). El objeto Request se clona y se resuelve como si se tratase de una nueva petición. 78 | 79 | 80 | ### Renderizar 81 | La mayoría de aplicaciones web hacen uso de plantillas para renderizar contenido HTML y devolverlo en la respuesta. El controlador base de Symfony dispone de un método para ello: 82 | 83 | ``` 84 | public function showAction($id) 85 | { 86 | // ... 87 | return $this->render('MyRecipesBundle:Recipe:show.html.twig', array( 88 | 'recipe' => $recipe, 89 | )); 90 | } 91 | ``` 92 | 93 | ### Acceder a servicios 94 | El controlador base de Symfony implementa la interfaz `ContainerAwareInterface`, y por tanto el framework automáticamente proporciona el contenedor de inyección de dependencias a cualquier clase que lo extienda. 95 | 96 | ``` 97 | public function showAction($id) 98 | { 99 | $templating = $this->get('templating'); 100 | $router = $this->get('router'); 101 | $mailer = $this->get('mailer'); 102 | } 103 | ``` 104 | 105 | ### Errores y excepciones 106 | 107 | Para generar una respuesta con el código 404 basta con levantar una excepción de tipo `NotFoundException`. 108 | 109 | ``` 110 | public function showAction($id) 111 | { 112 | throw $this->createNotFoundException('...'); 113 | throw \Exception(...); 114 | } 115 | ``` 116 | 117 | Cualquier excepción será capturada por Symfony, devolviendo un código de error 500. 118 | 119 | --------------------------- 120 | ¡Ojo! En entornos de desarrollo, las excepciones son capturadas y se muestra una página de debugging, pero el código de respuesta es 200. Debe tenerse este factor en cuenta cuando se realicen pruebas manuales o automáticas sobre los códigos de respuesta, para evitar posibles falsos negativos/positivos. 121 | --------------------------- 122 | 123 | 124 | ## La sesión 125 | Por defecto, Symfony 2 utiliza cookies para almacenar los datos de la sesión del cliente. Es posible manipular el contenido de estas cookies mediante el objeto sesión contenido en la petición: 126 | 127 | ``` 128 | public function showAction($id, Request $request) 129 | { 130 | $session = $request->getSession(); 131 | $session->set('clave', 'valor'); 132 | $session->get('clave'); 133 | } 134 | ``` 135 | 136 | 137 | ### Mensajes Flash 138 | Los mensajes Flash se almacenan en la sesión y su objetivo es mostrar mensajes del sistema al cliente. Pueden definirse varios niveles de mensaje flash para representar la urgencia o importancia de cada uno de ellos. 139 | 140 | 141 | ```php 142 | public function createAction(Request $request) 143 | { 144 | $session = $request->getSession(); 145 | $session->getFlashBag()->add( 146 | 'notice', 147 | 'Has publicado una nueva receta' 148 | ); 149 | } 150 | ``` 151 | 152 | Es nuestra labor renderizar estos mensajes en la plantilla correspondiente. Veremos más sobre esto en el capítulo de Twig. 153 | 154 | ```template.html.twig 155 | {% for flashMessage in app.session.flashbag.get('notice') %} 156 |
    157 | {{ flashMessage }} 158 |
    159 | {% endfor %} 160 | ``` 161 | 162 | ## SensioFrameworkExtraBundle 163 | El Bundle SensioFrameworkExtraBundle proporciona algunas funcionalidades interesantes que podemos añadir a nuestros controladores en forma de anotaciones. Consulta [la documentación oficial](http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html) para obtener una información más completa y actualizada. 164 | 165 | ### @Route y @Method 166 | 167 | Utiliza la anotación `@Route` para indicar la ruta de la que el controlador es responsable sin necesidad de ficheros de enrutamiento específicos. `@Method` permite definir el método (o métodos) HTTP permitidos para la ruta. 168 | 169 | 170 | ``` 171 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 172 | 173 | /** 174 | * @Route('/recipes') 175 | */ 176 | class RecipeController extends Controller 177 | 178 | /** 179 | * @Route('/{id}', name="recipe_show", requirements={"id" = "\d+"}) 180 | * @Method({"GET"}) 181 | */ 182 | public function showAction($id) 183 | { 184 | // ... 185 | } 186 | } 187 | ``` 188 | 189 | 190 | ### @ParamConverter 191 | 192 | Permite realizar algunas transformaciones sobre los parámetros de entrada del controlador. Útil, por ejemplo, en servicios REST donde los identificadores de los recursos están enmascarados. 193 | 194 | ```php 195 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 196 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 197 | 198 | /** 199 | * @Route('/{id}', name="recipe_show", requirements={"id" = "\d+"}) 200 | * @ParamConverter("id", class="My\ReceiptBundle\MaskedResource") 201 | */ 202 | public function showAction(MaskedResource $id) 203 | { 204 | $public_id = $id->getPublic(); 205 | $private_id = $id->getPrivate(); 206 | // ... 207 | } 208 | ``` 209 | 210 | El bundle proporciona dos conversores base. El de Doctrine permite cargar automáticamente entidades de la base de datos a partir de ids. 211 | 212 | ```php 213 | use My\RecipeBundle\Entity\Recipe; 214 | 215 | /** 216 | * @Route('/{id}', name="recipe_show", requirements={"id" = "\d+"}) 217 | */ 218 | public function showAction(Recipe $recipe) 219 | { 220 | // ... 221 | } 222 | ``` 223 | 224 | El DateTimeConverter transforma automáticamente fechas en objetos DateTime 225 | 226 | ```php 227 | use My\RecipeBundle\Entity\Recipe; 228 | 229 | /** 230 | * @Route('/{start}/{end}', name="recipe_show") 231 | * @ParamConverter("start", options={"format": "Y-m-d"}) 232 | * @ParamConverter("end", options={"format": "Y-m-d"}) 233 | */ 234 | public function listByDatesAction(\DateTime $start, \DateTime $end) 235 | { 236 | // ... 237 | } 238 | ``` 239 | 240 | ### @Template 241 | 242 | Con Template() podemos escribir controladores de un modo más elegante utilizando convenios en la organización de plantillas. Los métodos a continuación serían equivalentes: 243 | 244 | 245 | ``` 246 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; 247 | 248 | public function showAction($id) 249 | { 250 | // ... 251 | return $this->render('MyRecipesBundle:Recipe:show.html.twig', array( 252 | 'recipe' => $recipe, 253 | )); 254 | } 255 | 256 | /** 257 | * @Template() 258 | */ 259 | public function showAction($id) 260 | { 261 | // ... 262 | return array( 263 | 'recipe' => $recipe, 264 | ); 265 | } 266 | ``` 267 | -------------------------------------------------------------------------------- /5-formularios/conceptos-basicos.md: -------------------------------------------------------------------------------- 1 | # Conceptos básicos 2 | 3 | Es difícil imaginar una aplicación web sin formularios. El [componente de formularios de Symfony 2](https://github.com/symfony/Form) permite gestionar la creación, representación y renderizado de los mismos, además de proporcionar multitud de otras funcionalidades. Como el resto de componentes de Symfony 2, el componente de formularios puede ser instalado por separado en cualquier aplicación PHP. 4 | 5 | 6 | ## Formularios desde controladores 7 | 8 | Podemos crear formularios desde nuestros controladores con el método `createFormBuilder()`. Retomaremos nuestra aplicación de recetas proporcionando un formulario para crear autores. 9 | 10 | En primer lugar nos aseguraremos de que nuestra clase `Author` dispone de todos los métodos necesarios. 11 | 12 | ```bash 13 | $ app/console doctrine:generate:entities MyRecipesBundle:Author 14 | Generating entity "My\RecipesBundle\Entity\Author" 15 | > backing up Author.php to Author.php~ 16 | > generating My\RecipesBundle\Entity\Author 17 | ``` 18 | 19 | Añadiremos la nueva ruta al formulario. 20 | 21 | ```yaml 22 | # src/My/RecipesBundle/Resources/config/routing.yml 23 | my_recipes_author_create: 24 | pattern: /authors/create 25 | defaults: { _controller: MyRecipesBundle:Author:create } 26 | ``` 27 | 28 | Escribiremos la acción del controlador. 29 | 30 | ```php 31 | // src/My/RecipesBundle/Controller/AuthorController.php 32 | namespace My\RecipesBundle\Controller; 33 | 34 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 35 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; 36 | 37 | use My\RecipesBundle\Entity\Author; 38 | 39 | class AuthorController extends Controller 40 | { 41 | 42 | /** 43 | * @Template() 44 | */ 45 | public function createAction() 46 | { 47 | $author = new Author; 48 | $form = $this->createFormBuilder($author) 49 | ->add('name', 'text') 50 | ->add('surname', 'text') 51 | ->add('save', 'submit') 52 | ->getForm(); 53 | return array('form' => $form->createView()); 54 | } 55 | 56 | } 57 | ``` 58 | El argumento que recibe el método `createFormBuilder` es el objeto `$author`. El método `add()` permite añadir elementos al formulario. El primer argumento del elemento identifica el campo en el objeto, mientras que el segundo especifica qué tipo de campo es. En Symfony debe haber una correspondencia entre el nombre del campo y el atributo o método del objeto. Esto es así porque el componente de formularios accede a los getters y setters del objeto durante el renderizado y manipulación del formulario. 59 | 60 | Para terminar crearemos una plantilla a la que le pasaremos una vista del formulario con `$form->createView()`. 61 | 62 | ```html 63 | {# src/My/RecipesBundle/Resources/views/Author/create.html.twig #} 64 | {% extends '::base.html.twig' %} 65 | 66 | {% block title %}Create author{% endblock %} 67 | 68 | {% block body %} 69 | {{ form(form) }} 70 | {% endblock %} 71 | ``` 72 | 73 | Si accedemos a la ruta `/authors/create` podremos ver nuestro nuevo formulario. 74 | 75 | ![Formulario de Author](form.png "Formulario de Author") 76 | 77 | De momento este formulario no reacciona a los submits, por lo que vamos a añadir la lógica necesaria en el controlador. 78 | 79 | 80 | ```php 81 | // src/My/RecipesBundle/Controller/AuthorController.php 82 | // ... 83 | 84 | 85 | use Symfony\Component\HttpFoundation\Request; 86 | // ... 87 | 88 | public function createAction(Request $request) 89 | { 90 | $author = new Author; 91 | $form = $this->createFormBuilder($name) 92 | ->add('name', 'text') 93 | ->add('surname', 'text') 94 | ->add('save', 'submit') 95 | ->getForm(); 96 | 97 | $form->handleRequest($request); 98 | 99 | if ($form->isValid()) { 100 | $em = $this->getDoctrine()->getManager(); 101 | $em->persist($author); 102 | $em->flush(); 103 | return $this->redirect($this->generateUrl('my_recipes_author_show', array('id' => $author->getId()))); 104 | } 105 | return array('form' => $form->createView()); 106 | } 107 | ``` 108 | 109 | Cuando accedamos a `/authors/create`, el método `isValid()` de `$form` devolverá `false`. En `handleRequest()` hemos proporcionado al formulario el objeto `$request` por lo que el formulario sabe que estamos mostrando el formulario y no recibiendo información a través del mismo. 110 | 111 | Al hacer submit, los datos del objeto request serán cargados en la entidad Author en la llamada a `handleRequest()`. Internamente, Symfony invocará a los setters de `Author` y les pasará el contenido de `name` y `surname`. Posteriormente se ejecutará `isValid()` que efectuará las validaciones pertinentes y generará los mensajes de error necesarios. Si todo va bien y no hay errores de validación, se ejecutará el bloque del `if`, persistiendo la instancia y generando la redirección. 112 | 113 | 114 | 115 | ## Form Classes 116 | 117 | Aunque los controladores de Symfony permiten crear formularios con `createFormBuilder()`, es una buena práctica llevar la lógica de estos formularios a una clase aparte para _adelgazar_ los controladores y favorecer la reusabilidad. 118 | 119 | Crearemos nuestra propia clase AuthorType. 120 | 121 | ```php 122 | // src/My/RecipesBundle/Form/Type/AuthorType.php 123 | namespace My\RecipesBundle\Form\Type; 124 | 125 | use Symfony\Component\Form\AbstractType; 126 | use Symfony\Component\Form\FormBuilderInterface; 127 | 128 | class AuthorType extends AbstractType 129 | { 130 | public function buildForm(FormBuilderInterface $builder, array $options) 131 | { 132 | $builder 133 | ->add('name', 'string') 134 | ->add('surname', 'string') 135 | ->add('save', 'submit'); 136 | } 137 | 138 | public function getName() 139 | { 140 | return 'author'; 141 | } 142 | } 143 | ``` 144 | 145 | Ahora podemos reescribir la acción de nuestro controlador: 146 | 147 | ```php 148 | // src/My/RecipesBundle/Controller/AuthorController.php 149 | // ... 150 | use My\RecipesBundle\Form\Type\AuthorType; 151 | 152 | 153 | public function createAction(Request $request) 154 | { 155 | $author = new Author; 156 | $form = $this->createForm(new AuthorType, $author); 157 | $form->handleRequest($request); 158 | 159 | if ($form->isValid()) { 160 | $em = $this->getDoctrine()->getManager(); 161 | $em->persist($author); 162 | $em->flush(); 163 | return $this->redirect($this->generateUrl('my_recipes_author_show', array('id' => $author->getId()))); 164 | } 165 | return array('form' => $form->createView()); 166 | } 167 | 168 | 169 | ``` 170 | 171 | 172 | 173 | ## Renderizado 174 | 175 | Previamente hemos visto cómo renderizar un formulario completo con la función `form`. Veamos ahora un modo más detallado de renderizar un formulario: 176 | 177 | ```html 178 | {% extends '::base.html.twig' %} 179 | 180 | {% block title %}Create author{% endblock %} 181 | 182 | {% block body %} 183 | {{ form_start(form, {'attr' : { 'id' : 'author-create'}}) }} 184 | {{ form_errors(form) }} 185 | {{ form_row(form.name) }} 186 | {{ form_row(form.surname) }} 187 | {{ form_row(form.save) }} 188 | {{ form_end(form) }} 189 | {% endblock %} 190 | ``` 191 | 192 | - `form_start` introduce la cabecera `
    ` con los campos necesarios. 193 | - `form_errors` muestra los errores que aplican a todo el formulario. 194 | - `form_row` renderiza un campo concreto del formulario. Por defecto Symfony enmarca los campos en `
    `, aunque como veremos más adelante este comportamiento es modificable. 195 | - `form_end` renderiza todos los campos que no hayan sido renderizados explícitamente y cierra la etiqueta ``. 196 | 197 | Como vemos, en `form_start()` hemos pasado un diccionario con el atributo `id`. De este modo estamos indicando algunos atributos HTML que deseamos que se apliquen al formulario. De este modo podemos personalizar cada elemento del formulario. 198 | 199 | 200 | Una manera aún más detallada de renderizar el formulario es la siguiente: 201 | 202 | ```html 203 | {% extends '::base.html.twig' %} 204 | 205 | {% block title %}Create author{% endblock %} 206 | 207 | {% block body %} 208 | {{ form_start(form, {'attr' : { 'id' : 'author-create'}}) }} 209 | {{ form_errors(form) }} 210 | 225 | {{ form_end(form) }} 226 | {% endblock %} 227 | ``` 228 | 229 | - `form_label` genera automáticamente etiquetas para los campos proporcionados. 230 | - `form_errors`, cuando se proporciona un campo concreto, muestra los errores de validación que aplican a dicho campo. 231 | - `form_widget` genera el código mínimo para reproducir el campo en HTML. 232 | 233 | 234 | 235 | ## Personalizar la acción y método HTTP 236 | 237 | Como hemos visto, la función `form_start(form)` genera el encabezado del método. 238 | 239 | ```html 240 | 241 | ``` 242 | 243 | El método por defecto es `POST`, pero Symfony permite modificar el método de un formulario. 244 | 245 | ```php 246 | class AuthorType extends AbstractType 247 | { 248 | public function buildForm(FormBuilderInterface $builder, array $options) 249 | { 250 | $builder 251 | ->setMethod('PUT') 252 | //... 253 | ; 254 | } 255 | } 256 | ``` 257 | 258 | Al renderizar ahora el formulario, veremos la siguiente cabecera: 259 | 260 | ```html 261 | 262 | ``` 263 | 264 | Aunque el componente de formularios no modifica el método en los casos `PUT`, `PATCH` y `DELETE`, añade un campo oculto `_method` que utilizará posteriormente para construir el objeto Request con el método seleccionado. 265 | 266 | 267 | Podemos personalizar la acción y el método desde el controlador: 268 | 269 | ```php 270 | $form = $this->createForm(new AuthorType(), $author, array( 271 | 'action' => $this->generateUrl('my_recipes_author_create'), 272 | 'method' => 'PUT', 273 | )); 274 | ``` 275 | 276 | ## Protección CSRF 277 | 278 | El componente de formularios de Symfony proporcionan automáticamente protección contra ataques CSRF [Cross-Site Request Forgery](http://en.wikipedia.org/wiki/Cross-site_request_forgery) generando un ID único en cada formulario. Aunque está activada por defecto, podemos desactivar la protección CSRF en la configuración de la aplicación: 279 | 280 | ```yaml 281 | # app/config/config.yml 282 | framework: 283 | csrf_protection: ~ 284 | ``` 285 | 286 | 287 | Adicionalmente podemos controlar la protección CSRF por formulario, incluso añadiendo semillas para ayudar a la generación de códigos únicos: 288 | 289 | ```php 290 | use Symfony\Component\OptionsResolver\OptionsResolverInterface; 291 | 292 | 293 | class AuthorType extends AbstractType 294 | { 295 | // ... 296 | public function setDefaultOptions(OptionsResolverInterface $resolver) 297 | { 298 | $resolver->setDefaults(array( 299 | 'csrf_protection' => true, 300 | 'csrf_field_name' => '_token', 301 | // a unique key to help generate the secret token 302 | 'intention' => 'author_item', 303 | )); 304 | } 305 | } 306 | ``` 307 | -------------------------------------------------------------------------------- /3-doctrine/relaciones.md: -------------------------------------------------------------------------------- 1 | # Relaciones 2 | 3 | Los ORMs se basan en sistemas relacionales de bases de datos, por lo que facilitan la definición de relaciones entre las distintas entidades de nuestra aplicación. 4 | 5 | ## Definir relaciones 6 | 7 | Siguiendo con el ejemplo de recetas, podemos suponer que la entidad `Recipe` se relaciona con la entidad `Author` definida en la siguiente clase. 8 | 9 | ```Author.php 10 | // src/My/RecipesBundle/Entity/Author.php 11 | namespace My\RecipesBundle\Entity; 12 | 13 | 14 | class Author 15 | { 16 | private $id; 17 | 18 | protected $name; 19 | 20 | protected $surname; 21 | } 22 | ``` 23 | 24 | ```Author.orm.yml 25 | # src/My/RecipesBundle/Resources/config/doctrine/Author.orm.yml 26 | My\RecipesBundle\Entity\Author: 27 | type: entity 28 | table: authors 29 | id: 30 | id: 31 | type: integer 32 | generator: { strategy: AUTO } 33 | fields: 34 | name: 35 | type: string 36 | length: 255 37 | 38 | surname: 39 | type: string 40 | length: 255 41 | ``` 42 | 43 | Para añadir esta nueva entidad en la base de datos ejecutaríamos el siguiente comando de consola: 44 | 45 | ``` 46 | $ php app/console doctrine:schema:update --force 47 | Updating database schema... 48 | Database schema updated successfully! "1" queries were executed 49 | ``` 50 | 51 | En este punto tendríamos dos entidades, `Recipe` y `Author`, mapeadas en sendas tablas `recipes` y `authors`. ¿Como podemos establecer relaciones entre ellas? 52 | 53 | 54 | ## Muchos a uno 55 | 56 | Para crear una relación muchos a uno desde `Recipe` a `Author` añadiremos, en primer lugar, un atributo `$author` en la entidad `Recipe`. 57 | 58 | ``` 59 | // src/My/RecipesBundle/Entity/Recipe.php 60 | 61 | class Recipe 62 | { 63 | // ... 64 | protected $author; 65 | 66 | } 67 | ``` 68 | 69 | Y completaremos el archivo yaml de Doctrine. 70 | 71 | ```Recipe.orm.yml 72 | # src/My/RecipesBundle/Resources/config/doctrine/Recipe.orm.yml 73 | My\RecipesBundle\Entity\Recipe: 74 | type: entity 75 | table: recipes 76 | manyToOne: 77 | author: 78 | targetEntity: Author 79 | joinColumn: 80 | name: author_id 81 | referencedColumnName: id 82 | # ... 83 | ``` 84 | 85 | La sentencias a ejecutar por Doctrine son las siguientes: 86 | 87 | ``` 88 | $ php app/console doctrine:schema:update --dump-sql 89 | ALTER TABLE recipes ADD author_id INT DEFAULT NULL; 90 | ALTER TABLE recipes ADD CONSTRAINT FK_A369E2B5F675F31B FOREIGN KEY (author_id) REFERENCES authors (id); 91 | CREATE INDEX IDX_A369E2B5F675F31B ON recipes (author_id) 92 | ``` 93 | 94 | Actualizaremos la base de datos como hemos visto anteriormente. 95 | 96 | 97 | 98 | ## Muchos a muchos 99 | 100 | 101 | ```Ingredient.php 102 | // src/My/RecipesBundle/Entity/Ingredient.php 103 | namespace My\RecipesBundle\Entity; 104 | 105 | 106 | class Ingredient 107 | { 108 | private $id; 109 | 110 | protected $name; 111 | } 112 | ``` 113 | 114 | ```Ingredient.orm.yml 115 | # src/My/RecipesBundle/Resources/config/doctrine/Ingredient.orm.yml 116 | My\RecipesBundle\Entity\Ingredient: 117 | type: entity 118 | table: ingredients 119 | id: 120 | id: 121 | type: integer 122 | generator: { strategy: AUTO } 123 | fields: 124 | name: 125 | type: string 126 | length: 255 127 | ``` 128 | 129 | 130 | Para crear una relación muchos a muchos desde `Recipe` a `Ingredient` añadiremos, en primer lugar, un atributo `$ingredients` en la entidad. 131 | 132 | ``` 133 | // src/My/RecipesBundle/Entity/Recipe.php 134 | use Doctrine\Common\Collections\ArrayCollection; 135 | 136 | class Recipe 137 | { 138 | // ... 139 | protected $ingredients; 140 | 141 | public function __construct() 142 | { 143 | $this->ingredients = new ArrayCollection(); 144 | } 145 | } 146 | ``` 147 | 148 | Y completaremos el archivo yaml de Doctrine. 149 | 150 | ```Recipe.orm.yml 151 | # src/My/RecipesBundle/Resources/config/doctrine/Recipe.orm.yml 152 | My\RecipesBundle\Entity\Recipe: 153 | type: entity 154 | table: recipes 155 | manyToMany: 156 | ingredients: 157 | targetEntity: Ingredient 158 | joinTable: 159 | name: recipe_ingredients 160 | joinColumns: 161 | recipe_id: 162 | referencedColumnName: id 163 | inverseJoinColumns: 164 | ingredient_id: 165 | referencedColumnName: id 166 | # ... 167 | ``` 168 | 169 | Si ejecutamos un dump podremos ver que la relación muchos a muchos se realiza mediante una tercera tabla, definida por `joinTable`: 170 | 171 | ``` 172 | $ php app/console doctrine:schema:update --dump-sql 173 | CREATE TABLE recipe_ingredients (recipe_id INT NOT NULL, ingredient_id INT NOT NULL, INDEX IDX_9F925F2B59D8A214 (recipe_id), INDEX IDX_9F925F2B933FE08C (ingredient_id), PRIMARY KEY(recipe_id, ingredient_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; 174 | ALTER TABLE recipe_ingredients ADD CONSTRAINT FK_9F925F2B59D8A214 FOREIGN KEY (recipe_id) REFERENCES recipes (id); 175 | ALTER TABLE recipe_ingredients ADD CONSTRAINT FK_9F925F2B933FE08C FOREIGN KEY (ingredient_id) REFERENCES ingredients (id) 176 | ``` 177 | 178 | 179 | ## Manipular relaciones 180 | 181 | Ya tenemos una entidad `Recipe` bastante compleja, por lo que vamos a facilitar su configuración con un constructor más amplio. Además, añadiremos un método que permita añadir ingredientes. 182 | 183 | ``` 184 | class Recipe 185 | { 186 | // ... 187 | 188 | public function __construct(Author $author, $name, $description, $difficulty) 189 | { 190 | $this->author = $author; 191 | $this->name = $name; 192 | $this->description = $description; 193 | $this->difficulty = $difficulty; 194 | $this->ingredients = new ArrayCollection(); 195 | } 196 | 197 | public function add(Ingredient $ingredient) 198 | { 199 | $this->ingredients[] = $ingredient; 200 | } 201 | } 202 | ``` 203 | 204 | También añadiremos constructores al resto de entidades. 205 | 206 | ``` 207 | class Author 208 | { 209 | // ... 210 | 211 | public function __construct($name, $surname) 212 | { 213 | $this->name = $name; 214 | $this->surname = $surname; 215 | } 216 | 217 | } 218 | ``` 219 | 220 | ``` 221 | class Ingredient 222 | { 223 | // ... 224 | 225 | public function __construct($name) 226 | { 227 | $this->name = $name; 228 | } 229 | 230 | } 231 | ``` 232 | 233 | 234 | Podemos crear una completa receta con el siguiente controlador: 235 | 236 | ``` 237 | public function createAction() 238 | { 239 | $em = $this->getDoctrine()->getEntityManager(); 240 | 241 | $author = new Author('Karlos', 'Arguiñano'); 242 | $em->persist($author); 243 | 244 | $ingredient = new Ingredient('Pollo'); 245 | $em->persist($ingredient); 246 | 247 | $recipe = new Recipe($author, 'Pollo al pil-pil', 'Deliciosa y económica receta.', 'fácil'); 248 | $em->persist($recipe); 249 | 250 | $recipe->add($ingredient); 251 | 252 | $em->flush(); 253 | 254 | return $this->redirect($this->generateUrl('my_recipes_show', array('id' => $recipe->getId()))); 255 | } 256 | ``` 257 | 258 | Con un par de rutas, una sencilla plantilla y unos pocos métodos obtenemos el siguiente resultado: 259 | 260 | ![Una receta recién publicada](recipe_show.png "Una receta recién publicada") 261 | 262 | 263 | ## Operaciones en cascada 264 | 265 | Aunque hemos conseguido construir una receta con autor e ingredientes, el código de `createAction()` no parece muy elegante. El mayor problema está en que debemos persistir las entidades hoja (`$author`, `$ingredient`) antes de persistir la entidad padre (`$recipe`). Esto es así porque, a priori, Doctrine no sabe qué hacer con las entidades relacionadas que no han sido persistidas. Si eliminásemos los persist de `$author` e `$ingredient` obtendríamos el siguiente resultado. 266 | 267 | ![Error de almacenamiento en casacada](cascade_persist_exception.png "Error de almacenamiento en cascada") 268 | 269 | El mensaje nos sugiere dos soluciones; volver a la solución anterior de invocar explícitamente el método persist o configurar la asociación con la operación en casacada. Optaremos por la segunda opción, modificando el archivo de mapeo de la entida `Recipe`: 270 | 271 | ``` 272 | My\RecipesBundle\Entity\Recipe: 273 | type: entity 274 | table: recipes 275 | manyToOne: 276 | author: 277 | # ... 278 | cascade: ["persist"] 279 | manyToMany: 280 | ingredients: 281 | # ... 282 | cascade: ["persist"] 283 | # ... 284 | ``` 285 | 286 | De este modo podremos limpiar el controlador de esos incómodos `persist()`. 287 | 288 | ``` 289 | public function createAction() 290 | { 291 | $author = new Author('Karlos', 'Arguiñano'); 292 | $ingredient = new Ingredient('Pollo'); 293 | $recipe = new Recipe($author, 'Pollo al pil-pil', 'Deliciosa y económica receta.', 'fácil'); 294 | $recipe->add($ingredient); 295 | 296 | $this->persistAndFlush($recipe); 297 | 298 | return $this->redirect($this->generateUrl('my_recipes_show', array('id' => $recipe->getId()))); 299 | } 300 | 301 | private function persistAndFlush(Recipe $recipe) 302 | { 303 | $em = $this->getDoctrine()->getEntityManager(); 304 | $em->persist($recipe); 305 | $em->flush(); 306 | } 307 | ``` 308 | 309 | Doctrine facilita cuatro operaciones en cascada: 310 | - *persist*: Guardar entidades asociadas. 311 | - *remove*: Eliminar entidades asociadas. 312 | - *merge*: Combina entidades asociadas. 313 | - *detach*: Desvincula las entidades de sus equivalentes en la base de datos. 314 | 315 | La opción `all` configura todas ellas a la vez. 316 | 317 | Es importante destacar que estas operaciones se efectúan a nivel lógico, en memoria, y no en la base de datos. Para configurar el borrado en cascada a nivel de base de datos deberemos añadir la cláusula correspondiente en el mapeado de la entidad. 318 | 319 | ``` 320 | My\RecipesBundle\Entity\Recipe: 321 | # ... 322 | manyToOne: 323 | author: 324 | joinColumn: 325 | onDelete: "CASCADE" 326 | # ... 327 | ``` 328 | 329 | ``` 330 | $ php app/console doctrine:schema:update --dump-sql 331 | ALTER TABLE recipes DROP FOREIGN KEY FK_A369E2B5F675F31B; 332 | ALTER TABLE recipes ADD CONSTRAINT FK_A369E2B5F675F31B FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE 333 | ``` 334 | 335 | 336 | ## Relaciones unidireccionales y bidireccionales 337 | 338 | Los ejemplos mostrados anteriormente pertenecen a las denominadas _relaciones unidireccionales_. Significa que la relación es visible únicamente desde una parte. En este caso, podemos consultar el autor de una receta, pero no podemos recuperar las recetas de un autor sin un poco de trabajo extra. 339 | 340 | ``` 341 | // Recuperar el autor de una receta 342 | $repository = $this->getDoctrine()->getRepository('MyRecipesBundle:Recipe'); 343 | $recipe = $repository->find($id); 344 | $author = $recipe->getAuthor(); 345 | 346 | 347 | // No es posible realizar el camino inverso 348 | $repository = $this->getDoctrine()->getRepository('MyRecipesBundle:Author'); 349 | $author = $repository->find($id); 350 | $recipes = $author->getRecipes(); 351 | ``` 352 | 353 | Vamos a transformar la relación en bidireccional para hacer el código anterior posible. 354 | 355 | En primer lugar modificaremos la clase `Author` para que contenga una referencia a sus recetas. 356 | 357 | ``` 358 | class Author 359 | { 360 | // ... 361 | protected $recipes; 362 | 363 | public function __construct($name, $surname) 364 | { 365 | //... 366 | $this->recipes = new ArrayCollection; 367 | } 368 | 369 | public function getRecipes() 370 | { 371 | return $this->recipes; 372 | } 373 | } 374 | ``` 375 | 376 | Y modificaremos los archivos de mapeo de Doctrine: 377 | 378 | ``` 379 | My\RecipesBundle\Entity\Author: 380 | oneToMany: 381 | recipes: 382 | targetEntity: Recipe 383 | mappedBy: author 384 | 385 | My\RecipesBundle\Entity\Recipe: 386 | manyToOne: 387 | author: 388 | inversedBy: recipes 389 | // ... 390 | ``` 391 | 392 | Estos cambios no requieren modificaciones en la base de datos, ya que se llevan a cabo a nivel lógico. 393 | 394 | 395 | ## Compendio de relaciones 396 | 397 | Doctrine ofrece un amplio abanico de relaciones además de las vistas hasta ahora. Todas ellas están descritas en la [documentación oficial](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html). 398 | 399 | - [Many To One, Unidirectional](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#many-to-one-unidirectional) 400 | - [One To One, Unidirectional](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#one-to-one-unidirectional) 401 | - [One To One, Bidirectional](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#one-to-one-bidirectional) 402 | - [One To One, Self-referencing](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#one-to-one-self-referencing) 403 | - [One To Many, Bidirectional](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#one-to-many-bidirectional) 404 | - [One To Many, Unidirectional with join table](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#one-to-many-unidirectional-with-join-table) 405 | - [One To Many, Self-referencing](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#one-to-many-self-referencing) 406 | - [Many To Many, Unidirectional](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#many-to-many-unidirectional) 407 | - [Many To Many, Bidirectional](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#many-to-many-bidirectional) 408 | - [Many To Many, Self-referencing](http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#many-to-many-self-referencing) 409 | 410 | --------------------------------------------------------------------------------