Criar um Sistema de Rotas MVC em PHP 8 é bem interessante para sua experiência como desenvolvedor. Nesta série, aprenderemos como construir um sistema de rotas simples, usando URLs amigáveis e boas práticas de organização para desenvolver um roteamento eficiente.
URLs Amigáveis e Roteamento
As URLs amigáveis tornam a navegação mais simples. Elas eliminam parâmetros de query complexos, como index.php?prod=451
, substituindo-os por algo mais intuitivo, como produto/451
. Dessa forma, você cria um roteador que centraliza as requisições e as direciona aos controladores corretos.
Como funciona o roteamento?
O roteador centraliza todas as requisições. Como resultado, apenas um arquivo (geralmente o index.php
) lida com as entradas. O sistema verifica se a rota existe. Quando a rota é encontrada, ele chama o controlador e a ação necessários. Por outro lado, se não encontrar a rota, o sistema retorna um erro 404.
Estrutura do Projeto
Nossa estrutura de projeto será organizada da seguinte maneira:
App/ └── Controller.php src/ ├── Router.php ├── RouteCollection.php ├── Request.php └── Dispatcher.php public/ └── index.php routes/ └── routes.php .htaccess bootstrap.php
Essa estrutura garante um código modular e organizado, facilitando a manutenção e a expansão do projeto.
Começando
Primeiramente, crie a estrutura de diretórios e arquivos do projeto:
mkdir App src public routes
touch bootstrap.php .htaccess App/Controller.php
touch src/Router.php src/RouteCollection.php src/Request.php src/Dispatcher.php
touch public/index.php public/.htaccess routes/routes.php
Criando o Autoloader com Composer
O Composer será utilizado para autoloading das classes. Se você ainda não instalou o Composer, pode fazer isso acessando o site oficial do Composer e siguindo as instruções para instalaçãoEm seguida, poderá criar o arquivo composer.json
com o seguinte comando:
composer init
O Composer faz perguntas sobre o projeto. Depois disso, preencha as informações necessárias. Logo após, abra o arquivo composer.json
e adicione as linhas para configurar o autoloading:
"autoload": {
"psr-4": {
"App\\": "App/",
"Src\\": "src/"
}
}
Finalmente, execute o seguinte comando para o Composer configurar o autoloading:
composer dump-autoload
Dessa forma, o Composer estará preparado para carregar automaticamente as classes da pasta src/ e App/.
Estrutura das Classes
Agora que o autoloading está configurado, vamos criar as principais classes do nosso Sistema de Rotas MVC em PHP. Cada classe tem um papel específico dentro do sistema de roteamento. Portanto, apresento a seguir as implementações iniciais das classes que compõem o sistema de rotas.
Criando a classe Controller
A classe Controller será responsável por gerenciar as ações associadas às rotas. Dessa forma, você poderá implementar as ações do controlador, como métodos para processar requisições específicas.
declare(strict_types=1);
namespace App;
class Controller
{
// As ações do controlador serão implementadas aqui
}
Por enquanto, esta implementação simbólica será suficiente. Podemos então começar a construir a estrutura do roteamento.
Criando a classe RouteCollection
A primeira classe que tem um papel importante no rotemanto é RouteCollection. Esta classe, gerencia a coleção de configurações de rotas. Além disso, a fim de tornar mais compreensivo o processo, vamos adicionar a lógica de correspondência entre as requisições e os padrões de rotas definidos também nesta classa. Então, você pode implementar, inicialmente, dessa forma:
declare(strict_types=1);
namespace Src;
class RouteCollection
{
private array $routesPost = [];
private array $routesGet = [];
private array $routesPut = [];
private array $routesDelete = [];
public function add(string $method, string $pattern, callable|string $callback): void
{
$route = ['pattern' => $this->definePattern($pattern), 'callback' => $callback];
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.'),
};
}
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];
}
}
return null;
}
private function parseUri(string $uri): string
{
return implode('/', array_filter(explode('/', $uri)));
}
private function definePattern(string $pattern): string
{
$pattern = implode('/', array_filter(explode('/', $pattern)));
// Transforma {param} em regex
$pattern = preg_replace('/\{[a-zA-Z_]+\}/', '([a-zA-Z0-9_]+)', $pattern);
return '/^' . str_replace('/', '\/', $pattern) . '$/';
}
}
Você pode notar que a classe armazena rotas de diferentes métodos HTTP em arrays diferentes. Além disso, ela possui o match, um método que ajuda a determinar se uma requisição corresponde a uma rota válida. Esse método é o meio usado pela classe de roteamento para descobrir se há uma correspondência para rota na requisição.
RouterCollection ainda está nos seus primeiros passos e sem dúvida, irá precisar de novos métodos. Mas, por enquanto, essa implementação acima, já nos permitirá obter um resultado inicial satisfatório.
Criando a classe Request
Não faz sentido trabalharmos com uma estrutura de rotemanto baseando na POO, e não termos um objeto que represente a requisição. Por isso, estaremos implementando uma versão simplificada da classe Request. Essa classe será responsável por capturar e processar as informações da requisição HTTP, como a URI e o método HTTP utilizado. Então, você poderá implementar o código da classe Request inicialmente dessa maneira:
declare(strict_types=1);
namespace Src;
class Request
{
public static function uri(): string
{
return trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
}
public static function method(): string
{
return $_SERVER['REQUEST_METHOD'];
}
}
Observe que a classe Request nos apresenta métodos estáticos para situações quando não precisamos de uma instância dela. Mas, novos métodos serão apresentados no próximo artigo.
Criando a classe Router
Enfim chegamos a classe principal de todo sistema de Rotas MVC em PHP 8, a classe Router. Essa classe será o coração do sistema de rotas, porque que orquestra todo o processo, lendo a requisição recebida por meio da instância de Request e consultando a RouteCollection para determinar se esta rota recebida na requisição tem uma correspondência em sua coleção. Se houver, então Router fará uso da classe que veremos a seguir para invocar uma ação em um controlador. Então, você pode implementá-la inicialmente da seguinte maneira:
declare(strict_types=1);
namespace Src;
use Exception;
use Src\Request;
use Src\RouteCollection;
class Router
{
private RouteCollection $routes;
public function __construct()
{
$this->routes = new RouteCollection;
}
public function get(array|string $pattern, callable|string $callback): void
{
$this->routes->add('GET', $pattern, $callback);
}
public function post(array|string $pattern, callable|string $callback): void
{
$this->routes->add('POST', $pattern, $callback);
}
public function put(array|string $pattern, callable|string $callback): void
{
$this->routes->add('PUT', $pattern, $callback);
}
public function delete(array|string $pattern, callable|string $callback): void
{
$this->routes->add('DELETE', $pattern, $callback);
}
public function dispatch(): void
{
$callback = $this->routes->match(Request::method(), Request::uri());
if (!$callback) {
throw new Exception('404 - Not Found');
}
call_user_func($callback);
}
}
Criando a classe Dispatcher
Na etapa anterior, construímos uma versão inicial da classe Router. Agora, precisamos a classe Dispatcher que instanciará o controlador correto e chamará os métodos apropriados do controladores com base na rota mapeada nas coleções de RouterCollection. O Router orquestra a operação, interpreta a requisição e consulta a RouterCollection para verificar o match. Mas após isso, outro elemento importante será Dispacher. a classe que sabe como chegar até as ações dos controladores. Sem essas classes, Router teria muitas responsabilidades. Você pode implementar inicialmente assim:
declare(strict_types=1);
namespace Src;
use Exception;
class Dispatcher
{
public function dispatch(string $controller, string $action): void
{
$controller = "App\\Controllers\\" . $controller;
if (!method_exists($controller, $action)) {
throw new Exception('Action not found');
}
(new $controller)->$action();
}
}
Você pode estar se perguntando onde que Dispacher se encaixa em Router e esta sua dúvida sobre o DIspacher faz sentido. Mas se você observar, já temos um método na classe Router chamado dispach fazendo o papel de Dispacher. Será exatamente neste método o local onde a instância de Dispacher será invocada futuramente.
Configurando o Bootstrap
Por fim. com todas as classes necessárias construídas, precisamos de um local para definir onde carregar todas instâncias da maneira necessária. Para isso, definiremos o arquivo bootstrap.php
da seuinte maneira:
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Src\Router;
$router = new Router();
require __DIR__ . '/routes/routes.php';
$router->dispatch();
Configurando as Rotas
Finalmente estamos com toda estrutura inicial completa do sistema de Rotas MVC em PHP 8! Mas, para vermos todo código funcionando, precisamos antes configurar novas rotas.
Então, para configurar as rotas, vamos definir o arquivo routes.php
que irá centralizar todas configurações de rotas. Dessa forma, você pode implementar da seguinte forma:
declare(strict_types=1);
global $router;
$router->get('', fn() => print('Página Inicial'));
$router->get('sobre', fn() => print('Página Sobre'));
Mas como receber uma requisição? No, como vimos no início do artigo, através do arquivo index.php
. Este arquivo deve ser criado dentro do diretório public/
. Então, primeiro crie o arquivo index.php
e adicione o seuinte código:
declare(strict_types=1);
require __DIR__ . '/../bootstrap.php';
Agora, quando você fizar a chamada para o projeto, o fluxo de dados será iniciado através do index.php e passado para o sistema de roteamento. Você pode ter uma visão de como o projeto deverá se parecer ao termino das nossas implementações. Veja a seguir um diagrama de sequencia também apresentando de forma simplificada o fluxo de dados.


