Files
myep/src/Service/BookingSessionManager.php
T

242 lines
7.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service;
use App\Exception\BookingSessionNotFoundException;
use App\Exception\HotelNotInTravelException;
use App\Exception\TravelNotFoundException;
use App\Form\Model\BookingDto;
use Symfony\Component\HttpFoundation\Request;
/**
* Owns booking-flow session state.
*
* This service keeps the HTTP session concerns separate from booking
* orchestration and pricing logic. It handles DTO persistence, baseline
* room snapshots, and return URL storage for the booking create/edit flows.
*/
class BookingSessionManager
{
public const BOOKING_CREATE_KEY = 'booking_create';
public const BOOKING_CREATE_BASELINE_KEY = 'booking_create_baseline_snapshot';
public const BOOKING_CREATE_AVAILABILITY_REFRESHED_AT_KEY = 'booking_create_availability_refreshed_at';
public const BOOKING_EDIT_KEY = 'booking_edit';
public const RETURN_URL_KEY = 'booking_return_url';
public const DEFAULT_RETURN_URL = 'https://www.ep-reisen.de';
private const CREATE_AVAILABILITY_REFRESH_INTERVAL = 600;
public function __construct(
private readonly TravelDataProvider $travelDataService,
) {
}
/**
* Retrieves the booking creation DTO from the session.
*
* @throws BookingSessionNotFoundException
* @throws TravelNotFoundException
* @throws HotelNotInTravelException
*/
public function getOrCreateBookingCreateDto(Request $request): BookingDto
{
$bookingDto = $this->getBookingDto($request, BookingDto::MODE_CREATE);
if (null !== $bookingDto) {
return $bookingDto;
}
throw new BookingSessionNotFoundException();
}
/**
* Gets or creates the baseline room selection snapshot for change detection.
*
* @return array<int, array{int, int, float}>
*/
public function getOrCreateBaselineSnapshot(Request $request, BookingDto $bookingCreateDto): array
{
$baselineKey = self::BOOKING_CREATE_BASELINE_KEY;
if (!$request->getSession()->has($baselineKey) || $request->query->has('reset_baseline')) {
return $this->saveBaselineSnapshot($request, $bookingCreateDto);
}
return $request->getSession()->get($baselineKey);
}
/**
* Stores the current room selection snapshot in the session.
*
* @return array<int, array{int, int, float}>
*/
public function saveBaselineSnapshot(Request $request, BookingDto $bookingCreateDto): array
{
$baseline = $bookingCreateDto->createRoomSelectionSnapshot();
$request->getSession()->set(self::BOOKING_CREATE_BASELINE_KEY, $baseline);
return $baseline;
}
/**
* Clears the baseline snapshot from the session.
*/
public function clearBaselineSnapshot(Request $request): void
{
$request->getSession()->remove(self::BOOKING_CREATE_BASELINE_KEY);
}
/**
* Saves the booking DTO to the session.
*/
public function saveBookingDto(Request $request, BookingDto $bookingDto, string $mode): void
{
$sessionKey = $this->getSessionKey($mode);
$request->getSession()->set($sessionKey, $bookingDto);
}
/**
* Retrieves the booking DTO from the session and restores the Travel object.
*/
public function getBookingDto(Request $request, string $mode): ?BookingDto
{
$sessionKey = $this->getSessionKey($mode);
$session = $request->getSession();
if (false === $session->has($sessionKey)) {
return null;
}
$bookingDto = $session->get($sessionKey);
$shouldCheck = BookingDto::MODE_CREATE === $mode && $this->shouldRefreshCreateAvailability($request);
$hydrated = $this->hydrate($bookingDto, $shouldCheck);
if (BookingDto::MODE_CREATE === $mode && true === $shouldCheck && true === $hydrated) {
$this->markCreateAvailabilityRefreshed($request);
}
return $bookingDto;
}
/**
* Clears the booking DTO from the session.
*/
public function clearBookingDto(Request $request, string $mode): void
{
$sessionKey = $this->getSessionKey($mode);
$request->getSession()->remove($sessionKey);
}
public function refreshCreateAvailability(
Request $request,
BookingDto $bookingDto,
bool $forceRefresh = false,
): BookingAvailabilityRefreshResult {
$beforeSnapshot = $bookingDto->createRoomSelectionSnapshot();
$availabilityRefreshed = $this->travelDataService->enrichWithFreshAvailabilities(
$bookingDto->travel,
true,
$forceRefresh,
refreshRooms: true,
);
$roomSelectionQuantityReset = $bookingDto->syncRoomSelectionsWithAvailableRooms();
$afterSnapshot = $bookingDto->createRoomSelectionSnapshot();
if (true === $roomSelectionQuantityReset) {
$bookingDto->resetParticipantAssignments();
}
$this->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE);
if (true === $forceRefresh && true === $availabilityRefreshed) {
$this->markCreateAvailabilityRefreshed($request);
}
return new BookingAvailabilityRefreshResult(
availabilityRefreshed: $availabilityRefreshed,
roomSelectionQuantityReset: $roomSelectionQuantityReset,
roomSelectionSnapshotChanged: $beforeSnapshot !== $afterSnapshot,
hasAvailableRooms: [] !== $bookingDto->travel->getAvailableRooms(),
);
}
/**
* Clears all booking-related session data except the return URL.
*/
public function clearBookingSession(Request $request): void
{
$session = $request->getSession();
$session->remove(self::BOOKING_CREATE_KEY);
$session->remove(self::BOOKING_CREATE_BASELINE_KEY);
$session->remove(self::BOOKING_CREATE_AVAILABILITY_REFRESHED_AT_KEY);
}
/**
* Stores the return URL in the session.
*/
public function storeReturnUrl(Request $request, ?string $returnUrl): void
{
$url = self::DEFAULT_RETURN_URL;
if (null !== $returnUrl && '' !== trim($returnUrl)) {
if (false !== filter_var($returnUrl, \FILTER_VALIDATE_URL)
&& 1 === preg_match('#^https?://#i', $returnUrl)) {
$url = $returnUrl;
}
}
$request->getSession()->set(self::RETURN_URL_KEY, $url);
}
/**
* Retrieves the return URL from the session.
*/
public function getReturnUrl(Request $request): string
{
return $request->getSession()->get(self::RETURN_URL_KEY, self::DEFAULT_RETURN_URL);
}
private function getSessionKey(string $mode): string
{
return BookingDto::MODE_EDIT === $mode ? self::BOOKING_EDIT_KEY : self::BOOKING_CREATE_KEY;
}
/**
* Restores the full Travel object after session deserialization.
*/
public function markCreateAvailabilityRefreshed(Request $request): void
{
$request->getSession()->set(self::BOOKING_CREATE_AVAILABILITY_REFRESHED_AT_KEY, time());
}
private function shouldRefreshCreateAvailability(Request $request): bool
{
$lastRefreshedAt = (int) $request->getSession()->get(self::BOOKING_CREATE_AVAILABILITY_REFRESHED_AT_KEY, 0);
return 0 === $lastRefreshedAt
|| (time() - $lastRefreshedAt) >= self::CREATE_AVAILABILITY_REFRESH_INTERVAL;
}
private function hydrate(BookingDto $bookingDto, bool $forceRefresh = false): bool
{
$travel = $this->travelDataService->getTravelData(
$bookingDto->travel->id,
$bookingDto->hotelId
);
if (null === $travel) {
return false;
}
$availabilityRefreshed = $this->travelDataService->enrichWithFreshAvailabilities($travel, true, $forceRefresh, refreshRooms: true);
$bookingDto->travel = $travel;
if (null !== $bookingDto->booking) {
$bookingDto->booking->travelData = $travel;
}
return $availabilityRefreshed;
}
}