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
, Variablepage
=1
- Ruta:
/blog/1
, Variablepage
=1
- Ruta:
/blog/2
, Variablepage
=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.