Enrutamiento

El Router se encarga de todo lo relacionado con las direcciones URL, para que usted ya no tenga que pensar en ellas. Mostraremos:

  • cómo configurar el router para que las URL sean según sus deseos
  • hablaremos de SEO y redirección
  • y mostraremos cómo escribir su propio router

Las URL más humanas (o también cool o pretty URL) son más usables, memorables y contribuyen positivamente al SEO. Nette piensa en esto y apoya plenamente a los desarrolladores. Puede diseñar para su aplicación exactamente la estructura de direcciones URL que desee. Incluso puede diseñarla cuando la aplicación ya está terminada, porque se puede hacer sin intervenciones en el código o las plantillas. Se define de manera elegante en un único lugar, en el router, y no está dispersa en forma de anotaciones en todos los presenters.

El Router en Nette es extraordinario porque es bidireccional. Sabe tanto decodificar URL en la petición HTTP como crear enlaces. Juega, por lo tanto, un papel fundamental en Nette Application, porque por un lado decide qué presenter y acción ejecutará la petición actual, pero también se utiliza para generar URL en la plantilla, etc.

Sin embargo, el router no está limitado solo a este uso, puede usarlo en aplicaciones donde no se usan presenters en absoluto, para API REST, etc. Más en la sección Uso independiente.

Colección de rutas

La forma más agradable de definir la apariencia de las direcciones URL en la aplicación la ofrece la clase Nette\Application\Routers\RouteList. La definición consiste en una lista de las llamadas rutas, es decir, máscaras de direcciones URL y sus presenters y acciones asociados mediante una API simple. No necesitamos nombrar las rutas de ninguna manera.

$router = new Nette\Application\Routers\RouteList;
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('article/<id>', 'Article:view');
// ...

El ejemplo dice que si abrimos https://domain.com/rss.xml en el navegador, se mostrará el presenter Feed con la acción rss, si https://domain.com/article/12, se mostrará el presenter Article con la acción view, etc. En caso de no encontrar una ruta adecuada, Nette Application reacciona lanzando una excepción BadRequestException, que se muestra al usuario como una página de error 404 Not Found.

Orden de las rutas

Es absolutamente clave el orden en que se indican las rutas individuales, porque se evalúan secuencialmente de arriba abajo. Se aplica la regla de que declaramos las rutas de específicas a generales:

// MAL: 'rss.xml' lo captura la primera ruta y entiende esta cadena como <slug>
$router->addRoute('<slug>', 'Article:view');
$router->addRoute('rss.xml', 'Feed:rss');

// BIEN
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('<slug>', 'Article:view');

Las rutas también se evalúan de arriba abajo al generar enlaces:

// MAL: el enlace a 'Feed:rss' se genera como 'admin/feed/rss'
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
$router->addRoute('rss.xml', 'Feed:rss');

// BIEN
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');

No le ocultaremos que la correcta composición de las rutas requiere cierta habilidad. Antes de que la domine, le será útil el panel de enrutamiento.

Máscara y parámetros

La máscara describe la ruta relativa desde el directorio raíz del sitio web. La máscara más simple es una URL estática:

$router->addRoute('products', 'Products:default');

A menudo, las máscaras contienen los llamados parámetros. Estos se indican entre corchetes angulares (p. ej., <year>) y se pasan al presenter de destino, por ejemplo, al método renderShow(int $year) o al parámetro persistente $year:

$router->addRoute('chronicle/<year>', 'History:show');

El ejemplo dice que si abrimos https://example.com/chronicle/2020 en el navegador, se mostrará el presenter History con la acción show y el parámetro year: 2020.

Podemos asignar un valor predeterminado a los parámetros directamente en la máscara y así se vuelven opcionales:

$router->addRoute('chronicle/<year=2020>', 'History:show');

La ruta ahora aceptará también la URL https://example.com/chronicle/, que nuevamente mostrará History:show con el parámetro year: 2020.

