wip: booking process, refactoring
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class ProductController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly CacheInterface $cache,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/products', name: 'api_products')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
try {
|
||||
$products = $this->cache->get('api_products', function (ItemInterface $item) {
|
||||
$item->expiresAfter(3600); // 1 hour cache
|
||||
|
||||
return $this->apiClient->getProducts();
|
||||
});
|
||||
|
||||
return $this->json($products);
|
||||
} catch (InvalidArgumentException|ApiClientException $e) {
|
||||
return $this->json(['error' => $e->getMessage(), 'type' => 'api_error']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,116 +5,91 @@ namespace App\Controller\Api;
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\Utility\TravelCodeUtility;
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use App\BusProNet\Utility\DateCodeUtility;
|
||||
use App\Service\TravelDataService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapDateTime;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class TravelController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TravelLoader $travelXmlLoader,
|
||||
private readonly HotelLoader $hotelXmlLoader,
|
||||
private readonly PickupLoader $pickupXmlLoader,
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly TravelDataService $travelDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/travels', name: 'api_travel_mapping')]
|
||||
public function mapping(): JsonResponse
|
||||
{
|
||||
$mapping = $this->travelXmlLoader->generateFilesMap();
|
||||
$mapping = $this->travelDataService->generateFilesMap();
|
||||
|
||||
return $this->json($mapping);
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/travels/{travelId}/{hotelId}',
|
||||
path: '/travels/{dateId}/{hotelId}',
|
||||
name: 'api_travel_single_id',
|
||||
requirements: ['travelId' => '\d+', 'hotelId' => '\d+'],
|
||||
requirements: ['dateId' => '\d+', 'hotelId' => '\d+'],
|
||||
defaults: ['hotelId' => null],
|
||||
)]
|
||||
public function singleById(int $travelId, ?int $hotelId = null): JsonResponse
|
||||
public function byId(Request $request, int $dateId, ?int $hotelId = null): JsonResponse
|
||||
{
|
||||
$travel = $this->loadCached($travelId, $hotelId);
|
||||
$source = $request->query->get('source');
|
||||
$preferRemote = $request->query->getBoolean('prefer_remote');
|
||||
|
||||
$travel = $this->loadTravelData($dateId, $hotelId, $source, $preferRemote);
|
||||
|
||||
if (null === $travel) {
|
||||
return $this->json(['message' => 'Travel not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->json($travel, Response::HTTP_OK, [], ['groups' => ['api:list', 'api:single']]);
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/travels/{travelId}/remote',
|
||||
name: 'api_travel_single_id_remote',
|
||||
requirements: ['travelId' => '\d+'],
|
||||
)]
|
||||
public function singleByIdRemote(int $travelId): JsonResponse
|
||||
{
|
||||
$cacheKey = sprintf('bpn_travel_remote_%d', $travelId);
|
||||
|
||||
try {
|
||||
$result = $this->cache->get($cacheKey, function (ItemInterface $item) use ($travelId) {
|
||||
$item->expiresAfter(300); // 5 minutes cache for remote API calls
|
||||
|
||||
try {
|
||||
return $this->apiClient->getTravelData($travelId);
|
||||
} catch (ApiClientException $e) {
|
||||
// Return error info instead of throwing to avoid cache wrapping issues
|
||||
return ['error' => $e->getMessage(), 'type' => 'api_error'];
|
||||
}
|
||||
});
|
||||
|
||||
// Check if result is an error
|
||||
if (is_array($result) && isset($result['error'], $result['type'])) {
|
||||
return $this->json($result['error'], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return $this->json($result, Response::HTTP_OK, [], ['groups' => ['api:list', 'api:single']]);
|
||||
} catch (InvalidArgumentException) {
|
||||
return $this->json('Cache error occurred', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/travels/{travelCode}/{hotelCode}',
|
||||
path: '/travels/{dateCode}/{hotelCode}',
|
||||
name: 'api_travel_single_code',
|
||||
defaults: ['hotelCode' => null],
|
||||
)]
|
||||
public function singleByCode(string $travelCode, ?string $hotelCode = null): JsonResponse
|
||||
public function byCode(Request $request, $dateCode, ?string $hotelCode = null): JsonResponse
|
||||
{
|
||||
// sanitize travel code by removing potential dividers
|
||||
$travelCode = (new TravelCodeUtility())->sanitize($travelCode);
|
||||
// sanitize date code by removing potential dividers
|
||||
$dateCode = (new DateCodeUtility())->sanitize($dateCode);
|
||||
|
||||
$travelId = $this->travelXmlLoader->mapCodeToId($travelCode);
|
||||
$hotelId = $hotelCode ? $this->hotelXmlLoader->mapCodeToId($travelCode) : null;
|
||||
$dateId = $this->travelDataService->mapDateCodeToId($dateCode);
|
||||
$hotelId = $hotelCode ? $this->travelDataService->mapHotelCodeToId($hotelCode) : null;
|
||||
|
||||
if (null === $travelId) {
|
||||
if (null === $dateId) {
|
||||
return $this->json(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$travel = $this->loadCached($travelId, $hotelId);
|
||||
$source = $request->query->get('source');
|
||||
$preferRemote = $request->query->getBoolean('prefer_remote');
|
||||
|
||||
$travel = $this->loadTravelData($dateId, $hotelId, $source, $preferRemote);
|
||||
|
||||
if (null === $travel) {
|
||||
return $this->json(['message' => 'Travel not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->json($travel, Response::HTTP_OK, [], ['groups' => ['api:list', 'api:single']]);
|
||||
}
|
||||
|
||||
#[Route('/travels/{travelId}/{hotelId}/{dateTo}/availability', name: 'api_travel_hotel_availability')]
|
||||
#[Route('/travels/{dateId}/{hotelId}/{dateTo}/availability', name: 'api_travel_hotel_availability')]
|
||||
public function hotelAvailability(
|
||||
int $travelId,
|
||||
int $dateId,
|
||||
int $hotelId,
|
||||
#[MapDateTime(format: 'Y-m-d')] \DateTimeImmutable $dateTo
|
||||
#[MapDateTime(format: 'Y-m-d')] \DateTimeImmutable $dateTo,
|
||||
): JsonResponse {
|
||||
try {
|
||||
$result = $this->apiClient->getHotelAvailability($travelId, $hotelId, $dateTo);
|
||||
$result = $this->apiClient->getHotelAvailability($dateId, $hotelId, $dateTo);
|
||||
|
||||
return $this->json($result);
|
||||
} catch (ApiClientException $e) {
|
||||
@@ -122,32 +97,26 @@ class TravelController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
private function loadCached(int $travelId, ?int $hotelId = null): ?Travel
|
||||
{
|
||||
$cacheKey = sprintf('bpn_travel_%d_%d', $travelId, $hotelId ?? 0);
|
||||
|
||||
try {
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($travelId, $hotelId) {
|
||||
$item->expiresAfter(60);
|
||||
|
||||
try {
|
||||
$travel = $this->travelXmlLoader->loadById($travelId, $hotelId);
|
||||
|
||||
if (null === $travel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->pickupXmlLoader->patchPickupsDetails($travel);
|
||||
$this->hotelXmlLoader->patchHotelDetails($travel);
|
||||
|
||||
return $travel;
|
||||
} catch (\Exception) {
|
||||
// Return null for any loader exceptions to avoid cache wrapping issues
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} catch (InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Loads travel data based on the provided parameters.
|
||||
*
|
||||
* @param int $dateId the ID of the date for which travel data is requested
|
||||
* @param int|null $hotelId the ID of the hotel for which travel data is requested (optional)
|
||||
* @param string|null $source the source of the travel data ('local', 'remote', or null for default behavior)
|
||||
* @param bool $preferRemote whether to prefer remote data when the source is not explicitly specified
|
||||
*
|
||||
* @return Travel|null returns a Travel object if data is found, or null if no data is available
|
||||
*/
|
||||
private function loadTravelData(
|
||||
int $dateId,
|
||||
?int $hotelId = null,
|
||||
?string $source = null,
|
||||
bool $preferRemote = false,
|
||||
): ?Travel {
|
||||
return match ($source) {
|
||||
'local' => $this->travelDataService->getTravelDataFromXml($dateId, $hotelId),
|
||||
'remote' => $this->travelDataService->getTravelDataFromApi($dateId, $hotelId),
|
||||
default => $this->travelDataService->getTravelData($dateId, $hotelId, $preferRemote),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
|
||||
namespace App\Controller\Booking;
|
||||
|
||||
use App\Service\BookingService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use App\Form\BookingCreateStep1Type;
|
||||
use App\Form\BookingCreateStep2Type;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Service\BookingService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class CreateController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BookingService $bookingCreateService,
|
||||
) {
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/bookings/create', name: 'app_booking_create_step_1')]
|
||||
@@ -25,14 +26,16 @@ class CreateController extends AbstractController
|
||||
// Validate step access - allow step 1 or redirect to current step
|
||||
$this->validateStepAccess($bookingCreateDto, 1);
|
||||
|
||||
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto);
|
||||
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
|
||||
'validation_groups' => ['booking_create_step_1'],
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$bookingCreateDto->currentStep = 2;
|
||||
$this->bookingCreateService->saveBookingCreateDto($request, $bookingCreateDto);
|
||||
|
||||
|
||||
return $this->redirectToRoute('app_booking_create_step_2');
|
||||
}
|
||||
|
||||
@@ -46,18 +49,30 @@ class CreateController extends AbstractController
|
||||
public function participants(Request $request): Response
|
||||
{
|
||||
$bookingCreateDto = $this->bookingCreateService->getOrCreateBookingCreateDto($request);
|
||||
|
||||
|
||||
// Validate step access
|
||||
$this->validateStepAccess($bookingCreateDto, 2);
|
||||
|
||||
$form = $this->createForm(BookingCreateStep2Type::class, $bookingCreateDto);
|
||||
// Ensure correct number of participants
|
||||
if ($bookingCreateDto->getParticipantsCount() !== count($bookingCreateDto->participants)) {
|
||||
$participants = $bookingCreateDto->participants;
|
||||
$bookingCreateDto->participants = [];
|
||||
for ($i = 0; $i < $bookingCreateDto->getParticipantsCount(); ++$i) {
|
||||
$bookingCreateDto->participants[] = $participants[$i] ?? new ParticipantDto();
|
||||
}
|
||||
}
|
||||
|
||||
$form = $this->createForm(BookingCreateStep2Type::class, $bookingCreateDto, [
|
||||
'attr' => ['novalidate' => 'novalidate'],
|
||||
'validation_groups' => ['booking_create_step_2'],
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$bookingCreateDto->currentStep = 3;
|
||||
$this->bookingCreateService->saveBookingCreateDto($request, $bookingCreateDto);
|
||||
|
||||
|
||||
return $this->redirectToRoute('app_booking_create_step_3');
|
||||
}
|
||||
|
||||
@@ -74,7 +89,7 @@ class CreateController extends AbstractController
|
||||
|
||||
// Validate step access
|
||||
$this->validateStepAccess($bookingCreateDto, 3);
|
||||
|
||||
|
||||
return $this->render('booking/confirm.html.twig', [
|
||||
'bookingCreateDto' => $bookingCreateDto,
|
||||
]);
|
||||
@@ -84,14 +99,13 @@ class CreateController extends AbstractController
|
||||
* Validates step access and redirects if necessary.
|
||||
*
|
||||
* @param \App\Form\Model\BookingCreateDto $bookingCreateDto
|
||||
* @param int $expectedStep
|
||||
*/
|
||||
private function validateStepAccess($bookingCreateDto, int $expectedStep): void
|
||||
{
|
||||
// Allow access to current step or any previous step
|
||||
if ($expectedStep > $bookingCreateDto->currentStep) {
|
||||
$this->addFlash('error', 'Bitte erst die vorherigen Schritte abschließen.');
|
||||
|
||||
|
||||
$this->redirectToCurrentStep($bookingCreateDto);
|
||||
}
|
||||
}
|
||||
@@ -102,17 +116,17 @@ class CreateController extends AbstractController
|
||||
private function redirectToCurrentStep($bookingCreateDto): void
|
||||
{
|
||||
$routeParams = [
|
||||
'travel_id' => $bookingCreateDto->travelData->id,
|
||||
'date_id' => $bookingCreateDto->travelData->id,
|
||||
'hotel_id' => $bookingCreateDto->travelData->hotelId,
|
||||
];
|
||||
|
||||
$route = match ($bookingCreateDto->currentStep) {
|
||||
1 => 'app_booking_create_step_1',
|
||||
2 => 'app_booking_create_step_2',
|
||||
2 => 'app_booking_create_step_2',
|
||||
3 => 'app_booking_create_step_3',
|
||||
default => 'app_booking_create_step_1',
|
||||
};
|
||||
|
||||
$this->redirectToRoute($route, $routeParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
|
||||
@@ -6,12 +6,12 @@ use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use App\Controller\Traits\BookingDataTrait;
|
||||
use App\Entity\User;
|
||||
use App\Form\BookingEditType;
|
||||
use App\Form\Model\BookingEditDto;
|
||||
use App\Security\Crypt;
|
||||
use App\Service\TravelDataService;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -28,7 +28,7 @@ class EditController extends AbstractController
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly TravelLoader $travelDataLoader,
|
||||
private readonly TravelDataService $travelDataService,
|
||||
private readonly PickupLoader $pickupDataLoader,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly Security $security,
|
||||
@@ -58,7 +58,7 @@ class EditController extends AbstractController
|
||||
$this->denyAccessUnlessGranted('EDIT', $bookingData);
|
||||
|
||||
// Load according travel data
|
||||
$travelData = $this->travelDataLoader->loadById($bookingData->travelId);
|
||||
$travelData = $this->travelDataService->getTravelData($bookingData->dateId);
|
||||
|
||||
if (null === $travelData) {
|
||||
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
|
||||
@@ -66,34 +66,26 @@ class EditController extends AbstractController
|
||||
return $this->redirectToRoute('app_bookings');
|
||||
}
|
||||
|
||||
// Fetch mutability information via API
|
||||
try {
|
||||
$mutableData = $this->apiClient->getMutableData($bookingData->travelId);
|
||||
} catch (ApiClientException $e) {
|
||||
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
|
||||
// Fetch mutability and availability information via service
|
||||
$mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId);
|
||||
$availabilities = $this->travelDataService->getAvailabilityData($bookingData->dateId);
|
||||
|
||||
return $this->redirectToRoute('app_bookings');
|
||||
}
|
||||
|
||||
// Fetch availability information via API
|
||||
try {
|
||||
$availabilities = $this->apiClient->getAvailabilities($bookingData->travelId);
|
||||
} catch (ApiClientException $e) {
|
||||
if (null === $mutableData || null === $availabilities) {
|
||||
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
|
||||
|
||||
return $this->redirectToRoute('app_bookings');
|
||||
}
|
||||
|
||||
// Patch travel data with additional information from above
|
||||
$this->travelDataLoader->patchAvailabilities($travelData, $availabilities);
|
||||
$this->travelDataLoader->patchMutability($travelData, $mutableData);
|
||||
$this->pickupDataLoader->patchPickupsDetails($travelData);
|
||||
$this->travelDataService->patchAvailabilities($travelData, $availabilities);
|
||||
$this->travelDataService->patchMutability($travelData, $mutableData);
|
||||
|
||||
// Create DTO for form
|
||||
$formData = BookingEditDto::fromBooking($bookingData, $travelData);
|
||||
|
||||
$form = $this->createForm(BookingEditType::class, $formData, [
|
||||
'attr' => ['novalidate' => 'novalidate'],
|
||||
'validation_groups' => ['booking_edit'],
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -26,9 +26,9 @@ 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 LoggerInterface $logger Logger for audit trails and debugging
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
@@ -45,6 +45,7 @@ class PersonalDataController extends AbstractController
|
||||
* communication details. Uses the Post-Redirect-Get pattern for form processing.
|
||||
*
|
||||
* @param Request $request The HTTP request containing form data
|
||||
*
|
||||
* @return Response The rendered personal data page or redirect response
|
||||
*
|
||||
* @throws ApiClientException When BusProNet API communication fails
|
||||
|
||||
Reference in New Issue
Block a user