52 KiB
Technical Documentation
Customer Portal for Ski Travel Company
This document provides comprehensive technical documentation for experienced PHP and Symfony developers working with this codebase. It covers the architecture, components, and implementation details necessary to navigate and extend the application.
Table of Contents
- Architecture Overview
- Directory Structure & Namespaces
- BusProNet API Integration
- Controllers
- Service Layer
- Form System
- Security
- Database & Entities
- Frontend Integration
- CLI Commands
- Logging & Debugging
- Validation
- BusProNet API Quirks & Workarounds
1. Architecture Overview
1.1 System Overview
This is a Symfony 6.4 web application serving as a customer portal for a ski travel company. The application integrates with the BusProNet API to manage customer data, bookings, and travel information.
┌─────────────────────────────────────────────────────────────────────────────┐
│ User Interface │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Browser │ │ HTMX │ │ Stimulus │ │ TailwindCSS │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └─────────────────────┘ │
└─────────┼────────────────┼────────────────┼─────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Symfony Application │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Controllers ││
│ │ Security │ Booking/Create │ Booking/Edit │ Account │ API │ Admin ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Service Layer ││
│ │ BookingService │ TravelDataService │ PricingCalculators │ Insurance ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Form System ││
│ │ Form Types │ Field Handlers │ Conditions │ State Providers │ DTOs ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ BusProNet Integration ││
│ │ ApiClient │ XmlParsers │ DataProcessors │ XmlLoaders │ Models ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ Database │ │ BusProNet API │
│ User │ LogEntry │ Draft│ │ Socket-based XML Protocol │
└─────────────────────────┘ └─────────────────────────────────┘
1.2 Key Application Flows
Multi-Step Booking Creation
Step 1 (Room Selection)
│
▼
Step 2 (Participant Details + Services)
│
▼
Step 3 (Payment - API Inquiry Validation)
│
▼
Step 4 (Confirmation - API Booking Submission)
│
▼
Success Page
Booking Edit Flow
Load Booking from API
│
├─ Apply Saved Draft (if exists)
│
▼
Card-based Participant Editing
│
├─ Auto-save Draft on Each Participant
│
▼
Submit Update to API
│
▼
Delete Draft on Success
1.3 Core Technologies
| Component | Technology |
|---|---|
| Framework | Symfony 6.4 |
| PHP Version | 8.1+ |
| Database | Doctrine ORM |
| Frontend | Stimulus, HTMX, TailwindCSS |
| Build | Webpack Encore |
| API Communication | Socket-based XML |
| Caching | Symfony Cache (TagAware) |
2. Directory Structure & Namespaces
2.1 Source Directory (src/)
src/
├── Admin/ # EasyAdmin custom fields
│ └── Field/JsonDataField.php
├── BusProNet/ # BusProNet API integration layer
│ ├── ApiClient.php # Main API client
│ ├── Constants.php # Service tokens and constants
│ ├── DataProcessor/ # DTO ↔ API payload transformation
│ ├── DataProvider/ # Data providers (countries)
│ ├── Exception/ # API-specific exceptions
│ ├── Form/ # Form choice loaders
│ ├── Model/ # Data models (39 classes)
│ ├── Traits/ # Shared traits
│ ├── Utility/ # Helper utilities
│ ├── XmlLoader/ # Cached XML file loaders
│ └── XmlParser/ # XML response parsers (28 classes)
├── Command/ # CLI commands (9 commands)
├── Controller/ # HTTP controllers
│ ├── Account/ # Account management
│ ├── Admin/ # EasyAdmin controllers
│ ├── Api/ # OAuth2-protected JSON endpoints
│ ├── Booking/ # Booking workflows
│ │ ├── Create/ # Multi-step creation
│ │ ├── Edit/ # Booking editing
│ │ └── Traits/ # Shared controller logic
│ ├── RegistrationController.php
│ ├── ResetPasswordController.php
│ └── SecurityController.php
├── Entity/ # Doctrine entities
│ ├── User.php
│ ├── LogEntry.php
│ └── BookingEditDraft.php
├── EventListener/ # Symfony event listeners
├── Exception/ # Application exceptions
├── Form/ # Form system
│ ├── Model/ # DTOs (BookingDto, ParticipantDto, etc.)
│ ├── Service/ # Field handlers & conditions
│ │ └── Condition/ # 22+ condition classes
│ ├── DataTransformer/ # Form data transformers
│ └── Extension/ # Form extensions (XSS protection)
├── Htmx/ # HTMX utilities
├── Logger/ # Custom Monolog handlers/processors
├── Model/ # Application models
├── Repository/ # Doctrine repositories
├── Security/ # Authentication & authorization
│ └── Voter/ # Custom voters
├── Service/ # Business logic services (21 services)
├── Twig/ # Template extensions
└── Validator/ # Custom validation constraints
└── Constraints/ # Constraint classes
2.2 Configuration (config/)
config/
├── packages/ # Bundle configurations
│ ├── security.yaml # Firewalls, access control
│ ├── league_oauth2_server.yaml
│ └── monolog.yaml # Logging configuration
├── routes/ # Route definitions
├── services.yaml # Service definitions, BPN config
├── secret/ # RSA keys for encryption
└── bundles.php # Enabled bundles
2.3 Assets (assets/)
assets/
├── app.js # Main entry point
├── bootstrap.js # Stimulus initialization
├── loading.js # HTMX loading indicator
├── controllers/ # Stimulus controllers (10)
├── styles/ # TailwindCSS styles
│ ├── app.css # Main stylesheet
│ └── components/ # Component styles
├── images/ # Static images
├── fonts/ # Web fonts (Lato)
└── favicon/ # Favicon files
3. BusProNet API Integration
3.1 Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Application Layer │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ ApiClient │
│ src/BusProNet/ApiClient.php │
│ - Socket connection management with load balancing │
│ - MD5 authentication (username + password + date + type) │
│ - Automatic retry on busy server │
│ - XML debugging dumps │
└─────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────────┐
│ DataProcessor/ │ │ XmlParser/ │
│ - BookingDataProcessor │ │ - ApiResponseParser (router) │
│ - BookingPayloadBuilder │ │ - TravelParser, BookingParser │
│ - ParticipantServiceProcessor│ │ - PersonalDataParser, etc. │
└──────────────────────────────┘ └──────────────────────────────────┘
(DTO → API payload) (XML → Model objects)
3.2 ApiClient
Location: src/BusProNet/ApiClient.php
Connection & Authentication
// Load balancing across multiple ports
$this->selectedPort = $this->config['bpn_api_ports'][array_rand($this->config['bpn_api_ports'])];
// MD5-based authentication key
private function createKey(string $username, string $password, string $type): string
{
$date = (new \DateTimeImmutable())->format('Ymd');
return md5($username.$password.$date.$type);
}
Request Types
| Constant | Value | Purpose |
|---|---|---|
TYPE_CUSTOMER_DATA |
KUNDENKONTO |
Customer account operations |
TYPE_BOOKING |
BUCHUNG |
Create new bookings |
TYPE_BOOKING_UPDATE |
BUCHUNGAENDERUNG |
Modify existing bookings |
TYPE_PRODUCT_DATA |
PRODUKTDATEN |
Travel/product details |
TYPE_AVAILABILITY |
VERFUEGBARKEIT |
Service availability |
TYPE_PURCHASE_VOUCHER |
GUTSCHEINPRUEFUNGEINLOESUNG |
Validate purchase vouchers |
TYPE_PROMO_VOUCHER |
AKTIONSGUTSCHEIN |
Validate promotional codes |
Key Methods
| Method | Return Type | Purpose |
|---|---|---|
getPersonalData(email, password) |
PersonalData|Notification |
Authenticate and fetch profile |
getBookings(email, password) |
BaseData|Notification |
List customer bookings |
getBooking(email, password, id) |
Booking|Notification |
Single booking details |
getTravelData(travelId, hotelId) |
Travel|Notification |
Travel package details |
createBookingInquiry(BookingDto) |
BookingResponse|Notification |
Validate booking (phase 1) |
createBooking(BookingDto) |
BookingResponse|Notification |
Submit booking (phase 2) |
updateBooking(BookingDto) |
BookingUpdate|Notification |
Modify existing booking |
Two-Phase Booking Process
- Inquiry Phase (
createBookingInquiry): Validates data and returns pricing without creating a booking - Booking Phase (
createBooking): Creates the actual booking after inquiry validation
Timeout Configuration
| Setting | Default | Purpose |
|---|---|---|
connection_timeout |
5s | Socket connect |
stream_timeout |
30s | Read/write operations |
total_timeout |
45s | Entire operation including retries |
busy_retry_attempts |
3 | Retry count for busy server |
busy_retry_delay |
1s | Delay between retries |
3.3 Response Parsing
Router: src/BusProNet/XmlParser/ApiResponseParser.php
Routes XML responses to specialized parsers based on response type and sub-type.
Parser Classes
| Parser | Output Model | Purpose |
|---|---|---|
TravelParser |
Travel |
Travel packages with services, rooms, pickups |
BookingParser |
Booking |
Complete booking with participants |
PersonalDataParser |
PersonalData |
Customer profile and address |
RoomsParser |
Room[] |
Hotel room information |
ServicesParser |
Service[] |
Additional services and transportation |
InsuranceParser |
Insurance[] |
Insurance options |
PickupsParser |
Pickup[] |
Pickup/dropoff locations |
PurchaseVoucherParser |
PurchaseVoucher |
Voucher validation results |
PromoVoucherParser |
PromoVoucher |
Promo code validation results |
3.4 Data Models
Location: src/BusProNet/Model/
Travel
class Travel
{
public ?int $id = null;
public ?int $hotelId = null;
public ?\DateTimeImmutable $dateFrom = null;
public ?\DateTimeImmutable $dateTo = null;
public array $additionalServices = [];
public array $transportationServices = [];
public array $rooms = [];
public array $pickups = [];
public array $dropOffs = [];
public array $insurances = [];
public function getAdditionalServicesBySubTypes(mixed $subTypes): array;
public function getTransportationServicesByDirection(string $direction): array;
public function getAvailableRooms(): array;
public function requiresInquiryBooking(): bool;
}
Service Token Constants
| Token | Meaning |
|---|---|
KUR |
Courses (ski lessons) |
SPA |
Ski passes |
SON |
Additional services |
VPF |
Board/catering |
VER-VE8 |
Equipment rentals |
PAR |
Parking |
RRV, PAK |
Insurance types |
LVS |
Rental insurance |
3.5 Data Processors
Location: src/BusProNet/DataProcessor/
| Class | Purpose |
|---|---|
BookingDataProcessor |
Orchestrates booking data transformation |
BookingPayloadBuilder |
Constructs nested XML payload structure |
ParticipantServiceProcessor |
Maps participant service selections |
ServiceMappingCollector |
Collects service IDs by type |
PersonalDataSynchronizer |
Syncs personal data between applicant/participants |
PickupPlanningTransformer |
Transforms pickup planning data |
3.6 XML Loaders
Location: src/BusProNet/XmlLoader/
Load data from pre-cached XML files for performance:
| Loader | Purpose |
|---|---|
TravelLoader |
Travel packages |
HotelLoader |
Hotel details |
InsuranceLoader |
Insurance options |
PickupLoader |
Pickup locations |
AgencyLoader |
Agency information |
4. Controllers
4.1 Authentication Controllers
SecurityController (/)
GET /- Login page with booking flow detectionGET /logout- Logout endpoint
Detects OAuth2 authorization requests and booking flow context from session.
RegistrationController (/registration)
POST /registration- Handle customer registration via BusProNet API
ResetPasswordController (/reset-password)
POST /reset-password- Password reset request via BusProNet API
4.2 Booking Creation Flow
Create/IndexController
GET /bookings/create- Entry point with HTMX loadingPOST /bookings/create/init- Initialize fresh booking sessionPOST /bookings/cancel- Cancel and redirect to return URLGET /bookings/create/error- Error display page
Validates query parameters (dateId, hotelId, agency), resolves agency codes, creates fresh BookingDto.
Create/Step1Controller (Room Selection)
GET /bookings/create/rooms- Room selection formPOST /bookings/create/refresh- HTMX refresh for sidebar
Validates step access, detects room changes, updates booking status for inquiry mode.
Create/Step2Controller (Participant Details)
GET /bookings/create/participants- Participant cards overviewGET/POST /bookings/create/participants/{index}- Edit single participantPOST /bookings/create/participants/{index}/refresh- HTMX form refresh
Card-based UI for scalability (50+ participants). Auto-assigns rooms, preselects mandatory services, saves drafts.
Create/Step3Controller (Payment Validation)
GET /bookings/create/payment- Payment formPOST /bookings/create/payment/refresh- HTMX refresh
Calls createBookingInquiry() for validation, compares calculated vs. API prices.
Create/Step4Controller (Confirmation)
GET /bookings/create/confirmation- Final confirmation
Calls createBooking() for submission, clears caches, stores booking number.
Create/SuccessController
GET /bookings/create/success- Success page with booking number
4.3 Booking Management
Booking/IndexController
GET /bookings- List user's bookings
Booking/DownloadController
GET /bookings/{id}/documents- Download booking documentsGET /bookings/{id}/invoice- Download invoice/statement
Booking/Edit/IndexController
GET /bookings/{id}/edit/start- Clear cache, redirect to editGET /bookings/{id}/edit- Edit participant cardsGET/POST /bookings/{id}/edit/participants/{index}- Edit single participantPOST /bookings/{id}/edit/reload- Reload from API (discard changes)POST /bookings/{id}/edit/cancel- Cancel edit (preserve draft)
Similar card-based UI as Step 2. Auto-saves drafts, uses fingerprinting for change detection.
4.4 Account Controllers
Account/IndexController
GET /account- Account dashboard (requiresROLE_USER)
Account/PersonalDataController
GET /personal-data- View/edit personal dataPOST /personal-data/newsletter- Toggle newsletter subscription
4.5 API Controllers (OAuth2 Protected)
All under /api prefix, require OAuth2 authentication.
| Controller | Routes | Purpose |
|---|---|---|
UserinfoController |
GET /api/userinfo |
User profile (scope-filtered) |
ProductController |
GET /api/products |
Product listing |
TravelController |
GET /api/travels/* |
Travel data endpoints |
HotelController |
GET /api/hotels/* |
Hotel information |
PickupController |
GET/POST /api/pickups* |
Pickup locations and planning |
CountryController |
GET /api/countries |
Country list |
CrmAttributeController |
GET /api/crm-attributes |
User CRM attributes |
ContactFormController |
POST /api/contactform |
Contact form submission |
LastUpdateController |
GET /api/last-update |
Data sync timestamp |
4.6 Admin Controllers (EasyAdmin)
| Controller | Entity | Purpose |
|---|---|---|
DashboardController |
- | Admin panel home |
UserCrudController |
User |
User management (read-only) |
BookingEditDraftCrudController |
BookingEditDraft |
Draft management + export |
LogEntryCrudController |
LogEntry |
Activity logging |
XmlDumpController |
- | API request debugging |
4.7 Shared Traits
| Trait | Purpose |
|---|---|
BookingCreateTrait |
Step validation, redirects, error handling |
BookingExceptionHandlerTrait |
Safe DTO retrieval with error messages |
ParticipantCardFlowTrait |
Card UI, form creation, notifications |
5. Service Layer
5.1 Booking Services
BookingService
Location: src/Service/BookingService.php
Orchestrates booking workflow, session management, participant calculations.
| Method | Purpose |
|---|---|
startFreshBooking() |
Initialize new booking |
getOrCreateBookingCreateDto() |
Session DTO management |
ensureCorrectNumberOfParticipants() |
Sync participant count |
preselectMandatoryServices() |
Pre-select required services |
updateBookingStatusFromRoomSelection() |
Update inquiry status |
getParticipantsCount() |
Calculate from room selections |
groupRoomsBySelectionType() |
Group by "by_pax" or "by_room" |
BookingEditDataLoaderService
Loads booking data for edit mode with automatic draft restoration.
| Method | Purpose |
|---|---|
loadFormData() |
Load or initialize for editing |
initializeFromApi() |
Fresh load with draft application |
fetchBookingData() |
Cached API fetch (5-min TTL) |
invalidateUserBookingCaches() |
Bulk cache invalidation |
BookingEditDraftService
Draft persistence for edit sessions.
| Method | Purpose |
|---|---|
findDraft() |
Retrieve existing draft |
saveDraft() |
Create or update draft |
deleteDraft() |
Remove after submission |
applyDraftToDto() |
Merge draft onto API data |
5.2 Pricing Calculators
BookingPriceCalculatorService
Facade for all pricing calculations.
| Method | Purpose |
|---|---|
getPricingBreakdown() |
Complete breakdown with surcharges |
calculateGrandTotal() |
Total booking price |
calculateRoomPricing() |
Room costs only |
calculateServicePricing() |
Service costs only |
calculateIndividualParticipantPrice() |
Per-participant total |
Supporting Calculators
| Class | Purpose |
|---|---|
RoomPricingCalculator |
Room cost calculations |
ServicePricingCalculator |
Service aggregation by subtype |
ParticipantPricingCalculator |
Per-participant with caching |
5.3 Travel Data Services
TravelDataService
Unified interface for travel data from XML files and API.
| Method | Purpose |
|---|---|
getTravelData() |
Primary method with source selection |
getTravelDataFromXml() |
Load from cached XML |
getTravelDataFromApi() |
Load from live API |
getMutabilityData() |
Mutable services flags (12h cache) |
getAvailabilityData() |
Service availability (10-min cache) |
enrichWithFreshAvailabilities() |
Ensure current data |
InsuranceService
Consolidated insurance operations with eligibility filtering.
| Method | Purpose |
|---|---|
getSelectableInsurances() |
Non-complementary options |
getEligibleInsurances() |
Filtered by participant criteria |
filterByType() |
Group by subType + familyInsurance |
reassignInsuranceForPriceChange() |
Find compatible insurance |
batchAssignInsuranceToParticipants() |
Bulk assignment |
VoucherValidationService
Voucher validation with API caching (15-min TTL).
| Method | Purpose |
|---|---|
validatePurchaseVoucher() |
Validate redemption codes |
validatePromoVoucher() |
Validate promo codes with context |
5.4 Supporting Services
| Service | Purpose |
|---|---|
ParticipantEligibilityService |
Check booking eligibility by skipass availability |
ServiceAvailabilityCalculator |
Track dynamic availability within session |
RoomAssignmentService |
Automatic room assignment |
BookingSummaryDataService |
Consolidate sidebar display data |
ParticipantCardDataService |
Extract card display data |
BookingFingerprintService |
SHA-256 change detection |
BookingExportService |
Excel export from drafts |
CmsDataService |
CMS data retrieval (hotel images) |
XmlDumpService |
API debugging file management |
6. Form System
6.1 Architecture Overview
The form system implements sophisticated conditional field visibility with dynamic state management.
┌─────────────────────────────────────────────────────────────────────┐
│ Form Submission │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ ParticipantFieldHandlerRegistry │
│ - Topological sort by dependencies │
│ - Execute handlers in correct order │
│ - Sync DTO back to submitted data │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Field Handlers (20+) │
│ DateOfBirth → SkiPass → Rentals → Insurance → ... │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Field State Providers │
│ CreateFieldStateProvider │ EditFieldStateProvider │
│ - Evaluate conditions for each field │
│ - Determine: hidden, static_text, readonly, disabled, required │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Conditions (22+) │
│ CompositeCondition (AND/OR/NOT) │
│ AgeRangeCondition │ SkiPassSelectionCondition │ ... │
└─────────────────────────────────────────────────────────────────────┘
6.2 Form Types
Booking Flow Forms
| Form Type | Purpose |
|---|---|
BookingCreateStep1Type |
Room selection |
BookingCreateStep2Type |
Embeds BookingParticipantType |
BookingCreateStep3Type |
Additional participants |
BookingCreateStep4Type |
Confirmation/payment |
BookingEditType |
Edit mode root form |
BookingEditParticipantType |
Edit individual participants |
BookingParticipantType |
Base participant form (create + edit) |
Supporting Forms
| Form Type | Purpose |
|---|---|
AddressType |
Street, postal code, city, country |
BodyDimensionsType |
Height, weight, shoe size |
RoomAssignmentType |
Room selection |
PersonalDataType |
Customer profile |
PaymentType |
Payment information |
RegistrationType |
User registration |
BankAccountType |
Bank details |
6.3 DTOs
ParticipantDto
class ParticipantDto
{
// Personal Data
public ?string $firstName = null;
public ?string $lastName = null;
public ?string $gender = null;
public ?\DateTimeImmutable $dateOfBirth = null;
public ?Address $address = null;
// Service Selections
public ?int $assignedRoomId = null;
public array $courses = [];
public array $additionalServices = [];
public ?Service $skiPass = null;
public array $rentals = [];
public ?Service $transportationOutbound = null;
public ?Service $transportationInbound = null;
public ?Pickup $pickup = null;
// Insurance
public ?Insurance $insurance = null;
public bool $bulkInsuranceBooking = false;
// Vouchers
public ?string $purchaseVoucherCode = null;
public ?string $promoVoucherCode = null;
}
BookingDto
class BookingDto
{
public const MODE_CREATE = 'create';
public const MODE_EDIT = 'edit';
public string $mode = self::MODE_CREATE;
public ?Travel $travel = null;
public ?Booking $booking = null;
public array $participants = [];
public array $selectedRooms = [];
public ?int $agencyId = null;
public ?string $paymentMethod = null;
}
6.4 Field Handler System
Handler Registry
Location: src/Form/Service/ParticipantFieldHandlerRegistry.php
Uses topological sort (Kahn's algorithm) to execute handlers in dependency order.
interface ParticipantFieldHandlerInterface
{
public function getFieldName(): string;
public function getDependencies(): array;
public function shouldProcess(...): bool;
public function processField(...): void;
public function getFieldStateModifications(...): array;
}
Handler Implementations
| Handler | Field | Dependencies |
|---|---|---|
ParticipantDateOfBirthFieldHandler |
dateOfBirth | None (first) |
ParticipantSkiPassFieldHandler |
skiPass | dateOfBirth |
ParticipantRentalsFieldHandler |
rentals | dateOfBirth, skiPass |
ParticipantCoursesFieldHandler |
courses | dateOfBirth |
ParticipantBoardFieldHandler |
board | dateOfBirth |
ParticipantTransportationOutboundFieldHandler |
transportationOutbound | dateOfBirth |
ParticipantTransportationInboundFieldHandler |
transportationInbound | dateOfBirth |
ParticipantInsuranceFieldHandler |
insurance | All price-affecting fields |
ParticipantBulkInsuranceFieldHandler |
bulkInsuranceBooking | None |
ParticipantPickupFieldHandler |
pickup | transportation |
ParticipantParkingFieldHandler |
parking | transportationOutbound |
ParticipantPromoVoucherFieldHandler |
promoVoucherCode | None |
ParticipantPurchaseVoucherFieldHandler |
purchaseVoucherCode | None |
6.5 Condition System
CompositeCondition
CompositeCondition::and(condition1, condition2);
CompositeCondition::or(condition1, condition2);
CompositeCondition::not(condition);
Core Conditions
| Condition | Purpose |
|---|---|
FirstParticipantCondition |
Is applicant (index 0) |
MultipleParticipantsCondition |
2+ participants |
DateOfBirthProvidedCondition |
Date of birth is set |
AgeRangeCondition(min, max) |
Age in range |
BabyAgeCondition |
Age 0-2 |
SkiPassSelectionCondition |
Ski pass selected |
RentalSelectionCondition |
Rentals selected |
RoomSelectionCondition(['codes']) |
Specific room type |
ServiceSubTypeCondition::equals(field, type) |
Service type match |
BookingEligibilityCondition |
Has eligible services |
BulkInsuranceBookingCondition |
Bulk insurance active |
PersonalDataMutabilityCondition |
BPN mutability flag |
BookingModeCondition(mode) |
Create or edit mode |
6.6 Field State Providers
| State | Behavior |
|---|---|
hidden |
Field excluded from form |
static_text |
Rendered as read-only text |
readonly |
Visible but not editable |
disabled |
User cannot interact |
required |
Field is mandatory |
Example: Insurance Field Visibility (Create Mode)
// Hidden if:
// - Date of birth not provided OR
// - Bulk insurance active for dependents OR
// - Participant ineligible (no skipasses available)
CompositeCondition::or(
CompositeCondition::not(new DateOfBirthProvidedCondition()),
new BulkInsuranceBookingCondition(),
new BookingEligibilityCondition()
)
7. Security
7.1 Authentication
BpnAuthenticator
Location: src/Security/BpnAuthenticator.php
Custom authenticator that validates credentials against BusProNet API.
Login Form → MD5 Hash Password → ApiClient::getPersonalData()
│
├─ Valid → Create/Update User Entity
│ Get CRM Attributes (roles, hotel codes)
│ Encrypt password with RSA
│ Store in database
│
└─ Invalid → CustomUserMessageAuthenticationException
User Entity Password Handling
- Plain password hashed with MD5 for API authentication
- MD5 hash encrypted with RSA before database storage
- Decrypted when needed for subsequent API calls
7.2 Encryption
Location: src/Security/Crypt.php
Uses spatie/crypto library with RSA asymmetric encryption.
| Method | Purpose |
|---|---|
encrypt(string) |
Encrypt with private key |
decrypt(string) |
Decrypt with public key |
sign(string) |
Create digital signature |
verify(string, signature) |
Verify signature |
Keys stored in config/secret/ directory.
7.3 Authorization
BookingVoter
Location: src/Security/Voter/BookingVoter.php
This voter currently exists as legacy authorization logic and is not actively invoked by booking controllers.
Current booking routes (/bookings/*) are guarded with ROLE_USER, while final document access checks are enforced by BusProNet.
7.4 OAuth2
Configured via League OAuth2 Server for API endpoints.
- Access Token TTL: 10 minutes
- Scopes:
email,id,profile,roles,api - Grants: Authorization code, client credentials
API Protection
# config/packages/security.yaml
api:
pattern: ^/api
security: true
stateless: true
oauth2: true
7.5 Access Control
| Path | Role |
|---|---|
^/authorize |
IS_AUTHENTICATED_REMEMBERED |
^/admin |
ROLE_ADMIN |
^/ |
PUBLIC_ACCESS |
8. Database & Entities
8.1 User Entity
Location: src/Entity/User.php
| Property | Type | Purpose |
|---|---|---|
email |
string (unique) | User identifier |
password |
string (nullable) | RSA-encrypted API password |
personId |
int (nullable) | BusProNet person ID |
addressId |
int (nullable) | BusProNet address ID |
roles |
array (JSON) | CRM roles |
hotelCodes |
array (JSON) | Associated hotels |
lastLoginAt |
DateTimeImmutable | Audit timestamp |
8.2 LogEntry Entity
Location: src/Entity/LogEntry.php
| Property | Type | Purpose |
|---|---|---|
channel |
string | Log channel (core, bpn, auth) |
message |
string | Log message |
context |
array (JSON) | Contextual data |
extra |
array (JSON) | Request ID, URI, user info |
createdAt |
DateTimeImmutable | Timestamp |
8.3 BookingEditDraft Entity
Location: src/Entity/BookingEditDraft.php
| Property | Type | Purpose |
|---|---|---|
user |
User (ManyToOne) | Draft owner |
bookingId |
int | BusProNet booking ID |
bookingNumber |
int (nullable) | User-facing number (vorgang) |
travelDate |
DateTimeImmutable | For cleanup queries |
dateId |
int (nullable) | Travel date ID |
hotelId |
int (nullable) | Hotel ID |
formData |
array (JSON) | Saved form values |
createdAt |
DateTimeImmutable | Creation timestamp |
updatedAt |
DateTimeImmutable | Last update |
Unique constraint: (user_id, booking_id)
8.4 Repositories
LogEntryRepository
public function deleteOlderThan(DateTimeImmutable $threshold): int;
BookingEditDraftRepository
public function findByUserAndBooking(User $user, int $bookingId): ?BookingEditDraft;
public function deleteByUserAndBooking(User $user, int $bookingId): void;
public function deleteExpiredDrafts(): int;
9. Frontend Integration
9.1 Stimulus Controllers
Location: assets/controllers/
| Controller | Purpose |
|---|---|
modal_controller |
Modal dialog management |
toggle_controller |
Collapsible sections with state persistence |
checkbox_toggle_controller |
Container visibility based on checkboxes |
select_toggle_controller |
Container visibility based on select values |
form_collection_controller |
Dynamic add/remove form fields |
step_input_controller |
Numeric spinner input |
birthday_controller |
Date of birth validation/completion event |
toast_controller |
Toast notifications via Toastify |
tooltip_controller |
Tooltips via Tippy.js |
backbutton_controller |
Browser back navigation |
9.2 HTMX Integration
HxRedirectResponse
// src/Htmx/HxRedirectResponse.php
class HxRedirectResponse extends Response
{
public function __construct(string $url)
{
parent::__construct('', Response::HTTP_OK, ['HX-Redirect' => $url]);
}
}
Triggers full-page navigation without CORS issues.
HxTrait
// Out-of-band swap for multiple sections
protected function htmxOobResponse(
string $templateName,
array $blockNames,
array $context = [],
?string $pushUrl = null
): Response;
// Intelligent redirect for HTMX and regular requests
protected function htmxRedirect(Request $request, string $url): Response;
HTMX Configuration
// assets/app.js
htmx.config.includeIndicatorStyles = false;
htmx.config.historyEnabled = true;
htmx.config.historyCacheSize = 10;
htmx.config.allowScriptTags = false;
htmx.config.withCredentials = true;
htmx.config.timeout = 50000;
9.3 Asset Pipeline
Webpack Encore Configuration
Encore
.setOutputPath('public/build/')
.setPublicPath('/build')
.addEntry('app', './assets/app.js')
.enableStimulusBridge('./assets/controllers.json')
.enablePostCssLoader()
.enableVersioning(Encore.isProduction())
TailwindCSS
/* assets/styles/app.css */
@import "tailwindcss/base";
@import "_base.css";
@import "tailwindcss/components";
@import "_components.css";
@import "tailwindcss/utilities";
9.4 NPM Scripts
{
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
}
10. CLI Commands
10.1 BusProNet API Commands
app:bpn:fetch-travel
php bin/console app:bpn:fetch-travel <travel-id> [--filename|-f <name>] [--dry-run]
Fetches travel data from BusProNet API and saves to XML export directory.
app:bpn:replay
php bin/console app:bpn:replay <file> [--dry-run|-d] [--output|-o <file>]
Replays stored XML requests against the API for debugging.
app:bpn:xml-sync
php bin/console app:bpn:xml-sync [--force|-f] [--dry-run]
Synchronizes XML export files from remote SFTP. Invalidates caches on completion.
10.2 Maintenance Commands
app:draft:cleanup
php bin/console app:draft:cleanup [--dry-run]
Deletes booking edit drafts for past travels.
app:draft:inspect
php bin/console app:draft:inspect [--days|-d <n>] [--booking-id|-b <id>] [--delete]
Inspects and optionally deletes drafts.
app:cleanup:log-entries
php bin/console app:cleanup:log-entries [--retention|-r <months>]
Removes log entries older than retention period (default: 6 months).
app:cleanup:xml-dumps
php bin/console app:cleanup:xml-dumps
Removes XML debug dumps older than 3 days.
app:cleanup:newsletter-opt-in-requests
php bin/console app:cleanup:newsletter-opt-in-requests
Removes expired pending newsletter double opt-in requests.
- Request TTL is controlled by
NEWSLETTER_CONFIRMATION_TTL_HOURS(default: 1 hour). - Expired pending requests are removed immediately when they are encountered (new request / confirmation attempt).
- Scheduled cleanup removes all currently expired pending requests (
expires_at <= now) as a safety net. - Confirmed requests are converted into durable per-list newsletter consent records and then deleted.
app:mailjet:newsletter-webhook
php bin/console app:mailjet:newsletter-webhook register
php bin/console app:mailjet:newsletter-webhook remove 123
php bin/console app:mailjet:newsletter-webhook deactivate
Creates or deletes the Mailjet unsub callback for POST /webhooks/mailjet/newsletter.
- Uses
APP_BASE_URLby default and appends the webhook path. - Pass
--urlto target a different full webhook URL. registerprints the API response payload so you can note the callback ID.removeanddeactivaterequire the callback ID returned by Mailjet.
10.3 Setup Commands
app:crypto:generate-keys
php bin/console app:crypto:generate-keys
Generates RSA keypair for encryption in configured path.
11. Logging & Debugging
11.1 Log Channels
| Channel | Purpose |
|---|---|
core |
Core application logic |
bpn |
BusProNet API interactions |
auth |
Authentication/security events |
11.2 Custom Processors
Location: src/Logger/
| Processor | Adds to Record |
|---|---|
RequestIdProcessor |
extra['request_id'] |
RequestInfoProcessor |
extra['uri'], extra['method'] |
UserDataProcessor |
extra['user']['username'], extra['user']['roles'] |
11.3 Database Handler
Location: src/Logger/DatabaseHandler.php
Persists INFO+ level logs to LogEntry entity. Replaces message placeholders with context values.
11.4 XML Debugging
When debug mode is enabled, API requests/responses are dumped to var/bpn/:
r-00m8z7k58a-8k1pvd9q_1_request.xml
r-00m8z7k58a-8k1pvd9q_1_response.xml
View via Admin Panel: Admin → Log → XML Dumps action.
12. Validation
12.1 Custom Constraints
Location: src/Validator/Constraints/
ParticipantValidator
Cross-field validation for participant data.
// Requires pickup when bus transportation selected
if (($hasOutboundBus || $hasInboundBus) && null === $participant->pickup) {
$this->context->buildViolation('Bitte auswählen')
->atPath('pickup')
->addViolation();
}
RoomSelectionValidator
- At least one room must be selected
- Baby rooms cannot be booked standalone
// Baby room + regular room required
if ($hasBabyRoom && false === $hasRegularRoom) {
$this->context->buildViolation($constraint->onlyBabyRoomsMessage)
->addViolation();
}
PromoVoucherValidator
Validates promo codes against BusProNet API with travel and price context.
PurchaseVoucherValidator
Validates purchase voucher codes for existence and remaining balance.
13. BusProNet API Quirks & Workarounds
This section documents known API behaviors and the solutions implemented.
13.1 Server Busy - Immediate Connection Close
Issue: Server may close socket immediately without data when busy.
Solution: Automatic retry with configurable attempts (default: 3) and delay (default: 1s).
Location: src/BusProNet/ApiClient.php:625-659
private function executeWithRetry(callable $operation, ?string $type = null): mixed
{
for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) {
try {
return $operation();
} catch (ImmediateConnectionCloseException $e) {
if ($attempt < $maxAttempts) {
sleep($retryDelay);
}
}
}
}
13.2 Pickup API Limitation
Issue: API only supports pickup submission via outbound (zustiege) section. When outbound is car but inbound is bus, pickup cannot be properly submitted.
Solution:
- Pickup field shown when either direction is bus
- Pickup pricing only calculated when outbound is bus
- Summary shows "Zustieg (inkl.)" without price for inbound-only bus
Documentation: docs/pickup-api-limitation.md
13.3 Insurance Not Available in Edit Mode
Issue: API does not return insurance data in booking responses.
Solution: Insurance field skipped in edit mode. Original insurance preserved and passed through unchanged.
Location: src/Form/Service/ParticipantInsuranceFieldHandler.php:96-100
public function shouldProcess(...): bool
{
if (BookingDto::MODE_EDIT === $mode) {
return false; // Skip processing entirely in edit mode
}
}
13.4 Minimal Data for First Participant
Issue: API may return full data only in <anmelder> but minimal/empty in <teilnehmer id="1">.
Solution: Copy address from applicant if first participant has empty address.
Location: src/BusProNet/DataProcessor/BookingDataProcessor.php:71-81
13.5 Automatic Customer Matching
Issue: Including customer IDs prevents automatic matching by personal data.
Solution: Exclude personId and addressId from create mode payload.
Location: src/BusProNet/DataProcessor/BookingPayloadBuilder.php:247-249
13.6 Personal Data Mutability Flag
Issue: Per-participant aenderungmoeglich flag controls editability.
Solution: Hide personal data fields and render as static text when not mutable.
Exception: Internal agency bookings (code '0004') ignore mutability.
Location: src/Form/Service/Condition/PersonalDataMutabilityCondition.php
13.7 Synthetic "No Insurance" Option
Issue: Insurance is optional but requires explicit choice for legal compliance.
Solution: Inject synthetic option with ID '0' that satisfies validation but is excluded from API transmission.
Location: src/BusProNet/Model/Insurance.php:19-26
public const NO_INSURANCE_ID = '0';
public function isNoInsurance(): bool
{
return self::NO_INSURANCE_ID === $this->id;
}
13.8 Goodwill (Kulanz) Vouchers
Issue: Goodwill vouchers must be treated differently from regular purchase vouchers.
Solution: Detect by type="Kulanz", send as <aktionscode> per participant instead of aggregated.
Location: src/BusProNet/Model/PurchaseVoucher.php:42-49
13.9 API-Applied Discounts
Issue: API may apply automatic discounts (group discounts) not known to local calculator.
Solution: Extract ERM-type negative price items from response and account for them in price comparison.
Location: src/BusProNet/Model/BookingResponse.php:106-120
13.10 Insurance Auto-Reassignment
Issue: Insurance may become ineligible when participant price changes.
Solution: Automatically reassign to same type with appropriate price tier, preserving user intent.
Location: src/Form/Service/ParticipantInsuranceFieldHandler.php:115-224
13.11 Address Data Cloning
Issue: Shared address references could cause unintended modifications.
Solution: Clone address objects when loading bookings and creating new instances during updates.
Locations:
src/BusProNet/DataProcessor/BookingDataProcessor.php:43-48src/BusProNet/DataProcessor/PersonalDataSynchronizer.php:60-71
13.12 Company Bookings - Missing Fields
Issue: Travel agencies create bookings with company data lacking required personal fields.
Solution: Fill missing mandatory fields with sensible defaults.
Location: src/BusProNet/DataProcessor/PersonalDataSynchronizer.php:91-122
// Default values for company applicants
$applicant->firstName = 'Anmelder';
$applicant->gender = 'D';
$applicant->nationality = 'D';
$applicant->dateOfBirth = new \DateTimeImmutable('-20 years');
$applicant->mobile = '12345';
13.13 Message Length Header
Issue: All API responses are prefixed with 10-byte message length.
Solution: Strip first 10 bytes from all socket responses.
Location: src/BusProNet/ApiClient.php:695-696
$xml = substr($response, 10);
Appendix: Key File Locations
Configuration
| File | Purpose |
|---|---|
config/services.yaml |
Service definitions, BPN config |
config/packages/security.yaml |
Firewalls, access control |
config/packages/monolog.yaml |
Logging configuration |
config/secret/ |
RSA encryption keys |
Entry Points
| File | Purpose |
|---|---|
src/Controller/SecurityController.php |
Login entry |
src/Controller/Booking/Create/IndexController.php |
Booking creation entry |
src/BusProNet/ApiClient.php |
API communication |
assets/app.js |
Frontend entry |
Core Business Logic
| File | Purpose |
|---|---|
src/Service/BookingService.php |
Booking orchestration |
src/Service/BookingPriceCalculatorService.php |
Pricing facade |
src/Form/Service/ParticipantFieldHandlerRegistry.php |
Form field processing |
src/BusProNet/DataProcessor/BookingDataProcessor.php |
DTO ↔ API transformation |