El controlador

¿Todavía aquí después de las dos primeras partes? ¡Ya te estás volviendo adicto a Symfony2! Sin más preámbulos, descubre lo que pueden hacer por ti los controladores.

Usando Formatos

Hoy día, una aplicación web debe ser capaz de ofrecer algo más que solo páginas HTML. Desde XML para alimentadores RSS o Servicios Web, hasta JSON para peticiones Ajax, hay un montón de formatos diferentes a elegir. Apoyar estos formatos en Symfony2 es sencillo. Modifica la ruta añadiendo un valor predeterminado de xml a la variable _format:

// src/Acme/DemoBundle/Controller/DemoController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

// ...

/**
 * @Route("/hello/{name}", defaults={"_format"="xml"}, name="_demo_hello")
 * @Template()
 */
public function helloAction($name)
{
    return array('name' => $name);
}

Al utilizar el formato de la petición (como lo define el valor _format), Symfony2 automáticamente selecciona la plantilla adecuada, aquí hello.xml.twig:

<!-- src/Acme/DemoBundle/Resources/views/Demo/hello.xml.twig -->
<hello>
    <name>{{ name }}</name>
</hello>

Eso es todo lo que hay que hacer. Para los formatos estándar, Symfony2 también elije automáticamente la mejor cabecera Content-Type para la respuesta. Si quieres apoyar diferentes formatos para una sola acción, en su lugar, usa el marcador de posición {_format} en el patrón de la ruta:

// src/Acme/DemoBundle/Controller/DemoController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

// ...

/**
 * @Route("/hello/{name}.{_format}", defaults={"_format"="html"}, ↓
          requirements={"_format"="html|xml|json"}, name="_demo_hello")
 * @Template()
 */
public function helloAction($name)
{
    return array('name' => $name);
}

El controlador ahora será llamado por la URL como /demo/hello/Fabien.xml o /demo/hello/Fabien.json.

La entrada requirements define las expresiones regulares con las cuales los marcadores de posición deben coincidir. En este ejemplo, si tratas de solicitar el recurso /demo/hello/Fabien.js, obtendrás un error HTTP 404, ya que no coincide con el requisito de _format.

Redirigiendo y reenviando

Si deseas redirigir al usuario a otra página, utiliza el método redirect():

return $this->redirect($this->generateUrl('_demo_hello',
                                          array('name' => 'Lucas')
                              )
       );

El método generateUrl() es el mismo que la función path() utilizada en las plantillas. Este toma el nombre de la ruta y una serie de parámetros como argumentos y devuelve la URL amigable asociada.

Además, fácilmente puedes reenviar a otra acción con el método forward(). Internamente, Symfony hace una «subpetición», y devuelve el objeto Respuesta desde la subpetición:

$response = $this->forward('AcmeDemoBundle:Hello:fancy',
                           array('name' => $name,
                                 'color' => 'green'
                           )
            );

// ... hace algo con la respuesta o la devuelve directamente

Obteniendo información de la petición

Además del valor de los marcadores de posición de enrutado, el controlador también tiene acceso al objeto Petición:

$request = $this->getRequest();

$request->isXmlHttpRequest(); // ¿es una petición Ajax?

$request->getPreferredLanguage(array('en', 'fr'));

$request->query->get('page'); // obtiene un parámetro $_GET

$request->request->get('page'); // obtiene un parámetro $_POST

En una plantilla, también puedes acceder al objeto Petición por medio de la variable app.request:

{{ app.request.query.get('pag') }}

{{ app.request.parameter('pag') }}

Persistiendo datos en la sesión

Aunque el protocolo HTTP es sin estado, Symfony2 proporciona un agradable objeto sesión que representa al cliente (sea una persona real usando un navegador, un robot o un servicio web). Entre dos peticiones, Symfony2 almacena los atributos en una cookie usando las sesiones nativas de PHP.

Almacenar y recuperar información de la sesión se puede conseguir fácilmente desde cualquier controlador:

$session = $this->getRequest()->getSession();

// guarda un atributo para reutilizarlo durante una
// posterior petición del usuario
$session->set('foo', 'bar');

// en otro controlador por otra petición
$foo = $session->get('foo');

// usa un valor predefinido si la clave no existe
$filters = $session->set('filters', array());

También puedes almacenar pequeños mensajes que sólo estarán disponibles para la siguiente petición:

// almacena un mensaje para la próxima petición (en un controlador)
$session->getFlashBag()->add('notice',
                             'Congratulations, your action succeeded!'
                         );

