Symfony 2.0, el libro oficial

6.3. Creando rutas

Symfony carga todas las rutas de tu aplicación desde un archivo de configuración de enrutamiento. Normalmente este archivo es app/config/routing.yml, pero puedes utilizar cualquier otro archivo e incluso otros formatos de configuración como XML o PHP. Para ello, cambiar el valor de la siguiente opción de configuración:

# app/config/config.yml
framework:
    # ...
    router:        { resource: "%kernel.root_dir%/config/routing.yml" }
<!-- app/config/config.xml -->
<framework:config ...>
    <!-- ... -->
    <framework:router resource="%kernel.root_dir%/config/routing.xml" />
</framework:config>
// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...
    'router'        => array('resource' => '%kernel.root_dir%/config/routing.php'),
));

Truco Aunque todas las rutas se cargan desde un solo archivo, es muy común importar otros archivos desde ese archivo principal de configuración de enrutamiento. Más adelante en este mismo capítulo se explica cómo importar otros archivos.

6.3.1. Configuración básica de rutas

Las aplicaciones típicas definen decenas de rutas. Afortunadamente, definir una ruta es bastante fácil. Una ruta básica consta de dos partes: el pattern o patrón que debe cumplir la URL y un array de opciones llamado defaults:

_welcome:
    pattern:   /
    defaults:  { _controller: AcmeDemoBundle:Main:homepage }
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="_welcome" pattern="/">
        <default key="_controller">AcmeDemoBundle:Main:homepage</default>
    </route>

</routes>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('_welcome', new Route('/', array(
    '_controller' => 'AcmeDemoBundle:Main:homepage',
)));

return $collection;

Esta ruta corresponde a la portada del sitio (por eso el valor / en el patrón) y está asociada con el controlador AcmeDemoBundle:Main:homepage. Symfony2 utiliza el valor de la opción _controller para determinar la función PHP que se ejecuta para responder a la petición. La transformación de _controller en una función PHP se explica más adelante.

6.3.2. Rutas con variables

El sistema de enrutamiento ofrece unas posibilidades mucho más interesantes que las de la sección anterior. Muchas rutas contienen una o más variables, también llamados placeholders:

blog_show:
    pattern:   /blog/{slug}
    defaults:  { _controller: AcmeBlogBundle:Blog:show }
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog_show" pattern="/blog/{slug}">
        <default key="_controller">AcmeBlogBundle:Blog:show</default>
    </route>
</routes>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog_show', new Route('/blog/{slug}', array(
    '_controller' => 'AcmeBlogBundle:Blog:show',
)));

return $collection;

El patrón de esta ruta se cumple para cualquier URL que empiece por /blog/ y después contenga cualquier valor. Ese valor variable se almacena en una variable llamada slug (sin las llaves { y }) y se pasa al controlador. En otras palabras, si la URL es /blog/hello-world, el controlador dispone de una variable $slug cuyo valor es hello-world. Esta variable es la que utilizará por ejemplo el controlador para buscar en la base de datos el artículo solicitado.

Si pruebas a acceder a la URL /blog, verás que Symfony2 no ejecuta el controlador de esta ruta. El motivo es que por defecto todas las variables de las rutas deben tener un valor. La solución consiste en asignar un valor por defecto a determinadas variables de la ruta mediante el array defaults.

6.3.3. Variables obligatorias y opcionales

Vamos a complicar un poco el ejemplo de la sección anterior y para ello, se va a añadir una nueva ruta llamada blog que muestra un listado de todos los artículos del blog

blog:
    pattern:   /blog
    defaults:  { _controller: AcmeBlogBundle:Blog:index }
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog" pattern="/blog">
        <default key="_controller">AcmeBlogBundle:Blog:index</default>
    </route>
</routes>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog', new Route('/blog', array(
    '_controller' => 'AcmeBlogBundle:Blog:index',
)));

return $collection;

Por el momento esta ruta es muy simple, ya que no incluye ninguna variable y por tanto, solo se activará cuando el usuario solicite exactamente la URL /blog. Pero, ¿qué sucede si necesitamos que esta ruta sea compatible con la paginación, donde la URL /blog/2 muestra la segunda página de los artículos del blog? Modifica la ruta anterior para que incluya una nueva variable llamada {page}:

blog:
    pattern:   /blog/{page}
    defaults:  { _controller: AcmeBlogBundle:Blog:index }
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog" pattern="/blog/{page}">
        <default key="_controller">AcmeBlogBundle:Blog:index</default>
    </route>
</routes>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog', new Route('/blog/{page}', array(
    '_controller' => 'AcmeBlogBundle:Blog:index',
)));

return $collection;

Al igual que la variable {slug} del ejemplo anterior, el valor asociado a {page} ahora estará disponible dentro del controlador. Así que ya puedes utilizar su valor para saber qué artículos debes mostrar en una determinada página.

