Roteamento

O Roteador cuida de tudo relacionado aos endereços URL, para que você não precise mais pensar neles. Vamos mostrar:

  • como configurar o roteador para que as URLs fiquem como desejado
  • falaremos sobre SEO e redirecionamento
  • e mostraremos como escrever seu próprio roteador

URLs mais amigáveis (ou também cool ou pretty URLs) são mais usáveis, memoráveis e contribuem positivamente para o SEO. O Nette pensa nisso e atende plenamente aos desenvolvedores. Você pode projetar para sua aplicação exatamente a estrutura de URLs que desejar. Você pode até projetá-la quando a aplicação já estiver pronta, pois isso pode ser feito sem intervenções no código ou nos templates. É definido de forma elegante em um único local, no roteador, e não está espalhado na forma de anotações em todos os presenters.

O Roteador no Nette é extraordinário por ser bidirecional. Ele pode tanto decodificar URLs na requisição HTTP quanto criar links. Portanto, desempenha um papel crucial na Nette Application, pois decide qual presenter e ação executará a requisição atual, mas também é usado para gerar URLs no template, etc.

No entanto, o roteador não está limitado apenas a este uso, você pode usá-lo em aplicações onde presenters não são usados de forma alguma, para APIs REST, etc. Mais na seção samostatné použití.

Coleção de rotas

A maneira mais agradável de definir a aparência das URLs na aplicação é oferecida pela classe Nette\Application\Routers\RouteList. A definição consiste em uma lista das chamadas rotas, ou seja, máscaras de URLs e seus presenters e ações associados por meio de uma API simples. Não precisamos nomear as rotas de forma alguma.

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

O exemplo diz que se abrirmos https://domain.com/rss.xml no navegador, o presenter Feed com a ação rss será exibido, se https://domain.com/article/12, o presenter Article com a ação view será exibido, etc. No caso de não encontrar uma rota adequada, a Nette Application reage lançando a exceção BadRequestException, que é exibida ao usuário como uma página de erro 404 Not Found.

Ordem das rotas

ordem em que as rotas individuais são listadas é absolutamente crucial, pois elas são avaliadas sequencialmente de cima para baixo. A regra é que declaramos as rotas das mais específicas para as mais gerais:

// ERRADO: 'rss.xml' é capturado pela primeira rota e entende esta string como <slug>
$router->addRoute('<slug>', 'Article:view');
$router->addRoute('rss.xml', 'Feed:rss');

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

As rotas também são avaliadas de cima para baixo ao gerar links:

// ERRADO: link para 'Feed:rss' gera como 'admin/feed/rss'
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
$router->addRoute('rss.xml', 'Feed:rss');

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

Não esconderemos de você que a montagem correta das rotas requer alguma habilidade. Antes de dominá-la, o painel de roteamento será um auxiliar útil.

Máscara e parâmetros

A máscara descreve o caminho relativo a partir do diretório raiz da web. A máscara mais simples é uma URL estática:

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

Frequentemente, as máscaras contêm os chamados parâmetros. Eles são indicados entre colchetes angulares (por exemplo, <year>) e são passados para o presenter de destino, por exemplo, para o método renderShow(int $year) ou para o parâmetro persistente $year:

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

O exemplo diz que se abrirmos https://example.com/chronicle/2020 no navegador, o presenter History com a ação show e o parâmetro year: 2020 será exibido.

Podemos definir um valor padrão para os parâmetros diretamente na máscara, tornando-os opcionais:

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

A rota agora também aceitará a URL https://example.com/chronicle/, que novamente exibirá History:show com o parâmetro year: 2020.

O parâmetro também pode ser, obviamente, o nome do presenter e da ação. Por exemplo, assim:

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

A rota especificada aceita, por exemplo, URLs no formato /article/edit ou também /catalog/list e as entende como presenters e ações Article:edit e Catalog:list.

Ao mesmo tempo, ela atribui aos parâmetros presenter e action os valores padrão Home e default, tornando-os também opcionais. Portanto, a rota também aceita URLs no formato /article e a entende como Article:default. Ou vice-versa, um link para Product:default gerará o caminho /product, um link para o padrão Home:default o caminho /.

A máscara pode descrever não apenas o caminho relativo a partir do diretório raiz da web, mas também o caminho absoluto, se começar com uma barra, ou até mesmo a URL absoluta inteira, se começar com duas barras:

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

// caminho absoluto (relativo ao domínio)
$router->addRoute('/<presenter>/<action>', /* ... */);

// URL absoluta incluindo domínio (relativa ao esquema)
$router->addRoute('//<lang>.example.com/<presenter>/<action>', /* ... */);

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

Expressões de validação

Para cada parâmetro, pode-se estabelecer uma condição de validação usando uma expressão regular. Por exemplo, para o parâmetro id, determinamos que ele só pode conter dígitos usando a regex \d+:

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