El parámetro puede ser, por supuesto, también el nombre del presenter y la acción. Por ejemplo, así:

$router->addRoute('<presenter>/<action>', 'Home:default');

La ruta indicada acepta, p. ej., URL en la forma /article/edit o también /catalog/list y las entiende como presenters y acciones Article:edit y Catalog:list.

Al mismo tiempo, da a los parámetros presenter y action los valores predeterminados Home y default y, por lo tanto, también son opcionales. Así que la ruta acepta también URL en la forma /article y la entiende como Article:default. O al revés, un enlace a Product:default generará la ruta /product, un enlace al predeterminado Home:default la ruta /.

La máscara puede describir no solo la ruta relativa desde el directorio raíz del sitio web, sino también la ruta absoluta, si comienza con una barra inclinada, o incluso una URL absoluta completa, si comienza con dos barras inclinadas:

// relativo al document root
$router->addRoute('<presenter>/<action>', /* ... */);

// ruta absoluta (relativa al dominio)
$router->addRoute('/<presenter>/<action>', /* ... */);

// URL absoluta incluyendo dominio (relativa al esquema)
$router->addRoute('//<lang>.example.com/<presenter>/<action>', /* ... */);

// URL absoluta incluyendo esquema
$router->addRoute('https://<lang>.example.com/<presenter>/<action>', /* ... */);

Expresiones de validación

Para cada parámetro se puede establecer una condición de validación mediante una expresión regular. Por ejemplo, para el parámetro id determinamos que solo puede tomar dígitos usando la expresión regular \d+:

$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */);

La expresión regular predeterminada para todos los parámetros es [^/]+, es decir, todo excepto la barra inclinada. Si un parámetro debe aceptar también barras inclinadas, indicamos la expresión .+:

// acepta https://example.com/a/b/c, path será 'a/b/c'
$router->addRoute('<path .+>', /* ... */);

Secuencias opcionales

En la máscara se pueden marcar partes opcionales usando corchetes. Opcional puede ser cualquier parte de la máscara, también pueden contener parámetros:

$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);

// Acepta rutas:
//    /cs/download  => lang => cs, name => download
//    /download     => lang => null, name => download

Cuando un parámetro es parte de una secuencia opcional, se vuelve, por supuesto, también opcional. Si no tiene un valor predeterminado indicado, será null.

Las partes opcionales también pueden estar en el dominio:

$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */);

Las secuencias se pueden anidar y combinar libremente:

$router->addRoute(
	'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
	'Home:default',
);

// Acepta rutas:
// 	/cs/hello
// 	/en-us/hello
// 	/hello
// 	/hello/page-12

Al generar URL, se busca la variante más corta, por lo que todo lo que se puede omitir, se omite. Por eso, por ejemplo, la ruta index[.html] genera la ruta /index. Se puede invertir el comportamiento indicando un signo de exclamación después del corchete izquierdo:

// acepta /hello y /hello.html, genera /hello
$router->addRoute('<name>[.html]', /* ... */);

// acepta /hello y /hello.html, genera /hello.html
$router->addRoute('<name>[!.html]', /* ... */);

Los parámetros opcionales (es decir, parámetros que tienen un valor predeterminado) sin corchetes se comportan básicamente como si estuvieran entre paréntesis de la siguiente manera:

$router->addRoute('<presenter=Home>/<action=default>/<id=>', /* ... */);

// corresponde a esto:
$router->addRoute('[<presenter=Home>/[<action=default>/[<id>]]]', /* ... */);

Si quisiéramos influir en el comportamiento de la barra inclinada final, para que, por ejemplo, en lugar de /home/ se genere solo /home, se puede lograr así:

$router->addRoute('[<presenter=Home>[/<action=default>[/<id>]]]', /* ... */);

Comodines

