wip: booking process, refactoring

This commit is contained in:
Björn Fromme
2025-07-16 19:37:17 +02:00
parent 47faa6b08e
commit eecea0abd1
43 changed files with 2269 additions and 366 deletions
+40
View File
@@ -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']);
}
}
}
+56 -87
View File
@@ -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),
};
}
}