corentin_wakdo/src/Core/Request.php

145 lines
3.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Core;
/**
* Representation immuable de la requete HTTP entrante.
*
* Construite depuis les super-globales par fromGlobals() ; le reste de
* l'application ne touche jamais $_SERVER / $_GET directement.
*/
final class Request
{
/**
* @param array<string, string> $query
* @param array<string, string> $headers
*/
public function __construct(
private readonly string $method,
private readonly string $path,
private readonly array $query,
private readonly array $headers,
private readonly string $rawBody,
) {
}
public static function fromGlobals(): self
{
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
// REQUEST_URI inclut la query string ; on isole le chemin seul.
$uri = (string) ($_SERVER['REQUEST_URI'] ?? '/');
$path = parse_url($uri, PHP_URL_PATH);
$path = is_string($path) ? $path : '/';
$path = self::normalizePath($path);
/** @var array<string, string> $query */
$query = $_GET;
return new self(
$method,
$path,
$query,
self::extractHeaders(),
(string) file_get_contents('php://input'),
);
}
/**
* Garde un slash de tete et retire le slash de fin (sauf racine) pour
* que "/api/health/" et "/api/health" matchent la meme route.
*/
private static function normalizePath(string $path): string
{
if ($path === '') {
return '/';
}
if ($path[0] !== '/') {
$path = '/' . $path;
}
if ($path !== '/' && str_ends_with($path, '/')) {
$path = rtrim($path, '/');
}
return $path;
}
/**
* @return array<string, string>
*/
private static function extractHeaders(): array
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (str_starts_with($key, 'HTTP_')) {
$name = str_replace('_', '-', substr($key, 5));
$headers[strtolower($name)] = (string) $value;
}
}
// Content-Type / Content-Length ne sont pas prefixes HTTP_ par PHP.
if (isset($_SERVER['CONTENT_TYPE'])) {
$headers['content-type'] = (string) $_SERVER['CONTENT_TYPE'];
}
if (isset($_SERVER['CONTENT_LENGTH'])) {
$headers['content-length'] = (string) $_SERVER['CONTENT_LENGTH'];
}
return $headers;
}
public function method(): string
{
return $this->method;
}
public function path(): string
{
return $this->path;
}
public function query(string $key, ?string $default = null): ?string
{
return $this->query[$key] ?? $default;
}
/**
* @return array<string, string>
*/
public function allQuery(): array
{
return $this->query;
}
public function header(string $name, ?string $default = null): ?string
{
return $this->headers[strtolower($name)] ?? $default;
}
public function rawBody(): string
{
return $this->rawBody;
}
/**
* Decode le corps JSON ; renvoie un tableau vide si le corps est vide ou
* invalide, pour laisser la validation metier decider (pas de fatale ici).
*
* @return array<string, mixed>
*/
public function json(): array
{
if ($this->rawBody === '') {
return [];
}
$decoded = json_decode($this->rawBody, true);
return is_array($decoded) ? $decoded : [];
}
}