$headers * @return array{status:int, body:string, json:mixed, error:?string} */ public static function request(string $method, string $url, ?string $body = null, array $headers = [], int $timeout = 8): array { $ch = curl_init($url); $hdrs = []; foreach ($headers as $k => $v) { $hdrs[] = "$k: $v"; } curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => min(5, $timeout), CURLOPT_TIMEOUT => $timeout, CURLOPT_HTTPHEADER => $hdrs, CURLOPT_USERAGENT => 'visit.waterschurch.org/1.0 (+https://visit.waterschurch.org)', CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $out = curl_exec($ch); $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $err = curl_error($ch) ?: null; $text = is_string($out) ? $out : ''; $json = null; if ($text !== '') { $decoded = json_decode($text, true); $json = json_last_error() === JSON_ERROR_NONE ? $decoded : null; } return ['status' => $status, 'body' => $text, 'json' => $json, 'error' => $err]; } /** * @param array $payload * @param array $headers * @return array{status:int, body:string, json:mixed, error:?string} */ public static function json(string $method, string $url, ?array $payload, array $headers = [], int $timeout = 8): array { $headers = ['Content-Type' => 'application/json', 'Accept' => 'application/json'] + $headers; return self::request($method, $url, $payload === null ? null : json_encode($payload, JSON_UNESCAPED_SLASHES), $headers, $timeout); } /** True for statuses/transport errors that are worth retrying later. */ public static function isTransient(int $status, ?string $error): bool { return $error !== null || $status === 0 || $status === 408 || $status === 429 || $status >= 500; } }