A expressão regular padrão para todos os parâmetros é [^/]+, ou seja, tudo exceto a barra. Se um parâmetro precisar aceitar também barras, especificamos a expressão .+:

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

Sequências opcionais

Na máscara, partes opcionais podem ser marcadas usando colchetes. Qualquer parte da máscara pode ser opcional, e elas também podem conter parâmetros:

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

// Aceita caminhos:
//    /pt/download  => lang => pt, name => download
//    /download     => lang => null, name => download

Quando um parâmetro faz parte de uma sequência opcional, ele obviamente também se torna opcional. Se não tiver um valor padrão especificado, será null.

Partes opcionais também podem estar no domínio:

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

As sequências podem ser aninhadas e combinadas livremente:

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

// Aceita caminhos:
// 	/pt/ola
// 	/en-us/ola
// 	/ola
// 	/ola/page-12

Ao gerar URLs, busca-se a variante mais curta, então tudo que pode ser omitido, é omitido. Por isso, por exemplo, a rota index[.html] gera o caminho /index. É possível reverter o comportamento especificando um ponto de exclamação após o colchete esquerdo:

// aceita /ola e /ola.html, gera /ola
$router->addRoute('<name>[.html]', /* ... */);

// aceita /ola e /ola.html, gera /ola.html
$router->addRoute('<name>[!.html]', /* ... */);

Parâmetros opcionais (ou seja, parâmetros com valor padrão) sem colchetes se comportam basicamente como se estivessem entre colchetes da seguinte forma:

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

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

Se quiséssemos influenciar o comportamento da barra final, para que, por exemplo, em vez de /home/ fosse gerado apenas /home, poderíamos fazer isso assim:

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

Caracteres curinga

Na máscara de caminho absoluto, podemos usar os seguintes caracteres curinga para evitar, por exemplo, a necessidade de escrever o domínio na máscara, que pode diferir entre os ambientes de desenvolvimento e produção:

  • %tld% = top level domain, por exemplo, com ou org
  • %sld% = second level domain, por exemplo, example
  • %domain% = domínio sem subdomínios, por exemplo, example.com
  • %host% = host completo, por exemplo, www.example.com
  • %basePath% = caminho para o diretório raiz
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action', /* ... */);

Notação estendida

O destino da rota, geralmente escrito na forma Presenter:action, também pode ser escrito usando um array que define os parâmetros individuais e seus valores padrão:

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

Para uma especificação mais detalhada, pode-se usar uma forma ainda mais estendida, onde, além dos valores padrão, podemos definir outras propriedades dos parâmetros, como uma expressão regular de validação (veja o parâmetro id):

use Nette\Routing\Route;

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

É importante notar que se os parâmetros definidos no array não estiverem listados na máscara do caminho, seus valores não podem ser alterados, nem mesmo usando parâmetros de consulta especificados após o ponto de interrogação na URL.

Filtros e traduções

Escrevemos o código-fonte da aplicação em inglês, mas se o site precisar ter URLs em português, então um roteamento simples do tipo:

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

gerará URLs em inglês, como /product/123 ou /cart. Se quisermos ter presenters e ações na URL representados por palavras em português (por exemplo, /produto/123 ou /carrinho), podemos usar um dicionário de tradução. Para escrevê-lo, já precisamos da variante “mais verbosa” do segundo parâmetro:

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => [
		Route::Value => 'Home',
		Route::FilterTable => [
			// string na URL => presenter
			'produto' => 'Product',
			'carrinho' => 'Cart',
			'catalogo' => 'Catalog',
		],
	],
	'action' => [
		Route::Value => 'default',
		Route::FilterTable => [
			'lista' => 'list',
		],
	],
]);

Várias chaves do dicionário de tradução podem levar ao mesmo presenter. Assim, diferentes aliases são criados para ele. A variante canônica (ou seja, aquela que estará na URL gerada) é considerada a última chave.

A tabela de tradução pode ser usada desta forma para qualquer parâmetro. Se a tradução não existir, o valor original é usado. Podemos alterar esse comportamento adicionando Route::FilterStrict => true, e a rota então rejeitará a URL se o valor não estiver no dicionário.

Além do dicionário de tradução na forma de array, também é possível aplicar funções de tradução personalizadas.

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,
]);

A função Route::FilterIn converte entre o parâmetro na URL e a string que é então passada para o presenter, a função FilterOut garante a conversão na direção oposta.

Os parâmetros presenter, action e module já possuem filtros predefinidos que convertem entre o estilo PascalCase ou camelCase e o kebab-case usado na URL. O valor padrão dos parâmetros já é escrito na forma transformada, então, por exemplo, no caso do presenter, escrevemos <presenter=ProductEdit>, não <presenter=product-edit>.

Filtros gerais

