feat: replace *Service suffix with role-based class names
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\XmlLoader\AgencyLoader;
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Security\Crypt;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\Cache\TagAwareCacheInterface;
|
||||
|
||||
/**
|
||||
* Handles loading and initializing booking data for edit mode.
|
||||
*
|
||||
* Encapsulates the logic for loading booking data from session or API,
|
||||
* refreshing availability data, and restoring drafts when available.
|
||||
*/
|
||||
class BookingEditDataLoader
|
||||
{
|
||||
public const string CACHE_TAG_USER_PREFIX = 'user_bookings_';
|
||||
|
||||
private bool $draftWasRestored = false;
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly BookingDataProcessor $bookingDataProcessor,
|
||||
private readonly BookingSessionStore $bookingSessionService,
|
||||
private readonly BookingChangeTracker $fingerprintService,
|
||||
private readonly TravelDataProvider $travelDataService,
|
||||
private readonly BookingEditDraftManager $draftService,
|
||||
private readonly AgencyLoader $agencyLoader,
|
||||
private readonly Crypt $crypt,
|
||||
private readonly TagAwareCacheInterface $bpnCache,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a draft was restored during the last load operation.
|
||||
*
|
||||
* This flag is reset on each call to loadFormData() and can be used
|
||||
* by the controller to show a flash message to the user.
|
||||
*/
|
||||
public function wasDraftRestored(): bool
|
||||
{
|
||||
return $this->draftWasRestored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads booking data from session or initializes from API.
|
||||
*
|
||||
* Uses session data if available and valid (same booking ID). This preserves
|
||||
* participant edits between form submissions within the same edit session.
|
||||
*
|
||||
* If no session data exists, loads fresh data from API. If a draft exists,
|
||||
* it is automatically applied on top of the fresh API data to restore
|
||||
* pending user edits.
|
||||
*
|
||||
* @param Request $request The HTTP request
|
||||
* @param int $bookingId The booking ID to load
|
||||
* @param User $user The authenticated user
|
||||
*
|
||||
* @return BookingDto|null The loaded booking DTO, or null if loading failed
|
||||
*/
|
||||
public function loadFormData(Request $request, int $bookingId, User $user): ?BookingDto
|
||||
{
|
||||
$this->draftWasRestored = false;
|
||||
|
||||
$formData = $this->bookingSessionService->getBookingDto($request, BookingDto::MODE_EDIT);
|
||||
|
||||
// Validate session data matches requested booking - clear stale data if mismatched
|
||||
if (null !== $formData && $formData->booking?->id !== $bookingId) {
|
||||
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
|
||||
$formData = null;
|
||||
}
|
||||
|
||||
if (null === $formData) {
|
||||
return $this->initializeFromApi($request, $bookingId, $user);
|
||||
}
|
||||
|
||||
// Refresh availability and mutability data to ensure current state.
|
||||
// Availability is refreshed on every load; mutability is patched from cache.
|
||||
$this->travelDataService->enrichWithFreshAvailabilities($formData->travel);
|
||||
|
||||
$bookingData = $this->fetchBookingData($bookingId, $user);
|
||||
if (null !== $bookingData && !($bookingData instanceof Notification)) {
|
||||
$mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId, cached: true);
|
||||
if (null !== $mutableData) {
|
||||
$this->travelDataService->patchMutability($formData->travel, $mutableData);
|
||||
}
|
||||
}
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes booking data from API on first load and stores in session.
|
||||
*
|
||||
* If a draft exists for this user and booking, it is automatically applied
|
||||
* on top of the fresh API data. This ensures user input is preserved while
|
||||
* structural data (services, prices) remains current.
|
||||
*
|
||||
* @param Request $request The HTTP request
|
||||
* @param int $bookingId The booking ID to load
|
||||
* @param User $user The authenticated user
|
||||
*
|
||||
* @return BookingDto|null The loaded booking DTO, or null if loading failed
|
||||
*/
|
||||
public function initializeFromApi(Request $request, int $bookingId, User $user): ?BookingDto
|
||||
{
|
||||
$bookingData = $this->fetchBookingData($bookingId, $user);
|
||||
|
||||
if (null === $bookingData || $bookingData instanceof Notification) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$travelData = $this->travelDataService->getTravelData($bookingData->dateId);
|
||||
if (null === $travelData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use cached mutability data for form/UI state.
|
||||
$mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId, cached: true);
|
||||
// Force refresh ensures fresh data at edit start, then populates cache for subsequent loads
|
||||
$availabilities = $this->travelDataService->getAvailabilityData($bookingData->dateId, cached: true, forceRefresh: true);
|
||||
|
||||
if (null === $mutableData || null === $availabilities) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->travelDataService->patchAvailabilities($travelData, $availabilities);
|
||||
$this->travelDataService->patchMutability($travelData, $mutableData);
|
||||
|
||||
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData);
|
||||
|
||||
// Set agency code for internal agency detection (used by field state conditions)
|
||||
$formData->agencyCode = null !== $bookingData->agencyId
|
||||
? $this->agencyLoader->loadById($bookingData->agencyId)?->code
|
||||
: null;
|
||||
|
||||
// Set original fingerprint BEFORE applying draft, so dirty detection
|
||||
// compares against the original API data (not the draft-modified data)
|
||||
$formData->originalFingerprint = $this->fingerprintService->generateFingerprint($formData, true);
|
||||
|
||||
// Check for existing draft and apply if found
|
||||
$draft = $this->draftService->findDraft($user, $bookingId);
|
||||
if (null !== $draft) {
|
||||
$applied = $this->draftService->applyDraftToDto($draft, $formData, $travelData);
|
||||
if (true === $applied) {
|
||||
$this->draftWasRestored = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->bookingSessionService->saveBookingDto($request, $formData, BookingDto::MODE_EDIT);
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches booking data from API with caching.
|
||||
*
|
||||
* Cache entries are tagged with the user ID to allow bulk invalidation
|
||||
* when the user updates their personal data.
|
||||
*
|
||||
* @param int $bookingId The booking ID to fetch
|
||||
* @param User $user The authenticated user
|
||||
*
|
||||
* @return Booking|Notification|null The booking data, notification on error, or null on cache failure
|
||||
*/
|
||||
public function fetchBookingData(int $bookingId, User $user): Booking|Notification|null
|
||||
{
|
||||
$cacheKey = $this->getBookingCacheKey($bookingId, $user);
|
||||
$userTag = self::CACHE_TAG_USER_PREFIX.$user->getId();
|
||||
$email = $user->getEmail();
|
||||
$password = $this->crypt->decrypt($user->getPassword());
|
||||
|
||||
try {
|
||||
return $this->bpnCache->get($cacheKey, function (ItemInterface $item) use ($email, $password, $bookingId, $userTag) {
|
||||
$item->expiresAfter(300);
|
||||
$item->tag([$userTag]);
|
||||
|
||||
return $this->apiClient->getBooking($email, $password, $bookingId);
|
||||
});
|
||||
} catch (InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the booking cache after successful update.
|
||||
*
|
||||
* @param int $bookingId The booking ID to invalidate
|
||||
* @param User $user The authenticated user
|
||||
*/
|
||||
public function invalidateBookingCache(int $bookingId, User $user): void
|
||||
{
|
||||
try {
|
||||
$cacheKey = $this->getBookingCacheKey($bookingId, $user);
|
||||
$this->bpnCache->delete($cacheKey);
|
||||
} catch (InvalidArgumentException) {
|
||||
// Ignore cache deletion errors
|
||||
}
|
||||
}
|
||||
|
||||
private function getBookingCacheKey(int $bookingId, User $user): string
|
||||
{
|
||||
return sprintf('bpn_booking_%d_u_%d', $bookingId, $user->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates all cached bookings for a user.
|
||||
*
|
||||
* Called when the user updates their personal data to ensure
|
||||
* booking edits show the latest applicant information.
|
||||
*
|
||||
* @param User $user The user whose booking caches should be invalidated
|
||||
*/
|
||||
public function invalidateUserBookingCaches(User $user): void
|
||||
{
|
||||
try {
|
||||
$userTag = self::CACHE_TAG_USER_PREFIX.$user->getId();
|
||||
$this->bpnCache->invalidateTags([$userTag]);
|
||||
} catch (InvalidArgumentException) {
|
||||
// Ignore cache invalidation errors
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user