En la máscara de ruta absoluta, podemos usar los siguientes comodines y evitar así, por ejemplo, la necesidad de escribir en la máscara el dominio, que puede diferir en el entorno de desarrollo y producción:

  • %tld% = dominio de nivel superior, p. ej., comorg
  • %sld% = dominio de segundo nivel, p. ej., example
  • %domain% = dominio sin subdominios, p. ej., example.com
  • %host% = host completo, p. ej., www.example.com
  • %basePath% = ruta al directorio raíz
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action', /* ... */);

Notación extendida

El destino de la ruta, generalmente escrito en la forma Presenter:action, también puede escribirse usando un array que define los parámetros individuales y sus valores predeterminados:

$router->addRoute('<presenter>/<action>[/<id \d+>]', [
	'presenter' => 'Home',
	'action' => 'default',
]);

Para una especificación más detallada, se puede usar una forma aún más extendida, donde además de los valores predeterminados podemos establecer otras propiedades de los parámetros, como por ejemplo la expresión regular de validación (ver parámetro id):

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>[/<id>]', [
	'presenter' => [
		Route::Value => 'Home',
	],
	'action' => [
		Route::Value => 'default',
	],
	'id' => [
		Route::Pattern => '\d+',
	],
]);

Es importante señalar que si los parámetros definidos en el array no se indican en la máscara de la ruta, sus valores no se pueden cambiar, ni siquiera mediante parámetros de consulta indicados después del signo de interrogación en la URL.

Filtros y traducciones

Escribimos los códigos fuente de la aplicación en inglés, pero si el sitio web debe tener URL en español, entonces un enrutamiento simple como:

$router->addRoute('<presenter>/<action>', 'Home:default');

generará URL en inglés, como /product/123 o /cart. Si queremos que los presenters y las acciones en la URL estén representados por palabras en español (p. ej., /producto/123 o /carrito), podemos utilizar un diccionario de traducción. Para su escritura ya necesitamos la variante “más detallada” del segundo parámetro:

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => [
		Route::Value => 'Home',
		Route::FilterTable => [
			// cadena en la URL => presenter
			'producto' => 'Product',
			'carrito' => 'Cart',
			'catalogo' => 'Catalog',
		],
	],
	'action' => [
		Route::Value => 'default',
		Route::FilterTable => [
			'lista' => 'list',
		],
	],
]);

Varias claves del diccionario de traducción pueden llevar al mismo presenter. De esta manera se crean diferentes alias para él. La variante canónica (es decir, la que estará en la URL generada) se considera la última clave.

La tabla de traducción se puede usar de esta manera para cualquier parámetro. Si la traducción no existe, se toma el valor original. Este comportamiento se puede cambiar agregando Route::FilterStrict => true y la ruta rechazará la URL si el valor no está en el diccionario.

Además del diccionario de traducción en forma de array, se pueden implementar funciones de traducción propias.

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>/<id>', [
	'presenter' => [
		Route::Value => 'Home',
		Route::FilterIn => function (string $s): string { /* ... */ },
		Route::FilterOut => function (string $s): string { /* ... */ },
	],
	'action' => 'default',
	'id' => null,
]);

La función Route::FilterIn convierte entre el parámetro en la URL y la cadena que luego se pasa al presenter, la función FilterOut asegura la conversión en la dirección opuesta.

Los parámetros presenter, action y module ya tienen filtros predefinidos que convierten entre el estilo PascalCase resp. camelCase y kebab-case utilizado en la URL. El valor predeterminado de los parámetros se escribe ya en la forma transformada, por lo que, por ejemplo, en el caso del presenter escribimos <presenter=ProductEdit>, no <presenter=product-edit>.

Filtros generales

Además de los filtros destinados a parámetros específicos, también podemos definir filtros generales que reciben un array asociativo de todos los parámetros, que pueden modificar de cualquier manera y luego devolverlos. Los filtros generales los definimos bajo la clave null.

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => 'Home',
	'action' => 'default',
	null => [
		Route::FilterIn => function (array $params): array { /* ... */ },
		Route::FilterOut => function (array $params): array { /* ... */ },
	],
]);