Além dos filtros destinados a parâmetros específicos, também podemos definir filtros gerais que recebem um array associativo de todos os parâmetros, que podem modificar de qualquer forma e depois retorná-los. Definimos filtros gerais sob a chave 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 { /* ... */ },
	],
]);

Filtros gerais oferecem a possibilidade de ajustar o comportamento da rota de absolutamente qualquer maneira. Podemos usá-los, por exemplo, para modificar parâmetros com base em outros parâmetros. Por exemplo, traduzir <presenter> e <action> com base no valor atual do parâmetro <lang>.

Se um parâmetro tiver um filtro próprio definido e, ao mesmo tempo, existir um filtro geral, o FilterIn próprio será executado antes do geral e, inversamente, o FilterOut geral antes do próprio. Ou seja, dentro do filtro geral, os valores dos parâmetros presenter ou action estão escritos no estilo PascalCase ou camelCase.

Rotas de sentido único (OneWay)

Rotas de sentido único são usadas para preservar a funcionalidade de URLs antigas que a aplicação não gera mais, mas ainda aceita. Nós as marcamos com o sinalizador OneWay:

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

Ao acessar a URL antiga, o presenter redireciona automaticamente para a nova URL, para que os motores de busca não indexem essas páginas duas vezes (veja SEO e canonização).

Roteamento dinâmico com callbacks

O roteamento dinâmico com callbacks permite atribuir diretamente funções (callbacks) às rotas, que são executadas quando o caminho correspondente é visitado. Esta funcionalidade flexível permite criar rápida e eficientemente vários endpoints para a sua aplicação:

$router->addRoute('test', function () {
	echo 'você está no endereço /test';
});

Você também pode definir parâmetros na máscara, que são passados automaticamente para o seu callback:

$router->addRoute('<lang pt|en>', function (string $lang) {
	echo match ($lang) {
		'pt' => 'Bem-vindo à versão em português do nosso site!',
		'en' => 'Welcome to the English version of our website!',
	};
});

Módulos

Se tivermos várias rotas que pertencem a um módulo comum, usamos withModule():

$router = new RouteList;
$router->withModule('Forum') // as rotas seguintes fazem parte do módulo Forum
	->addRoute('rss', 'Feed:rss') // o presenter será Forum:Feed
	->addRoute('<presenter>/<action>')

	->withModule('Admin') // as rotas seguintes fazem parte do módulo Forum:Admin
		->addRoute('sign:in', 'Sign:in');

Uma alternativa é usar o parâmetro module:

// URL manage/dashboard/default mapeia para o presenter Admin:Dashboard
$router->addRoute('manage/<presenter>/<action>', [
	'module' => 'Admin',
]);

Subdomínios

Podemos agrupar coleções de rotas por subdomínios:

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

No nome do domínio, também é possível usar zástupné znaky:

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

Prefixo de caminho

Podemos agrupar coleções de rotas pelo caminho na URL:

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

Combinações

Podemos combinar as agrupações acima:

$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 (Query)

As máscaras também podem conter parâmetros de consulta (parâmetros após o ponto de interrogação na URL). Não é possível definir uma expressão de validação para eles, mas pode-se alterar o nome sob o qual são passados para o presenter:

// queremos usar o parâmetro de consulta 'cat' na aplicação com o nome 'categoryId'
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);

Parâmetros Foo

Agora estamos indo mais a fundo. Parâmetros Foo são basicamente parâmetros sem nome que permitem corresponder a uma expressão regular. Um exemplo é uma rota que aceita /index, /index.html, /index.htm e /index.php:

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

Também é possível definir explicitamente a string que será usada ao gerar a URL. A string deve ser colocada diretamente após o ponto de interrogação. A seguinte rota é semelhante à anterior, mas gera /index.html em vez de /index, porque a string .html está definida como o valor de geração:

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

Integração na aplicação

Para integrar o roteador criado na aplicação, precisamos informar o Contêiner de DI sobre ele. O caminho mais fácil é preparar uma fábrica que produzirá o objeto roteador e informar na configuração do contêiner que ele deve usá-la. Digamos que, para esse fim, escrevamos o 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;
	}
}

Na configuração, então escrevemos:

services:
	- App\Core\RouterFactory::createRouter

Quaisquer dependências, como banco de dados, etc., são passadas para o método de fábrica como seus parâmetros usando autowiring:

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

SimpleRouter

Um roteador muito mais simples do que a coleção de rotas é o SimpleRouter. Usamo-lo quando não temos requisitos especiais para a forma da URL, se mod_rewrite (ou suas alternativas) não estiver disponível, ou se ainda não quisermos lidar com URLs bonitas.

Ele gera endereços aproximadamente neste formato:

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

O parâmetro do construtor SimpleRouter é o presenter & ação padrão para o qual deve ser direcionado se abrirmos a página sem parâmetros, por exemplo, http://example.com/.

