Sistema de Rotas MVC em PHP 8 – Parte 2

Na primeira parte desta série sobre Rotas MVC em PHP 8, criamos a estrutura básica e implementamos um roteador inicial. Agora, nesta segunda parte, vamos configurar o servidor para suportar URLs amigáveis e aprimorar as classes Request e Dispatcher. Caso você não tenha acompanhado a primeira parte, recomendo antes revisitar o artigos anterior da série para uma compreensão completa:

Sistema de Rotas MVC em PHP 8 – Parte 1

Configurando o Servidor Web

Para configurar o servidor e permitir o uso de URLs amigáveis, você pode optar pelo Apache ou NGINX. Primeiramente, vamos ver como fazer isso no Apache.

1. Configuração no Apache

Se você utiliza o Apache, especialmente em um ambiente local como WAMP ou em um servidor Linux, será necessário habilitar o módulo mod_rewrite para suportar URLs amigáveis.

Passo a passo para habilitar o mod_rewrite no Linux:

Para começar, execute os seguintes comandos para habilitar o módulo rewrite e configurar as permissões:

sudo a2enmod rewrite
sudo nano /etc/apache2/apache2.conf

Em seguida, dentro do arquivo de configuração, modifique a diretiva para permitir o uso de .htaccess:

<Directory /var/www/>
    AllowOverride All
</Directory>

Finalmente, após fazer a alteração, reinicie o Apache:

sudo systemctl restart apache2
No Windows (WAMP):

Se você utiliza o WAMP, então precisa habilitar o mod_rewrite editando o arquivo httpd.conf dessa maneira:

  • primeiro, abra o arquivo de configuração do Apache, localizado em httpd.conf, e habilite o módulo mod_rewrite:
LoadModule rewrite_module modules/mod_rewrite.so
  • e por fim, após salvar o arquivo, reinicie o servidor Apache pelo painel de controle do WAMP.

Criando o Arquivo .htaccess (para Apache)

Um detalhe importante para, quando você utilizar o Apache, será criar o arquivo .htaccess no diretório public/ do seu projeto. Então, crie, este arquivo para redirecionar as requisições para o index.php e garantir que as URLs amigáveis funcionem corretamente.

Então, crie o arquivo .htaccess com o seguinte conteúdo:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /rotas/public
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^([A-Za-z0-9_/\\-]+)$ index.php?uri=$1 [QSA,L]
</IfModule>

Essa configuração redirecionará todas as requisições para o arquivo index.php, a menos que um arquivo ou diretório real seja encontrado.

Agora que já abordamos o Apache, vamos ver como configurar URLs amigáveis no NGINX.

2. Configuração no NGINX

No Linux (NGINX):
  • primeiramente edite o arquivo de configuração do NGINX dessa forma:
sudo nano /etc/nginx/sites-available/default
  • depois, eentro do bloco server {}, adicione as seguintes configurações:
server {
    listen 80;
    server_name your_domain.com;
    root /caminho/para/seu/projeto/public;

    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php?uri=$uri;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}

Finalmente, reinicie o NGINX para aplicar as novas configurações utilizando esse comando:

sudo systemctl restart nginx

No NGINX, o arquivo .htaccess, que configuramos para apache, não é necessário porque todas as regras de redirecionamento e manipulação de URLs são centralizadas no próprio arquivo de configuração do servidor.

Medidas de Segurança

Agora que as configurações principais foram feitas, vamos adicionar uma camada extra de segurança, impedindo a indexação de diretórios.

No Apache:

Crie um arquivo .htaccess no diretório raiz do projeto e adicione a linha abaixo:

Options -Indexes
No NGINX:

Adicione a seguinte configuração ao arquivo do servidor:

location ~ /\.ht {
    deny all;
}

Tanto no Apache quanto no NGINX, essas medidas garantem que os diretórios e arquivos ocultos, como .htaccess, não sejam acessíveis.

Refatorando a Classe Request

Agora que o servidor está configurado corretamente, então podemos avançar e partir para as refatorações. Vamos começar com a classe Request. Essa classe precisa capturar adequadamente os dados enviados via requisição, então ajuste-a da seguinte maneira:

declare(strict_types=1);

namespace Src;

class Request
{
    private string $uri;
    private string $method;
    private array $data = [];
    private array $files = [];

    public function __construct()
    {
        $this->uri = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
        $this->method = strtolower($_SERVER['REQUEST_METHOD']);
        $this->setData();
        $this->setFiles();
    }

