getCode() . ') at ' . $throwable->getFile() . ':' . (string)$throwable->getLine() . ': ' . $throwable->getMessage(); } $loggerFailure = null; try { $logger = null; if (function_exists('abj_service_optional')) { $logger = abj_service_optional('logging'); } if (!is_object($logger) && class_exists('ABJ_404_Solution_Logging', false)) { $logger = ABJ_404_Solution_Logging::getInstance(); } if (is_object($logger) && method_exists($logger, 'warn')) { $logger->warn($line); return; } if (is_object($logger) && method_exists($logger, 'errorMessage')) { $logger->errorMessage($line, $throwable instanceof Exception ? $throwable : null); return; } } catch (\Throwable $loggingError) { $loggerFailure = $loggingError; } $fallback = $line; if ($loggerFailure !== null) { $fallback .= ' | logger failure (code ' . (string)$loggerFailure->getCode() . ') at ' . $loggerFailure->getFile() . ':' . (string)$loggerFailure->getLine() . ': ' . $loggerFailure->getMessage(); } abj404_logPhpFallback('early-boot', $fallback); } } if (!function_exists('abj404_resolve_clock')) { /** * Resolve the project clock from the root plugin bootstrap. * * The normal service helper is not available until Loader.php pulls in the * bootstrap files, but root-file callbacks also run before or after that * boundary. This keeps those callbacks on the same clock abstraction without * making early boot depend on the fully initialized container. * * @return object|null */ function abj404_resolve_clock() { static $fallbackClock = null; try { if (function_exists('abj_clock')) { return abj_clock(); } if (is_object($fallbackClock)) { return $fallbackClock; } if (class_exists('ABJ_404_Solution_SystemClock')) { $fallbackClock = new ABJ_404_Solution_SystemClock(); return $fallbackClock; } } catch (\Throwable $e) { abj404_logRuntimeWarning('Clock resolution failed in root bootstrap', $e); } return null; } } if (!function_exists('abj404_now')) { /** * Current epoch seconds for root-file callbacks. * * @return int */ function abj404_now(): int { $clock = abj404_resolve_clock(); if (is_object($clock) && method_exists($clock, 'now')) { try { return (int)$clock->now(); } catch (\Throwable $e) { abj404_logRuntimeWarning('Clock now() failed in root bootstrap', $e); } } static $reportedUnavailable = false; if (!$reportedUnavailable) { abj404_logRuntimeWarning('Clock unavailable in root bootstrap; using zero epoch fallback'); $reportedUnavailable = true; } return 0; } } if (!function_exists('abj404_now_float')) { /** * Current epoch seconds with sub-second precision for root-file callbacks. * * @return float */ function abj404_now_float(): float { $clock = abj404_resolve_clock(); if (is_object($clock) && method_exists($clock, 'nowFloat')) { try { return (float)$clock->nowFloat(); } catch (\Throwable $e) { abj404_logRuntimeWarning('Clock nowFloat() failed in root bootstrap', $e); } } return (float)abj404_now(); } } if (!function_exists('abj404_current_user_is_plugin_admin')) { /** * Root-file policy gate for admin callbacks declared before normal classes run. * * Degraded boot screens still use direct WordPress capabilities because the * policy class can be among the missing files. Runtime admin callbacks should * call this helper so delegated plugin admins follow the same authorization * decision everywhere. * * @return bool */ function abj404_current_user_is_plugin_admin(): bool { try { if (class_exists('ABJ_404_Solution_PluginAdminAccessPolicy')) { return ABJ_404_Solution_PluginAdminAccessPolicy::currentUserCanAccessPluginAdmin(); } abj404_logRuntimeWarning( 'plugin admin access policy resolution failed because PluginAdminAccessPolicy is unavailable.' ); } catch (\Throwable $e) { abj404_logRuntimeWarning('plugin admin access policy resolution failed', $e); } return false; } }