El componente Routing

El componente Routing asocia una Petición HTTP a un conjunto de variables de configuración.

Instalando

Puedes instalar el componente de varias maneras diferentes:

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;

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

$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 ruta completa puede contener hasta siete partes:

  1. El trayecto a la URL de la ruta. 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. Un arreglo de valores predeterminados. Esta contiene un conjunto de valores arbitrarios que serán devueltos cuando la petición coincida 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 comúnmente son las menos necesarias.
  5. Un servidor. Este se empareja contra el servidor de la petición. Ve Cómo emparejar una ruta basándose en el servidor para más detalles.
  6. Un arreglo de esquemas. Estos imponen un determinado esquema HTTP (http, https).
  7. Un arreglo de métodos. Estos fuerzan un determinado método de petición HTTP (HEAD, GET, POST, ...).

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

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}', 'subdomain' => 'www|m'), // requisitos
    array(), // opciones
    '{subdomain}.example.com', // servidor
    array(), // esquemas
    array() // métodos
);

// ...

$parameters = $matcher->match('/archive/2012-01');
// array(
//     'controller' => 'showArchive',
//     'month' => '2012-01',
//     'subdomain' => 'www',
//     '_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.

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, requisitos predeterminados, opciones predefinidas y servidores a todas las rutas de un subárbol con el método addPrefix():

$rootCollection = new RouteCollection();

$subCollection = new RouteCollection();
$subCollection->add(...);
$subCollection->add(...);
$subCollection->addPrefix(
    '/prefix', // prefijo
    array(), // requisitos
    array(), // opciones
    'admin.example.com', // servidor
    array('https') // esquemas
);

$rootCollection->addCollection($subCollection);

Nuevo en la versión 2.2: El método addPrefix se añadió en Symfony 2.2. Este era parte del método addCollection en versiones anteriores.

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, puedes utilizar su clase Symfony\Component\HttpFoundation\Request para alimentar al 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 definiste un esquema, se genera una URL absoluta si el esquema de la clase Symfony\Component\Routing\RequestContext actual no concuerda 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:
    path:     /foo
    defaults: { _controller: 'MyController::fooAction' }

route2:
    path:     /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