connect(); define("redis", $redis_instance); } catch (Exception $e) { // Redis might not be available, autoloader will fall back to manual search } /** * Lightweight project autoloader: load classes/interfaces/traits/modules on demand. * Maps top-level namespaces to folders under the app root (WD). * - classes\* -> WD/classes/* * - interfaces\* -> WD/interfaces/* * - traits\* -> WD/traits/* * - objects\* -> WD/objects/* * - statistics\* -> WD/statistics/* * - modules\...\* -> WD/modules/... * - \...\* -> WD/modules//... (e.g., bird\bird_c) */ spl_autoload_register(function (string $class): void { $class = ltrim($class, '\\'); $cache_key = 'autoload:' . $class; $wdReal = rtrim((string) realpath(WD), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; $modulesRoot = $wdReal . 'modules' . DIRECTORY_SEPARATOR; $isPathInside = static function (string $path, string $root): bool { $resolved = realpath($path); if ($resolved === false) { return false; } $resolved = rtrim($resolved, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; return str_starts_with($resolved, $root); }; $is_loaded = static function (string $candidate): bool { return class_exists($candidate, false) || interface_exists($candidate, false) || trait_exists($candidate, false) || (function_exists('enum_exists') && enum_exists($candidate, false)); }; // Check Redis cache first if (defined('redis')) { try { $cached = redis->get($cache_key); if (is_string($cached) && $cached !== '' && is_file($cached)) { if ($isPathInside($cached, $wdReal)) { require_once $cached; if ($is_loaded($class)) { return; } } // Stale, invalid, or unsafe class mapping in cache; continue with normal lookup. redis->delete($cache_key); } elseif (is_string($cached) && $cached !== '') { // Remove non-existing cached path to avoid repeated failed lookups. redis->delete($cache_key); } } catch (\Throwable $e) { // Fall back to manual search on Redis error } } $parts = explode('\\', $class); $top = strtolower($parts[0] ?? ''); $relative = implode(DIRECTORY_SEPARATOR, array_slice($parts, 1)); $base = WD . DIRECTORY_SEPARATOR; $candidates = []; // 1. Core folders: classes, interfaces, traits, objects, statistics $core_folders = ['classes', 'interfaces', 'traits', 'objects', 'statistics']; if (in_array($top, $core_folders)) { if ($top === 'classes' && str_starts_with(strtolower($relative), 'edge_gateway_')) { $candidates[] = $base . 'modules' . DIRECTORY_SEPARATOR . 'edgegateway' . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . $relative; } $candidates[] = $base . $top . DIRECTORY_SEPARATOR . $relative; } // 2. Modules folder: explicitly starting with 'modules' elseif ($top === 'modules') { $candidates[] = $base . 'modules' . DIRECTORY_SEPARATOR . $relative; } // 3. Fallback for module-specific classes without 'modules' prefix or nested in modules else { // Try directly under modules (e.g. bird\bird_c -> modules/bird/bird_c.php) $candidates[] = $base . 'modules' . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $class); // Try inside each module subfolder (e.g. customers\economicCustomers -> modules/economic/customers/economicCustomers.php) static $module_dirs = null; if ($module_dirs === null) { if (defined('redis')) { try { $module_dirs = redis->get_array('autoload:module_dirs'); } catch (\Throwable $e) {} } if (is_array($module_dirs)) { $module_dirs = array_values(array_filter($module_dirs, static function ($item) use ($base, $modulesRoot, $isPathInside): bool { if (!is_string($item) || $item === '' || str_contains($item, DIRECTORY_SEPARATOR) || str_contains($item, '..')) { return false; } $candidate = $base . 'modules' . DIRECTORY_SEPARATOR . $item; return is_dir($candidate) && $isPathInside($candidate, $modulesRoot); })); } if ($module_dirs === null) { $module_dirs = array_filter(scandir($base . 'modules'), function($item) use ($base) { return $item !== '.' && $item !== '..' && is_dir($base . 'modules' . DIRECTORY_SEPARATOR . $item); }); if (defined('redis')) { try { redis->set_array('autoload:module_dirs', $module_dirs); redis->expire('autoload:module_dirs', 3600); // Cache for 1 hour } catch (\Throwable $e) {} } } } foreach ($module_dirs as $mod) { $candidates[] = $base . 'modules' . DIRECTORY_SEPARATOR . $mod . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $class); } } $suffixes = ['', '_t', '_o', '_s', '_i', '_c', '_m']; foreach ($candidates as $path) { if (empty($path)) continue; foreach ($suffixes as $suffix) { $file = $path . $suffix . '.php'; if (is_file($file)) { require_once $file; if ($is_loaded($class)) { if (defined('redis')) { try { redis->setEx($cache_key, $file, 86400); // Cache for 24 hours } catch (\Throwable $e) {} } return; } } } } }); use classes\application_write_freeze; use classes\db; use classes\replication_bootstrap_config; use classes\replication_manager; use classes\release_manager; use classes\redis; use classes\request; use classes\response; use classes\router; // Start the session $response = new response(); $request = new request(); $db = new db($CONFIG_DB); // Ensure Redis is connected if (!defined('redis')) { try { define("redis", (new redis())->connect()); } catch (Exception $e) { $response->error($e->getMessage(), 500); } } // Connect to the database, and ensure the connection is successful try { $db->connect(); } catch (Exception $e) { $response->error($e->getMessage(), 500); } // Self-healing schema bootstrap. Runs every *_schema_bootstrap::ensureSchema() // once per process. Each is additive + idempotent (SHOW COLUMNS check before // any ALTER), so this is safe on every request. Catches the // "merged-to-master-but-migration-never-applied" failure mode (e.g. TRU-77 // invoice_email) even when the deploy pipeline pre-deploy step is skipped // (missing GitHub secrets, network glitch, manual deploy, etc.). try { \classes\schema_bootstrap_runtime::runAll(); } catch (Throwable $e) { error_log('[schema-bootstrap] runtime::runAll() failed: ' . $e->getMessage()); } try { release_manager::initializeRequestContext(); $releaseIngressPath = (string)(parse_url((string)($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH) ?: ''); release_manager::normalizeReleaseApiIngressPath( preg_match('#^/[A-Za-z0-9_-]{1,64}/api(?:/|$)#', $releaseIngressPath) === 1 ? (new release_manager())->enabledReleaseChannelSlugs() : [] ); } catch (Throwable $e) { error_log('[release-manager] Could not initialize request context or normalize ingress path: ' . $e->getMessage()); } $router = new router(); try { $replicationBootstrapSnapshotForRequest = replication_bootstrap_config::loadSnapshot(); $pendingStartupFailovers = is_array($replicationBootstrapSnapshotForRequest['pending_failovers'] ?? null) ? $replicationBootstrapSnapshotForRequest['pending_failovers'] : []; if ($pendingStartupFailovers !== []) { (new replication_manager())->syncStartupFailoversFromSnapshot(); } } catch (Throwable $e) { error_log('[replication-bootstrap] Could not sync startup failover metadata: ' . $e->getMessage()); } if (application_write_freeze::shouldBlock( $_SERVER['REQUEST_METHOD'] ?? 'GET', $_SERVER['REQUEST_URI'] ?? '/', php_sapi_name() === 'cli' || isset($_GET['internalCronCall']) )) { $freezeState = application_write_freeze::state(); if (php_sapi_name() === 'cli') { fwrite(STDERR, 'Application writes are frozen: ' . (string)($freezeState['reason'] ?? 'replication promotion') . PHP_EOL); exit(75); } $response->error([ 'message' => 'Application writes are temporarily frozen.', 'reason' => $freezeState['reason'] ?? null, 'expires_at' => $freezeState['expires_at'] ?? null, ], 503); } // If the program was called from the command line, run the cli script if (php_sapi_name() === 'cli' || isset($_GET['internalCronCall'])) { require_once 'cli.php'; exit; } // If the route ends with a MIME type, then require the file_server.php if ((preg_match('/\.(jpg|jpeg|png)$/', $_SERVER['REQUEST_URI']) || str_contains($_SERVER['REQUEST_URI'], '/files/'))) { require_once 'file_server.php'; exit; } // Load enabled module routes before the global route scan. $load_enabled_module_routes = static function (): void { $modules_path = WD . DIRECTORY_SEPARATOR . 'modules'; if (!is_dir($modules_path)) { return; } $module_dirs = array_filter(scandir($modules_path), static function (string $item) use ($modules_path): bool { return $item !== '.' && $item !== '..' && is_dir($modules_path . DIRECTORY_SEPARATOR . $item); }); foreach ($module_dirs as $module_dir) { $routes_path = $modules_path . DIRECTORY_SEPARATOR . $module_dir . DIRECTORY_SEPARATOR . 'routes'; if (!is_dir($routes_path)) { continue; } $module_class = 'classes\\' . $module_dir; if (!class_exists($module_class)) { continue; } try { $module = new $module_class(); if (method_exists($module, 'isEnabled') && !$module->isEnabled()) { continue; } } catch (\Throwable $exception) { continue; } foreach (scandir($routes_path) as $file) { if ($file === '.' || $file === '..') { continue; } require_once $routes_path . DIRECTORY_SEPARATOR . $file; } } }; $load_enabled_module_routes(); // Autoload all the routes /** @var router $router */ $router->auto_load_routes(WD . '/routes');