feat: fetch additional content from cms via api

This commit is contained in:
Björn Fromme
2026-03-16 11:59:11 +01:00
parent fc05350ae4
commit a8c0a1aa61
18 changed files with 107 additions and 124 deletions
+1
View File
@@ -103,3 +103,4 @@ services:
App\Service\CmsDataService:
arguments:
$httpClient: '@typo3.client'
$apiKey: 'AbcAbc123'# dummy key, not yet implemented
+3
View File
@@ -26,6 +26,9 @@ class Travel
#[Groups(['api:list', 'api:single'])]
public ?string $code = null;
#[Groups(['api:list', 'api:single'])]
public ?string $productCode = null;
#[Groups(['api:list', 'api:single'])]
public ?string $label = null;
+7
View File
@@ -54,6 +54,13 @@ class TravelParser extends AbstractParser
$travel->dateFrom = $dateFrom;
$travel->dateTo = $dateTo;
$travel->code = $node->attr('code');
// Parse product code from parent reise node using DOM
$domNode = $node->getNode(0);
if (null !== $domNode && null !== $domNode->parentNode) {
$travel->productCode = $domNode->parentNode->getAttribute('code');
}
$travel->type = $node->attr('reiseart');
$travel->status = $this->getStringOrNullValue($node->filterXPath('//status_hin'));
$travel->priceFrom = $this->stringToFloat($this->getStringOrNullValue($node->filterXPath('//abpreis')));
@@ -9,6 +9,7 @@ use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
use App\Form\BookingCreateStep1Type;
use App\Htmx\HxTrait;
use App\Service\BookingService;
use App\Service\BookingSummaryDataService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -28,6 +29,7 @@ class Step1Controller extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSummaryDataService $summaryDataService,
) {
}
@@ -46,8 +48,6 @@ class Step1Controller extends AbstractController
// Get or create baseline snapshot for change detection
$oldRoomSelectionSnapshot = $this->bookingService->getOrCreateBaselineSnapshot($request, $bookingCreateDto);
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
// Validate step access - allow step 1 or redirect to current step
if ($redirect = $this->validateStepAccess($bookingCreateDto, 1)) {
return $redirect;
@@ -79,6 +79,9 @@ class Step1Controller extends AbstractController
return $this->hxRedirect($request, $this->generateUrl('app_booking_create_step_2'));
}
// Get complete summary data (pricing, rooms, CMS data)
$summary = $this->summaryDataService->getSummaryData($bookingCreateDto);
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
$groupedRooms = $this->bookingService->groupRoomsBySelectionType($availableRooms);
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($summary['selectedRooms'], $availableRooms);
@@ -87,7 +90,8 @@ class Step1Controller extends AbstractController
'bookingCreateDto' => $bookingCreateDto,
'roomSummary' => $summary['selectedRooms'],
'participantCount' => $summary['participantCount'],
'pricingData' => $summary['pricing'],
'pricingData' => $summary['pricingData'],
'cmsData' => $summary['cmsData'],
'form' => $form->createView(),
'groupedRooms' => $groupedRooms,
'groupedSelectedRooms' => $groupedSelectedRooms,
@@ -115,7 +119,8 @@ class Step1Controller extends AbstractController
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
// Get complete summary data (pricing, rooms, CMS data)
$summary = $this->summaryDataService->getSummaryData($bookingCreateDto);
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
$groupedRooms = $this->bookingService->groupRoomsBySelectionType($availableRooms);
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($summary['selectedRooms'], $availableRooms);
@@ -129,7 +134,8 @@ class Step1Controller extends AbstractController
'form' => $form->createView(),
'bookingCreateDto' => $bookingCreateDto,
'participantCount' => $summary['participantCount'],
'pricingData' => $summary['pricing'],
'pricingData' => $summary['pricingData'],
'cmsData' => $summary['cmsData'],
'groupedRooms' => $groupedRooms,
'groupedSelectedRooms' => $groupedSelectedRooms,
]
@@ -12,8 +12,8 @@ use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Service\ParticipantFieldOptionsProvider;
use App\Htmx\HxTrait;
use App\Service\BookingPriceCalculatorService;
use App\Service\BookingService;
use App\Service\BookingSummaryDataService;
use App\Service\ParticipantCardDataService;
use App\Service\ParticipantPrepopulationService;
use App\Service\RoomAssignmentService;
@@ -38,7 +38,7 @@ class Step2Controller extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingPriceCalculatorService $priceCalculator,
private readonly BookingSummaryDataService $summaryDataService,
private readonly TravelDataService $travelDataService,
private readonly RoomAssignmentService $roomAssignmentService,
private readonly ParticipantCardDataService $participantCardService,
@@ -99,18 +99,16 @@ class Step2Controller extends AbstractController
? $this->participantCardService->getAllCardsDataWithValidation($bookingCreateDto)
: $this->generateAllCardsData($bookingCreateDto);
// Calculate summary data
$summaryData = $this->calculateSummaryData($bookingCreateDto);
// Get detailed pricing data for summary sidebar
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
// Get complete summary data (pricing, rooms, CMS data)
$summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto);
$templateData = [
'form' => $form->createView(),
'bookingDto' => $bookingCreateDto,
'cardsData' => $cardsData,
'summaryData' => $summaryData,
'pricingData' => $summary['pricing'],
'pricingData' => $summaryData['pricingData'],
'cmsData' => $summaryData['cmsData'],
];
// HTMX request: render blocks only
@@ -160,18 +158,16 @@ class Step2Controller extends AbstractController
return $this->hxRedirect($request, $this->generateUrl('app_booking_create_step_2'));
}
// Calculate summary data for sidebar
$summaryData = $this->calculateSummaryData($bookingDto);
// Get detailed pricing data for summary sidebar
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingDto);
// Get complete summary data (pricing, rooms, CMS data)
$summaryData = $this->summaryDataService->getSummaryData($bookingDto);
$templateData = [
'form' => $form->createView(),
'participantIndex' => $index,
'bookingDto' => $bookingDto,
'summaryData' => $summaryData,
'pricingData' => $summary['pricing'],
'pricingData' => $summaryData['pricingData'],
'cmsData' => $summaryData['cmsData'],
'refreshRouteName' => 'app_booking_create_step_2_participant_refresh',
'submitRouteName' => 'app_booking_create_step_2_participant',
];
@@ -12,8 +12,8 @@ use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
use App\Form\BookingCreateStep3Type;
use App\Form\Model\BookingDto;
use App\Htmx\HxTrait;
use App\Service\BookingPriceCalculatorService;
use App\Service\BookingService;
use App\Service\BookingSummaryDataService;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
@@ -32,8 +32,8 @@ class Step3Controller extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSummaryDataService $summaryDataService,
private readonly ApiClient $apiClient,
private readonly BookingPriceCalculatorService $priceCalculator,
private readonly LoggerInterface $logger,
) {
}
@@ -172,10 +172,19 @@ class Step3Controller extends AbstractController
*/
private function renderStepForm(BookingDto $bookingCreateDto, FormInterface $form): Response
{
// Get complete summary data (pricing, rooms, CMS data)
$summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto);
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($summaryData['selectedRooms'], $availableRooms);
return $this->render('booking/create/step_3.html.twig', [
'bookingCreateDto' => $bookingCreateDto,
'form' => $form->createView(),
...$this->getSummaryVariables($bookingCreateDto),
'participantCount' => $summaryData['participantCount'],
'pricingData' => $summaryData['pricingData'],
'cmsData' => $summaryData['cmsData'],
'assignmentCounts' => $summaryData['assignmentCounts'],
'groupedSelectedRooms' => $groupedSelectedRooms,
]);
}
}
@@ -14,6 +14,7 @@ use App\Form\Model\BookingDto;
use App\Htmx\HxTrait;
use App\Service\BookingPriceCalculatorService;
use App\Service\BookingService;
use App\Service\BookingSummaryDataService;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
@@ -32,6 +33,7 @@ class Step4Controller extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSummaryDataService $summaryDataService,
private readonly BookingPriceCalculatorService $priceCalculator,
private readonly ApiClient $apiClient,
private readonly LoggerInterface $logger,
@@ -128,10 +130,19 @@ class Step4Controller extends AbstractController
*/
private function renderStepForm(BookingDto $bookingCreateDto, FormInterface $form): Response
{
// Get complete summary data (pricing, rooms, CMS data)
$summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto);
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($summaryData['selectedRooms'], $availableRooms);
return $this->render('booking/create/step_4.html.twig', [
'bookingCreateDto' => $bookingCreateDto,
'form' => $form->createView(),
...$this->getSummaryVariables($bookingCreateDto),
'participantCount' => $summaryData['participantCount'],
'pricingData' => $summaryData['pricingData'],
'cmsData' => $summaryData['cmsData'],
'assignmentCounts' => $summaryData['assignmentCounts'],
'groupedSelectedRooms' => $groupedSelectedRooms,
'participantPrices' => $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingCreateDto),
]);
}
+15 -15
View File
@@ -21,8 +21,8 @@ use App\Form\Service\ParticipantFieldHandlerRegistry;
use App\Htmx\HxTrait;
use App\Security\Crypt;
use App\Service\BookingFingerprintService;
use App\Service\BookingPriceCalculatorService;
use App\Service\BookingService;
use App\Service\BookingSummaryDataService;
use App\Service\ParticipantCardDataService;
use App\Service\TravelDataService;
use Psr\Cache\InvalidArgumentException;
@@ -57,7 +57,7 @@ class IndexController extends AbstractController
private readonly TravelDataService $travelDataService,
private readonly BookingService $bookingService,
private readonly BookingFingerprintService $fingerprintService,
private readonly BookingPriceCalculatorService $priceCalculator,
private readonly BookingSummaryDataService $summaryDataService,
private readonly ParticipantCardDataService $participantCardService,
private readonly ParticipantFieldHandlerRegistry $fieldHandlerRegistry,
private readonly CacheInterface $cache,
@@ -166,9 +166,8 @@ class IndexController extends AbstractController
? $this->participantCardService->getAllCardsDataWithValidation($bookingDto)
: $this->participantCardService->getAllCardsData($bookingDto);
// Calculate summary data for sidebar
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingDto);
$roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingDto);
// Get complete summary data (pricing, rooms, CMS data)
$summaryData = $this->summaryDataService->getSummaryData($bookingDto);
// Group selected rooms for summary display
$availableRooms = $bookingDto->travel->getAvailableRooms();
@@ -184,9 +183,10 @@ class IndexController extends AbstractController
'mutableData' => $mutableData,
'cardsData' => $cardsData,
'participantsCount' => count($bookingDto->participants),
'pricingData' => $summary['pricing'],
'pricingData' => $summaryData['pricingData'],
'cmsData' => $summaryData['cmsData'],
'groupedSelectedRooms' => $groupedSelectedRooms,
'assignmentCounts' => $roomAssignmentCounts,
'assignmentCounts' => $summaryData['assignmentCounts'],
'isDirty' => $this->fingerprintService->isDirty($bookingDto),
'hasValidationErrors' => $form->isSubmitted() && false === $form->isValid(),
];
@@ -275,9 +275,8 @@ class IndexController extends AbstractController
return $this->hxRedirect($request, $this->generateUrl('app_booking_edit', ['id' => $id]));
}
// Calculate summary data using trait helper
$summaryData = $this->calculateSummaryData($bookingDto);
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingDto);
// Get complete summary data (pricing, rooms, CMS data)
$summaryData = $this->summaryDataService->getSummaryData($bookingDto);
// Fetch mutable data for form constraints
$mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId);
@@ -289,7 +288,8 @@ class IndexController extends AbstractController
'bookingData' => $bookingData,
'mutableData' => $mutableData,
'summaryData' => $summaryData,
'pricingData' => $summary['pricing'],
'pricingData' => $summaryData['pricingData'],
'cmsData' => $summaryData['cmsData'],
'refreshRouteName' => 'app_booking_edit_participant_refresh',
'refreshRouteParams' => ['id' => $id, 'index' => $index],
'submitRouteName' => 'app_booking_edit_participant',
@@ -374,9 +374,8 @@ class IndexController extends AbstractController
$participant->notifications = [];
}
// Calculate summary data using trait helper
$summaryData = $this->calculateSummaryData($bookingDto);
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingDto);
// Get complete summary data (pricing, rooms, CMS data)
$summaryData = $this->summaryDataService->getSummaryData($bookingDto);
// Render form + sidebar using htmxOobResponse
$response = $this->htmxOobResponse(
@@ -389,7 +388,8 @@ class IndexController extends AbstractController
'bookingData' => $bookingData,
'mutableData' => $mutableData,
'summaryData' => $summaryData,
'pricingData' => $summary['pricing'],
'pricingData' => $summaryData['pricingData'],
'cmsData' => $summaryData['cmsData'],
'refreshRouteName' => 'app_booking_edit_participant_refresh',
'refreshRouteParams' => ['id' => $id, 'index' => $index],
'submitRouteName' => 'app_booking_edit_participant',
@@ -76,52 +76,6 @@ trait ParticipantCardFlowTrait
return $this->createForm(BookingParticipantType::class, $wrapper, $formOptions);
}
/**
* Calculate summary data (pricing, room counts, etc.).
*
* @return array{
* participantsCount: int,
* totalPrice: string,
* groupedSelectedRooms: array,
* assignmentCounts: array
* }
*/
private function calculateSummaryData(BookingDto $bookingDto): array
{
// Calculate individual prices for all participants
$participantPrices = $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingDto);
// Calculate total price
$totalPrice = array_sum($participantPrices);
// Get room assignment counts
$roomCounts = [];
foreach ($bookingDto->participants as $participant) {
if (null !== $participant->assignedRoomId) {
$roomCounts[$participant->assignedRoomId] = ($roomCounts[$participant->assignedRoomId] ?? 0) + 1;
}
}
// Group selected rooms with counts
$groupedSelectedRooms = [];
foreach ($roomCounts as $roomId => $count) {
$room = $bookingDto->travel->getRoomById($roomId);
if (null !== $room) {
$groupedSelectedRooms[] = [
'room' => $room,
'count' => $count,
];
}
}
return [
'participantsCount' => count($bookingDto->participants),
'totalPrice' => number_format($totalPrice, 2, ',', '.').' €',
'groupedSelectedRooms' => $groupedSelectedRooms,
'assignmentCounts' => $roomCounts,
];
}
/**
* Process single participant form refresh.
*
@@ -155,11 +109,8 @@ trait ParticipantCardFlowTrait
// Save updated booking data to session (after collecting & clearing notifications)
$this->bookingService->saveBookingDto($request, $bookingDto, $bookingDto->getMode());
// Calculate summary data for sidebar
$summaryData = $this->calculateSummaryData($bookingDto);
// Get detailed pricing data for summary sidebar
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingDto);
// Get complete summary data (pricing, rooms, CMS data)
$summaryData = $this->summaryDataService->getSummaryData($bookingDto);
// Render form and sidebar with OOB swap using htmxOobResponse
// This renders ONLY the specific blocks, not the entire template
@@ -171,7 +122,8 @@ trait ParticipantCardFlowTrait
'participantIndex' => $index,
'bookingDto' => $bookingDto,
'summaryData' => $summaryData,
'pricingData' => $summary['pricing'],
'pricingData' => $summaryData['pricingData'],
'cmsData' => $summaryData['cmsData'],
'refreshRouteName' => $refreshRouteName,
'submitRouteName' => $submitRouteName,
]
@@ -193,7 +145,7 @@ trait ParticipantCardFlowTrait
* Controllers using this trait must have the following properties:
* - BookingService $bookingService
* - ParticipantCardDataService $participantCardService
* - BookingPriceCalculatorService $priceCalculator
* - BookingSummaryDataService $summaryDataService
*/
abstract private function createForm(string $type, $data = null, array $options = []): FormInterface;
+6 -24
View File
@@ -7,36 +7,18 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class CmsDataService
{
public const MODE_PRODUCT = 'product';
public const MODE_HOTEL = 'hotel';
public function __construct(private readonly HttpClientInterface $httpClient)
public function __construct(private readonly HttpClientInterface $httpClient, private readonly string $apiKey)
{
}
public function getProductDetails(string $code): array
public function getProductDetails(string $productCode, ?string $hotelCode = null): array
{
return $this->makeApiCall(static::MODE_PRODUCT, $code);
}
public function getHotelDetails(string $code): array
{
return $this->makeApiCall(static::MODE_HOTEL, $code);
}
public function makeApiCall(string $mode, string $code): array
{
if (false === in_array($mode, [self::MODE_PRODUCT, self::MODE_HOTEL])) {
throw new \InvalidArgumentException('Invalid mode provided');
}
try {
$request = $this->httpClient->request('GET', '/', [
$request = $this->httpClient->request('GET', 'api/product', [
'query' => [
'type' => 1720,
'tx_epproducts_api[code]' => $code,
'tx_epproducts_api[action]' => $mode,
'tx_epproducts_api[controller]' => 'Api',
'product' => $productCode,
'hotel' => $hotelCode,
'key' => $this->apiKey,
],
]);
} catch (ExceptionInterface $e) {
+10 -1
View File
@@ -45,7 +45,16 @@
{# Travel Information #}
<div class="mb-6 pb-4 border-b border-gray-200">
<h4 class="font-semibold text-lg mb-3 text-gray-800">Reiseinformationen</h4>
{% if cmsData.hotel.images is defined %}
<img src="{{ cmsData.hotel.images.resized.l[0].url }}" alt="{{ cmsData.hotel.images.resized.s[0].alt }}" class="w-full rounded mb-4">
{% endif %}
{% if cmsData.hotel.address is defined %}
<address>
{{ cmsData.hotel.name }}
<br>
{{ cmsData.hotel.address | nl2br }}
</address>
{% endif %}
<div class="space-y-2 text-sm text-gray-600">
<div><span class="font-medium">Reise:</span> {{ bookingCreateDto.travel.label }}</div>
<div><span class="font-medium">Zeitraum:</span> {{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }}</div>
@@ -69,6 +69,7 @@
'bookingCreateDto': bookingCreateDto,
'participantCount': participantCount,
'pricingData': pricingData,
'cmsData': cmsData,
'groupedSelectedRooms': groupedSelectedRooms,
'assignmentCounts': []
} %}
+3 -2
View File
@@ -64,10 +64,11 @@
<div id="booking-summary"{% if htmx_oob_swap|default(false) %} hx-swap-oob="true"{% endif %}>
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingDto,
'participantCount': summaryData.participantsCount,
'participantCount': summaryData.participantCount,
'groupedSelectedRooms': summaryData.groupedSelectedRooms,
'assignmentCounts': summaryData.assignmentCounts,
'pricingData': pricingData
'pricingData': pricingData,
'cmsData': cmsData
} %}
</div>
{% endblock %}
@@ -14,10 +14,11 @@
<div id="booking-summary">
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingDto,
'participantCount': summaryData.participantsCount,
'participantCount': summaryData.participantCount,
'groupedSelectedRooms': summaryData.groupedSelectedRooms,
'assignmentCounts': summaryData.assignmentCounts,
'pricingData': pricingData
'pricingData': pricingData,
'cmsData': cmsData
} %}
</div>
</div>
+3 -1
View File
@@ -59,7 +59,9 @@
<div id="booking-summary">
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingCreateDto,
'participantCount': participantsCount,
'participantCount': participantCount,
'pricingData': pricingData,
'cmsData': cmsData,
'groupedSelectedRooms': groupedSelectedRooms,
'assignmentCounts': assignmentCounts
} %}
+1 -1
View File
@@ -98,7 +98,7 @@
{# Participants Summary #}
<div class="mb-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
<h3 class="text-xl font-semibold mb-4">Teilnehmer ({{ participantsCount }})</h3>
<h3 class="text-xl font-semibold mb-4">Teilnehmer ({{ participantCount }})</h3>
{% for participant in bookingCreateDto.participants %}
<div class="mb-6 pb-6 {% if not loop.last %}border-b border-gray-300{% endif %}">
<div class="flex justify-between items-start mb-3">
+1
View File
@@ -102,6 +102,7 @@
'bookingCreateDto': bookingDto,
'participantCount': participantsCount,
'pricingData': pricingData,
'cmsData': cmsData,
'groupedSelectedRooms': groupedSelectedRooms,
'assignmentCounts': assignmentCounts,
'mutableData': mutableData|default(null)
+3 -2
View File
@@ -14,10 +14,11 @@
<div id="booking-summary">
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingDto,
'participantCount': summaryData.participantsCount,
'participantCount': summaryData.participantCount,
'groupedSelectedRooms': summaryData.groupedSelectedRooms,
'assignmentCounts': summaryData.assignmentCounts,
'pricingData': pricingData
'pricingData': pricingData,
'cmsData': cmsData
} %}
</div>
</div>