Symfony 2.4, 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 path o patrón que debe cumplir la URL y un array de opciones llamado defaults:

_welcome:
    path:      /
    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" path="/">
        <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:
    path:      /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" path="/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:
    path:      /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" path="/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:
    path:      /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" path="/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:
    path:      /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" path="/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:

URL Ruta Parámetros
/blog blog {page} = 1
/blog/1 blog {page} = 1
/blog/2 blog {page} = 2

Advertencia También es posible definir más de una variable opcional en la ruta (por ejemplo: /blog/{slug}/{page}). La única restricción es que todo lo que va después de una variable opcional también tiene que ser opcional. Así, para la ruta /{page}/blog la variable page siempre será obligatoria (/blog no es una URL válida para esta ruta).

Truco Las rutas con variables opcionales al final no son válidas cuando la URL solicitada por el usuario tiene una barra / al final. Así que la URL /blog/ no será válida para esta ruta, pero sí lo será la URL /blog).

6.3.4. Agregando requisitos

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

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

blog_show:
    path:      /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" path="/blog/{page}">
        <default key="_controller">AcmeBlogBundle:Blog:index</default>
        <default key="page">1</default>
    </route>

    <route id="blog_show" path="/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 o condiciones 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:
    path:      /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" path="/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
/blog/2-mi-post blog_show {slug} = 2-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:
    path:      /{_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" path="/{_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:
    path:     /contact
    defaults: { _controller: AcmeDemoBundle:Main:contact }
    methods:  [GET]

contact_process:
    path:     /contact
    defaults: { _controller: AcmeDemoBundle:Main:contactProcess }
    methods:  [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" path="/contact"  methods="GET">
        <default key="_controller">AcmeDemoBundle:Main:contact</default>
    </route>

    <route id="contact_process" path="/contact" methods="POST">
        <default key="_controller">AcmeDemoBundle:Main:contactProcess</default>
    </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(), array(), '', array(), array('GET')));

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

return $collection;

Nota La opción methods solamente está definida desde la versión 2.2 de Symfony. En las versiones anteriores esta opción se llamaba _method y se definía bajo la opción requirements.

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 la opción methods, la ruta se ejecuta para todos los métodos HTTP.

6.3.6. Agregando el host a las rutas

A partir de la versión 2.2 de Symfony, también puedes hacer que una ruta sólo se ejecute si coincide con el host configurado. Para obtener más información, consulta el artículo How to match a route based on the Host.

6.3.7. Definiendo condiciones personalizadas mediante expresiones

Hasta ahora, se ha explicado cómo restringir las rutas para que sólo se ejecuten cuando se cumplen determinadas condiciones en sus variables, métodos HTTP o nombres de host. Sin embargo, a partir de la versión 2.4 de Symfony, las rutas disponen de una flexibilidad casi ilimitada gracias a las condiciones definidas mediante la propiedad condition:

contact:
    path:     /contact
    defaults: { _controller: AcmeDemoBundle:Main:contact }
    condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"
<?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"
        path="/contact"
        condition="context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"
    >
        <default key="_controller">AcmeDemoBundle:Main:contact</default>
    </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(),
    array(),
    '',
    array(),
    array(),
    'context.getMethod() in ["GET", "HEAD"] and request.headers.get("User-Agent") matches "/firefox/i"'
));

return $collection;

El valor de la propiedad condition es una expresión cuya sintaxis se define en la documentación del componente ExpressionLanguage. En este ejemplo, la ruta no se ejecutará a menos que el método HTTP sea GET o HEAD y el navegador sea Firefox (ya que la cabecera User-Agent debe contener la palabra firefox).

La lógica de la expresión puede ser tan compleja como sea necesaria. Dentro de la expresión puedes utilizar las dos siguientes variables:

  • context: es una instancia de la clase Symfony\Component\Routing\RequestContext, y contiene toda la información básica de la ruta que está siendo comprobada.
  • request: el objeto de tipo Symfony\Component\HttpFoundation\Request que representa a la petición del usuario.

Advertencia Las condiciones no se tienen en cuenta al generar las URL a partir de las rutas.

6.3.8. 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:
  path:     /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"
        path="/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

Nota En ocasiones es conveniente hacer que algunas partes de las rutas se puedan configurar globalmente en la aplicación. A partir de la versión 2.1 de Symfony es posible hacerlo gracias a los parámetros del contenedor de servicios. Para más información, lee el artículo How to use Service Container Parameters in your Routes.

6.3.9. 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 petición del usuario.