Los filtros generales dan la posibilidad de modificar el comportamiento de la ruta de absolutamente cualquier manera. Podemos usarlos, por ejemplo, para modificar parámetros basándose en otros parámetros. Por ejemplo, la traducción de <presenter> y <action> basada en el valor actual del parámetro <lang>.

Si un parámetro tiene definido un filtro propio y al mismo tiempo existe un filtro general, se ejecuta el FilterIn propio antes del general y, a la inversa, el FilterOut general antes del propio. Es decir, dentro del filtro general, los valores de los parámetros presenter resp. action están escritos en estilo PascalCase resp. camelCase.

Rutas de un solo sentido OneWay

Las rutas de un solo sentido se utilizan para mantener la funcionalidad de las URL antiguas que la aplicación ya no genera, pero sigue aceptando. Las marcamos con el indicador OneWay:

// URL antigua /product-info?id=123
$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY);
// nueva URL /product/123
$router->addRoute('product/<id>', 'Product:detail');

Al acceder a la URL antigua, el presenter redirige automáticamente a la nueva URL, por lo que los motores de búsqueda no indexarán estas páginas dos veces (ver SEO y canonización).

Enrutamiento dinámico con callbacks

El enrutamiento dinámico con callbacks le permite asignar directamente funciones (callbacks) a las rutas, que se ejecutarán cuando se visite la ruta dada. Esta funcionalidad flexible le permite crear rápida y eficientemente diferentes puntos finales (endpoints) para su aplicación:

$router->addRoute('test', function () {
	echo 'estás en la dirección /test';
});

También puede definir parámetros en la máscara, que se pasarán automáticamente a su callback:

$router->addRoute('<lang cs|en>', function (string $lang) {
	echo match ($lang) {
		'cs' => '¡Bienvenido a la versión checa de nuestro sitio web!',
		'en' => 'Welcome to the English version of our website!',
	};
});

Módulos

Si tenemos varias rutas que pertenecen a un módulo común, utilizamos withModule():

$router = new RouteList;
$router->withModule('Forum') // las siguientes rutas son parte del módulo Forum
	->addRoute('rss', 'Feed:rss') // el presenter será Forum:Feed
	->addRoute('<presenter>/<action>')

	->withModule('Admin') // las siguientes rutas son parte del módulo Forum:Admin
		->addRoute('sign:in', 'Sign:in');

Una alternativa es usar el parámetro module:

// La URL manage/dashboard/default se mapea al presenter Admin:Dashboard
$router->addRoute('manage/<presenter>/<action>', [
	'module' => 'Admin',
]);

Subdominios

Podemos dividir las colecciones de rutas según subdominios:

$router = new RouteList;
$router->withDomain('example.com')
	->addRoute('rss', 'Feed:rss')
	->addRoute('<presenter>/<action>');

En el nombre del dominio también se pueden usar Comodines:

$router = new RouteList;
$router->withDomain('example.%tld%')
	// ...

Prefijo de ruta

Podemos dividir las colecciones de rutas según la ruta en la URL:

$router = new RouteList;
$router->withPath('eshop')
	->addRoute('rss', 'Feed:rss') // captura la URL /eshop/rss
	->addRoute('<presenter>/<action>'); // captura la URL /eshop/<presenter>/<action>

Combinaciones

Podemos combinar las divisiones anteriores entre sí:

$router = (new RouteList)
	->withDomain('admin.example.com')
		->withModule('Admin')
			->addRoute(/* ... */)
			->addRoute(/* ... */)
		->end()
		->withModule('Images')
			->addRoute(/* ... */)
		->end()
	->end()
	->withDomain('example.com')
		->withPath('export')
			->addRoute(/* ... */)
			// ...

Parámetros de consulta

Las máscaras también pueden contener parámetros de consulta (parámetros después del signo de interrogación en la URL). A estos no se les puede definir una expresión de validación, pero se puede cambiar el nombre bajo el cual se pasan al presenter:

// queremos usar el parámetro de consulta 'cat' en la aplicación bajo el nombre 'categoryId'
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);