    private function setData(): void
    {
        $this->data = match ($this->method) {
            'post' => $_POST,
            'get' => $_GET,
            'put', 'delete', 'patch' => json_decode(file_get_contents('php://input'), true) ?? [],
            default => []
        };
    }

    private function setFiles(): void
    {
        $this->files = $_FILES ?? [];
    }

    public function uri(): string
    {
        return $this->uri;
    }

    public function method(): string
    {
        return $this->method;
    }

    public function all(): array
    {
        return $this->data;
    }

    public function hasFile(string $key): bool
    {
        return isset($this->files[$key]);
    }

    public function file(string $key): array|null
    {
        return $this->files[$key] ?? null;
    }
}

Observe que agora essa classe captura e organiza os dados enviados com a requisição, sejam eles via POST, GET, ou arquivos, e trata outros métodos HTTP como PUT e DELETE.

Refatorando a Classe Dispatcher

A classe Dispacher deve serr inteligente o suficiente para lidar com os controladores de maneira correta. Então, nessa etapa, vamos refatorar a classe Dispatcher para garantir que o controlador correto seja chamado com base na rota definida.

declare(strict_types=1);

namespace Src;

use Exception;

class Dispatcher
{
    public function dispatch(string|callable $callback, array $params = [], string $namespace = "App\\"): mixed
    {
        if (is_callable($callback)) {
            return call_user_func_array($callback, array_values($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], array_values($params));
    }
}

Finalizando essa classe, agora ele trata as rotas que chamam métodos de controladores, verificando se o método e a classe existem antes de invocá-los.

Atualizando Router

A classe de roteamento é o coração do nosso projeto e por isso, precisamos adicionar uma melhoria na resolução de rotas e o despacho das solicitações ao controlador correto na classe Router. Então, faça o seguinte ajuste:

declare(strict_types=1);

namespace Src;

use Src\Request;
use Src\Dispatcher;

class Router
{
    public function __construct(
        private RouteCollection $routes,
        private Dispatcher $dispatcher
    ) {}

    public function get(string $pattern, callable|string $callback): void
    {
        $this->routes->add('GET', $pattern, $callback);
    }

    public function post(string $pattern, callable|string $callback): void
    {
        $this->routes->add('POST', $pattern, $callback);
    }

    private function notFound(): void
    {
        header("HTTP/1.0 404 Not Found");
        echo "404 - Página não encontrada";
    }

    public function resolve(Request $request): void
    {
        $route = $this->routes->match($request->method(), $request->uri());
        if ($route === null) {
            $this->notFound();
            return;
        }

        $this->dispatch($route);
    }

    protected function dispatch(object $route, string $namespace = "App\\"): mixed
    {
        return $this->dispatcher->dispatch($route->callback, $route->uri, $namespace);
    }
}

Pronto! Mas agora, será necessário garantir a intanciação dos objetos de forma correta. Faça também esse ajuste no arquivo bootstrap.php para isso:

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Src\Dispatcher;
use Src\RouteCollection;
use Src\Router;

$routes = new RouteCollection;
$dispatcher = new Dispatcher;
$router = new Router($routes, $dispatcher);
require __DIR__ . '/routes/routes.php';

Testando o Sistema de Rotas

Veja que o sistema de rotas foi atualizado, então agora você precisa editar o arquivo index.php em public/ para resolver as rotas da maneira correta.

declare(strict_types=1);

require __DIR__ . '/../bootstrap.php';

use Src\Request;

$request = new Request;
$router->resolve($request);

Por fim, crie algumas rotas no arquivo routes.php:

declare(strict_types=1);

global $router;

$router->get('', fn() => print('Página Inicial'));
$router->get('sobre', fn() => print('Página Sobre'));
$router->get('/contatos/telefones', 'Controller@getTelefones');
$router->post('/contatos/store', 'Controller@store');

Você pode observar que agora quando uma rota nova é invocada e o controller não existe ou não possui o método, uma exceção deve ser lançada. Mas se o método for adicionado no controller apenas com finalidade de teste, o controller deve responder corretamente.
Para finalizar, adicione o controller de exemplo:

declare(strict_types=1);

namespace App;

class Controller
{
    public function getTelefones(): void
    {
        echo json_encode([
           'telefones' => [
               '11-4321-1234'
           ]
        ]);
    }
}

Conclusão

Com essas melhorias, agora você terá um Sistema de Rotas MVC em PHP 8, mais robusto e que será capaz de suportar URLs amigáveis, invocar controladores e métodos de forma dinâmica. Vamos parar por aqui por hora mas no próximo artigo, vamos integrar controladores reais e trabalhar com rotas mais complexas. Continue acompanhando!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Rolar para cima