Fazendo um router na mão em PHP - continuação das postagens: caso você precise programar em PHP e não saiba MVC
Previamente:
Você provavelmente já deve saber o que é Request e o que é Response, mas caso você não saiba:
Request (Requisição):
É como se você pedisse um lanche no balcão. O pedido que você faz é o request. Ele contém a informação do que você quer. O cliente, que no nosso caso é o navegador, faz um pedido para o servidor.
Response (Resposta):
É o lanche que você recebe de volta. O vendedor (servidor) processou o seu pedido e te entrega o que você pediu ou uma mensagem de que algo deu errado (como "não temos esse ingrediente", que seria o equivalente a um erro como 404.
sequenceDiagram
participant Cliente as 🧑💻 Cliente (Navegador)
participant Servidor as 💻 Servidor (Aplicação)
participant Rota as 🛣️ Rota / Controller
Cliente->>Servidor: Request (GET /lanche)
note right of Cliente: O cliente faz um pedido (requisição)
Servidor->>Rota: Verifica rota /lanche
note right of Servidor: O servidor encaminha o pedido<br/>para a rota correta
Rota->>Servidor: Retorna dados (por ex: JSON com o lanche)
note right of Rota: A rota processa e devolve uma resposta
Servidor->>Cliente: Response (200 OK + Lanche 🍔)
note right of Servidor: O servidor envia a resposta<br/>para o cliente com o resultado
note over Cliente,Servidor: Se algo der errado → Response 404 ou 500
Na prática
Request:
Vamos criar uma classe para representar essa requisição do usuário com os dados que vem dessa requisição, como os parametrôs da URL, os headers...
Queremos que nosso usuário acesse URL amig
namespace App\Http;
class Request
{
private array $getParams;
private array $postParams;
private string $body;
private array $headers;
private string $uri;
private string $method;
public function __construct()
{
$this->getParams = $_GET ?? [];
$this->postParams = $_POST ?? [];
$this->body = json_decode(file_get_contents("php://input"),true) ?? "";
$this->headers = getallheaders();
$this->uri = $_SERVER["REQUEST_URI"];
$this->method = $_SERVER["REQUEST_METHOD"];
}
Response:
A nossa resposta é quase como se fosse uma requisição inversa, vamos ter nossos pŕoprios headers, com o conteúdo da resposta e o código HTTP:
<?php
namespace App\Http;
class Response
{
const CONTENT_TYPE_HTTML = "text/html";
const CONTENT_TYPE_JSON = "application/json";
private array $headers;
private int $httpCode;
private string $content;
private string $contentType;
public function __construct(string $content, int $httpCode = 200,string $contentType = "text/html")
{
$this->content = $content;
$this->contentType = $contentType;
$this->setHeaders("Content-Type",$contentType);
$this->setHttpCode($httpCode);
}
public function setHeaders(string $key, string $value):void
{
$this->headers[$key] = $value;
header("$key: $value");
}
public function response():void
{
echo $this->content;
exit;
}
private function setHttpCode(int $httpCode):void
{
$this->httpCode = $httpCode;
http_response_code($httpCode);
}
}
Um Router 🐷 mas fácil de entender
Vou trazer para vocês agora, um router funcional, sem usar REGEX, o que nos limita em muito e deixa tudo mais quebravel
Queremos usar nosso router de uma maneira bem parecida com o laravel, então vamos deixar assim:
$router = new Router($_ENV["SERVER_HOST"]);
$router->get("/", fn () => Login::index());
$router->get("/exemple/{id}", function($id = 1) {
return "<h1>id: $id</h1>";
});
Cada método chamado (GET, POST, PUT, DELETE...) terá seu próprio método implementado na nossa classe Router, vamos fazer o GET:
public function get(string $path, callable $function)
{
$this->addRoute("GET", $path, $function);
}
agora nosso addRouter que vai adicionar nossa rota no nosso atributo de array de rotas
private function addRoute(string $method, string $path, ?callable $function = null): void
{
$vars = $this->getVarsFromPath($path);
$this->routes[$path][$method] = ["callable" => $function, "vars" => $vars];
}
private function getVarsFromPath(string $path): array
{
$xPath = explode("/", $path);
$vars = array_filter($xPath, function ($item) {
return str_contains($item, "{");
});
return array_values($vars);
}
Agora nosso código já pega o caso que teremos variáveis no nosso PATH (caminho da URL)
Vamos criar uma função chamada dispatch para dizer quando o Router tem que parar de receber rotas e executar a rota solicitada na requisição:
public function dispatch(): void
{
try {
$content = $this->getRoute();
(new Response($content,200))->response();
} catch (Exception $e) {
(new Response($e->getMessage(), $e->getCode()))->response();
}
}
Até aqui, tudo tranquilo:
private function getRoute(): string
{
$currentRoute = $this->getCurrentRoute();
$function = $currentRoute["callable"];
$vars = $currentRoute["currentVars"];
return call_user_func_array($function, $vars);
}
Caos que funciona
E agora só sobrou fazer os matches das rotas e das variáveis sem usar preg_match nem nada no tipo e agora vou explicar esse getCurrentRoute:
O que nós precisamos da requisição nós pegamos com o Request:
private function getCurrentRoute()
{
$path = $this->request->getUri();
$method = $this->request->getMethod();
variáveis para auxiliar:
$xPath = explode("/", $path);
$currentRoute = null;
$methodNotAllowedError = false;
Agora tudo que temos que fazer é ir pela posição do array comparar com o path das rotas já existentes, por exemplo:
Se temos a rota /exemple/{id}
temos o usuário fazendo request em /exemple/10
Então vamos fazer 2 foreachs, um das rotas e um dos paths de cada rota para comparar as posições, já que a url do usuario tem a mesma regra de /, fazemos um explode e comparamos item por item do array:
foreach ($this->routes as $pathRoute => $route) {
$xPathRoute = explode("/", $pathRoute);
$variablesCount = count($route[$method]["vars"] ?? []);
$xPathCopy = $xPath;
$currentVariables = array_splice($xPathCopy,count($xPathCopy) - $variablesCount);
$maxMatches = count($xPathRoute) - $variablesCount;
if (count($xPath) == count($xPathRoute)) {
$matches = 0;
foreach ($xPathRoute as $key => $value) {
if ($xPathRoute[$key] == $xPath[$key])
{
$matches++;
if ($matches == $maxMatches) {
if (isset($route[$method])) {
$methodNotAllowedError = false;
$currentRoute = $route;
$currentRoute[$method]["currentVars"] = $currentVariables;
continue;
}
$methodNotAllowedError = true;
}
}
}
}
}
quando debugamos nosso route temos:
[/exemple/{id}] => Array
(
[GET] => Array
(
[callable] => Closure Object
(
[name] => {closure:/var/www/html/Routes/web.php:14}
[file] => /var/www/html/Routes/web.php
[line] => 14
[parameter] => Array
(
[$id] =>
)
)
[vars] => Array
(
[0] => {id}
)
)
)
e no momento de execução vamos ter o nosso currentVars para usarmos na chamada da função que passamos no arquivo que criamos nossas rotas, se for executar esse código, deixo claro que estou usando apache e tenho o .htaccess configurado:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} .+/$
RewriteRule ^(.+)/$ /$1 [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [QSA,NC,L]
Quero deixar claro que nosso router tem grandes limitações e não está e um código limpo e fácil de manter
Próximas etapas:
- View
- Request
- Response
- Router
- Middleware
Pode parecer que sim, mas na verdade estamos longe de terminar nosso Router, ele merece uma boa refatorada e ainda precisamos implementar nosso sistema de Middlewares dentro do nosso Router.
Então é isso — o repositório do projeto está aqui
Até a próxima postagem! 🚀
Att:
Amorim do futuro projeto pixpeixe! 🐟