feat: add typed dto for cms data

This commit is contained in:
Björn Fromme
2026-04-12 18:17:54 +02:00
parent 6ab34fa87c
commit 16cab4b768
13 changed files with 78 additions and 34 deletions
+1 -1
View File
@@ -178,7 +178,7 @@ services:
tags:
- { name: monolog.processor }
App\Service\Newsletter\NewsletterDoubleOptInService:
App\Service\NewsletterDoubleOptInService:
arguments:
$newsletterConfirmationTtlHours: '%newsletter_confirmation_ttl_hours%'
@@ -138,6 +138,8 @@ class Step2ParticipantController extends AbstractController
'form' => $form->createView(),
'participantIndex' => $index,
'bookingCreateContext' => $bookingCreateContext,
'bookingDto' => $bookingCreateContext->bookingDto,
'summaryData' => $bookingCreateContext->summaryData,
'refreshRouteName' => 'app_booking_create_step_2_participant_refresh',
]);
}
@@ -207,6 +209,8 @@ class Step2ParticipantController extends AbstractController
'form' => $form->createView(),
'participantIndex' => $index,
'bookingCreateContext' => $bookingCreateContext,
'bookingDto' => $bookingCreateContext->bookingDto,
'summaryData' => $bookingCreateContext->summaryData,
'refreshRouteName' => $refreshRouteName,
]
);
@@ -102,6 +102,9 @@ class ParticipantController extends AbstractController
'form' => $form->createView(),
'participantIndex' => $index,
'bookingEditContext' => $context,
'bookingDto' => $context->bookingDto,
'summaryData' => $context->summaryData,
'mutableData' => $context->mutableData,
'refreshRouteName' => 'app_booking_edit_participant_refresh',
'refreshRouteParams' => ['id' => $id, 'index' => $index],
'cancelRouteName' => 'app_booking_edit',
@@ -158,6 +161,9 @@ class ParticipantController extends AbstractController
'form' => $form->createView(),
'participantIndex' => $index,
'bookingEditContext' => $context,
'bookingDto' => $context->bookingDto,
'summaryData' => $context->summaryData,
'mutableData' => $context->mutableData,
'refreshRouteName' => 'app_booking_edit_participant_refresh',
'refreshRouteParams' => ['id' => $id, 'index' => $index],
'cancelRouteName' => 'app_booking_edit',
+3 -1
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Form\Model;
use App\Model\BookingSummaryCmsHotelDto;
/**
* DTO containing all booking summary data for sidebar display.
*/
@@ -17,7 +19,7 @@ class BookingSummaryDto
public readonly int $participantCount,
public readonly BookingSummaryPricingDto $pricing,
public readonly BookingSummaryVoucherDto $vouchers,
public readonly ?array $cmsData,
public readonly ?BookingSummaryCmsHotelDto $cmsData,
) {
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* Typed hotel CMS payload for the booking summary.
*/
final readonly class BookingSummaryCmsHotelDto
{
/**
* @param array<string, mixed>|null $images
*/
public function __construct(
public ?string $name,
public ?string $address,
public ?array $images,
) {
}
}
+7 -9
View File
@@ -12,6 +12,7 @@ use App\Form\Model\BookingDto;
use App\Form\Model\BookingSummaryDto;
use App\Form\Model\BookingSummaryPricingDto;
use App\Form\Model\BookingSummaryVoucherDto;
use App\Model\BookingSummaryCmsHotelDto;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
@@ -153,7 +154,7 @@ class BookingSummaryDataService
* Data is cached for 1 hour. This method can be called early in the booking
* flow to warm the cache.
*/
public function getCmsDataForProduct(?string $productCode, ?string $hotelCode): ?array
public function getCmsDataForProduct(?string $productCode, ?string $hotelCode): ?BookingSummaryCmsHotelDto
{
if (null === $hotelCode) {
return null;
@@ -171,14 +172,11 @@ class BookingSummaryDataService
// Fetch CMS images (nice to have)
$images = $this->cmsDataService->getProductImages($productCode, $hotelCode);
// Return combined data structure compatible with templates
return [
'hotel' => [
'name' => $baseHotel?->name,
'address' => $this->formatHotelAddress($baseHotel),
'images' => $images,
],
];
return new BookingSummaryCmsHotelDto(
name: $baseHotel?->name,
address: $this->formatHotelAddress($baseHotel),
images: $images,
);
});
} catch (\Throwable $e) {
$this->logger->error('Failed to fetch hotel display data', [
@@ -141,10 +141,10 @@
{% endmacro %}
{# Shared by booking create and edit flows; callers should pass one of the flow contexts. #}
{% set bookingFlowContext = bookingEditContext|default(bookingCreateContext|default(null)) %}
{% set bookingDto = bookingFlowContext.bookingDto %}
{% set summaryData = bookingFlowContext.summaryData %}
{% set mutableData = bookingFlowContext.mutableData|default(null) %}
{% set bookingFlowContext = bookingFlowContext|default(bookingEditContext|default(bookingCreateContext|default(null))) %}
{% set bookingDto = bookingDto|default(bookingFlowContext ? bookingFlowContext.bookingDto : null) %}
{% set summaryData = summaryData|default(bookingFlowContext ? bookingFlowContext.summaryData : null) %}
{% set mutableData = mutableData|default(bookingFlowContext and bookingFlowContext.mutableData is defined ? bookingFlowContext.mutableData : null) %}
{# Standalone participant form view (replaces main content area) #}
{% block participant_form %}
+3 -1
View File
@@ -69,7 +69,9 @@
</table>
</div>
{% include 'booking/_summary_hotel.html.twig' %}
{% include 'booking/_summary_hotel.html.twig' with {
'summaryData': summaryData
} %}
{# Mutability information (edit mode only) #}
{% if bookingDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and mutableData %}
+8 -8
View File
@@ -1,14 +1,14 @@
{% if summaryData.cmsData.hotel.name %}
{% if summaryData.cmsData and summaryData.cmsData.name %}
<div class="grid grid-cols-3 gap-x-4 py-4 border-t border-primary-bg">
{% if summaryData.cmsData.hotel.images.resized.l[0] is defined %}
<img src="{{ summaryData.cmsData.hotel.images.resized.l[0].url }}"
alt="{{ summaryData.cmsData.hotel.images.resized.l[0].alt }}"
{% if summaryData.cmsData.images and summaryData.cmsData.images.resized.l[0] is defined %}
<img src="{{ summaryData.cmsData.images.resized.l[0].url }}"
alt="{{ summaryData.cmsData.images.resized.l[0].alt }}"
class="block w-full h-auto">
{% endif %}
<div class="{{ summaryData.cmsData.hotel.images.resized.l[0] is defined ? 'col-span-2' : 'col-span-3' }}">
<span class="block font-semibold uppercase">{{ summaryData.cmsData.hotel.name }}</span>
{% if summaryData.cmsData.hotel.address %}
{{ summaryData.cmsData.hotel.address | nl2br }}
<div class="{{ summaryData.cmsData.images and summaryData.cmsData.images.resized.l[0] is defined ? 'col-span-2' : 'col-span-3' }}">
<span class="block font-semibold uppercase">{{ summaryData.cmsData.name }}</span>
{% if summaryData.cmsData.address %}
{{ summaryData.cmsData.address | nl2br }}
{% endif %}
</div>
</div>
@@ -10,9 +10,9 @@
</h1>
<div class="lg:flex lg:space-x-8 pb-8 mb-8 border-b border-primary-bg/40">
{% if cmsData.hotel.images.resized.l[0] is defined %}
<img src="{{ cmsData.hotel.images.resized.l[0].url }}"
alt="{{ cmsData.hotel.images.resized.l[0].alt }}"
{% if cmsData and cmsData.images and cmsData.images.resized.l[0] is defined %}
<img src="{{ cmsData.images.resized.l[0].url }}"
alt="{{ cmsData.images.resized.l[0].alt }}"
class="block w-full max-w-64 h-auto mb-4">
{% endif %}
<div class="text-white max-w-96">
@@ -22,11 +22,11 @@
<div class="text-lg font-semibold uppercase">
{{ travel_date_from | date('d.m.Y') }} - {{ travel_date_to | date('d.m.Y') }}
</div>
{% if cmsData.hotel.name is defined and cmsData.hotel.name %}
<div class="mt-4 font-semibold">{{ cmsData.hotel.name }}</div>
{% if cmsData and cmsData.name %}
<div class="mt-4 font-semibold">{{ cmsData.name }}</div>
{% endif %}
{% if cmsData.hotel.address is defined and cmsData.hotel.address %}
<div>{{ cmsData.hotel.address | nl2br }}</div>
{% if cmsData and cmsData.address %}
<div>{{ cmsData.address | nl2br }}</div>
{% endif %}
</div>
</div>
@@ -30,7 +30,11 @@
<div id="main-content" class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
{# Flash messages - must be inside main-content for HTMX swap to display them #}
{% include '_partials/_flashes.html.twig' %}
{% include 'booking/_participant_form.html.twig' %}
{% include 'booking/_participant_form.html.twig' with {
'bookingCreateContext': bookingCreateContext,
'bookingDto': bookingCreateContext.bookingDto,
'summaryData': bookingCreateContext.summaryData
} %}
</div>
</div>
</div>
+6 -2
View File
@@ -65,7 +65,9 @@
</table>
</div>
{% include 'booking/_summary_hotel.html.twig' %}
{% include 'booking/_summary_hotel.html.twig' with {
'summaryData': bookingCreateContext.summaryData
} %}
</div>
</div>
@@ -123,7 +125,9 @@
</tr>
</table>
{% include 'booking/_summary_hotel.html.twig' %}
{% include 'booking/_summary_hotel.html.twig' with {
'summaryData': bookingCreateContext.summaryData
} %}
</div>
{# Detailed Pricing Breakdown #}
@@ -29,6 +29,9 @@
{% include '_partials/_flashes.html.twig' %}
{% include 'booking/_participant_form.html.twig' with {
'bookingEditContext': bookingEditContext,
'bookingDto': bookingEditContext.bookingDto,
'summaryData': bookingEditContext.summaryData,
'mutableData': bookingEditContext.mutableData|default(null),
'participantIndex': participantIndex,
'refreshRouteName': refreshRouteName,
'refreshRouteParams': refreshRouteParams|default({}),