pruneExpired(); $token = trim((string) $request->input('nav', '')); if ($token !== '' && $this->contextExists($token)) { return $token; } $token = bin2hex(random_bytes(8)); $this->saveContext($token, [ 'updated_at' => time(), 'stack' => [], ]); return $token; } public function rememberCurrentPage(Request $request, string $token): void { if (!$this->isGetPage($request)) { return; } $this->appendUrl($token, $request->fullUrl()); } public function backUrl(Request $request, string $token, ?string $fallback = null): ?string { $context = $this->context($token); $stack = $context['stack'] ?? []; if (empty($stack)) { return $fallback; } $currentUrl = $this->normalizeUrl($request->fullUrl()); $lastUrl = end($stack); if ($lastUrl === $currentUrl) { $previousUrl = prev($stack); return $previousUrl !== false ? $previousUrl : $fallback; } return $lastUrl ?: $fallback; } public function parentUrl(string $token, ?string $fallback = null): ?string { $context = $this->context($token); $stack = $context['stack'] ?? []; if (empty($stack)) { return $fallback; } $lastUrl = array_pop($stack); if (empty($stack)) { return $fallback ?? $lastUrl; } return end($stack) ?: $fallback; } public function routeParams(array $params, string $token): array { return array_merge($params, ['nav' => $token]); } public function pruneExpired(): void { $contexts = session(self::SESSION_KEY, []); if (empty($contexts)) { return; } $now = time(); foreach ($contexts as $token => $context) { $updatedAt = (int) ($context['updated_at'] ?? 0); if ($updatedAt < ($now - self::TTL_SECONDS)) { unset($contexts[$token]); } } if (count($contexts) > self::MAX_CONTEXTS) { uasort($contexts, static fn (array $left, array $right) => ($left['updated_at'] ?? 0) <=> ($right['updated_at'] ?? 0)); $contexts = array_slice($contexts, -self::MAX_CONTEXTS, null, true); } session([self::SESSION_KEY => $contexts]); } public function forgetToken(string $token): void { $contexts = session(self::SESSION_KEY, []); unset($contexts[$token]); session([self::SESSION_KEY => $contexts]); } private function appendUrl(string $token, string $url): void { $normalizedUrl = $this->normalizeUrl($url); if ($normalizedUrl === '') { return; } $context = $this->context($token); $stack = $context['stack'] ?? []; $stack = array_values(array_filter($stack, static fn (string $storedUrl) => $storedUrl !== $normalizedUrl)); $stack[] = $normalizedUrl; if (count($stack) > self::MAX_STACK_SIZE) { $stack = array_slice($stack, -self::MAX_STACK_SIZE); } $context['updated_at'] = time(); $context['stack'] = $stack; $this->saveContext($token, $context); } private function normalizeUrl(string $url): string { $parts = parse_url($url); if ($parts === false || empty($parts['path'])) { return ''; } $query = []; if (!empty($parts['query'])) { parse_str($parts['query'], $query); unset($query['nav']); } $normalizedUrl = ''; if (!empty($parts['scheme'])) { $normalizedUrl .= $parts['scheme'] . '://'; } if (!empty($parts['user'])) { $normalizedUrl .= $parts['user']; if (!empty($parts['pass'])) { $normalizedUrl .= ':' . $parts['pass']; } $normalizedUrl .= '@'; } if (!empty($parts['host'])) { $normalizedUrl .= $parts['host']; } if (!empty($parts['port'])) { $normalizedUrl .= ':' . $parts['port']; } $normalizedUrl .= $parts['path']; if (!empty($query)) { $normalizedUrl .= '?' . http_build_query($query); } return $normalizedUrl; } private function isGetPage(Request $request): bool { return strtoupper($request->method()) === 'GET'; } private function contextExists(string $token): bool { return array_key_exists($token, session(self::SESSION_KEY, [])); } private function context(string $token): array { return session(self::SESSION_KEY . '.' . $token, [ 'updated_at' => time(), 'stack' => [], ]); } private function saveContext(string $token, array $context): void { session([self::SESSION_KEY . '.' . $token => $context]); } }