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>
124 lines
4.0 KiB
PHP
124 lines
4.0 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Configuration lives OUTSIDE the web root in a private directory containing a
|
|
* `.env` file (see .env.example at the repo root for every key).
|
|
*
|
|
* The private directory is resolved in this order:
|
|
* 1. VISIT_PRIVATE_DIR environment variable (set via `SetEnv` in .htaccess, or cPanel cron env)
|
|
* 2. <parent of DOCUMENT_ROOT>/visit-private (e.g. /home/USER/visit-private)
|
|
* 3. <repo>/private (local development only)
|
|
*/
|
|
final class Config
|
|
{
|
|
/** @var array<string,string> */
|
|
private static array $values = [];
|
|
private static ?string $privateDir = null;
|
|
private static bool $loaded = false;
|
|
|
|
public static function load(): void
|
|
{
|
|
if (self::$loaded) {
|
|
return;
|
|
}
|
|
self::$loaded = true;
|
|
$dir = self::privateDir();
|
|
$env = $dir . '/.env';
|
|
if (is_readable($env)) {
|
|
foreach (file($env, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
|
|
$line = trim($line);
|
|
if ($line === '' || $line[0] === '#' || !str_contains($line, '=')) {
|
|
continue;
|
|
}
|
|
[$k, $v] = explode('=', $line, 2);
|
|
$k = trim($k);
|
|
$v = trim($v);
|
|
if (str_starts_with($k, 'export ')) {
|
|
$k = trim(substr($k, 7));
|
|
}
|
|
if ((str_starts_with($v, '"') && str_ends_with($v, '"')) || (str_starts_with($v, "'") && str_ends_with($v, "'"))) {
|
|
$v = substr($v, 1, -1);
|
|
}
|
|
self::$values[$k] = $v;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static function privateDir(): string
|
|
{
|
|
if (self::$privateDir !== null) {
|
|
return self::$privateDir;
|
|
}
|
|
$candidates = [];
|
|
$fromEnv = getenv('VISIT_PRIVATE_DIR') ?: ($_SERVER['VISIT_PRIVATE_DIR'] ?? ($_SERVER['REDIRECT_VISIT_PRIVATE_DIR'] ?? null));
|
|
if ($fromEnv) {
|
|
$candidates[] = $fromEnv;
|
|
}
|
|
if (!empty($_SERVER['DOCUMENT_ROOT'])) {
|
|
$candidates[] = dirname(rtrim($_SERVER['DOCUMENT_ROOT'], '/')) . '/visit-private';
|
|
}
|
|
$candidates[] = dirname(__DIR__, 3) . '/private'; // repo/private (dev)
|
|
foreach ($candidates as $c) {
|
|
if (is_dir($c)) {
|
|
return self::$privateDir = rtrim($c, '/');
|
|
}
|
|
}
|
|
// Fall back to the first candidate so error messages point somewhere useful.
|
|
return self::$privateDir = rtrim($candidates[0], '/');
|
|
}
|
|
|
|
public static function get(string $key, ?string $default = null): ?string
|
|
{
|
|
$env = getenv($key);
|
|
if ($env !== false && $env !== '') {
|
|
return $env;
|
|
}
|
|
return (isset(self::$values[$key]) && self::$values[$key] !== '') ? self::$values[$key] : $default;
|
|
}
|
|
|
|
public static function require(string $key): string
|
|
{
|
|
$v = self::get($key);
|
|
if ($v === null) {
|
|
throw new RuntimeException("Missing required configuration: $key");
|
|
}
|
|
return $v;
|
|
}
|
|
|
|
public static function bool(string $key, bool $default = false): bool
|
|
{
|
|
$v = self::get($key);
|
|
if ($v === null) {
|
|
return $default;
|
|
}
|
|
return in_array(strtolower($v), ['1', 'true', 'yes', 'on'], true);
|
|
}
|
|
|
|
public static function int(string $key, int $default): int
|
|
{
|
|
$v = self::get($key);
|
|
return $v === null ? $default : (int)$v;
|
|
}
|
|
|
|
/** "mock" (default when unset), "live". */
|
|
public static function mode(): string
|
|
{
|
|
return strtolower(self::get('INTEGRATION_MODE', 'mock') ?? 'mock') === 'live' ? 'live' : 'mock';
|
|
}
|
|
|
|
public static function isMock(): bool
|
|
{
|
|
return self::mode() === 'mock';
|
|
}
|
|
|
|
public static function dataDir(): string
|
|
{
|
|
$d = self::get('DATA_DIR') ?: self::privateDir() . '/data';
|
|
if (!is_dir($d)) {
|
|
@mkdir($d, 0750, true);
|
|
}
|
|
return $d;
|
|
}
|
|
}
|