diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml index 6899b72..cc0967e 100644 --- a/config/packages/cache.yaml +++ b/config/packages/cache.yaml @@ -15,5 +15,7 @@ framework: #app: cache.adapter.apcu # Namespaced pools use the above "app" backend by default - #pools: - #my.dedicated.cache: null + pools: + bpn.cache: + adapter: cache.adapter.filesystem + tags: true diff --git a/config/services.yaml b/config/services.yaml index 5230898..c4b2c24 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -84,6 +84,10 @@ services: $preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%' $enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%' + App\Service\BookingEditDataLoaderService: + arguments: + $bpnCache: '@bpn.cache' + App\Service\VoucherValidationService: arguments: $cache: '@cache.app' diff --git a/src/Controller/Account/PersonalDataController.php b/src/Controller/Account/PersonalDataController.php index 5fde2c3..9bcdc08 100644 --- a/src/Controller/Account/PersonalDataController.php +++ b/src/Controller/Account/PersonalDataController.php @@ -9,6 +9,7 @@ use App\BusProNet\Model\PersonalData; use App\Entity\User; use App\Form\PersonalDataType; use App\Security\Crypt; +use App\Service\BookingEditDataLoaderService; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -26,13 +27,15 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; class PersonalDataController extends AbstractController { /** - * @param ApiClient $apiClient BusProNet API client for data operations - * @param Crypt $crypt Encryption service for password handling - * @param LoggerInterface $logger Logger for audit trails and debugging + * @param ApiClient $apiClient BusProNet API client for data operations + * @param Crypt $crypt Encryption service for password handling + * @param BookingEditDataLoaderService $dataLoader Data loader for cache invalidation + * @param LoggerInterface $logger Logger for audit trails and debugging */ public function __construct( private readonly ApiClient $apiClient, private readonly Crypt $crypt, + private readonly BookingEditDataLoaderService $dataLoader, private readonly LoggerInterface $logger, ) { } @@ -87,6 +90,10 @@ class PersonalDataController extends AbstractController if ($personalDataForm->isSubmitted() && $personalDataForm->isValid()) { try { $this->apiClient->updatePersonalData($email, $password, $personalData); + + // Invalidate cached bookings to ensure edit mode shows updated applicant data + $this->dataLoader->invalidateUserBookingCaches($user); + $this->addFlash('success', 'Deine persönlichen Daten wurden aktualisiert'); $this->logger->info('Updated personal data', [ 'email' => $user->getEmail(), diff --git a/src/Controller/Booking/DownloadController.php b/src/Controller/Booking/DownloadController.php index fa72591..6d24ef1 100644 --- a/src/Controller/Booking/DownloadController.php +++ b/src/Controller/Booking/DownloadController.php @@ -5,7 +5,6 @@ namespace App\Controller\Booking; use App\BusProNet\ApiClient; use App\BusProNet\Exception\ApiClientException; use App\BusProNet\Model\Notification; -use App\Controller\Booking\Traits\BookingDataTrait; use App\Entity\User; use App\Security\Crypt; use Psr\Log\LoggerInterface; @@ -15,17 +14,13 @@ use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -use Symfony\Contracts\Cache\CacheInterface; use function Symfony\Component\String\u; class DownloadController extends AbstractController { - use BookingDataTrait; - public function __construct( private readonly ApiClient $apiClient, - private readonly CacheInterface $cache, private readonly Crypt $crypt, private readonly LoggerInterface $logger, ) { diff --git a/src/Controller/Booking/Edit/IndexController.php b/src/Controller/Booking/Edit/IndexController.php index fbceca7..b12ca90 100644 --- a/src/Controller/Booking/Edit/IndexController.php +++ b/src/Controller/Booking/Edit/IndexController.php @@ -94,7 +94,7 @@ class IndexController extends AbstractController } // Fetch booking data for display (surcharges, canceled status, etc.) - $bookingData = $this->dataLoader->fetchBookingData($email, $password, $id); + $bookingData = $this->dataLoader->fetchBookingData($email, $password, $id, $user); if (null === $bookingData || $bookingData instanceof Notification) { $this->addFlash('error', 'Buchungsdaten nicht (mehr) verfügbar'); @@ -166,7 +166,7 @@ class IndexController extends AbstractController } // Fetch booking data to check for canceled status - $bookingData = $this->dataLoader->fetchBookingData($email, $password, $id); + $bookingData = $this->dataLoader->fetchBookingData($email, $password, $id, $user); if (null === $bookingData || $bookingData instanceof Notification) { $this->addFlash('error', 'Buchungsdaten nicht (mehr) verfügbar'); @@ -269,7 +269,7 @@ class IndexController extends AbstractController $this->travelDataService->enrichWithFreshAvailabilities($bookingDto->travel); // Fetch booking data and mutable data - $bookingData = $this->dataLoader->fetchBookingData($email, $password, $id); + $bookingData = $this->dataLoader->fetchBookingData($email, $password, $id, $user); $mutableData = null !== $bookingData && !($bookingData instanceof Notification) ? $this->travelDataService->getMutabilityData($bookingData->dateId) : null; diff --git a/src/Controller/Booking/Traits/BookingDataTrait.php b/src/Controller/Booking/Traits/BookingDataTrait.php deleted file mode 100644 index 2ef45e0..0000000 --- a/src/Controller/Booking/Traits/BookingDataTrait.php +++ /dev/null @@ -1,27 +0,0 @@ -cache->get($cacheKey, function (ItemInterface $item) use ($email, $password, $id) { - $item->expiresAfter(300); - - return $this->apiClient->getBooking($email, $password, $id); - }); - } catch (InvalidArgumentException $e) { - $bookingData = null; - } - - return $bookingData; - } -} diff --git a/src/Service/BookingEditDataLoaderService.php b/src/Service/BookingEditDataLoaderService.php index e3ea58e..cbf65df 100644 --- a/src/Service/BookingEditDataLoaderService.php +++ b/src/Service/BookingEditDataLoaderService.php @@ -12,8 +12,8 @@ use App\Entity\User; use App\Form\Model\BookingDto; use Psr\Cache\InvalidArgumentException; use Symfony\Component\HttpFoundation\Request; -use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; /** * Handles loading and initializing booking data for edit mode. @@ -23,6 +23,8 @@ use Symfony\Contracts\Cache\ItemInterface; */ class BookingEditDataLoaderService { + public const CACHE_TAG_USER_PREFIX = 'user_bookings_'; + private bool $draftWasRestored = false; public function __construct( @@ -32,7 +34,7 @@ class BookingEditDataLoaderService private readonly BookingFingerprintService $fingerprintService, private readonly TravelDataService $travelDataService, private readonly BookingEditDraftService $draftService, - private readonly CacheInterface $cache, + private readonly TagAwareCacheInterface $bpnCache, ) { } @@ -101,7 +103,7 @@ class BookingEditDataLoaderService */ public function initializeFromApi(Request $request, int $bookingId, string $email, string $password, User $user): ?BookingDto { - $bookingData = $this->fetchBookingData($email, $password, $bookingId); + $bookingData = $this->fetchBookingData($email, $password, $bookingId, $user); if (null === $bookingData || $bookingData instanceof Notification) { return null; @@ -145,13 +147,26 @@ class BookingEditDataLoaderService /** * 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 string $email User email for API authentication + * @param string $password User password for API authentication + * @param int $bookingId The booking ID to fetch + * @param User $user The user for cache tagging + * + * @return Booking|Notification|null The booking data, notification on error, or null on cache failure */ - public function fetchBookingData(string $email, string $password, int $bookingId): Booking|Notification|null + public function fetchBookingData(string $email, string $password, int $bookingId, User $user): Booking|Notification|null { $cacheKey = sprintf('bpn_booking_%d', $bookingId); + $userTag = self::CACHE_TAG_USER_PREFIX.$user->getId(); + try { - return $this->cache->get($cacheKey, function (ItemInterface $item) use ($email, $password, $bookingId) { + 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); }); @@ -162,14 +177,34 @@ class BookingEditDataLoaderService /** * Invalidates the booking cache after successful update. + * + * @param int $bookingId The booking ID to invalidate */ public function invalidateBookingCache(int $bookingId): void { try { $cacheKey = sprintf('bpn_booking_%d', $bookingId); - $this->cache->delete($cacheKey); + $this->bpnCache->delete($cacheKey); } catch (InvalidArgumentException) { // Ignore cache deletion errors } } + + /** + * 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 + } + } }