El problema es que, como por defecto las variables de las rutas son obligatorias, esta ruta ya no funciona cuando un usuario solicita la URL /blog. Así que para ver la portada del blog, el usuario tendrás que utilizar la URL /blog/1. Obligar al usuario a recordar que siempre debe entrar en la primera página es simplemente ridículo. Así que vamos a modificar la ruta para que la variable {page} sea opcional. Para ello, asigna un valor por defecto a la variable dentro del array defaults:

blog:
    pattern:   /blog/{page}
    defaults:  { _controller: AcmeBlogBundle:Blog:index, page: 1 }
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog" pattern="/blog/{page}">
        <default key="_controller">AcmeBlogBundle:Blog:index</default>
        <default key="page">1</default>
    </route>
</routes>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog', new Route('/blog/{page}', array(
    '_controller' => 'AcmeBlogBundle:Blog:index',
    'page' => 1,
)));

return $collection;

Como la variable page ya dispone de un valor por defecto dentro de defaults, ya no es necesario indicarla siempre en la URL. Así que cuando el usuario solicite la URL /blog, esta ruta sí que se ejecutará y se asignará automáticamente el valor 1 a la variable page. Si se solicita la URL /blog/2, la ruta también se ejecuta y el valor de la variable page será 2, tal y como se indica en la propia ruta. Resumiendo:

  • Ruta: /blog, Variable page = 1
  • Ruta: /blog/1, Variable page = 1
  • Ruta: /blog/2, Variable page = 2

6.3.4. Agregando requisitos

Observa las rutas definidas en los ejemplos de las secciones anteriores:

blog:
    pattern:   /blog/{page}
    defaults:  { _controller: AcmeBlogBundle:Blog:index, page: 1 }

blog_show:
    pattern:   /blog/{slug}
    defaults:  { _controller: AcmeBlogBundle:Blog:show }
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog" pattern="/blog/{page}">
        <default key="_controller">AcmeBlogBundle:Blog:index</default>
        <default key="page">1</default>
    </route>

    <route id="blog_show" pattern="/blog/{slug}">
        <default key="_controller">AcmeBlogBundle:Blog:show</default>
    </route>
</routes>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog', new Route('/blog/{page}', array(
    '_controller' => 'AcmeBlogBundle:Blog:index',
    'page' => 1,
)));

$collection->add('blog_show', new Route('/blog/{show}', array(
    '_controller' => 'AcmeBlogBundle:Blog:show',
)));

return $collection;

¿Has visto cuál es el problema? Las dos rutas tienen el mismo patrón, por lo que las dos podrían responder a las URL que empiecen por /blog/ y continuen con cualquier cosa. En estas situaciones, el sistema de enrutamiento de Symfony siempre elige la ruta que se ha definido primero. Así que la ruta blog_show jamás se ejecutará. Además, URL como /blog/mi-post harán que se ejecute la primera ruta (blog), lo que provocará un error, ya que no tiene sentido que la variable page valga mi-post.

URL Ruta Parámetros
/blog/2 blog {page} = 2
/blog/mi-post blog {page} = mi-post

La solución a este problema consiste en añadir requisitos a la ruta. En este caso, si el patrón /blog/{page} sólo funcionara cuando {page} es un número entero, las dos rutas funcionarían sin problemas. Afortunadamente, es posible añadir requisitos a cada variable de la ruta mediante expresiones regulares:

blog:
    pattern:   /blog/{page}
    defaults:  { _controller: AcmeBlogBundle:Blog:index, page: 1 }
    requirements:
        page:  \d+
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="blog" pattern="/blog/{page}">
        <default key="_controller">AcmeBlogBundle:Blog:index</default>
        <default key="page">1</default>
        <requirement key="page">\d+</requirement>
    </route>
</routes>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('blog', new Route('/blog/{page}', array(
    '_controller' => 'AcmeBlogBundle:Blog:index',
    'page' => 1,
), array(
    'page' => '\d+',
)));

return $collection;

El requisito \d+ es una expresión regular que obliga a que el valor del parámetro {page} esté formado exclusivamente por dígitos (es decir, que sea un número entero y positivo). Después de este cambio, la ruta blog seguirá funcionando para URL como /blog/2 (ya que 2 es un número), pero ya no se ejecutará para URL como /blog/mi-post (porque mi-post no es un número). Por tanto, ahora la URL /blog/mi-post se procesa mediante la ruta blog_show.

URL Ruta Parámetros
/blog/2 blog {page} = 2
/blog/mi-post blog_show {slug} = mi-post

Como el parámetro requirements es una serie de expresiones regulares, puedes hacer que los requisitos sean tan complejos y flexibles como necesites. Imagina que la portada de tu aplicación está disponible en dos idiomas en función de la URL accedida:

