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>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user