HttpClient.php 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. <?php
  2. declare(strict_types=1);
  3. namespace EKSRelay\Core;
  4. /**
  5. * Thin cURL wrapper for JSON APIs.
  6. */
  7. final class HttpClient
  8. {
  9. /**
  10. * @return array{status: int, body: string, json: mixed}
  11. */
  12. public static function request(
  13. string $method,
  14. string $url,
  15. array $headers = [],
  16. ?array $jsonBody = null,
  17. int $timeoutSeconds = 30,
  18. ): array {
  19. $ch = curl_init();
  20. $opts = [
  21. CURLOPT_URL => $url,
  22. CURLOPT_RETURNTRANSFER => true,
  23. CURLOPT_TIMEOUT => $timeoutSeconds,
  24. CURLOPT_CONNECTTIMEOUT => 10,
  25. CURLOPT_FOLLOWLOCATION => true,
  26. CURLOPT_MAXREDIRS => 3,
  27. ];
  28. // Use bundled CA certs if PHP/system doesn't have them configured
  29. $caBundle = Env::get('CURL_CA_BUNDLE');
  30. if ($caBundle === '') {
  31. // Common fallback locations
  32. foreach ([
  33. dirname(__DIR__, 2) . '/cacert.pem',
  34. (getenv('APPDATA') ?: '') . '/php/cacert.pem',
  35. ] as $candidate) {
  36. if ($candidate !== '' && is_file($candidate)) {
  37. $caBundle = $candidate;
  38. break;
  39. }
  40. }
  41. }
  42. if ($caBundle !== '') {
  43. $opts[CURLOPT_CAINFO] = $caBundle;
  44. }
  45. curl_setopt_array($ch, $opts);
  46. $headers[] = 'Accept: application/json';
  47. if ($jsonBody !== null) {
  48. $encoded = json_encode($jsonBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  49. curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded);
  50. $headers[] = 'Content-Type: application/json';
  51. }
  52. $method = strtoupper($method);
  53. match ($method) {
  54. 'GET' => null,
  55. 'POST' => curl_setopt($ch, CURLOPT_POST, true),
  56. 'PUT' => curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'),
  57. 'PATCH' => curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'),
  58. 'DELETE' => curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'),
  59. default => curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method),
  60. };
  61. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  62. $body = curl_exec($ch);
  63. $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
  64. $error = curl_error($ch);
  65. curl_close($ch);
  66. if ($body === false) {
  67. throw new HttpException(502, 'UPSTREAM_ERROR', "cURL error: {$error}");
  68. }
  69. $json = json_decode((string)$body, true);
  70. return [
  71. 'status' => $status,
  72. 'body' => (string)$body,
  73. 'json' => $json,
  74. ];
  75. }
  76. }