Enrutando

Las URL bonitas absolutamente son una necesidad para cualquier aplicación web seria. Esto significa dejar atrás las URL feas como index.php?article_id=57 en favor de algo así como /leer/intro-a-symfony.

Tener tal flexibilidad es más importante aún. ¿Qué pasa si necesitas cambiar la URL de una página de /blog a /noticias? ¿Cuántos enlaces necesitas cazar y actualizar para hacer el cambio? Si estás utilizando el enrutador de Symfony, el cambio es sencillo.

El enrutador de Symfony2 te permite definir URL creativas que se asignan a diferentes áreas de la aplicación. Al final de este capítulo, serás capaz de:

  • Crear rutas complejas asignadas a controladores
  • Generar URL que contienen plantillas y controladores
  • Cargar recursos de enrutado desde paquetes (o de cualquier otro lugar)
  • Depurar tus rutas

Enrutador en acción

Una ruta es un mapa desde la trayectoria de una URL hasta un controlador. Por ejemplo, supongamos que deseas adaptar cualquier URL como /blog/mi-post o /blog/todo-sobre-symfony y enviarla a un controlador que puede buscar y reproducir esta entrada del blog. La ruta es simple:

  • YAML
    # app/config/routing.yml
    blog_show:
        path:      /blog/{slug}
        defaults:  { _controller: AcmeBlogBundle:Blog:show }
    
  • XML
    <!-- app/config/routing.xml -->
    <?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>
    
  • PHP
    // app/config/routing.php
    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;
    

Nuevo en la versión 2.2: La opción path es nueva en Symfony 2.2, en versiones anteriores se utilizaba pattern.

