retrieve('classic', 'items', 5191); // foreach ($wowdb->iterate('/classic/items', ['type' => 'dagger', 'rogue' => true]) as $dague) { // echo $dague['name'], PHP_EOL; // } declare(strict_types=1); namespace WowDb; /** Erreur de l'API (corps { error: { type, code, message, param, doc_url } }) ou du réseau. */ final class WowDbException extends \RuntimeException { public function __construct( string $message, public readonly int $status = 0, public readonly string $type = 'api_error', public readonly string $errorCode = 'network_error', public readonly ?string $param = null, public readonly ?string $docUrl = null, public readonly ?string $requestId = null, ) { parent::__construct($message, $status); } } final class Client { public const VERSION = '1.0.0'; public const BASE_URL = 'https://api.wowdb.assemblee-defias.fr/v1'; private string $baseUrl; public function __construct( private string $userAgent = 'WowDb.php/1.0.0', string $baseUrl = self::BASE_URL, private int $maxRetries = 2, private float $timeout = 30.0, ) { $this->baseUrl = rtrim($baseUrl, '/'); } /** Requête GET brute : chemin relatif à /v1 (« /classic/items/5191 ») ; renvoie le JSON décodé. */ public function request(string $path, array $params = []): array { $propres = []; foreach ($params as $cle => $valeur) { if ($valeur === null) { continue; } $propres[$cle] = is_bool($valeur) ? ($valeur ? 'true' : 'false') : (is_array($valeur) ? implode(',', $valeur) : (string) $valeur); } $url = $this->baseUrl . '/' . ltrim($path, '/') . ($propres ? '?' . http_build_query($propres) : ''); $context = stream_context_create(['http' => [ 'method' => 'GET', 'header' => "User-Agent: {$this->userAgent}\r\nAccept: application/json\r\n", 'ignore_errors' => true, 'timeout' => $this->timeout, ]]); for ($essai = 0; ; $essai++) { $corps = @file_get_contents($url, false, $context); // PHP 8.4+ : http_get_last_response_headers() ; avant : la variable locale $http_response_header. $entetes = function_exists('http_get_last_response_headers') ? (http_get_last_response_headers() ?? []) : ($http_response_header ?? []); if ($corps === false || !$entetes) { throw new WowDbException('Requête impossible : ' . (error_get_last()['message'] ?? 'réseau')); } $statut = preg_match('#^HTTP/\S+\s+(\d{3})#', $entetes[0], $m) ? (int) $m[1] : 0; $entete = static function (string $nom) use ($entetes): ?string { foreach ($entetes as $ligne) { if (stripos($ligne, $nom . ':') === 0) { return trim(substr($ligne, strlen($nom) + 1)); } } return null; }; if ($statut === 429 && $essai < $this->maxRetries) { sleep(max(1, (int) ($entete('Retry-After') ?? 1))); continue; } $json = json_decode($corps, true); if ($statut >= 400 || !is_array($json)) { $e = is_array($json) ? ($json['error'] ?? []) : []; throw new WowDbException($e['message'] ?? "Erreur HTTP $statut", $statut, $e['type'] ?? 'api_error', $e['code'] ?? 'http_error', $e['param'] ?? null, $e['doc_url'] ?? null, $entete('X-Request-Id')); } return $json; } } /** Une page d'une liste : ['object' => 'list', 'data' => [...], 'has_more' => …, 'next_cursor' => …]. */ public function list(string $path, array $params = []): array { return $this->request($path, $params); } /** Tous les éléments d'une liste, page après page (pages de 100). */ public function iterate(string $path, array $params = []): \Generator { $params += ['limit' => 100]; do { $page = $this->request($path, $params); foreach ($page['data'] as $element) { yield $element; } $params['cursor'] = $page['next_cursor'] ?? null; } while ($params['cursor']); } /** Fiche d'une ressource : retrieve('classic', 'items', 5191). */ public function retrieve(string $version, string $resource, int $id): array { return $this->request("/$version/$resource/$id"); } /** Recherche plein texte dans toute la base. */ public function search(string $q, array $params = []): array { return $this->request('/search', ['q' => $q] + $params); } public function stats(): array { return $this->request('/stats'); } public function versions(): array { return $this->request('/versions'); } }