get('/customers', ['q' => 'Petit']); * $client = $cz->post('/customers', [...], idempotencyKey: 'commande-1234'); * Erreurs : ClientZenApiException (statut HTTP + problème RFC 9457 dans ->problem). */ final class ClientZenClient { public function __construct(private readonly string $token, private readonly string $baseUrl = 'https://clientzen.fr/api/v1', private readonly int $timeout = 15) {} /** @param array $query @return array */ public function get(string $path, array $query = []): array { return $this->request('GET', $path.($query !== [] ? '?'.http_build_query($query) : '')); } /** @param array $body @return array */ public function post(string $path, array $body, ?string $idempotencyKey = null): array { return $this->request('POST', $path, $body, $idempotencyKey); } /** @param array $body @return array */ public function patch(string $path, array $body): array { return $this->request('PATCH', $path, $body); } /** Télécharge un binaire (ex. /invoices/{id}/pdf). */ public function download(string $path): string { [$status, $raw] = $this->raw('GET', $path, null, null, 'application/pdf'); if ($status >= 400) { throw ClientZenApiException::fromResponse($status, $raw); } return $raw; } /** Itère toutes les pages d'une liste. @param array $query @return iterable> */ public function each(string $path, array $query = []): iterable { $page = 1; do { $res = $this->get($path, $query + ['page' => $page, 'per_page' => 100]); foreach ($res['data'] ?? [] as $row) { yield $row; } $page++; } while (($res['meta']['last_page'] ?? 1) >= $page); } /** @param array|null $body @return array */ private function request(string $method, string $path, ?array $body = null, ?string $idempotencyKey = null): array { [$status, $raw] = $this->raw($method, $path, $body, $idempotencyKey, 'application/json'); $json = json_decode($raw, true); if ($status >= 400) { throw ClientZenApiException::fromResponse($status, $raw); } return is_array($json) ? $json : []; } /** @param array|null $body @return array{0: int, 1: string} */ private function raw(string $method, string $path, ?array $body, ?string $idempotencyKey, string $accept): array { $headers = ['Authorization: Bearer '.$this->token, 'Accept: '.$accept, 'User-Agent: ClientZen-PHP-SDK/1']; $ch = curl_init($this->baseUrl.$path); if ($ch === false) { throw new RuntimeException('cURL indisponible.'); } if ($body !== null) { $headers[] = 'Content-Type: application/json'; curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_THROW_ON_ERROR)); } if ($idempotencyKey !== null) { $headers[] = 'Idempotency-Key: '.$idempotencyKey; } curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_FOLLOWLOCATION => false]); $raw = curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $error = curl_error($ch); curl_close($ch); if ($raw === false) { throw new RuntimeException('Erreur réseau : '.$error); } return [$status, (string) $raw]; } } final class ClientZenApiException extends RuntimeException { /** @param array $problem */ public function __construct(public readonly int $status, public readonly array $problem) { parent::__construct(sprintf('ClientZen API %d : %s%s', $status, (string) ($problem['title'] ?? 'erreur'), isset($problem['detail']) ? ' — '.$problem['detail'] : '')); } public static function fromResponse(int $status, string $raw): self { $json = json_decode($raw, true); return new self($status, is_array($json) ? $json : ['title' => 'Réponse non JSON']); } }