feat: tracking integration with GTM and CMP
This commit is contained in:
@@ -85,6 +85,3 @@ OAUTH_ENCRYPTION_KEY=580084fd179e67399467f59ee96658ac
|
|||||||
# postgresql+advisory://db_user:db_password@localhost/db_name
|
# postgresql+advisory://db_user:db_password@localhost/db_name
|
||||||
LOCK_DSN=flock
|
LOCK_DSN=flock
|
||||||
###< symfony/lock ###
|
###< symfony/lock ###
|
||||||
|
|
||||||
# Domain-to-theme mapping (JSON format)
|
|
||||||
DOMAIN_THEME_MAP='{"ski-boarderweek.de":"sbw","semesterende-skireisen.de":"ser","snowzone.net":"snz"}'
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
parameters:
|
||||||
|
domain_config:
|
||||||
|
ep-reisen.de:
|
||||||
|
theme: base
|
||||||
|
gtm_id: GTM-56LWRF
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.ep-reisen.de-28eabc35-7085-4d8f-b450-522bd1a5247c/consent.js
|
||||||
|
ski-boarderweek.de:
|
||||||
|
theme: sbw
|
||||||
|
gtm_id: GTM-MS2C8CH
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.ski-boarderweek.de-d3641c34-8183-4b8d-b56e-c052e7a9223e/consent.js
|
||||||
|
semesterende-skireisen.de:
|
||||||
|
theme: ser
|
||||||
|
gtm_id: GTM-MNKDKL9
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.semesterende-skireisen.de-c9b3422d-eb62-4ce4-b8f7-91e9b509e26f/consent.js
|
||||||
|
snowzone.net:
|
||||||
|
theme: snz
|
||||||
|
gtm_id: GTM-M96CKL
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.snowzone.net-8e6aea0c-7d93-42a0-a822-29fff2aa5613/consent.js
|
||||||
@@ -3,13 +3,15 @@
|
|||||||
|
|
||||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
|
imports:
|
||||||
|
- { resource: domain_config.yaml }
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
travel_info_base_url: '%env(APP_TRAVEL_INFO_BASE_URL)%'
|
travel_info_base_url: '%env(APP_TRAVEL_INFO_BASE_URL)%'
|
||||||
terms_and_conditions_url: '%env(APP_TERMS_AND_CONDITIONS_URL)%'
|
terms_and_conditions_url: '%env(APP_TERMS_AND_CONDITIONS_URL)%'
|
||||||
path_to_keys: '%kernel.project_dir%/config/secret'
|
path_to_keys: '%kernel.project_dir%/config/secret'
|
||||||
bpn_debug: '%env(APP_BPN_DEBUG)%'
|
bpn_debug: '%env(APP_BPN_DEBUG)%'
|
||||||
default_booking_status: '%env(DEFAULT_BOOKING_STATUS)%'
|
default_booking_status: '%env(DEFAULT_BOOKING_STATUS)%'
|
||||||
domain_theme_map: '%env(json:DOMAIN_THEME_MAP)%'
|
|
||||||
|
|
||||||
# Body dimensions choices for BodyDimensionsType
|
# Body dimensions choices for BodyDimensionsType
|
||||||
body_dimensions.height_choices:
|
body_dimensions.height_choices:
|
||||||
@@ -147,7 +149,7 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: monolog.processor }
|
- { name: monolog.processor }
|
||||||
|
|
||||||
App\EventListener\DomainThemeListener:
|
App\Service\DomainConfigProvider:
|
||||||
arguments:
|
arguments:
|
||||||
$domainThemeMap: '%domain_theme_map%'
|
$domainConfig: '%domain_config%'
|
||||||
|
|
||||||
|
|||||||
@@ -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_number', $bookingResponse->bookingNumber);
|
||||||
|
$this->addFlash('booking_total', $summaryData->payableAmount);
|
||||||
|
$this->addFlash('booking_travel_name', $bookingCreateDto->travel->label);
|
||||||
$this->clearTravelDataCache($bookingCreateDto);
|
$this->clearTravelDataCache($bookingCreateDto);
|
||||||
$this->bookingService->clearBookingDto($request, BookingDto::MODE_CREATE);
|
$this->bookingService->clearBookingDto($request, BookingDto::MODE_CREATE);
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ class SuccessController extends AbstractController
|
|||||||
#[Route('/bookings/create/success', name: 'app_booking_create_success')]
|
#[Route('/bookings/create/success', name: 'app_booking_create_success')]
|
||||||
public function success(Request $request): Response
|
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);
|
$returnUrl = $this->bookingService->getReturnUrl($request);
|
||||||
|
|
||||||
// Redirect to return URL if no booking number (direct access or refresh)
|
// 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', [
|
return $this->render('booking/create/success.html.twig', [
|
||||||
'bookingNumber' => $bookingNumber,
|
'bookingNumber' => $bookingNumber,
|
||||||
|
'bookingTotal' => $bookingTotal,
|
||||||
|
'travelName' => $travelName,
|
||||||
'returnUrl' => $returnUrl,
|
'returnUrl' => $returnUrl,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\EventListener;
|
namespace App\EventListener;
|
||||||
|
|
||||||
|
use App\Service\DomainConfigProvider;
|
||||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||||
use Symfony\Component\HttpKernel\KernelEvents;
|
use Symfony\Component\HttpKernel\KernelEvents;
|
||||||
@@ -12,13 +13,10 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
|||||||
class DomainThemeListener
|
class DomainThemeListener
|
||||||
{
|
{
|
||||||
public const THEME_ATTRIBUTE = '_theme';
|
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(
|
public function __construct(
|
||||||
private readonly array $domainThemeMap,
|
private readonly DomainConfigProvider $domainConfigProvider,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,24 +27,9 @@ class DomainThemeListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
$request = $event->getRequest();
|
$request = $event->getRequest();
|
||||||
$host = $request->getHost();
|
$config = $this->domainConfigProvider->getConfigForHost($request->getHost());
|
||||||
|
|
||||||
$theme = $this->resolveTheme($host);
|
$request->attributes->set(self::THEME_ATTRIBUTE, $config->theme);
|
||||||
$request->attributes->set(self::THEME_ATTRIBUTE, $theme);
|
$request->attributes->set(self::DOMAIN_CONFIG_ATTRIBUTE, $config);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,8 @@ class AppExtension extends AbstractExtension
|
|||||||
new TwigFunction('is_hidden', [AppRuntime::class, 'isHidden']),
|
new TwigFunction('is_hidden', [AppRuntime::class, 'isHidden']),
|
||||||
new TwigFunction('collect_invalid_field_labels', [AppRuntime::class, 'collectInvalidFieldLabels']),
|
new TwigFunction('collect_invalid_field_labels', [AppRuntime::class, 'collectInvalidFieldLabels']),
|
||||||
new TwigFunction('booking_theme', [AppRuntime::class, 'getBookingTheme']),
|
new TwigFunction('booking_theme', [AppRuntime::class, 'getBookingTheme']),
|
||||||
|
new TwigFunction('gtm_id', [AppRuntime::class, 'getGtmId']),
|
||||||
|
new TwigFunction('cmp_url', [AppRuntime::class, 'getCmpUrl']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-2
@@ -9,6 +9,7 @@ use App\EventListener\DomainThemeListener;
|
|||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Service\CreateFieldStateProvider;
|
use App\Form\Service\CreateFieldStateProvider;
|
||||||
use App\Form\Service\EditFieldStateProvider;
|
use App\Form\Service\EditFieldStateProvider;
|
||||||
|
use App\Model\DomainConfig;
|
||||||
use App\Service\ParticipantEligibilityService;
|
use App\Service\ParticipantEligibilityService;
|
||||||
use Symfony\Component\Form\FormView;
|
use Symfony\Component\Form\FormView;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
@@ -234,14 +235,42 @@ class AppRuntime implements RuntimeExtensionInterface
|
|||||||
* Defaults to 'base' if no theme is set.
|
* Defaults to 'base' if no theme is set.
|
||||||
*/
|
*/
|
||||||
public function getBookingTheme(): string
|
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();
|
$request = $this->requestStack->getCurrentRequest();
|
||||||
|
|
||||||
if (null === $request) {
|
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()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
var consent = (window.CookieFirst && window.CookieFirst.consent) || { advertising: false };
|
||||||
|
if (consent.advertising) {
|
||||||
|
dataLayer.push({
|
||||||
|
'event':'VirtualPageview',
|
||||||
|
'virtualPageURL': '{{ url }}',
|
||||||
|
'virtualPageTitle': '{{ title }}',
|
||||||
|
'userInfo': '{{ is_granted('ROLE_USER') ? 'mitglied' : 'gast' }}'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -8,6 +8,16 @@
|
|||||||
<link rel="icon" href="{{ asset('build/favicon/favicon.ico') }}" sizes="32x32">
|
<link rel="icon" href="{{ asset('build/favicon/favicon.ico') }}" sizes="32x32">
|
||||||
<link rel="icon" href="{{ asset('build/favicon/icon.svg') }}" type="image/svg+xml">
|
<link rel="icon" href="{{ asset('build/favicon/icon.svg') }}" type="image/svg+xml">
|
||||||
<link rel="apple-touch-icon" href="{{ asset('build/favicon/apple-touch-icon.png') }}">
|
<link rel="apple-touch-icon" href="{{ asset('build/favicon/apple-touch-icon.png') }}">
|
||||||
|
{% if cmp_url() is not empty %}
|
||||||
|
<script src="{{ cmp_url() }}"></script>
|
||||||
|
<script type="text/plain" data-cookiefirst-category="necessary">
|
||||||
|
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||||
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||||
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||||
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||||
|
})(window,document,'script','dataLayer','{{ gtm_id() }}');
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
{{ encore_entry_link_tags('app') }}
|
{{ encore_entry_link_tags('app') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -95,6 +95,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
{% include '_partials/_tracking.html.twig' with {
|
||||||
|
'url': '/trichter_Z1/schritt0_login_oder_gast',
|
||||||
|
'title': 'Login oder Gast'
|
||||||
|
} %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
date_id: params.dateId,
|
date_id: params.dateId,
|
||||||
hotel_id: params.hotelId,
|
hotel_id: params.hotelId,
|
||||||
agency: params.agency,
|
agency: params.agency,
|
||||||
theme: params.theme,
|
|
||||||
r: params.returnUrl
|
r: params.returnUrl
|
||||||
}) }}"
|
}) }}"
|
||||||
hx-trigger="load"
|
hx-trigger="load"
|
||||||
|
|||||||
@@ -101,4 +101,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ form_rest(form) }}
|
{{ form_rest(form) }}
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
|
{% include '_partials/_tracking.html.twig' with {
|
||||||
|
'url': '/trichter_Z1/schritt1_zimmerauswahl',
|
||||||
|
'title': 'Zimmerauswahl'
|
||||||
|
} %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -75,4 +75,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ form_rest(form) }}
|
{{ form_rest(form) }}
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
|
{% include '_partials/_tracking.html.twig' with {
|
||||||
|
'url': '/trichter_Z1/schritt2_teilnehmende_leistungen',
|
||||||
|
'title': 'Teilnehmende und Leistungen'
|
||||||
|
} %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -85,4 +85,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ form_rest(form) }}
|
{{ form_rest(form) }}
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
|
{% include '_partials/_tracking.html.twig' with {
|
||||||
|
'url': '/trichter_Z1/schritt3_zahlungsart',
|
||||||
|
'title': 'Zahlungsart'
|
||||||
|
} %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -567,4 +567,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ form_rest(form) }}
|
{{ form_rest(form) }}
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
|
{% include '_partials/_tracking.html.twig' with {
|
||||||
|
'url': '/trichter_Z1/schritt4_uerbersicht_bestaetigung',
|
||||||
|
'title': 'Übersicht und Bestätigung'
|
||||||
|
} %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -55,4 +55,23 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if bookingTotal is not null and travelName is not null %}
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
var consent = (window.CookieFirst && window.CookieFirst.consent) || { advertising: false };
|
||||||
|
if (consent.advertising) {
|
||||||
|
dataLayer.push({
|
||||||
|
'event': 'Transaction',
|
||||||
|
'userInfo': '{{ is_granted('ROLE_USER') ? 'mitglied' : 'gast' }}',
|
||||||
|
'transactionId': '{{ bookingNumber }}',
|
||||||
|
'transactionTotal': {{ bookingTotal }},
|
||||||
|
'transactionProducts': [{
|
||||||
|
'name': '{{ travelName|e('js') }}',
|
||||||
|
'price': {{ bookingTotal }},
|
||||||
|
'quantity': 1
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ declare(strict_types=1);
|
|||||||
namespace App\Tests\EventListener;
|
namespace App\Tests\EventListener;
|
||||||
|
|
||||||
use App\EventListener\DomainThemeListener;
|
use App\EventListener\DomainThemeListener;
|
||||||
|
use App\Model\DomainConfig;
|
||||||
|
use App\Service\DomainConfigProvider;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||||
|
|
||||||
@@ -21,9 +24,9 @@ class DomainThemeListenerTest extends TestCase
|
|||||||
|
|
||||||
public function testSetsThemeAttributeForMatchingDomain(): void
|
public function testSetsThemeAttributeForMatchingDomain(): void
|
||||||
{
|
{
|
||||||
$listener = new DomainThemeListener([
|
$listener = $this->createListener([
|
||||||
'sbw-reisen' => 'sbw',
|
'sbw-reisen.de' => ['theme' => 'sbw'],
|
||||||
'ser-reisen' => 'ser',
|
'ser-reisen.de' => ['theme' => 'ser'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$request = Request::create('https://www.sbw-reisen.de/bookings/create');
|
$request = Request::create('https://www.sbw-reisen.de/bookings/create');
|
||||||
@@ -34,10 +37,32 @@ class DomainThemeListenerTest extends TestCase
|
|||||||
$this->assertSame('sbw', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE));
|
$this->assertSame('sbw', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSetsDomainConfigAttribute(): void
|
||||||
|
{
|
||||||
|
$listener = $this->createListener([
|
||||||
|
'sbw-reisen.de' => [
|
||||||
|
'theme' => 'sbw',
|
||||||
|
'gtm_id' => 'GTM-123',
|
||||||
|
'cmp_url' => 'https://cmp.example.com',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request = Request::create('https://www.sbw-reisen.de/bookings/create');
|
||||||
|
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||||
|
|
||||||
|
$listener($event);
|
||||||
|
|
||||||
|
$config = $request->attributes->get(DomainThemeListener::DOMAIN_CONFIG_ATTRIBUTE);
|
||||||
|
$this->assertInstanceOf(DomainConfig::class, $config);
|
||||||
|
$this->assertSame('sbw', $config->theme);
|
||||||
|
$this->assertSame('GTM-123', $config->gtmId);
|
||||||
|
$this->assertSame('https://cmp.example.com', $config->cmpUrl);
|
||||||
|
}
|
||||||
|
|
||||||
public function testSetsDefaultThemeForUnknownDomain(): void
|
public function testSetsDefaultThemeForUnknownDomain(): void
|
||||||
{
|
{
|
||||||
$listener = new DomainThemeListener([
|
$listener = $this->createListener([
|
||||||
'sbw-reisen' => 'sbw',
|
'sbw-reisen.de' => ['theme' => 'sbw'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$request = Request::create('https://www.ep-reisen.de/bookings/create');
|
$request = Request::create('https://www.ep-reisen.de/bookings/create');
|
||||||
@@ -50,8 +75,8 @@ class DomainThemeListenerTest extends TestCase
|
|||||||
|
|
||||||
public function testMatchesPartialDomain(): void
|
public function testMatchesPartialDomain(): void
|
||||||
{
|
{
|
||||||
$listener = new DomainThemeListener([
|
$listener = $this->createListener([
|
||||||
'ser-reisen' => 'ser',
|
'ser-reisen.de' => ['theme' => 'ser'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$request = Request::create('https://portal.ser-reisen.de/bookings');
|
$request = Request::create('https://portal.ser-reisen.de/bookings');
|
||||||
@@ -64,8 +89,8 @@ class DomainThemeListenerTest extends TestCase
|
|||||||
|
|
||||||
public function testIgnoresSubRequests(): void
|
public function testIgnoresSubRequests(): void
|
||||||
{
|
{
|
||||||
$listener = new DomainThemeListener([
|
$listener = $this->createListener([
|
||||||
'sbw-reisen' => 'sbw',
|
'sbw-reisen.de' => ['theme' => 'sbw'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$request = Request::create('https://www.sbw-reisen.de/bookings/create');
|
$request = Request::create('https://www.sbw-reisen.de/bookings/create');
|
||||||
@@ -78,22 +103,23 @@ class DomainThemeListenerTest extends TestCase
|
|||||||
|
|
||||||
public function testFirstMatchWins(): void
|
public function testFirstMatchWins(): void
|
||||||
{
|
{
|
||||||
$listener = new DomainThemeListener([
|
// When a host matches multiple patterns, the first pattern in iteration order wins
|
||||||
'reisen' => 'generic',
|
$listener = $this->createListener([
|
||||||
'sbw-reisen' => 'sbw',
|
'portal.example.com' => ['theme' => 'portal'],
|
||||||
|
'example.com' => ['theme' => 'main'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$request = Request::create('https://www.sbw-reisen.de/bookings/create');
|
$request = Request::create('https://deep.portal.example.com/bookings/create');
|
||||||
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||||
|
|
||||||
$listener($event);
|
$listener($event);
|
||||||
|
|
||||||
$this->assertSame('generic', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE));
|
$this->assertSame('portal', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testEmptyMapReturnsDefaultTheme(): void
|
public function testEmptyMapReturnsDefaultTheme(): void
|
||||||
{
|
{
|
||||||
$listener = new DomainThemeListener([]);
|
$listener = $this->createListener([]);
|
||||||
|
|
||||||
$request = Request::create('https://www.sbw-reisen.de/bookings/create');
|
$request = Request::create('https://www.sbw-reisen.de/bookings/create');
|
||||||
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||||
@@ -105,8 +131,8 @@ class DomainThemeListenerTest extends TestCase
|
|||||||
|
|
||||||
public function testMatchesLocalhostForDevelopment(): void
|
public function testMatchesLocalhostForDevelopment(): void
|
||||||
{
|
{
|
||||||
$listener = new DomainThemeListener([
|
$listener = $this->createListener([
|
||||||
'localhost' => 'sbw',
|
'localhost' => ['theme' => 'sbw'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$request = Request::create('http://localhost/bookings/create');
|
$request = Request::create('http://localhost/bookings/create');
|
||||||
@@ -116,4 +142,15 @@ class DomainThemeListenerTest extends TestCase
|
|||||||
|
|
||||||
$this->assertSame('sbw', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE));
|
$this->assertSame('sbw', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array{theme?: string, gtm_id?: string, cmp_url?: string}> $domainConfig
|
||||||
|
*/
|
||||||
|
private function createListener(array $domainConfig): DomainThemeListener
|
||||||
|
{
|
||||||
|
$requestStack = new RequestStack();
|
||||||
|
$provider = new DomainConfigProvider($domainConfig, $requestStack);
|
||||||
|
|
||||||
|
return new DomainThemeListener($provider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Model\DomainConfig;
|
||||||
|
use App\Service\DomainConfigProvider;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
|
class DomainConfigProviderTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testReturnsConfigForExactMatch(): void
|
||||||
|
{
|
||||||
|
$provider = $this->createProvider([
|
||||||
|
'example.com' => [
|
||||||
|
'theme' => 'custom',
|
||||||
|
'gtm_id' => 'GTM-123',
|
||||||
|
'cmp_url' => 'https://cmp.example.com',
|
||||||
|
],
|
||||||
|
], 'example.com');
|
||||||
|
|
||||||
|
$config = $provider->getConfig();
|
||||||
|
|
||||||
|
$this->assertSame('custom', $config->theme);
|
||||||
|
$this->assertSame('GTM-123', $config->gtmId);
|
||||||
|
$this->assertSame('https://cmp.example.com', $config->cmpUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsConfigForSubdomainMatch(): void
|
||||||
|
{
|
||||||
|
$provider = $this->createProvider([
|
||||||
|
'example.com' => [
|
||||||
|
'theme' => 'custom',
|
||||||
|
'gtm_id' => 'GTM-456',
|
||||||
|
'cmp_url' => 'https://cmp.example.com',
|
||||||
|
],
|
||||||
|
], 'www.example.com');
|
||||||
|
|
||||||
|
$config = $provider->getConfig();
|
||||||
|
|
||||||
|
$this->assertSame('custom', $config->theme);
|
||||||
|
$this->assertSame('GTM-456', $config->gtmId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsDefaultConfigForUnknownDomain(): void
|
||||||
|
{
|
||||||
|
$provider = $this->createProvider([
|
||||||
|
'example.com' => [
|
||||||
|
'theme' => 'custom',
|
||||||
|
'gtm_id' => 'GTM-123',
|
||||||
|
'cmp_url' => 'https://cmp.example.com',
|
||||||
|
],
|
||||||
|
], 'unknown.com');
|
||||||
|
|
||||||
|
$config = $provider->getConfig();
|
||||||
|
|
||||||
|
$this->assertSame(DomainConfig::DEFAULT_THEME, $config->theme);
|
||||||
|
$this->assertSame('', $config->gtmId);
|
||||||
|
$this->assertSame('', $config->cmpUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsDefaultConfigWhenNoRequest(): void
|
||||||
|
{
|
||||||
|
$requestStack = new RequestStack();
|
||||||
|
$provider = new DomainConfigProvider([], $requestStack);
|
||||||
|
|
||||||
|
$config = $provider->getConfig();
|
||||||
|
|
||||||
|
$this->assertSame(DomainConfig::DEFAULT_THEME, $config->theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandlesMissingConfigValues(): void
|
||||||
|
{
|
||||||
|
$provider = $this->createProvider([
|
||||||
|
'example.com' => [
|
||||||
|
'theme' => 'custom',
|
||||||
|
],
|
||||||
|
], 'example.com');
|
||||||
|
|
||||||
|
$config = $provider->getConfig();
|
||||||
|
|
||||||
|
$this->assertSame('custom', $config->theme);
|
||||||
|
$this->assertSame('', $config->gtmId);
|
||||||
|
$this->assertSame('', $config->cmpUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCachesConfigForSameHost(): void
|
||||||
|
{
|
||||||
|
$provider = $this->createProvider([
|
||||||
|
'example.com' => [
|
||||||
|
'theme' => 'custom',
|
||||||
|
'gtm_id' => 'GTM-123',
|
||||||
|
'cmp_url' => 'https://cmp.example.com',
|
||||||
|
],
|
||||||
|
], 'example.com');
|
||||||
|
|
||||||
|
$config1 = $provider->getConfigForHost('example.com');
|
||||||
|
$config2 = $provider->getConfigForHost('example.com');
|
||||||
|
|
||||||
|
$this->assertSame($config1, $config2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFirstMatchWins(): void
|
||||||
|
{
|
||||||
|
// When a host matches multiple patterns, the first pattern in iteration order wins
|
||||||
|
// Here 'deep.portal.example.com' matches both 'portal.example.com' (via subdomain)
|
||||||
|
// and 'example.com' (via subdomain), but portal.example.com is checked first
|
||||||
|
$provider = $this->createProvider([
|
||||||
|
'portal.example.com' => ['theme' => 'portal'],
|
||||||
|
'example.com' => ['theme' => 'main'],
|
||||||
|
], 'deep.portal.example.com');
|
||||||
|
|
||||||
|
$config = $provider->getConfig();
|
||||||
|
|
||||||
|
$this->assertSame('portal', $config->theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCaseInsensitiveMatching(): void
|
||||||
|
{
|
||||||
|
$provider = $this->createProvider([
|
||||||
|
'Example.COM' => [
|
||||||
|
'theme' => 'custom',
|
||||||
|
],
|
||||||
|
], 'EXAMPLE.com');
|
||||||
|
|
||||||
|
$config = $provider->getConfig();
|
||||||
|
|
||||||
|
$this->assertSame('custom', $config->theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array{theme?: string, gtm_id?: string, cmp_url?: string}> $domainConfig
|
||||||
|
*/
|
||||||
|
private function createProvider(array $domainConfig, string $host): DomainConfigProvider
|
||||||
|
{
|
||||||
|
$request = Request::create('https://'.$host.'/');
|
||||||
|
$requestStack = new RequestStack();
|
||||||
|
$requestStack->push($request);
|
||||||
|
|
||||||
|
return new DomainConfigProvider($domainConfig, $requestStack);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user