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 claseSymfony\Component\Routing\RequestContext
, y contiene toda la información básica de la ruta que está siendo comprobada.request
: el objeto de tipoSymfony\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.