Files
visit.waterschurch.org/public/api/lib/Http.php
T
Waters ChurchandClaude Fable 5.1 78cd0cc550 Plan Your Visit landing page for visit.waterschurch.org
Next.js 16 static export + PHP 8 API for cPanel/Apache hosting.

- Location-aware campus finder (IP estimate, optional precise location, ZIP search)
- Three-step Plan Your Visit form with separate email/SMS consent
- PHP API: validation, anti-spam, rate limiting, SQLite lead store,
  Mailchimp and Sinch Engage adapters, idempotent retries via cron
- Attribution capture (utm_*, gclid/gbraid/wbraid, fbclid, ttclid, twclid) and
  consent-aware analytics hooks
- Vitest and PHP test suites, cPanel deployment script and docs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 13:22:26 -04:00

61 lines
2.4 KiB
PHP

<?php
declare(strict_types=1);
/** Small cURL wrapper with hard timeouts, used by every provider adapter. */
final class Http
{
/**
* @param array<string,string> $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<string,mixed> $payload
* @param array<string,string> $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;
}
}