// muestra algún mensaje en la próxima petición (en una plantilla)

{% for flashMessage in app.session.flashbag.get('notice') %}
    <div>{{ flashMessage }}</div>
{% endfor %}

Esto es útil cuando es necesario configurar un mensaje de éxito antes de redirigir al usuario a otra página (la cual entonces mostrará el mensaje). Por favor, ten en cuenta que cuándo utilizas has() en vez de get(), el mensaje flash no será limpiado y por lo tanto queda disponible para las siguientes peticiones.

Protegiendo recursos

La edición estándar de Symfony viene con una configuración de seguridad sencilla, adaptada a las necesidades más comunes:

# app/config/security.yml
security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        in_memory:
            memory:
                users:
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        login:
            pattern:  ^/demo/secured/login$
            security: false

        secured_area:
            pattern:    ^/demo/secured/
            form_login:
                check_path: /demo/secured/login_check
                login_path: /demo/secured/login
            logout:
                path:   /demo/secured/logout
                target: /demo/

Esta configuración requiere que los usuarios inicien sesión para cualquier URL que comience con /demo/secured/ y define dos usuarios válidos: user y admin. Por otra parte, el usuario admin tiene un rol ROLE_ADMIN, el cual incluye el rol ROLE_USER también (consulta el ajuste role_hierarchy).

Truco

Para facilitar la lectura, las contraseñas se almacenan en texto plano en esta configuración simple, pero puedes usar cualquier algoritmo de codificación ajustando la sección encoders.

Al ir a la dirección http://localhost/app_dev.php/demo/secured/hello automáticamente te redirigirá al formulario de acceso, porque el recurso está protegido por un cortafuegos.

También puedes forzar la acción para exigir un determinado rol usando la anotación @Secure en el controlador:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use JMS\SecurityExtraBundle\Annotation\Secure;

/**
 * @Route("/hello/admin/{name}", name="_demo_secured_hello_admin")
 * @Secure(roles="ROLE_ADMIN")
 * @Template()
 */
public function helloAdminAction($name)
{
    return array('name' => $name);
}

Ahora, inicia sesión como user (el cual no tiene el rol ROLE_ADMIN) y desde la página protegida hello, haz clic en el enlace «Hola recurso protegido». Symfony2 debe devolver un código de estado HTTP 403, el cual indica que el usuario tiene «prohibido» el acceso a ese recurso.

Nota

La capa de seguridad de Symfony2 es muy flexible y viene con muchos proveedores de usuario diferentes (por ejemplo, uno para el ORM de Doctrine) y proveedores de autenticación (como HTTP básica, suma de comprobación HTTP o certificados X509). Lee el capítulo «Seguridad» del libro para más información en cómo se usa y configura.

Memorizando recursos en caché

Tan pronto como tu sitio web comience a generar más tráfico, tendrás que evitar se genere el mismo recurso una y otra vez. Symfony2 utiliza cabeceras de caché HTTP para administrar los recursos en caché. Para estrategias de memorización en caché simples, utiliza la conveniente anotación @Cache():

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;

/**
 * @Route("/hello/{name}", name="_demo_hello")
 * @Template()
 * @Cache(maxage="86400")
 */
public function helloAction($name)
{
    return array('name' => $name);
}

En este ejemplo, el recurso se mantiene en caché por un día. Pero también puedes utilizar validación en lugar de caducidad o una combinación de ambas, si se ajusta mejor a tus necesidades.

El recurso memorizado en caché es gestionado por el delegado inverso integrado en Symfony2. Pero debido a que la memorización en caché se gestiona usando cabeceras de caché HTTP normales, puedes sustituir el delegado inverso integrado, con Varnish o Squid y escalar tu aplicación fácilmente.

Nota

Pero ¿qué pasa si no puedes guardar en caché todas las páginas? Symfony2 todavía tiene la solución vía ESI (Edge Side Includes o Inclusión de borde lateral), con la cual es compatible nativamente. Consigue más información leyendo el capítulo «Caché HTTP» del libro.

Consideraciones finales

Eso es todo lo que hay que hacer, y ni siquiera estoy seguro de que hayan pasado los 10 minutos completos. En la primera parte te presentamos brevemente los paquetes, y todas las características que hemos explorado hasta ahora son parte del paquete básico de la plataforma. Pero gracias a los paquetes, todo en Symfony2 se puede extender o sustituir. Ese, es el tema de la siguiente parte de esta guía.

Bifúrcame en GitHub