MODULE 08
API REST PHP
Construire une API RESTful avec PHP natif : routage, JSON, authentification JWT et CORS.
1 — Principes REST
REST (Representational State Transfer) est un style d'architecture pour les APIs Web. Les ressources sont identifiées par des URLs et manipulées via les méthodes HTTP.
| Méthode | URL | Action | Idempotent |
|---|---|---|---|
| GET | /api/tasks | Lister toutes les tâches | ✓ |
| GET | /api/tasks/42 | Récupérer la tâche #42 | ✓ |
| POST | /api/tasks | Créer une nouvelle tâche | ✗ |
| PUT | /api/tasks/42 | Remplacer la tâche #42 | ✓ |
| PATCH | /api/tasks/42 | Modifier partiellement | ✓ |
| DELETE | /api/tasks/42 | Supprimer la tâche #42 | ✓ |
2 — Routage PHP natif
<?php
// index.php — point d'entrée unique (configure .htaccess : RewriteRule . index.php)
header('Content-Type: application/json; charset=utf-8');
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Extraire /api/tasks/42 → ['', 'api', 'tasks', '42']
$segments = explode('/', trim($path, '/'));
// Router simple
match(true) {
$method === 'GET' && $segments[0] === 'api' && $segments[1] === 'tasks' && !isset($segments[2])
=> handleGetAll(),
$method === 'GET' && $segments[0] === 'api' && $segments[1] === 'tasks' && isset($segments[2])
=> handleGetOne((int)$segments[2]),
$method === 'POST' && $segments[0] === 'api' && $segments[1] === 'tasks'
=> handleCreate(),
$method === 'PUT' && $segments[0] === 'api' && $segments[1] === 'tasks' && isset($segments[2])
=> handleUpdate((int)$segments[2]),
$method === 'DELETE' && $segments[0] === 'api' && $segments[1] === 'tasks' && isset($segments[2])
=> handleDelete((int)$segments[2]),
default => respond(404, ['error' => 'Route non trouvée']),
};
3 — Entrées/Sorties JSON
<?php
// ── Réponse JSON ──────────────────────────────────────────
function respond(int $code, mixed $data): never {
http_response_code($code); // 200, 201, 400, 404, 401, 403, 500...
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit; // never return type : la fonction ne retourne jamais
}
// ── Lire le body JSON (POST/PUT/PATCH) ────────────────────
function getJsonBody(): array {
$raw = file_get_contents('php://input'); // lit le corps brut de la requête
if (!$raw) return [];
$data = json_decode($raw, true); // true = tableau associatif
if (json_last_error() !== JSON_ERROR_NONE) {
respond(400, ['error' => 'JSON invalide : ' . json_last_error_msg()]);
}
return $data;
}
// ── Validation ────────────────────────────────────────────
function valider(array $data, array $regles): array {
$erreurs = [];
foreach ($regles as $champ => $regle) {
if ($regle === 'required' && empty($data[$champ])) {
$erreurs[] = "$champ est requis";
}
}
return $erreurs;
}
// Exemple d'utilisation
function handleCreate(): never {
$body = getJsonBody();
$erreurs = valider($body, ['titre' => 'required']);
if ($erreurs) respond(422, ['errors' => $erreurs]);
// Sauvegarder...
respond(201, ['id' => 42, 'message' => 'Créé avec succès', 'data' => $body]);
}
4 — Authentification JWT (simplifié)
<?php
// JWT = JSON Web Token : trois parties encodées en base64 séparées par des points
// header.payload.signature
function base64url_encode(string $data): string {
// base64url : remplace +/ par -_ et supprime les = de padding
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function creerToken(array $payload, string $secret): string {
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload['iat'] = time(); // issued at
$payload['exp'] = time() + 3600; // expire dans 1h
$body = base64url_encode(json_encode($payload));
$signature = base64url_encode(hash_hmac('sha256', "$header.$body", $secret, true));
return "$header.$body.$signature";
}
function verifierToken(string $token, string $secret): ?array {
$parts = explode('.', $token);
if (count($parts) !== 3) return null;
[$header, $body, $sig] = $parts;
// Recalculer la signature et comparer en temps constant (anti timing-attack)
$expected = base64url_encode(hash_hmac('sha256', "$header.$body", $secret, true));
if (!hash_equals($expected, $sig)) return null;
$payload = json_decode(base64_decode(strtr($body, '-_', '+/')), true);
if ($payload['exp'] < time()) return null; // token expiré
return $payload;
}
// Middleware d'authentification
function requireAuth(): array {
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!preg_match('/^Bearer\s+(.+)$/i', $authHeader, $m)) {
respond(401, ['error' => 'Token manquant']);
}
$payload = verifierToken($m[1], $_ENV['JWT_SECRET'] ?? 'dev-secret');
if (!$payload) respond(401, ['error' => 'Token invalide ou expiré']);
return $payload;
}
5 — CORS & Headers
<?php
// CORS : Cross-Origin Resource Sharing
// Nécessaire quand le frontend (ex: React sur :3000) appelle l'API (PHP sur :8000)
function setCorsHeaders(): void {
$allowed = ['https://monsite.com', 'http://localhost:3000'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
// Whitelist : n'autoriser que les origines connues (jamais Access-Control-Allow-Origin: *)
// pour les APIs avec authentification
if (in_array($origin, $allowed, true)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Credentials: true');
}
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
header('Access-Control-Max-Age: 86400'); // cache preflight 24h
}
setCorsHeaders();
// Gérer les requêtes OPTIONS (preflight CORS)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204); // No Content
exit;
}
6 — Codes HTTP & Gestion des erreurs
<?php
// Codes HTTP standard pour une API REST
// 200 OK — lecture/mise à jour réussie
// 201 Created — création réussie (+ header Location: /api/tasks/42)
// 204 No Content — suppression réussie (pas de body)
// 400 Bad Request — données invalides (manquantes ou mal formées)
// 401 Unauthorized — non authentifié (token absent ou invalide)
// 403 Forbidden — authentifié mais pas autorisé (mauvais rôle)
// 404 Not Found — ressource introuvable
// 405 Method Not Allowed — méthode HTTP non supportée
// 422 Unprocessable Entity — validation échouée (données sémantiquement incorrectes)
// 429 Too Many Requests — rate limiting
// 500 Internal Server Error — erreur serveur
// Handler global d'erreurs
set_exception_handler(function (Throwable $e): never {
error_log($e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
$code = $e instanceof InvalidArgumentException ? 400
: ($e instanceof RuntimeException ? 500 : 500);
respond($code, [
'error' => 'Erreur serveur',
'message' => defined('APP_DEBUG') && APP_DEBUG ? $e->getMessage() : 'Contactez l\'admin',
]);
});
// Exemple : réponse de liste paginée
function paginate(array $items, int $page, int $perPage): array {
$total = count($items);
return [
'data' => array_slice($items, ($page - 1) * $perPage, $perPage),
'meta' => ['total' => $total, 'page' => $page, 'per_page' => $perPage,
'last_page' => (int)ceil($total / $perPage)],
'links' => ['next' => $page < ceil($total / $perPage) ? "?page=" . ($page + 1) : null],
];
}