Na terceira parte desta série sobre a construção de um Sistema de Rotas MVC em PHP 8, estaremos concluindo a implementação, que envolve, implementar a rotas dinâmicas permitindo a passagem de parâmetros. Além disso, refinaremos a classe RouteCollection
, para dar suporte para rotas nomeadas e parâmetros dinâmicos, bem como a classe Dispatcher,
para direcionar corretamente os pedidos para as ações dos controladores . Porém, caso você não tenha acompanhado as partes anteriores, recomendo ler os artigos anteriores da série para uma compreensão completa:
Parte 2: Configurando o Sistema de Rotas
Parte 1: Introdução ao MVC em PHP 8
Refatoração na Classe RouteCollection
Primeiramente, vamos adicionar suporte para rotas dinâmicas e nomeadas, permitindo o uso de padrões como {id}
e expressões regulares para capturar variáveis diretamente da URL.
Ajuste para Capturar Parâmetros nas Rotas
O primeiro ajuste necessário, envolve adicionar um método toMap
que será responsável por capturar e mapear parâmetros das rotas. Portanto, você poderá implementá-lo da seguinte maneira:
declare(strict_types=1);
namespace Src;
class RouteCollection {
// Propriedades e métodos já existentes...
protected function toMap(string $pattern): array
{
$result = [];
$patternParts = array_filter(explode('/', $pattern));
foreach ($patternParts as $key => $part) {
if (str_starts_with($part, '{') && str_ends_with($part, '}')) {
$paramName = trim($part, '{}');
$result[$paramName] = $key;
}
}
return $result;
}
}
Esse método identificará os parâmetros nas rotas, como por exemplo, {id}
, e os armazena em um array associativo, dessa forma, facilitando o tratamento desses valores no código.
Atualizando o Método `add`
O método Add deve ser capaz de salvar rotas nomeadas na coleção de rotas. Com isso, utilizaremos um array associativo que mapeará os nomes de cada rota para seu padrão correspondente. Então, para isso, adicione a seguinte propriedade à sua classe:
private array $namedRoutes = [];
Depois disso, implemente o método add
para capturar e armazenar rotas nomeadas dessa forma:
public function add(string $method, array|string $pattern, callable|string $callback): void
{
$route = [
'pattern' => $this->definePattern($pattern['set'] ?? $pattern),
'callback' => $callback,
'parameters' => $this->toMap($pattern['set'] ?? $pattern),
];
if (isset($pattern['as'])) {
$this->namedRoutes[$pattern['as']] = $route['pattern'];
}
array_shift($matches);
match (strtolower($method)) {
'get' => $this->routesGet[] = $route,
'post' => $this->routesPost[] = $route,
'put' => $this->routesPut[] = $route,
'delete' => $this->routesDelete[] = $route,
default => throw new \Exception('Método HTTP não suportado.'),
};
}
Finalmente, observe que o sistema armazenará o padrão de rota junto com os parâmetros capturados. Portanto, dessa forma, o sistema permitirá que as rotas tenham seus nomes amigáveis para serem referenciadas no código.
Método isThereAnyHow
O método isThereAnyHow
verifica se uma rota nomeada existe, e ele será importante durante a consulta da coleção. Então, você poderá implementá-lo da seguinte maneira:
public function isThereAnyHow(string $name): ?string {
return $this->namedRoutes[$name] ?? null;
}
Ele busca o nome da rota no array namedRoutes
, retornando o padrão correspondente, se encontrado.
Método convert
O método convert
substituirrá os parâmetros dinâmicos das rota por valores fornecidos na requisição. Com isso, ele precisa ser implementado da seguinte maneira:
public function convert(string $pattern, array $params): string {
foreach ($params as $key => $value) {
$pattern = str_replace('{' . $key . '}', $value, $pattern);
}
return $pattern;
}
Note que este método substituirá os marcadores, como por exemplo, o {id}
, pelos valores fornecidos no array $params
.
Melhorias na Classe Router
Agora, é necessário ajustar a classe Router para processar adequadamente as rotas com parâmetros dinâmicos. Portanto, vamos adicionar o suporte para mapear esses parâmetros, garantindo que o sistema lide corretamente com as variações nas URLs e facilite o roteamento dinâmico.
Atualizando o Método resolve
Primeiramente, precisamos modificar o método resolve
para capturar os parâmetros e passá-los para o Dispatcher. Então, é exatamente isso que faremos aqui:
public function resolve(Request $request): void
{
$route = $this->routes->match($request->method(), $request->uri());
if ($route) {
$params = array_combine(array_keys($route->parameters), $route->uri);
$this->dispatcher->dispatch($route->callback, $params);
return;
}
$this->notFound();
}
Agora, o código passa os parâmetros capturados na URL diretamente para o método de callback definido na rota.
Adicionando Suporte para Rotas Nomeadas
Finalmente, vamos adicionar suporte para rotas nomeadas, permitindo que você as referencie diretamente em controladores ou views. Isso facilita a manutenção e reutilização das rotas no código.
Método `translate` para Rotas Nomeadas
Para isso, precisamos do método translate
no Router que deverá gerar URLs a partir de rotas nomeadas. Então, implemente-o da seguinte maneira:
public function translate(string $name, array $params = []): string {
$pattern = $this->routes->isThereAnyHow($name);
if ($pattern) {
return $this->routes->convert($pattern, $params);
}
throw new \Exception("Rota nomeada '{$name}' não encontrada.");
}
Esse método busca uma rota nomeada e substitui os parâmetros fornecidos na URL.
Necessário ajuste no método match:
public function match(string $method, string $uri): ?object
{
$routes = match (strtolower($method)) {
'get' => $this->routesGet,
'post' => $this->routesPost,
'put' => $this->routesPut,
'delete' => $this->routesDelete,
default => throw new \Exception('Método HTTP não suportado.'),
};
$parsedUri = $this->parseUri($uri);
foreach ($routes as $route) {
if (preg_match($route['pattern'], $parsedUri, $matches)) {
return (object)[
'callback' => $route['callback'],
'uri' => $matches,
'parameters' => $route['parameters'] ?? []
];
}
}
return null;
}
Pequeno Ajuste na Classe Dispatcher
Por fim, vamos garantir que o Dispatcher consiga lidar corretamente com parâmetros dinâmicos:
declare(strict_types=1);
namespace Src;
class Dispatcher {
public function dispatch(string|callable $callback, array $params = [], string $namespace = "App\\"): mixed
{
if (is_callable($callback)) {
return call_user_func_array($callback, $params);
}
[$controller, $method] = explode('@', $callback);
$controller = $namespace . $controller;
if (!class_exists($controller)) {
throw new Exception("Controller $controller não encontrado.");
}
$controllerInstance = new $controller;
if (!method_exists($controllerInstance, $method)) {
throw new Exception("Método $method não encontrado em $controller.");
}
return call_user_func_array([$controllerInstance, $method], $params);
}
}
Testando a Implementação
Chegou o momento de testarmos toda implementação. Então, primeiramente, crie uma rota nomeada em routes.php
para testar nossa implementação da seguinte maneira:
$router->get(['set' => 'cliente/{id}', 'as' => 'cliente.show'], "ClienteController@show");
$router->get('home', fn() => echo '<a href="' . $router->translate('cliente.show', ['id' => 1]) . '">Ver Cliente</a>');
Conclusão
Agora, temos um Sistema de Rotas MVC completo e moderno, utilizando PHP 8.3 com boas práticas como tipagem forte, sem uso de else
, e suporte para rotas dinâmicas e nomeadas. Esse sistema oferece flexibilidade e clareza no desenvolvimento, facilitando a manutenção e expansão do projeto.