Parámetros Foo

Ahora vamos más profundo. Los parámetros Foo son básicamente parámetros sin nombre que permiten hacer coincidir una expresión regular. Un ejemplo es una ruta que acepta /index, /index.html, /index.htm y /index.php:

$router->addRoute('index<? \.html?|\.php|>', /* ... */);

También se puede definir explícitamente la cadena que se usará al generar la URL. La cadena debe colocarse directamente después del signo de interrogación. La siguiente ruta es similar a la anterior, pero genera /index.html en lugar de /index, porque la cadena .html está configurada como valor de generación:

$router->addRoute('index<?.html \.html?|\.php|>', /* ... */);

Integración en la aplicación

Para incorporar el router creado en la aplicación, debemos decírselo al contenedor DI. La forma más fácil es preparar una fábrica que produzca el objeto router e indicar en la configuración del contenedor que debe usarla. Supongamos que para este propósito escribimos el método App\Core\RouterFactory::createRouter():

namespace App\Core;

use Nette\Application\Routers\RouteList;

class RouterFactory
{
	public static function createRouter(): RouteList
	{
		$router = new RouteList;
		$router->addRoute(/* ... */);
		return $router;
	}
}

En la configuración luego escribimos:

services:
	- App\Core\RouterFactory::createRouter

Cualquier dependencia, por ejemplo, a la base de datos, etc., se pasa al método de fábrica como sus parámetros mediante autowiring:

public static function createRouter(Nette\Database\Connection $db): RouteList
{
	// ...
}

SimpleRouter

Un router mucho más simple que la colección de rutas es SimpleRouter. Lo usamos cuando no tenemos requisitos especiales sobre la forma de la URL, si no está disponible mod_rewrite (o sus alternativas) o si aún no queremos ocuparnos de URL bonitas.

Genera direcciones aproximadamente en esta forma:

http://example.com/?presenter=Product&action=detail&id=123

El parámetro del constructor de SimpleRouter es el presenter y la acción predeterminados a los que se debe dirigir si abrimos la página sin parámetros, p. ej., http://example.com/.

// el presenter predeterminado será 'Home' y la acción 'default'
$router = new Nette\Application\Routers\SimpleRouter('Home:default');

Recomendamos definir SimpleRouter directamente en la configuración:

services:
	- Nette\Application\Routers\SimpleRouter('Home:default')

SEO y canonización

El framework contribuye al SEO (optimización para motores de búsqueda) evitando la duplicidad de contenido en diferentes URL. Si a un destino determinado conducen varias direcciones, p. ej., /index y /index.html, el framework determina la primera de ellas como primaria (canónica) y redirige las demás a ella usando el código HTTP 301. Gracias a esto, los motores de búsqueda no indexan sus páginas dos veces y no diluyen su page rank.

Este proceso se llama canonización. La URL canónica es la que genera el router, es decir, la primera ruta que cumple en la colección sin el indicador OneWay. Por eso, en la colección indicamos las rutas primarias primero.

La canonización la realiza el presenter, más en el capítulo canonización.

HTTPS

Para poder usar el protocolo HTTPS, es necesario habilitarlo en el hosting y configurar correctamente el servidor.

La redirección de todo el sitio a HTTPS debe configurarse a nivel de servidor, por ejemplo, mediante el archivo .htaccess en el directorio raíz de nuestra aplicación, y con el código HTTP 301. La configuración puede variar según el hosting y se ve aproximadamente así:

<IfModule mod_rewrite.c>
	RewriteEngine On
	...
	RewriteCond %{HTTPS} off
	RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
	...
</IfModule>

El router genera URL con el mismo protocolo con el que se cargó la página, por lo que no es necesario configurar nada más.

Pero si excepcionalmente necesitamos que diferentes rutas se ejecuten bajo diferentes protocolos, lo indicamos en la máscara de la ruta:

// Generará una dirección con HTTP
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);

// Generará una dirección con HTTPs
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);

