|null */ private static ?array $context = null; public static function start(array $overrides = []): void { if (self::$context === null) { self::$context = self::buildDefaultContext(); } self::applyOverrides($overrides); } public static function ensureStarted(): void { if (self::$context !== null) { return; } self::$context = self::buildDefaultContext(); } // Returns the request ID, generating one if missing. Use this when you need a guaranteed value. public static function id(): string { self::ensureStarted(); $requestId = trim((string) (self::$context['request_id'] ?? '')); if ($requestId === '') { $requestId = RepoQuery::uuidV4(); self::$context['request_id'] = $requestId; } return $requestId; } // Returns the ID only if already set — safe to call before start() (e.g. in error handlers). public static function currentId(): ?string { if (self::$context === null) { return null; } $requestId = trim((string) (self::$context['request_id'] ?? '')); return $requestId !== '' ? $requestId : null; } public static function setChannel(string $channel): void { self::ensureStarted(); $normalized = self::normalizeChannel($channel); if ($normalized !== '') { self::$context['channel'] = $normalized; } } public static function setPath(string $path): void { self::ensureStarted(); $path = trim($path); if ($path === '') { return; } self::$context['path'] = substr($path, 0, 255); } public static function context(): array { self::ensureStarted(); return self::$context ?? []; } public static function requestHeaderValue(): string { return self::id(); } // HMAC when a key is configured (e.g. hashing IPs for audit logs) — plain SHA256 as fallback. public static function hashValue(string $value): string { $secret = defined('APP_CRYPTO_KEY') ? trim((string) APP_CRYPTO_KEY) : ''; if ($secret === '') { return hash('sha256', $value); } return hash_hmac('sha256', $value, $secret); } public static function resetForTests(): void { self::$context = null; } /** * @return array */ private static function buildDefaultContext(): array { $method = strtoupper(trim((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'))); if ($method === '') { $method = PHP_SAPI === 'cli' ? 'CLI' : 'GET'; } $uri = (string) ($_SERVER['REQUEST_URI'] ?? ''); $path = parse_url($uri, PHP_URL_PATH); $path = is_string($path) ? trim($path) : ''; if ($path === '' && PHP_SAPI === 'cli') { $path = (string) ($_SERVER['argv'][0] ?? 'cli'); } $requestId = self::requestIdFromHeader(); if ($requestId === null) { $requestId = RepoQuery::uuidV4(); } return [ 'request_id' => $requestId, 'channel' => self::detectChannel($path), 'method' => substr($method, 0, 8), 'path' => substr($path, 0, 255), 'ip' => self::normalizeIp((string) ($_SERVER['REMOTE_ADDR'] ?? '')), 'user_agent' => self::normalizeUserAgent((string) ($_SERVER['HTTP_USER_AGENT'] ?? '')), ]; } private static function applyOverrides(array $overrides): void { foreach ($overrides as $key => $value) { if (!is_string($key)) { continue; } if ($key === 'request_id') { $requestId = trim((string) $value); if (self::isValidUuid($requestId)) { self::$context['request_id'] = $requestId; } continue; } if ($key === 'channel') { $channel = self::normalizeChannel((string) $value); if ($channel !== '') { self::$context['channel'] = $channel; } continue; } if ($key === 'method') { $method = strtoupper(trim((string) $value)); if ($method !== '') { self::$context['method'] = substr($method, 0, 8); } continue; } if ($key === 'path') { $path = trim((string) $value); if ($path !== '') { self::$context['path'] = substr($path, 0, 255); } continue; } } } private static function detectChannel(string $path): string { if (PHP_SAPI === 'cli') { return 'cli'; } if (defined('MINTY_API_REQUEST')) { return 'api'; } $normalizedPath = ltrim(strtolower(trim($path)), '/'); if (str_starts_with($normalizedPath, 'api/')) { return 'api'; } return 'web'; } private static function normalizeChannel(string $channel): string { $channel = strtolower(trim($channel)); return in_array($channel, ['web', 'api', 'scheduler', 'cli'], true) ? $channel : ''; } // Accept a caller-supplied request ID for cross-service trace correlation. // Only trusted if it's a valid UUID format — arbitrary strings are ignored. private static function requestIdFromHeader(): ?string { $header = trim((string) ($_SERVER['HTTP_X_REQUEST_ID'] ?? '')); if (self::isValidUuid($header)) { return strtolower($header); } return null; } private static function isValidUuid(string $value): bool { return preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i', $value) === 1; } private static function normalizeIp(string $ip): string { $ip = trim($ip); if ($ip === '') { return ''; } return substr($ip, 0, 45); } private static function normalizeUserAgent(string $userAgent): string { $userAgent = trim($userAgent); if ($userAgent === '') { return ''; } return substr($userAgent, 0, 255); } }