Explicação de como irá funcionar:
- O ator User (Usuário) faz uma requisição para o sistema.
- O arquivo index.php captura essa requisição e chama o método resolve() do Router.
- O Router cria um novo objeto Request para capturar os dados da requisição.
- O Router então pede ao RouteCollection para encontrar uma rota correspondente à URI e ao método HTTP.
- Se a rota for encontrada, o Router utiliza o Dispatcher para chamar a função de callback ou um método de um controlador, passando os parâmetros da requisição.
- Se não houver rota correspondente, é retornado um erro 404.
- Se houver uma rota correspondente, o Dispatcher chama o método do Controller que retorna a resposta ao User.
Observação: Neste ponto, dependendo da configuração do servidor web, o projeto deverá ser executado corretamente. Ao acessar o caminho raiz, você verá a mensagem ‘página inicial’. No entanto, se houver algum problema na configuração do servidor, o próximo artigo abordará as configurações necessárias para garantir a execução adequada do código.
Conclusão
Até aqui, configuramos a estrutura básica do sistema de rotas e preparamos as classes para a próxima etapa da contrução do nosso sistema de Rotas MVC em PHP 8. Por enquanto as classe Request e Dispacher tiveram apenas uma implementação inicial mas não foram usadas. Mas no próximo artigo, estas classes revelarão quão importante são neste processo. Além disso, também vamos evoluir o código com algumas refatorações a fim de preparar o sistema de rotas para o uso avançado, incluindo parâmetros dinâmicos e real chamada para os controladores. Então, por enquanto ficamos por aqui! Até o próximo artigo!