| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- <?php
- namespace App\Services;
- use Illuminate\Http\Request;
- use Illuminate\Support\Str;
- class NavigationContextService
- {
- private const SESSION_KEY = 'navigation';
- private const MAX_CONTEXTS = 30;
- private const MAX_STACK_SIZE = 20;
- private const TTL_SECONDS = 86400;
- public function getOrCreateToken(Request $request): string
- {
- $this->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]);
- }
- }
|