├── .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 |
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 | 
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 |
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 |
15 | {% for recipe in recipes %}
16 |
17 | {% endif %}
18 |
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 |
89 | {% for recipe in recipes %}
90 |
91 | {% endif %}
92 |
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 | 
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 | 
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 |
50 | {% for ingredient in recipe.ingredients %}
51 |
52 | {{ ingredient }}
53 |
54 | {% endfor %}
55 |
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 | 
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 | 
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 | 
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 |
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
51 | {% for recipe in last_recipes %}
52 |
{{ recipe.name }}
53 | {% endfor %}
54 |
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 |
146 | {% for recipe in recipes %}
147 |
{{ recipe.name }}
148 | {% endfor %}
149 |
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 | 
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 | 
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 | 
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 |
114 |
115 |
116 |
117 |
118 |
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 | 
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 |
110 | {% for recipe in author.recipes %}
111 |
{{ recipe.name }}
112 | {% endfor %}
113 |
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 |
132 | {% for recipe in recipes %}
133 |
{{ recipe.name }}
134 | {% endfor %}
135 |
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 |
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 | 
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 `