diff --git a/.ddev/config.yaml b/.ddev/config.yaml index fc02de4..0a37fab 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -4,7 +4,10 @@ docroot: public php_version: "8.3" webserver_type: apache-fpm xdebug_enabled: false -additional_hostnames: [] +additional_hostnames: + - mysnz + - mysbw + - myser additional_fqdns: [] database: type: mysql diff --git a/.env b/.env index da5f393..5fe9859 100644 --- a/.env +++ b/.env @@ -85,3 +85,6 @@ OAUTH_ENCRYPTION_KEY=580084fd179e67399467f59ee96658ac # postgresql+advisory://db_user:db_password@localhost/db_name LOCK_DSN=flock ###< symfony/lock ### + +# Domain-to-theme mapping (JSON format) +DOMAIN_THEME_MAP='{"ski-boarderweek.de":"sbw","semesterende-skireisen.de":"ser","snowzone.net":"snz"}' diff --git a/config/services.yaml b/config/services.yaml index 44c9487..18c05d8 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -9,6 +9,7 @@ parameters: path_to_keys: '%kernel.project_dir%/config/secret' bpn_debug: '%env(APP_BPN_DEBUG)%' default_booking_status: '%env(DEFAULT_BOOKING_STATUS)%' + domain_theme_map: '%env(json:DOMAIN_THEME_MAP)%' # Body dimensions choices for BodyDimensionsType body_dimensions.height_choices: @@ -146,3 +147,7 @@ services: tags: - { name: monolog.processor } + App\EventListener\DomainThemeListener: + arguments: + $domainThemeMap: '%domain_theme_map%' + diff --git a/src/Controller/Booking/Create/IndexController.php b/src/Controller/Booking/Create/IndexController.php index 0e2da41..0396ab9 100644 --- a/src/Controller/Booking/Create/IndexController.php +++ b/src/Controller/Booking/Create/IndexController.php @@ -103,9 +103,6 @@ class IndexController extends AbstractController // Store optional return URL in session (defaults to main EP site) $this->bookingService->storeReturnUrl($request, $params->returnUrl); - // Store theme for consistent styling throughout the booking flow - $this->bookingService->storeTheme($request, $params->theme); - // Create fresh booking session with the provided parameters $bookingDto = $this->bookingService->startFreshBooking($request, $dateId, $hotelId, $agencyId); diff --git a/src/EventListener/DomainThemeListener.php b/src/EventListener/DomainThemeListener.php new file mode 100644 index 0000000..1144794 --- /dev/null +++ b/src/EventListener/DomainThemeListener.php @@ -0,0 +1,52 @@ + $domainThemeMap + */ + public function __construct( + private readonly array $domainThemeMap, + ) { + } + + public function __invoke(RequestEvent $event): void + { + if (false === $event->isMainRequest()) { + return; + } + + $request = $event->getRequest(); + $host = $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; + } +} diff --git a/src/Model/BookingQueryParams.php b/src/Model/BookingQueryParams.php index a9cf1aa..fb7df48 100644 --- a/src/Model/BookingQueryParams.php +++ b/src/Model/BookingQueryParams.php @@ -21,7 +21,6 @@ readonly class BookingQueryParams public int $hotelId, public ?string $agency = null, - public string $theme = 'base', #[SerializedName('r')] public ?string $returnUrl = null, diff --git a/src/Service/BookingService.php b/src/Service/BookingService.php index 9f4748e..c3f6684 100644 --- a/src/Service/BookingService.php +++ b/src/Service/BookingService.php @@ -22,8 +22,6 @@ class BookingService public const BOOKING_EDIT_KEY = 'booking_edit'; public const RETURN_URL_KEY = 'booking_return_url'; public const DEFAULT_RETURN_URL = 'https://www.ep-reisen.de'; - public const THEME_KEY = 'booking_theme'; - public const DEFAULT_THEME = 'base'; public function __construct( private readonly TravelDataService $travelDataService, @@ -195,33 +193,6 @@ class BookingService return $request->getSession()->get(self::RETURN_URL_KEY, self::DEFAULT_RETURN_URL); } - /** - * Stores the theme identifier in the session. - * - * Sanitizes the theme value to allow only alphanumeric characters and hyphens. - * Falls back to the default theme if empty or invalid. - */ - public function storeTheme(Request $request, string $theme): void - { - $sanitized = preg_replace('/[^a-zA-Z0-9-]/', '', $theme); - - if ('' === $sanitized) { - $sanitized = self::DEFAULT_THEME; - } - - $request->getSession()->set(self::THEME_KEY, $sanitized); - } - - /** - * Retrieves the theme identifier from the session. - * - * Returns the default theme if not set in session. - */ - public function getTheme(Request $request): string - { - return $request->getSession()->get(self::THEME_KEY, self::DEFAULT_THEME); - } - /** * Creates a fresh booking session with the provided travel parameters. * diff --git a/src/Twig/AppRuntime.php b/src/Twig/AppRuntime.php index e0cc9c0..fbb6063 100644 --- a/src/Twig/AppRuntime.php +++ b/src/Twig/AppRuntime.php @@ -5,10 +5,10 @@ declare(strict_types=1); namespace App\Twig; use App\BusProNet\DataProvider\CountryDataProvider; +use App\EventListener\DomainThemeListener; use App\Form\Model\BookingDto; use App\Form\Service\CreateFieldStateProvider; use App\Form\Service\EditFieldStateProvider; -use App\Service\BookingService; use App\Service\ParticipantEligibilityService; use Symfony\Component\Form\FormView; use Symfony\Component\HttpFoundation\RequestStack; @@ -228,18 +228,20 @@ class AppRuntime implements RuntimeExtensionInterface } /** - * Returns the current booking theme from session. + * Returns the current booking theme from request attribute. * + * The theme is resolved by DomainThemeListener based on the request host. * Defaults to 'base' if no theme is set. */ public function getBookingTheme(): string { $request = $this->requestStack->getCurrentRequest(); + if (null === $request) { - return BookingService::DEFAULT_THEME; + return 'base'; } - return $request->getSession()->get(BookingService::THEME_KEY, BookingService::DEFAULT_THEME); + return $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE, 'base'); } /** diff --git a/tests/EventListener/DomainThemeListenerTest.php b/tests/EventListener/DomainThemeListenerTest.php new file mode 100644 index 0000000..117c021 --- /dev/null +++ b/tests/EventListener/DomainThemeListenerTest.php @@ -0,0 +1,119 @@ +kernel = $this->createMock(HttpKernelInterface::class); + } + + public function testSetsThemeAttributeForMatchingDomain(): void + { + $listener = new DomainThemeListener([ + 'sbw-reisen' => 'sbw', + 'ser-reisen' => 'ser', + ]); + + $request = Request::create('https://www.sbw-reisen.de/bookings/create'); + $event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener($event); + + $this->assertSame('sbw', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE)); + } + + public function testSetsDefaultThemeForUnknownDomain(): void + { + $listener = new DomainThemeListener([ + 'sbw-reisen' => 'sbw', + ]); + + $request = Request::create('https://www.ep-reisen.de/bookings/create'); + $event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener($event); + + $this->assertSame('base', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE)); + } + + public function testMatchesPartialDomain(): void + { + $listener = new DomainThemeListener([ + 'ser-reisen' => 'ser', + ]); + + $request = Request::create('https://portal.ser-reisen.de/bookings'); + $event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener($event); + + $this->assertSame('ser', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE)); + } + + public function testIgnoresSubRequests(): void + { + $listener = new DomainThemeListener([ + 'sbw-reisen' => 'sbw', + ]); + + $request = Request::create('https://www.sbw-reisen.de/bookings/create'); + $event = new RequestEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST); + + $listener($event); + + $this->assertFalse($request->attributes->has(DomainThemeListener::THEME_ATTRIBUTE)); + } + + public function testFirstMatchWins(): void + { + $listener = new DomainThemeListener([ + 'reisen' => 'generic', + 'sbw-reisen' => 'sbw', + ]); + + $request = Request::create('https://www.sbw-reisen.de/bookings/create'); + $event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener($event); + + $this->assertSame('generic', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE)); + } + + public function testEmptyMapReturnsDefaultTheme(): void + { + $listener = new DomainThemeListener([]); + + $request = Request::create('https://www.sbw-reisen.de/bookings/create'); + $event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener($event); + + $this->assertSame('base', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE)); + } + + public function testMatchesLocalhostForDevelopment(): void + { + $listener = new DomainThemeListener([ + 'localhost' => 'sbw', + ]); + + $request = Request::create('http://localhost/bookings/create'); + $event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener($event); + + $this->assertSame('sbw', $request->attributes->get(DomainThemeListener::THEME_ATTRIBUTE)); + } +}