feat: tracking integration with GTM and CMP

This commit is contained in:
Björn Fromme
2026-01-27 17:31:41 +01:00
parent d5944bf717
commit e9548d9f38
21 changed files with 453 additions and 51 deletions
@@ -92,8 +92,11 @@ class Step4Controller extends AbstractController
);
}
// Success: Store booking number in flash and clear session
// Success: Store booking data in flash for conversion tracking
$summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto);
$this->addFlash('booking_number', $bookingResponse->bookingNumber);
$this->addFlash('booking_total', $summaryData->payableAmount);
$this->addFlash('booking_travel_name', $bookingCreateDto->travel->label);
$this->clearTravelDataCache($bookingCreateDto);
$this->bookingService->clearBookingDto($request, BookingDto::MODE_CREATE);
@@ -23,7 +23,10 @@ class SuccessController extends AbstractController
#[Route('/bookings/create/success', name: 'app_booking_create_success')]
public function success(Request $request): Response
{
$bookingNumber = $request->getSession()->getFlashBag()->get('booking_number')[0] ?? null;
$flashBag = $request->getSession()->getFlashBag();
$bookingNumber = $flashBag->get('booking_number')[0] ?? null;
$bookingTotal = $flashBag->get('booking_total')[0] ?? null;
$travelName = $flashBag->get('booking_travel_name')[0] ?? null;
$returnUrl = $this->bookingService->getReturnUrl($request);
// Redirect to return URL if no booking number (direct access or refresh)
@@ -33,6 +36,8 @@ class SuccessController extends AbstractController
return $this->render('booking/create/success.html.twig', [
'bookingNumber' => $bookingNumber,
'bookingTotal' => $bookingTotal,
'travelName' => $travelName,
'returnUrl' => $returnUrl,
]);
}
+6 -23
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\EventListener;
use App\Service\DomainConfigProvider;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
@@ -12,13 +13,10 @@ use Symfony\Component\HttpKernel\KernelEvents;
class DomainThemeListener
{
public const THEME_ATTRIBUTE = '_theme';
private const DEFAULT_THEME = 'base';
public const DOMAIN_CONFIG_ATTRIBUTE = '_domain_config';
/**
* @param array<string, string> $domainThemeMap
*/
public function __construct(
private readonly array $domainThemeMap,
private readonly DomainConfigProvider $domainConfigProvider,
) {
}
@@ -29,24 +27,9 @@ class DomainThemeListener
}
$request = $event->getRequest();
$host = $request->getHost();
$config = $this->domainConfigProvider->getConfigForHost($request->getHost());
$theme = $this->resolveTheme($host);
$request->attributes->set(self::THEME_ATTRIBUTE, $theme);
}
private function resolveTheme(string $host): string
{
$host = strtolower($host);
foreach ($this->domainThemeMap as $pattern => $theme) {
$pattern = strtolower($pattern);
if ($host === $pattern || str_ends_with($host, '.' . $pattern)) {
return $theme;
}
}
return self::DEFAULT_THEME;
$request->attributes->set(self::THEME_ATTRIBUTE, $config->theme);
$request->attributes->set(self::DOMAIN_CONFIG_ATTRIBUTE, $config);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* Value object representing domain-specific configuration.
*
* Holds theme key, GTM container ID, and CMP (Consent Management Platform) URL
* for a specific domain. Used by DomainConfigProvider to resolve configuration
* based on the request host.
*/
final readonly class DomainConfig
{
public const DEFAULT_THEME = 'base';
public function __construct(
public string $theme = self::DEFAULT_THEME,
public string $gtmId = '',
public string $cmpUrl = '',
) {
}
/**
* Creates a DomainConfig instance from a configuration array.
*
* @param array{theme?: string, gtm_id?: string, cmp_url?: string} $config The configuration array
*/
public static function fromArray(array $config): self
{
return new self(
theme: $config['theme'] ?? self::DEFAULT_THEME,
gtmId: $config['gtm_id'] ?? '',
cmpUrl: $config['cmp_url'] ?? '',
);
}
/**
* Creates a default DomainConfig instance with base theme and empty tracking values.
*/
public static function default(): self
{
return new self();
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Model\DomainConfig;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Resolves domain-specific configuration based on the current request host.
*
* Matches the request host against configured domain patterns, supporting both
* exact matches and subdomain matching (e.g., "example.com" matches "www.example.com").
* Returns a default configuration when no matching domain is found.
*/
class DomainConfigProvider
{
/**
* @var array<string, DomainConfig>
*/
private array $configCache = [];
/**
* @param array<string, array{theme?: string, gtm_id?: string, cmp_url?: string}> $domainConfig
*/
public function __construct(
private readonly array $domainConfig,
private readonly RequestStack $requestStack,
) {
}
/**
* Returns the configuration for the current request host.
*/
public function getConfig(): DomainConfig
{
$request = $this->requestStack->getCurrentRequest();
if (null === $request) {
return DomainConfig::default();
}
return $this->getConfigForHost($request->getHost());
}
/**
* Returns the configuration for a specific host.
*
* @param string $host The host to resolve configuration for
*/
public function getConfigForHost(string $host): DomainConfig
{
$host = strtolower($host);
if (isset($this->configCache[$host])) {
return $this->configCache[$host];
}
foreach ($this->domainConfig as $pattern => $config) {
$pattern = strtolower($pattern);
if ($host === $pattern || str_ends_with($host, '.'.$pattern)) {
$domainConfig = DomainConfig::fromArray($config);
$this->configCache[$host] = $domainConfig;
return $domainConfig;
}
}
$this->configCache[$host] = DomainConfig::default();
return $this->configCache[$host];
}
}
+2
View File
@@ -40,6 +40,8 @@ class AppExtension extends AbstractExtension
new TwigFunction('is_hidden', [AppRuntime::class, 'isHidden']),
new TwigFunction('collect_invalid_field_labels', [AppRuntime::class, 'collectInvalidFieldLabels']),
new TwigFunction('booking_theme', [AppRuntime::class, 'getBookingTheme']),
new TwigFunction('gtm_id', [AppRuntime::class, 'getGtmId']),
new TwigFunction('cmp_url', [AppRuntime::class, 'getCmpUrl']),
];
}
}
+31 -2
View File
@@ -9,6 +9,7 @@ use App\EventListener\DomainThemeListener;
use App\Form\Model\BookingDto;
use App\Form\Service\CreateFieldStateProvider;
use App\Form\Service\EditFieldStateProvider;
use App\Model\DomainConfig;
use App\Service\ParticipantEligibilityService;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -234,14 +235,42 @@ class AppRuntime implements RuntimeExtensionInterface
* Defaults to 'base' if no theme is set.
*/
public function getBookingTheme(): string
{
return $this->getDomainConfig()->theme;
}
/**
* Returns the GTM container ID for the current domain.
*
* Returns an empty string if no GTM ID is configured for this domain.
*/
public function getGtmId(): string
{
return $this->getDomainConfig()->gtmId;
}
/**
* Returns the CMP (Consent Management Platform) URL for the current domain.
*
* Returns an empty string if no CMP URL is configured for this domain.
*/
public function getCmpUrl(): string
{
return $this->getDomainConfig()->cmpUrl;
}
private function getDomainConfig(): DomainConfig
{
$request = $this->requestStack->getCurrentRequest();
if (null === $request) {
return 'base';
return DomainConfig::default();
}
return $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE, 'base');
return $request->attributes->get(
DomainThemeListener::DOMAIN_CONFIG_ATTRIBUTE,
DomainConfig::default()
);
}
/**