Makrosites

Suas ideias em realidade digital!

Middleware em PHP puro: como estruturar (Auth, CORS, Rate Limit, JSON) sem framework

Middleware em PHP puro: como estruturar (Auth, CORS, Rate Limit, JSON) sem framework

O que é Middleware (e por que você vai querer isso em PHP puro)?

Middleware é uma camada que roda antes (e às vezes depois) do seu controller. Ele serve para centralizar regras repetidas como:

Sem middleware, você acaba copiando o mesmo código em todos os endpoints. Com middleware, você cria um pipeline (cadeia) e reaproveita.


Estrutura simples (recomendada)

/public/api/index.php
/src/Core/Router.php
/src/Core/Response.php
/src/Core/Request.php
/src/Middleware/MiddlewareInterface.php
/src/Middleware/MiddlewareQueue.php
/src/Middleware/JsonMiddleware.php
/src/Middleware/CorsMiddleware.php
/src/Middleware/AuthMiddleware.php
/src/Middleware/RateLimitMiddleware.php
/src/Controllers/...

1) Request e Response (bem básicos)

Request.php

method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
    $this->path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
    $this->headers = $this->readHeaders();
    $this->query = $_GET ?? [];
    $this->ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
  }

  private function readHeaders(): array {
    $h = [];
    foreach ($_SERVER as $k => $v) {
      if (str_starts_with($k, 'HTTP_')) {
        $name = str_replace('_', '-', strtolower(substr($k, 5)));
        $h[$name] = $v;
      }
    }
    // Authorization às vezes vem em outro server var
    if (!isset($h['authorization']) && isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
      $h['authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
    }
    return $h;
  }

  public function json(): array {
    if ($this->body !== null) return (array)$this->body;
    $raw = file_get_contents('php://input');
    $data = json_decode($raw ?: '', true);
    $this->body = is_array($data) ? $data : [];
    return $this->body;
  }

  public function header(string $name, string $default = ''): string {
    $key = strtolower($name);
    return $this->headers[$key] ?? $default;
  }
}

Response.php

 $message];
    if ($details !== null) $payload['details'] = $details;
    self::json($payload, $code);
  }
}

2) Interface de Middleware

Um middleware recebe o Request e um $next (próximo passo do pipeline).


3) MiddlewareQueue (pipeline)

Este é o motor que executa a cadeia.

stack = $stack;
  }

  public function run(Request $req, callable $finalHandler) {
    $runner = array_reduce(
      array_reverse($this->stack),
      function($next, $mw) {
        return function(Request $req) use ($mw, $next) {
          return $mw->handle($req, $next);
        };
      },
      $finalHandler
    );

    return $runner($req);
  }
}

4) Middlewares prontos (os mais úteis)

4.1) JsonMiddleware (garante JSON em tudo)

4.2) CorsMiddleware (com preflight OPTIONS)

origin}");
    header("Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS");
    header("Access-Control-Allow-Headers: Content-Type, Authorization");
    header("Access-Control-Allow-Credentials: true");

    if ($req->method === 'OPTIONS') {
      http_response_code(200);
      exit;
    }
    return $next($req);
  }
}

4.3) AuthMiddleware (Bearer/JWT) aplicado só em rotas protegidas

Você pode aplicar em um grupo de rotas ou individualmente. Aqui vai uma versão simples que exige Bearer em certas rotas.

protectedPrefixes as $p) {
      if (str_starts_with($req->path, $p)) {
        return $this->check($req, $next);
      }
    }
    return $next($req);
  }

  private function check(Request $req, callable $next) {
    $auth = $req->header('authorization', '');

    if (!preg_match('/Bearer\\s+(.*)$/i', $auth, $m)) {
      Response::error('Unauthorized', 401);
    }

    $token = trim($m[1]);

    // Aqui você valida JWT ou token no banco:
    // $payload = validateJWT($token, 'SEGREDO');
    // if (!$payload) Response::error('Token inválido', 401);

    return $next($req);
  }
}

4.4) RateLimitMiddleware (por rota + IP)

Versão simplificada (você pode ligar na sua implementação Redis/MySQL do post anterior).

path;
    $ip = $req->ip;

    $rule = $this->rules[$routeKey] ?? ['max' => 120, 'window' => 60];

    // Aqui você chama rateLimitRedis(...) ou rateLimitMySQL(...)
    // $r = rateLimitRedis($redis, $routeKey, $ip, $rule['max'], $rule['window']);

    // Exemplo fake:
    $r = ['allowed' => true, 'remaining' => 999, 'retry_after' => 0];

    header("X-RateLimit-Remaining: {$r['remaining']}");
    if (!$r['allowed']) {
      http_response_code(429);
      header("Retry-After: {$r['retry_after']}");
      echo json_encode(['error' => 'Too Many Requests', 'retry_after' => $r['retry_after']]);
      exit;
    }

    return $next($req);
  }
}

5) Integrando tudo no index.php da API

No seu /public/api/index.php, você cria o Request, roda o pipeline e por fim chama o router/controller.

 ['max' => 5, 'window' => 60],
    '/api/auth/refresh' => ['max' => 20, 'window' => 3600],
  ]),
  new AuthMiddleware(['/api/posts', '/api/private']),
];

$queue = new MiddlewareQueue($middlewares);

$queue->run($req, function(Request $req) {
  $router = new Router();
  // registre rotas...
  // $router->get('/api/health', fn() => Response::json(['ok' => true]));
  $router->dispatch($req->method, $req->path);
});

Boas práticas (pra não virar bagunça)

  • Middleware deve ser pequeno e focado (uma responsabilidade)
  • Evite colocar regra de negócio em middleware
  • Use prefixos de rotas protegidas
  • Centralize headers CORS/JSON
  • Padronize erro e resposta