El componente Routing

El componente Routing asigna una petición HTTP a un conjunto de variables de configuración.

Instalando

Puedes instalar el componente de varias maneras diferentes:

  • Usando el repositorio Git oficial (https://github.com/symfony/Routing);
  • Instalándolo a través de PEAR (pear.symfony.com/Routing);
  • Instalándolo vía Composer (symfony/routing en Packagist)

Usando

Con el fin de establecer un sistema de enrutado básico necesitas tres partes:

  • Una clase Symfony\Component\Routing\RouteCollection, que contiene las definiciones de las rutas (instancias de la clase Symfony\Component\Routing\Route)
  • Una clase Symfony\Component\Routing\RequestContext, que contiene información sobre la petición
  • Una clase Symfony\Component\Routing\Matcher\UrlMatcher, que realiza la asignación de la petición a una sola ruta

Veamos un ejemplo rápido. Ten en cuenta que esto supone que ya has configurado el cargador automático para cargar el componente Routing:

use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$routes = new RouteCollection();
$routes->add('route_name', new Route('/foo', array('controller' => 'MyController')));

$context = new RequestContext($_SERVER['REQUEST_URI']);

$matcher = new UrlMatcher($routes, $context);

$parameters = $matcher->match('/foo');
// array('controller' => 'MyController', '_route' => 'route_name')

Nota

Ten cuidado al usar $_SERVER['REQUEST_URI'], ya que este puede incluir los parámetros de consulta en la URL, lo cual causará problemas con la ruta coincidente. Una manera fácil de solucionar esto es usando el componente HTTPFoundation como se explica abajo.

Puedes agregar tantas rutas como quieras a una clase Symfony\Component\Routing\RouteCollection.

El método RouteCollection::add() toma dos argumentos. El primero es el nombre de la ruta. El segundo es un objeto Symfony\Component\Routing\Route, que espera una ruta URL y algún arreglo de variables personalizadas en su constructor. Este arreglo de variables personalizadas puede ser cualquier cosa que tenga significado para tu aplicación, y es devuelto cuando dicha ruta corresponda.

Si no hay ruta coincidente debe lanzar una Symfony\Component\Routing\Exception\ResourceNotFoundException.

Además de tu arreglo de variables personalizadas, se añade una clave _route, que contiene el nombre de la ruta buscada.

Definiendo rutas

Una definición de la ruta completa puede contener un máximo de cuatro partes:

  1. El patrón de la ruta URL. Este se compara con la URL pasada al RequestContext, y puede contener comodines marcadores de posición nombrados (por ejemplo, {placeholders}) para que coincidan con elementos dinámicos en la URL.
  2. Una matriz de valores predeterminados. Esta contiene un conjunto de valores arbitrarios que serán devueltos cuando la petición coincide con la ruta.
  3. Un arreglo de requisitos. Estos definen las restricciones para los valores de los marcadores de posición como las expresiones regulares.
  4. Un arreglo de opciones. Estas contienen la configuración interna de la ruta y son las menos necesarias comúnmente.

Veamos la siguiente ruta, que combina varias de estas ideas:

$route = new Route(
    '/archive/{month}',                    // ruta
    array('controller' => 'showArchive'),  // valores predefinidos
    array('month' => '[0-9]{4}-[0-9]{2}'), // requisitos
    array() // options
);

// ...

$parameters = $matcher->match('/archive/2012-01');
// array('controller' => 'showArchive', 'month' => '2012-01', '_route' => ...)

$parameters = $matcher->match('/archive/foo');
// lanza la ResourceNotFoundException

En este caso, la ruta coincide con /archive/2012-01, porque el comodín {month} coincide con el comodín de la expresión regular suministrada. Sin embargo, /archive/f oo no coincide, porque el comodín del mes “foo” no concuerda.

Además de las restricciones de la expresión regular, hay dos requisitos especiales que puedes definir:

  • el _method impone un determinado método de la petición HTTP (HEAD, GET, POST, ...)
  • _scheme impone un determinado esquema HTTP (http, https)

Por ejemplo, la siguiente ruta sólo acepta peticiones a /foo con el método POST y una conexión segura:

$route = new Route('/foo', array(), array('_method' => 'post', '_scheme' => 'https' ));

Truco

Si quieres hacer coincidir todas las URL que comiencen con una cierta ruta y terminan en un sufijo arbitrario puedes utilizar la siguiente definición de ruta:

$route = new Route('/start/{suffix}', array('suffix' => ''), array('suffix' => '.*'));

Usando prefijos

Puedes agregar rutas u otras instancias de Symfony\Component\Routing\RouteCollection a otra colección. De esta manera puedes construir un árbol de rutas. Además, puedes definir un prefijo, los requisitos predeterminados y las opciones predefinidas para todas las rutas de un subárbol:

$rootCollection = new RouteCollection();

$subCollection = new RouteCollection();
$subCollection->add(...);
$subCollection->add(...);

$rootCollection->addCollection($subCollection, '/prefix', array('_scheme' => 'https'));

Configurando los parámetros de la Petición

La clase Symfony\Component\Routing\RequestContext proporciona información sobre la petición actual. Puedes definir todos los parámetros de una petición HTTP con esta clase a través de su constructor:

public function __construct($baseUrl = '', $method = 'GET', $host = 'localhost', $scheme = 'http', $httpPort = 80, $httpsPort = 443)

Normalmente puedes pasar los valores de la variable $_SERVER para poblar la Symfony\Component\Routing\RequestContext. Pero si utilizas el componente HttpFoundation </components/http_foundation/index>, puedes utilizar su :class:`Symfony\Component\HttpFoundation\Request para alimentar el Symfony\Component\Routing\RequestContext en un método abreviado:

use Symfony\Component\HttpFoundation\Request;

$context = new RequestContext();
$context->fromRequest(Request::createFromGlobals());

Generando una URL

Si bien la Symfony\Component\Routing\Matcher\UrlMatcher trata de encontrar una ruta que se adapte a la petición dada, esta también puede construir una URL a partir de una ruta determinada:

use Symfony\Component\Routing\Generator\UrlGenerator;

$routes = new RouteCollection();
$routes->add('show_post', new Route('/show/{slug}'));

$context = new RequestContext($_SERVER['REQUEST_URI']);

$generator = new UrlGenerator($routes, $context);

$url = $generator->generate('show_post', array(
    'slug' => 'my-blog-post'
));
// /show/my-blog-post

Nota

Si has definido el requisito _scheme, se genera una URL absoluta si el esquema del Symfony\Component\Routing\RequestContext actual no coincide con el requisito.

Cargando rutas desde un archivo

Ya hemos visto lo fácil que es añadir rutas a una colección dentro de PHP. Pero también puedes cargar rutas de una serie de archivos diferentes.

El componente de enrutado viene con una serie de clases cargadoras, cada una dotándote de la capacidad para cargar una colección de definiciones de ruta desde un archivo externo en algún formato. Cada cargador espera una instancia del Symfony\Component\Config\FileLocator como argumento del constructor. Puedes utilizar el Symfony\Component\Config\FileLocator para definir una serie de rutas en las que el cargador va a buscar los archivos solicitados. Si se encuentra el archivo, el cargador devuelve una Symfony\Component\Routing\RouteCollection.

Si estás usando el YamlFileLoader, entonces las definiciones de ruta tienen este aspecto:

# routes.yml
route1:
    pattern: /foo
    defaults: { controller: 'MyController::fooAction' }

route2:
    pattern: /foo/bar
    defaults: { controller: 'MyController::foobarAction' }

Para cargar este archivo, puedes utilizar el siguiente código. Este asume que tu archivo routes.yml está en el mismo directorio que el código de abajo:

use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;

// busca dentro de *este* directorio
$locator = new FileLocator(array(__DIR__));
$loader = new YamlFileLoader($locator);
$collection = $loader->load('routes.yml');

Además del Symfony\Component\Routing\Loader\YamlFileLoader hay otros dos cargadores que funcionan de manera similar:

  • Symfony\Component\Routing\Loader\XmlFileLoader
  • Symfony\Component\Routing\Loader\PhpFileLoader

Si utilizas el Symfony\Component\Routing\Loader\PhpFileLoader debes proporcionar el nombre del un archivo php que devuelva una Symfony\Component\Routing\RouteCollection:

// ProveedorDeRuta.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('route_name', new Route('/foo', array('controller' => 'ExampleController')));
// ...

return $collection;

Rutas como cierres

También está el Symfony\Component\Routing\Loader\ClosureLoader, que llama a un cierre y utiliza el resultado como una Symfony\Component\Routing\RouteCollection:

use Symfony\Component\Routing\Loader\ClosureLoader;

$closure = function() {
    return new RouteCollection();
};

$loader = new ClosureLoader();
$collection = $loader->load($closure);

Rutas como anotaciones

Por último, pero no menos importante, están las Symfony\Component\Routing\Loader\AnnotationDirectoryLoader y Symfony\Component\Routing\Loader\AnnotationFileLoader para cargar definiciones de ruta a partir de las anotaciones de la clase. Los detalles específicos se dejan aquí.

El ruteador todo en uno

La clase Symfony\Component\Routing\Router es un paquete todo en uno para utilizar rápidamente el componente de enrutado. El constructor espera una instancia del cargador, una ruta a la definición de la ruta principal y algunas otras opciones:

public function __construct(LoaderInterface $loader, $resource, array $options = array(), RequestContext $context = null, array $defaults = array());

Con la opción cache_dir puedes habilitar la caché de enrutado (si proporcionas una ruta) o desactivar el almacenamiento en caché (si la configuras a null). El almacenamiento en caché automáticamente se hace en segundo plano si lo quieres usar. Un ejemplo básico de la clase Symfony\Component\Routing\Router se vería así:

$locator = new FileLocator(array(__DIR__));
$requestContext = new RequestContext($_SERVER['REQUEST_URI']);

$router = new Router(
    new YamlFileLoader($locator),
    "routes.yml",
    array('cache_dir' => __DIR__.'/cache'),
    $requestContext,
);
$router->match('/foo/bar');

Nota

Si utilizas el almacenamiento en caché, el componente Routing compilará las nuevas clases guardándolas en cache_dir. Esto significa que el archivo debe tener permisos de escritura en esa ubicación.

Bifúrcame en GitHub