La ruta definida por blog_show actúa como /blog/* donde al comodín se le da el nombre de slug. Para la URL /blog/my-blog-post, la variable slug obtiene un valor de my-blog-post, que está disponible para usarlo en el controlador (sigue leyendo).

El parámetro _controller es una clave especial que le dice a Symfony qué controlador se debe ejecutar cuando una URL coincida con esta ruta. La cadena _controller se conoce como el nombre lógico. Esta sigue un patrón que apunta a una clase y método PHP específicos:

// src/Acme/BlogBundle/Controller/BlogController.php
namespace Acme\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function showAction($slug)
    {
        // usa la variable $slug para consultar la base de datos
        $blog = ...;

        return $this->render('AcmeBlogBundle:Blog:show.html.twig', array(
            'blog' => $blog,
        ));
    }
}

¡Enhorabuena! Acabas de crear tu primera ruta y la conectaste a un controlador. Ahora, cuando visites /blog/my-post, el controlador showAction será ejecutado y la variable $slug será igual a my-post.

Este es el objetivo del enrutador de Symfony2: asignar la URL de una petición a un controlador. De paso, aprenderás todo tipo de trucos que incluso facilitan la asignación de URL complejas.

Enrutando: Bajo el capó

Cuando se hace una petición a tu aplicación, esta contiene una dirección al «recurso» exacto que solicitó el cliente. Esta dirección se conoce como URL (o URI), y podría ser /contact, /blog/read-me, o cualquier otra cosa. Tomemos la siguiente petición HTTP, por ejemplo:

GET /blog/my-blog-post

El objetivo del sistema de enrutado de Symfony2 es analizar esta URL y determinar qué controlador se debe ejecutar. Todo el proceso es el siguiente:

  1. La petición es manejada por el controlador frontal de Symfony2 (por ejemplo, app.php);
  2. El núcleo de Symfony2 (es decir, el Kernel) pregunta al enrutador que examine la petición;
  3. El enrutador busca la URL entrante para emparejarla con una ruta específica y devuelve información sobre la ruta, incluyendo el controlador que se debe ejecutar;
  4. El núcleo de Symfony2 ejecuta el controlador, que en última instancia, devuelve un objeto Respuesta.
Flujo de la petición en Symfony2

La capa del enrutador es una herramienta que traduce la URL entrante a un controlador específico a ejecutar.

Creando rutas

Symfony carga todas las rutas de tu aplicación desde un archivo de configuración de enrutado. El archivo usualmente es app/config/routing.yml, pero lo puedes configurar para que sea cualquier otro (incluyendo un archivo XML o PHP) vía el archivo de configuración de la aplicación:

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

Truco

A pesar de que todas las rutas se cargan desde un solo archivo, es práctica común incluir recursos de enrutado adicionales. Para ello, sólo indica en el archivo de configuración de enrutado principal cuáles archivos externos se tendrían que incluir. Consulta la sección Incluyendo recursos de enrutado externos para más información.

Configuración básica de rutas

Definir una ruta es fácil, y una aplicación típica tendrá un montón de rutas. Una ruta básica consta de dos partes: el path a emparejar y un arreglo defaults:

  • YAML
    _welcome:
        path:      /
        defaults:  { _controller: AcmeDemoBundle:Main:homepage }
    
  • XML
    <?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>
    
  • PHP
    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 coincide con la página de inicio (/) y la asigna al controlador de la página principal AcmeDemoBundle:Main:homepage. Symfony2 convierte la cadena _controller en una función PHP real y la ejecuta. Este proceso será explicado en breve en la sección Patrón de nomenclatura para controladores.

Enrutando con marcadores de posición

Por supuesto, el sistema de enrutado es compatible con rutas mucho más interesantes. Muchas rutas contienen uno o más «comodines» llamados marcadores de posición:

  • YAML
    blog_show:
        path:      /blog/{slug}
        defaults:  { _controller: AcmeBlogBundle:Blog:show }
    
  • XML
    <?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>
    
  • PHP
    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;
    

La ruta emparejará con cualquier cosa que se parezca a /blog/*. Aún mejor, el valor coincide con el marcador de posición {slug} que estará disponible dentro de tu controlador. En otras palabras, si la URL es /blog/hello-world, una variable $slug, con un valor de hello-world, estará disponible en el controlador. Esta se puede usar, por ejemplo, para cargar la entrada en el blog coincidente con esa cadena.

La ruta no debe, sin embargo, emparejar con un /blog sencillo. Eso es porque, por omisión, todos los marcadores de posición son obligatorios. Esto se puede cambiar agregando un valor marcador de posición al arreglo defaults.

Marcadores de posición obligatorios y opcionales

Para hacer las cosas más emocionantes, añade una nueva ruta que muestre una lista de todas las entradas del ‘blog’ para la petición imaginaria ‘blog’:

  • YAML
    blog:
        path:      /blog
        defaults:  { _controller: AcmeBlogBundle:Blog:index }
    
  • XML
    <?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>
    
  • PHP
    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;
    

Hasta el momento, esta ruta es tan simple como es posible — no contiene marcadores de posición y sólo coincidirá con la URL exacta /blog. ¿Pero si necesitamos que esta ruta sea compatible con paginación, donde /blog/2 muestra la segunda página de las entradas del blog? Actualiza la ruta para que tenga un nuevo marcador de posición {page}:

  • YAML
    blog:
        path:      /blog/{page}
        defaults:  { _controller: AcmeBlogBundle:Blog:index }
    
  • XML
    <?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>
    
  • PHP
    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 el marcador de posición {slug} anterior, el valor coincidente con {page} estará disponible dentro de tu controlador. Puedes utilizar su valor para determinar cual conjunto de entradas del blog muestra determinada página.

¡Pero espera! Puesto que los marcadores de posición de forma predeterminada son obligatorios, esta ruta ya no coincidirá con /blog simplemente. En su lugar, para ver la página 1 del blog, ¡habrá la necesidad de utilizar la URL /blog/1! Debido a que esa no es la manera en que se comporta una aplicación web rica, debes modificar la ruta para que el parámetro {page} sea opcional. Esto se consigue incluyéndolo en la colección defaults:

  • YAML
    blog:
        path:      /blog/{page}
        defaults:  { _controller: AcmeBlogBundle:Blog:index, page: 1 }
    
  • XML
    <?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>
    
  • PHP
    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;
    

Agregando page a la clave defaults, el marcador de posición {page} ya no es necesario. La URL /blog coincidirá con esta ruta y el valor del parámetro page se fijará en 1. La URL /blog/2 también coincide, dando al parámetro page un valor de 2. Perfecto.

/blog {page} = 1
/blog/1 {page} = 1
/blog/2 {page} = 2

Truco

Las rutas con parámetros opcionales al final no coincidirán con peticiones con una barra inclinada final (es decir, /blog/ no coincidirá, en cambio /blog concordará).

Agregando requisitos

Echa un vistazo a las rutas que hemos creado hasta ahora:

  • YAML
    blog:
        path:      /blog/{page}
        defaults:  { _controller: AcmeBlogBundle:Blog:index, page: 1 }
    
    blog_show:
        path:      /blog/{slug}
        defaults:  { _controller: AcmeBlogBundle:Blog:show }
    
  • XML
    <?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>
    
  • PHP
    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;
    

¿Puedes ver el problema? Ten en cuenta que ambas rutas tienen patrones que coinciden con las URL que se parezcan a /blog/*. El enrutador de Symfony siempre elegirá la primera ruta coincidente que encuentre. En otras palabras, la ruta blog_show nunca corresponderá. En cambio, una URL como /blog/my-blog-post coincidirá con la primera ruta (blog) y devolverá un valor sin sentido de my-blog-post para el parámetro {page}.

URL ruta parámetros
/blog/2 blog {page} = 2
/blog/mi-entrada-del-blog blog {page} = mi-entrada-del-blog

La respuesta al problema es añadir requisitos a la ruta. Las rutas en este ejemplo funcionarán perfectamente si el patrón /blog/{page} sólo concuerda con una URL dónde la parte {page} sea un número entero. Afortunadamente, se puede agregar fácilmente una expresión regular de requisitos para cada parámetro. Por ejemplo:

  • YAML
    blog:
        path:      /blog/{page}
        defaults:  { _controller: AcmeBlogBundle:Blog:index, page: 1 }
        requirements:
            page:  \d+
    
  • XML
    <?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>
    
  • PHP
    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 diciendo que el valor del parámetro {page} debe ser un dígito (es decir, un número). La ruta blog todavía coincide con una URL como /blog/2 (porque 2 es un número), pero ya no concuerda con una URL como /blog/my-blog-pos (porque my-blog-post no es un número).

Como resultado, una URL como /blog/my-blog-post ahora coincide correctamente con la ruta blog_show.

URL ruta parámetros
/blog/2 blog {page} = 2
/blog/mi-entrada-del-blog blog_show {ficha} = mi-entrada-del-blog

Puesto que el parámetro requirements consiste de expresiones regulares, la complejidad y flexibilidad de cada requisito es totalmente tuya. Supongamos que la página principal de tu aplicación está disponible en dos diferentes idiomas, basándose en la URL:

  • YAML
    homepage:
        path:      /{_locale}
        defaults:  { _controller: AcmeDemoBundle:Main:homepage, _locale: en }
        requirements:
            _locale:  en|fr
    
  • XML
    <?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="/{culture}">
            <default key="_controller">AcmeDemoBundle:Main:homepage</default>
            <default key="_locale">en</default>
            <requirement key="_locale">en|fr</requirement>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('homepage', new Route('/{culture}', array(
        '_controller' => 'AcmeDemoBundle:Main:homepage',
        'culture'     => 'en',
    ), array(
        'culture' => 'en|fr',
    )));
    
    return $collection;
    

Para las peticiones entrantes, la porción {_locale} de la dirección se compara con la expresión regular (en|es).

/ {_locale} = es
/en {_locale} = en
/es {_locale} = es
/fr no coincidirá con esta ruta

Agregando requisitos de método HTTP

Además de la URL, también puedes coincidir con el método de la petición entrante (es decir, GET, HEAD, POST, PUT, DELETE). Supongamos que tienes un formulario de contacto con dos controladores —uno para mostrar el formulario (en una petición GET) y uno para procesar el formulario una vez presentado (en una petición POST). Esto se puede lograr con la siguiente configuración de ruta:

  • YAML
    contact:
        path:     /contact
        defaults: { _controller: AcmeDemoBundle:Main:contact }
        methods:  [GET]
    
    contact_process:
        path:     /contact
        defaults: { _controller: AcmeDemoBundle:Main:contactProcess }
        methods:  [POST]
    
  • XML
    <?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>
    
  • PHP
    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;
    

Nuevo en la versión 2.2: La opción methods se añadió en Symfony 2.2. Usa el requisito _method en versiones anteriores.

A pesar del hecho de que estas dos rutas tienen patrones idénticos (/contact), la primera ruta sólo coincidirá con las peticiones GET y la segunda sólo coincidirá con las peticiones POST. Esto significa que puedes mostrar y enviar el formulario a través de la misma URL, mientras usas controladores distintos para las dos acciones.

Nota

Si no especifícas ningún método, la ruta emparejará con todos los métodos.

Añadiendo un servidor

Nuevo en la versión 2.2: El soporte necesario para emparejar con el servidor se añadió en Symfony 2.2

También puedes emparejar con el host HTTP de la petición entrante. Para más información, consulta el Cómo emparejar una ruta basándose en el servidor en la documentación del componente Routing.

Ejemplo de enrutado avanzado

En este punto, tienes todo lo necesario para crear una poderosa estructura de enrutado Symfony. El siguiente es un ejemplo de cuán flexible puede ser el sistema de enrutado:

  • YAML
    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
    <?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/{culture}/{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>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('homepage', new Route('/articles/{culture}/{year}/{title}.{_format}', array(
        '_controller' => 'AcmeDemoBundle:Article:show',
        '_format'     => 'html',
    ), array(
        'culture' => 'en|fr',
        '_format' => 'html|rss',
        'year'    => '\d+',
    )));
    
    return $collection;
    

Como hemos visto, esta ruta sólo coincide si la porción {_locale} de la URL es o bien «en» o «fr» y si {year} es un número. Esta ruta también muestra cómo puedes utilizar un punto entre los marcadores de posición en lugar de una barra inclinada. Las URL que coinciden con esta ruta se podrían ver como las siguientes:

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

Nota

A veces quieres hacer configurables globalmente ciertas partes de tus rutas. Symfony 2.1 te proporciona una manera de hacer esto aprovechando los parámetros del contenedor de servicios. Lee más sobre esto en «Cómo utilizar parámetros del contenedor de servicios en tus rutas».

Parámetros de enrutado especiales

Como has visto, cada parámetro de enrutado o valor predeterminado finalmente está disponible como un argumento en el método controlador. Adicionalmente, hay tres parámetros que son especiales: cada uno añade una única pieza de funcionalidad a tu aplicación:

  • _controller: Como hemos visto, este parámetro se utiliza para determinar qué controlador se ejecuta cuando la ruta concuerda;
  • _format: Se utiliza para establecer el formato de la petición (Leer más);
  • _locale: Se utiliza para establecer la configuración regional en la petición (Leer más);

Truco

Si utilizas el parámetro _locale en una ruta, ese valor también se almacenará en la sesión para las subsecuentes peticiones lo cual evita guardar la misma región.

Patrón de nomenclatura para controladores

Cada ruta debe tener un parámetro _controller, el cual determina qué controlador se debe ejecutar cuando dicha ruta concuerde. Este parámetro utiliza un patrón de cadena simple llamado el nombre lógico del controlador, que Symfony asigna a un método y clase PHP específico. El patrón consta de tres partes, cada una separada por dos puntos:

paquete:controlador:acción

Por ejemplo, un valor _controller de AcmeBlogBundle:Blog:show significa:

Paquete Clase de controlador Nombre método
AcmeBlogBundle BlogController showAction

El controlador podría tener este aspecto:

// src/Acme/BlogBundle/Controller/BlogController.php
namespace Acme\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function showAction($slug)
    {
        // ...
    }
}

Ten en cuenta que Symfony añade la cadena Controller al nombre de la clase (Blog => BlogController) y Action al nombre del método (show => showAction).

También podrías referirte a este controlador utilizando su nombre de clase y método completamente cualificado: Acme\BlogBundle\Controller\BlogController::showAction. Pero si sigues algunas simples convenciones, el nombre lógico es más conciso y permite mayor flexibilidad.

Nota

Además de utilizar el nombre lógico o el nombre de clase completamente cualificado, Symfony es compatible con una tercera forma de referirse a un controlador. Este método utiliza un solo separador de dos puntos (por ejemplo, service_name:indexAction) y hace referencia al controlador como un servicio (consulta Cómo definir controladores como servicios).

Parámetros de ruta y argumentos del controlador

Los parámetros de ruta (por ejemplo, {slug}) son especialmente importantes porque cada uno se pone a disposición como argumento para el método controlador:

public function showAction($slug)
{
  // ...
}

En realidad, toda la colección defaults se combina con los valores del parámetro para formar un solo arreglo. Cada clave de ese arreglo está disponible como un argumento en el controlador.

En otras palabras, por cada argumento de tu método controlador, Symfony busca un parámetro de ruta con ese nombre y asigna su valor a ese argumento. En el ejemplo avanzado anterior, podrías utilizar cualquier combinación (en cualquier orden) de las siguientes variables como argumentos para el método showAction():

  • $_locale
  • $year
  • $title
  • $_format
  • $_controller

Dado que los marcadores de posición y los valores de la colección defaults se combinan, incluso la variable $_controller está disponible. Para una explicación más detallada, consulta Parámetros de ruta como argumentos para el controlador.

Truco

También puedes utilizar una variable especial $_route, que se fija al nombre de la ruta que concordó.

Incluyendo recursos de enrutado externos

Todas las rutas se cargan a través de un único archivo de configuración —usualmente app/config/routing.yml (consulta Creando rutas más arriba). Por lo general, sin embargo, deseas cargar rutas para otros lugares, como un archivo de enrutado que vive dentro de un paquete. Esto se puede hacer «importando» ese archivo:

  • YAML
    # app/config/routing.yml
    acme_hello:
        resource: "@AcmeHelloBundle/Resources/config/routing.yml"
    
  • XML
    <!-- app/config/routing.xml -->
    <?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">
    
        <import resource="@AcmeHelloBundle/Resources/config/routing.xml" />
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->addCollection($loader->import("@AcmeHelloBundle/Resources/config/routing.php"));
    
    return $collection;
    

Nota

Cuando importas recursos desde YAML, la clave (por ejemplo, acme_hello) no tiene sentido. Sólo asegúrate de que es única para que no haya otras líneas que reemplazar.

La clave resource carga el recurso de la ruta dada. En este ejemplo, el recurso es la ruta completa a un archivo, donde la sintaxis contextual del atajo @AcmeHelloBundle se resuelve en la ruta a ese paquete. El archivo importado podría tener este aspecto:

  • YAML
     # src/Acme/HelloBundle/Resources/config/routing.yml
    acme_hello:
         path:     /hello/{name}
         defaults: { _controller: AcmeHelloBundle:Hello:index }
    
  • XML
    <!-- src/Acme/HelloBundle/Resources/config/routing.xml -->
    <?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="acme_hello" path="/hello/{name}">
            <default key="_controller">AcmeHelloBundle:Hello:index</default>
        </route>
    </routes>
    
  • PHP
    // src/Acme/HelloBundle/Resources/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('acme_hello', new Route('/hello/{name}', array(
        '_controller' => 'AcmeHelloBundle:Hello:index',
    )));
    
    return $collection;
    

Las rutas de este archivo se analizan y cargan en la misma forma que el archivo de enrutado principal.

Prefijando rutas importadas

También puedes optar por proporcionar un «prefijo» para las rutas importadas. Por ejemplo, supongamos que deseas que la ruta acme_hello tenga una ruta final de /admin/hello/{name} en lugar de simplemente /hello/{name}:

  • YAML
    # app/config/routing.yml
    acme_hello:
        resource: "@AcmeHelloBundle/Resources/config/routing.yml"
        prefix:   /admin
    
  • XML
    <!-- app/config/routing.xml -->
    <?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">
    
        <import resource="@AcmeHelloBundle/Resources/config/routing.xml" prefix="/admin" />
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->addCollection($loader->import("@AcmeHelloBundle/Resources/config/routing.php"), '/admin');
    
    return $collection;
    

La cadena /admin ahora será prefijada a la trayectoria de cada ruta cargada del nuevo recurso de enrutado.

Truco

Además puedes definir rutas usando anotaciones. Consulta la documentación del FrameworkExtraBundle para ver cómo hacerlo.

Añadiendo un servidor a la expresión regular de las rutas importadas

Nuevo en la versión 2.2: El soporte necesario para emparejar con el servidor se añadió en Symfony 2.2

Puedes poner el servidor en la expresión regular de las rutas importadas. Para más información, ve Añadiendo un servidor a la expresión regular de las rutas importadas.

Visualizando y depurando rutas

Si bien agregar y personalizar rutas, es útil para poder visualizar y obtener información detallada sobre tus rutas. Una buena manera de ver todas las rutas en tu aplicación es a través de la orden de consola router:debug. Ejecuta la siguiente orden desde la raíz de tu proyecto.

$ php app/console router:debug

Esta orden imprimirá una útil lista de todas las rutas configuradas en tu aplicación:

homepage              ANY       /
contact               GET       /contact
contact_process       POST      /contact
article_show          ANY       /articles/{_locale}/{year}/{title}.{_format}
blog                  ANY       /blog/{page}
blog_show             ANY       /blog/{slug}

También puedes obtener información muy específica de una sola ruta incluyendo el nombre de la ruta después de la orden:

$ php app/console router:debug article_show

Asímismo, si deseas probar acuál ruta coincide con una trayectoria dada, puedes usar la orden de consola router:match:

$ php app/console router:match /blog/my-latest-post

Esta orden imprimirá la ruta de la URL coincidente.

Route "blog_show" matches

Generando URL

El sistema de enrutado también se debe utilizar para generar URL. En realidad, el enrutado es un sistema bidireccional: asignando la URL a un controlador+parámetros y la ruta+parámetros a una URL. Los métodos match() y generate() de este sistema bidireccional. Tomando la ruta blog_show del ejemplo anterior:

$params = $this->get('router')->match('/blog/my-blog-post');
// array(
//     'slug'        => 'my-blog-post',
//     '_controller' => 'AcmeBlogBundle:Blog:show',
// )

$uri = $this->get('router')->generate('blog_show', array('slug' => 'my-blog-post'));
// /blog/my-blog-post

Para generar una URL, debes especificar el nombre de la ruta (por ejemplo, blog_show) y cualquier comodín (por ejemplo, slug = my-blog-post) usado en el patrón de esa ruta. Con esta información, fácilmente puedes generar cualquier URL:

class MainController extends Controller
{
    public function showAction($slug)
    {
        // ...

        $url = $this->generateUrl(
            'blog_show',
            array('slug' => 'my-blog-post')
        );
    }
}

Nota

En los controladores que extienden la clase base Symfony\Bundle\FrameworkBundle\Controller\Controller de Symfony, puedes utilizar el método generateUrl(), el cual llama al método generate() del servicio enrutador.

En una sección posterior, aprenderás cómo generar URL desde el interior de tus plantillas.

Truco

Si la interfaz de tu aplicación utiliza peticiones AJAX, posiblemente desees poder generar las direcciones URL en JavaScript basándote en tu configuración de enrutado. Usando el FOSJsRoutingBundle, puedes hacer eso exactamente:

var url = Routing.generate(
    'blog_show',
    {"slug": 'my-blog-post'}
);

Para más información, consulta la documentación del paquete.

Generando URL absolutas

De forma predeterminada, el enrutador va a generar URL relativas (por ejemplo /blog). Para generar una URL absoluta, sólo tienes que pasar true como tercer argumento del método generate():

$router->generate('blog_show', array('slug' => 'my-blog-post'), true);
// http://www.example.com/blog/my-blog-post

Nota

El servidor utilizado al generar una URL absoluta es el anfitrión del objeto Petición actual. Este se detercta automática basándose en la información del servidor suministrada por PHP. Al generar direcciones URL absolutas para archivos ejecutables desde la línea de ordenes, debes configurar manualmente el servidor que desees en el objeto RequestContext:

$router->getContext()->setHost('www.example.com');

Generando URL con cadena de consulta

El método generate toma un arreglo de valores comodín para generar la URI. Pero si pasas adicionales, se añadirán a la URI como cadena de consulta:

$router->generate('blog', array('page' => 2, 'category' => 'Symfony'));
// /blog/2?category=Symfony

Generando URL desde una plantilla

El lugar más común para generar una URL es dentro de una plantilla cuando creas enlaces entre las páginas de tu aplicación. Esto se hace igual que antes, pero utilizando una función ayudante de plantilla:

  • Twig
    <a href="{{ path('blog_show', {'slug': 'my-blog-post'}) }}">
      Read this blog post.
        </a>
    
  • PHP
    <a href="<?php echo $view['router']->generate('blog_show', array('slug' => 'my-blog-post')) ?>">
        Read this blog post.
        </a>
    

También puedes generar URL absolutas.

  • Twig
    <a href="{{ url('blog_show', {'slug': 'my-blog-post'}) }}">
      Read this blog post.
        </a>
    
  • PHP
    <a href="<?php echo $view['router']->generate('blog_show', array('slug' => 'my-blog-post'), true) ?>">
        Read this blog post.
        </a>
    

Resumen

El enrutado es un sistema para asignar la dirección de las peticiones entrantes a la función controladora que se debe llamar para procesar la petición. Este permite especificar ambas URL bonitas y mantiene la funcionalidad de tu aplicación disociada de las URL. El enrutado es un mecanismo de dos vías, lo cual significa que también lo debes usar para generar tus direcciones URL.

Bifúrcame en GitHub