homepage:
    pattern:   /{_locale}
    defaults:  { _controller: AcmeDemoBundle:Main:homepage, _locale: en }
    requirements:
        _locale:  en|fr
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="homepage" pattern="/{_locale}">
        <default key="_controller">AcmeDemoBundle:Main:homepage</default>
        <default key="_locale">en</default>
        <requirement key="_locale">en|fr</requirement>
    </route>
</routes>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('homepage', new Route('/{_locale}', array(
    '_controller' => 'AcmeDemoBundle:Main:homepage',
    '_locale' => 'en',
), array(
    '_locale' => 'en|fr',
)));

return $collection;

Cuando llegue una petición de algún usuario, el valor indicado en la variable {_locale} se compara con la expresión regular (en|fr).

Ruta Valor de la variable {_locale}
/ en
/en en
/fr fr
/es Ninguno, ya que la ruta no se ejecuta

6.3.5. Agregando requisitos sobre el método HTTP

Además de los requisitos de la URL, es posible restringir la ejecución de las rutas a un determinado método HTTP (es decir, GET, HEAD, POST, PUT, DELETE). Imagina que dispones de un formulario de contacto con dos controladores: uno para mostrar el formulario (en una petición GET) y otro para procesar el formulario enviado (en una petición POST*). La siguiente configuración permite definir estas dos rutas:

contact:
    pattern:  /contact
    defaults: { _controller: AcmeDemoBundle:Main:contact }
    requirements:
        _method:  GET

contact_process:
    pattern:  /contact
    defaults: { _controller: AcmeDemoBundle:Main:contactProcess }
    requirements:
        _method:  POST
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="contact" pattern="/contact">
        <default key="_controller">AcmeDemoBundle:Main:contact</default>
        <requirement key="_method">GET</requirement>
    </route>

    <route id="contact_process" pattern="/contact">
        <default key="_controller">AcmeDemoBundle:Main:contactProcess</default>
        <requirement key="_method">POST</requirement>
    </route>
</routes>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('contact', new Route('/contact', array(
    '_controller' => 'AcmeDemoBundle:Main:contact',
), array(
    '_method' => 'GET',
)));

$collection->add('contact_process', new Route('/contact', array(
    '_controller' => 'AcmeDemoBundle:Main:contactProcess',
), array(
    '_method' => 'POST',
)));

return $collection;

A pesar de que estas dos rutas tienen el mismo patrón (/contact), la primera ruta sólo se activa con las peticiones GET y la segunda sólo con las peticiones POST. Esto significa que puedes mostrar y enviar el formulario a través de la misma URL y usar dos controladores distintos para las dos acciones.

Nota Si no especificas el requisito _method, la ruta se ejecuta para todos los métodos HTTP.

Como todos los demás requisitos, el valor de la opción _method se considera una expresión regular. Así que para que una ruta responda a las peticiones GET y POST, utiliza el valor GET|POST.

6.3.6. Ejemplo de enrutado avanzado

Con todo lo explicado hasta ahora, ya puedes definir rutas realmente avanzadas para tu aplicación Symfony. El siguiente ejemplo muestra lo flexible que puede llegar a ser el sistema de enrutamiento:

article_show:
  pattern:  /articles/{_locale}/{year}/{title}.{_format}
  defaults: { _controller: AcmeDemoBundle:Article:show, _format: html }
  requirements:
      _locale:  en|fr
      _format:  html|rss
      year:     \d+
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="article_show" pattern="/articles/{_locale}/{year}/{title}.{_format}">
        <default key="_controller">AcmeDemoBundle:Article:show</default>
        <default key="_format">html</default>
        <requirement key="_locale">en|fr</requirement>
        <requirement key="_format">html|rss</requirement>
        <requirement key="year">\d+</requirement>
    </route>
</routes>
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('homepage', new Route('/articles/{_locale}/{year}/{title}.{_format}', array(
    '_controller' => 'AcmeDemoBundle:Article:show',
    '_format' => 'html',
), array(
    '_locale' => 'en|fr',
    '_format' => 'html|rss',
    'year' => '\d+',
)));

return $collection;

Esta ruta sólo se ejecutará cuando el valor de la variable {_locale} sea o en o fr y cuando la variable {year} sea un número. Además, esta ruta también muestra cómo utilizar un punto (.) para separar dos variables entre sí, en vez de la habitual barra inclinada (/). En resumen, cualquiera de las siguientes URL harían que esta ruta se ejecutara:

  • /articles/en/2010/my-post
  • /articles/fr/2010/my-post.rss
  • /articles/en/2013/my-latest-post.html

6.3.7. Variables de enrutamiento especiales

Como hemos visto, cada variable de enrutamiento y cada valor añadido al array defaults está disponible como argumento del controlador. Symfony también define tres variables especiales de enrutamiento:

  • _controller: determina el controlador asociado a la ruta que se ejecuta.
  • _format: establece el formato de la petición del usuario.
  • _locale: establece el idioma en la sesión del usuario.