Depuración del router

El panel de enrutamiento que se muestra en Tracy Bar es un ayudante útil que muestra la lista de rutas y también los parámetros que el router obtuvo de la URL.

La barra verde con el símbolo ✓ representa la ruta que procesó la URL actual, en color azul y con el símbolo ≈ están marcadas las rutas que también procesarían la URL si la verde no se les hubiera adelantado. Además, vemos el presenter y la acción actuales.

Al mismo tiempo, si ocurre una redirección inesperada debido a la canonización, es útil mirar el panel en la barra redirect, donde descubrirá cómo el router entendió originalmente la URL y por qué redirigió.

Al depurar el router, recomendamos abrir las Herramientas de desarrollador en el navegador (Ctrl+Shift+I o Cmd+Option+I) y en el panel Network desactivar la caché, para que no se guarden las redirecciones en ella.

Rendimiento

El número de rutas influye en la velocidad del router. Su número definitivamente no debería exceder varias decenas. Si su sitio web tiene una estructura de URL demasiado complicada, puede escribir un Router propio a medida.

Si el router no tiene dependencias, por ejemplo, a la base de datos, y su fábrica no acepta ningún argumento, podemos serializar su forma compilada directamente en el contenedor DI y así acelerar ligeramente la aplicación.

routing:
	cache: true

Router propio

Las siguientes líneas están destinadas a usuarios muy avanzados. Puede crear su propio router e integrarlo de forma completamente natural en la colección de rutas. El router es una implementación de la interfaz Nette\Routing\Router con dos métodos:

use Nette\Http\IRequest as HttpRequest;
use Nette\Http\UrlScript;

class MyRouter implements Nette\Routing\Router
{
	public function match(HttpRequest $httpRequest): ?array
	{
		// ...
	}

	public function constructUrl(array $params, UrlScript $refUrl): ?string
	{
		// ...
	}
}

El método match procesa la petición actual $httpRequest, de la cual se puede obtener no solo la URL, sino también las cabeceras, etc., en un array que contiene el nombre del presenter y sus parámetros. Si no puede procesar la petición, devuelve null. Al procesar la petición, debemos devolver como mínimo el presenter y la acción. El nombre del presenter es completo y contiene también posibles módulos:

[
	'presenter' => 'Front:Home',
	'action' => 'default',
]

El método constructUrl, por el contrario, construye la URL absoluta resultante a partir del array de parámetros. Para ello puede utilizar información del parámetro $refUrl, que es la URL actual.

Lo agrega a la colección de rutas usando add():

$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...

Uso independiente

Por uso independiente entendemos el uso de las capacidades del router en una aplicación que no utiliza Nette Application ni presenters. Se aplica casi todo lo que hemos mostrado en este capítulo, con estas diferencias:

Así que nuevamente creamos un método que nos construya el router, p. ej.:

namespace App\Core;

use Nette\Routing\RouteList;

class RouterFactory
{
	public static function createRouter(): RouteList
	{
		$router = new RouteList;
		$router->addRoute('rss.xml', [
			'controller' => 'RssFeedController',
		]);
		$router->addRoute('article/<id \d+>', [
			'controller' => 'ArticleController',
		]);
		// ...
		return $router;
	}
}

Si usa un contenedor DI, lo cual recomendamos, nuevamente agregamos el método a la configuración y luego obtenemos el router junto con la petición HTTP del contenedor:

$router = $container->getByType(Nette\Routing\Router::class);
$httpRequest = $container->getByType(Nette\Http\IRequest::class);

O fabricamos los objetos directamente:

$router = App\Core\RouterFactory::createRouter();
$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals();

Ahora solo queda poner el router a trabajar:

$params = $router->match($httpRequest);
if ($params === null) {
	// no se encontró una ruta que cumpliera, enviamos error 404
	exit;
}

// procesamos los parámetros obtenidos
$controller = $params['controller'];
// ...

Y a la inversa, usamos el router para construir un enlace:

$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());
versión: 4.0