Spaces:
Running
Running
| declare(strict_types=1); | |
| namespace App\Domain\Analytics; | |
| final class AnalyticsSessionTransitioner | |
| { | |
| private const ALLOWED = [ | |
| 'open' => ['closed'], | |
| ]; | |
| public function __construct(private readonly AnalyticsSessionRepository $sessions) {} | |
| /** | |
| * Transition a session from open β closed. | |
| * Silently skips if the session does not exist (e.g. already expired / beacon race). | |
| * | |
| * @param int $durationSeconds Wall-clock seconds from started_at to close; caller computes this. | |
| */ | |
| public function close(string $sessionId, string $exitPath, int $durationSeconds): void | |
| { | |
| $session = $this->sessions->findById($sessionId); | |
| if ($session === null) { | |
| return; | |
| } | |
| $this->guard($session->status, 'closed'); | |
| $this->sessions->close($sessionId, $exitPath, $durationSeconds); | |
| } | |
| private function guard(string $from, string $to): void | |
| { | |
| $allowed = self::ALLOWED[$from] ?? []; | |
| if (!in_array($to, $allowed, true)) { | |
| throw new InvalidTransitionException($from, $to); | |
| } | |
| } | |
| } | |