// o presenter padrão será 'Home' e a ação 'default'
$router = new Nette\Application\Routers\SimpleRouter('Home:default');

Recomendamos definir o SimpleRouter diretamente na configuração:

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

SEO e canonização

O framework contribui para o SEO (otimização para motores de busca) ao impedir a duplicação de conteúdo em URLs diferentes. Se houver vários endereços que levam ao mesmo destino, por exemplo, /index e /index.html, o framework determina o primeiro deles como primário (canônico) e redireciona os outros para ele usando o código HTTP 301. Graças a isso, os motores de busca não indexam suas páginas duas vezes e não diluem seu page rank.

Este processo é chamado de canonização. A URL canônica é aquela gerada pelo roteador, ou seja, a primeira rota correspondente na coleção sem o sinalizador OneWay. Por isso, na coleção, listamos as rotas primárias primeiro.

A canonização é realizada pelo presenter, mais no capítulo canonização.

HTTPS

Para usar o protocolo HTTPS, é necessário habilitá-lo na hospedagem e configurar corretamente o servidor.

O redirecionamento de todo o site para HTTPS deve ser configurado no nível do servidor, por exemplo, usando o arquivo .htaccess no diretório raiz da nossa aplicação, com o código HTTP 301. A configuração pode variar dependendo da hospedagem e se parece aproximadamente com isto:

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

O roteador gera URLs com o mesmo protocolo com que a página foi carregada, então nada mais precisa ser configurado.

No entanto, se excepcionalmente precisarmos que rotas diferentes sejam executadas sob protocolos diferentes, especificamos isso na máscara da rota:

// Gerará endereço com HTTP
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);

// Gerará endereço com HTTPS
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);

Depuração do roteador

O painel de roteamento exibido na Barra Tracy é um auxiliar útil que exibe a lista de rotas e também os parâmetros que o roteador obteve da URL.

A barra verde com o símbolo ✓ representa a rota que processou a URL atual, a cor azul e o símbolo ≈ indicam rotas que também processariam a URL se a verde não as tivesse precedido. Em seguida, vemos o presenter & ação atuais.

Ao mesmo tempo, se ocorrer um redirecionamento inesperado devido à canonização, é útil olhar para o painel na barra redirect, onde você descobrirá como o roteador entendeu originalmente a URL e por que redirecionou.

Ao depurar o roteador, recomendamos abrir as Ferramentas do Desenvolvedor no navegador (Ctrl+Shift+I ou Cmd+Option+I) e desativar o cache no painel Network, para que os redirecionamentos não sejam armazenados nele.

Desempenho

O número de rotas afeta a velocidade do roteador. Seu número definitivamente não deve exceder algumas dezenas. Se o seu site tiver uma estrutura de URL muito complicada, você pode escrever um vlastní router personalizado.

Se o roteador não tiver dependências, por exemplo, no banco de dados, e sua fábrica não aceitar argumentos, podemos serializar sua forma compilada diretamente no Contêiner de DI e, assim, acelerar ligeiramente a aplicação.

routing:
	cache: true

Roteador personalizado

As linhas a seguir são destinadas a usuários muito avançados. Você pode criar seu próprio roteador e integrá-lo naturalmente à coleção de rotas. O Roteador é uma implementação da interface Nette\Routing\Router com dois 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
	{
		// ...
	}
}

O método match processa a requisição atual $httpRequest, da qual é possível obter não apenas a URL, mas também cabeçalhos, etc., em um array contendo o nome do presenter e seus parâmetros. Se não puder processar a requisição, retorna null. Ao processar a requisição, devemos retornar pelo menos o presenter e a ação. O nome do presenter é completo e contém também eventuais módulos:

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

O método constructUrl, por outro lado, monta a URL absoluta final a partir do array de parâmetros. Para isso, pode usar informações do parâmetro $refUrl, que é a URL atual.

Você o adiciona à coleção de rotas usando add():

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

Uso independente

Por uso independente, entendemos a utilização das capacidades do roteador em uma aplicação que não utiliza Nette Application e presenters. Quase tudo o que mostramos neste capítulo se aplica a ele, com estas diferenças:

Então, novamente, criamos um método que montará o roteador para nós, por exemplo:

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;
	}
}

Se você usa um Contêiner de DI, o que recomendamos, adicionamos novamente o método à configuração e, em seguida, obtemos o roteador juntamente com a requisição HTTP do contêiner:

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

Ou fabricamos os objetos diretamente:

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

Agora resta apenas colocar o roteador para trabalhar:

$params = $router->match($httpRequest);
if ($params === null) {
	// não foi encontrada uma rota correspondente, enviamos erro 404
	exit;
}

// processamos os parâmetros obtidos
$controller = $params['controller'];
// ...

E, inversamente, usamos o roteador para montar um link:

$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());
versão: 4.0