11 KiB
Booking DTO Unification Plan
Problem Statement
The current implementation uses two separate DTOs (BookingCreateDto and BookingEditDto) with fundamentally different data structures:
- Create mode: Services stored in participant DTOs (e.g.,
$participant->insurance,$participant->courses) - Edit mode: Services stored in the booking object (e.g.,
$booking->insurances,$booking->additionalServices)
This divergence causes multiple issues:
- Pricing and summary calculations fail in edit mode
- Field handlers need complex mode-specific logic
- Data processor needs separate handling for create vs edit
- Code duplication and increased complexity
- Bugs due to assumptions about data structure
Solution: Unified BookingDto
Create a single BookingDto class that stores all service selections in participant DTOs for BOTH create and edit modes. Different modes are handled through different instantiation methods.
Implementation Plan
Phase 1: Create Unified BookingDto Class
File: src/Form/Model/BookingDto.php
Changes:
- Merge
BookingCreateDtoandBookingEditDtointo singleBookingDtoclass - Keep all participant-based service storage (insurance, courses, skiPass, rentals, transportation, etc.)
- Add
modeproperty (MODE_CREATE or MODE_EDIT) - Add
bookingproperty (null in create mode, Booking object in edit mode for metadata only) - Implement
BookingDtoInterfaceinterface
Constructor signatures:
// Create mode
public function __construct(Travel $travel, int $agencyId)
// Edit mode (static factory)
public static function fromBooking(Booking $booking, Travel $travel): static
Key method:
public function getMode(): string
{
return null !== $this->booking ? self::MODE_EDIT : self::MODE_CREATE;
}
Phase 2: Update BookingDto::fromBooking() for Edit Mode
File: src/Form/Model/BookingDto.php
Responsibilities:
- Extract services from booking object and assign to participant DTOs
- Map insurances:
$participant->insurance = $booking->getInsuranceForParticipant($index) - Map services:
$participant->courses = $booking->getAdditionalServicesForParticipantByGroup($index, 'COURSES') - Map transportation:
$participant->transportationOutbound = $booking->getTransportationForParticipant($index, 'OUTBOUND') - Map pickup locations
- Map room assignments
- Set body dimensions from applicant for participant 0
- Set parking from form data (need to check if stored in booking)
Data extraction methods needed:
Booking::getInsuranceForParticipant(int $index): ?Insurance- Existing methods for other services can be reused
Phase 3: Update ParticipantDto
File: src/Form/Model/ParticipantDto.php
Changes:
- Already has all necessary service properties
- Ensure
fromPersonalData()copies body dimensions correctly - No structural changes needed
Phase 4: Update Form Types
Files to update:
src/Form/BookingType.php→ Rename toBookingType(generic)src/Form/BookingEditType.php→ Delete (use unified BookingType)src/Form/BookingParticipantType.php→ Already works with ParticipantDto, should work unchanged
Changes:
- Update
BookingTypeto useBookingDto::classas data_class - Remove mode-specific form type distinction
- Pass
edit_modeoption to child forms for field state provider selection
Phase 5: Update Controllers
CreateStep2Controller
File: src/Controller/Booking/CreateStep2Controller.php
Changes:
- Change
BookingCreateDto→BookingDto - Constructor instantiation remains same
- All logic should work unchanged (services already in participant DTOs)
EditController
File: src/Controller/Booking/EditController.php
Changes:
- Change
BookingEditDto→BookingDto - Change
BookingEditDto::fromBooking()→BookingDto::fromBooking() - Form type: change
BookingEditType→BookingTypewith['edit_mode' => true]option - All pricing and summary calculations should now work (same DTO structure as create)
Phase 6: Update BookingDataProcessor
File: src/BusProNet/DataProcessor/BookingDataProcessor.php
Major simplification:
createBookingRequestPayload() (Create flow)
- Already works with participant DTOs
- Change signature:
BookingDtoinstead ofBookingCreateDto - No other changes needed
createUpdateRequestPayload() (Edit flow)
Current problems:
- Tries to read services from
$bookingDto->bookingobject - Complex mapping and resetting logic
processParticipantServices()needs to add services to booking data from travel data
New approach:
- Services already in participant DTOs (populated by
fromBooking()) - Can reuse create flow logic almost entirely
- Only difference: include
idbuchungand participantidadressepersonin payload
Unified approach:
public function createPayload(BookingDto $bookingDto, string $type): array
{
// Apply bulk insurance if enabled
$this->applyBulkInsuranceIfActive($bookingDto);
// Collect service mappings (works same for both modes)
$serviceMap = $this->collectServiceMappings($bookingDto);
$transportationMap = $this->collectTransportationMappings($bookingDto);
$roomMap = $this->collectRoomMappings($bookingDto);
$pickupMap = $this->collectPickupMappings($bookingDto);
$insuranceMap = $this->collectInsuranceMappings($bookingDto);
// Build payload based on mode
if (BookingDtoInterface::MODE_EDIT === $bookingDto->getMode()) {
return $this->buildUpdatePayload($bookingDto, ...maps);
} else {
return $this->buildCreatePayload($bookingDto, $type, ...maps);
}
}
Simplifications:
- Remove
processParticipantServices()complexity - Remove
resetServiceMappings() - Remove
removeUnusedServices() - Direct mapping from participant DTOs to payload
Phase 7: Update Field Handlers
Files: All handlers in src/Form/Service/
Changes needed:
- Handlers already work with participant DTOs
- No changes needed (they don't care about mode)
- Registry already handles both modes
Phase 8: Update Service Layer
BookingService
File: src/Service/BookingService.php
Changes:
- Update type hints:
BookingCreateDto|BookingEditDto→BookingDto getRoomSummaryAndParticipantCount()should now work for both modes (same DTO structure)- No logic changes needed
PriceCalculator
File: src/Service/PriceCalculator.php
Changes:
- Update type hints to use
BookingDto - All calculations work with participant DTOs, should work unchanged
- Mode detection:
$bookingDto->getMode()instead ofinstanceofchecks
Phase 9: Update Field State Providers
Files:
src/Form/Service/CreateFieldStateProvider.phpsrc/Form/Service/EditFieldStateProvider.php
Changes:
- Already use
BookingDtoInterface, no changes needed - Continue to be selected based on
edit_modeform option
Phase 10: Update Templates
Files:
templates/booking/create_step_2.html.twigtemplates/booking/edit.html.twig
Changes:
- Variable naming:
bookingCreateDto→bookingDto,bookingEditDto→bookingDto - All logic should work unchanged (both render participant forms)
Phase 11: Critical New Method in Booking Model
File: src/BusProNet/Model/Booking.php
Add method:
public function getInsuranceForParticipant(int $index): ?Insurance
{
foreach ($this->insurances as $insurance) {
if (in_array($index, $insurance->mapping)) {
return $insurance;
}
}
return null;
}
Similar methods may be needed for other services if not already present.
Migration Strategy
Step 1: Create new BookingDto (keep old DTOs)
- Create
src/Form/Model/BookingDto.php - Implement both constructor and
fromBooking() - Keep
BookingCreateDtoandBookingEditDtotemporarily
Step 2: Update edit flow to use new DTO
- Update
EditControllerto useBookingDto - Update
BookingDataProcessor::createUpdateRequestPayload()to acceptBookingDto - Test edit flow thoroughly
Step 3: Update create flow to use new DTO
- Update
CreateStep2Controllerto useBookingDto - Test create flow thoroughly
Step 4: Cleanup
- Delete
BookingCreateDto.php - Delete
BookingEditDto.php - Delete
BookingEditType.php - Update all remaining type hints
Testing Checklist
Edit Flow
- Load existing booking with all service types
- Form displays all current selections correctly
- Summary sidebar shows all services and pricing
- Body dimensions show for applicant
- Change insurance (individual and bulk)
- Change services (courses, skiPass, rentals, board)
- Change transportation
- Submit changes successfully
- API receives correct payload with insurances
- After redirect, summary shows correctly
Create Flow
- Select rooms
- Add participants
- Select services for participants
- Select insurances (individual and bulk)
- Pricing calculates correctly
- Summary shows all selections
- Submit creates booking successfully
Field State System
- Conditional fields show/hide correctly in both modes
- Readonly fields work in edit mode
- Age-dependent fields work in both modes
- Bulk insurance checkbox works
Data Integrity
- No service data loss during mode transitions
- Insurance IDs match correctly
- Participant indices correct in both modes
- Room assignments preserved
Benefits of Unified DTO
- Single source of truth: All service selections in one place (participant DTOs)
- Simplified calculations: Pricing, summary, and totals work identically for both modes
- Reduced complexity: No mode-specific logic in services and calculators
- Easier testing: One DTO structure to test
- Better maintainability: Changes to service structure only need updating in one place
- Consistent field handlers: Handlers work with same data structure regardless of mode
- Cleaner templates: Same rendering logic for both modes
Risks and Mitigation
Risk: Breaking existing create flow
Mitigation: Migrate edit flow first, test thoroughly, then migrate create flow
Risk: Data loss during form processing
Mitigation: Extensive logging during migration, compare payloads before/after
Risk: Field handler incompatibility
Mitigation: Field handlers already work with ParticipantDto, minimal changes needed
Risk: Performance impact from data extraction
Mitigation: fromBooking() runs once per request, acceptable overhead
Timeline Estimate
- Phase 1-3 (DTO creation): 2 hours
- Phase 4-5 (Forms & Controllers): 2 hours
- Phase 6 (DataProcessor refactor): 3 hours
- Phase 7-10 (Service layer & templates): 2 hours
- Phase 11 (Booking model methods): 1 hour
- Testing & Fixes: 3 hours
Total: ~13 hours
Notes
- Current code has accumulated technical debt from multiple iterations
- Clean refactor will improve long-term maintainability
- Most existing logic can be reused (field handlers, conditions, validators)
- Main work is in
fromBooking()extraction logic and DataProcessor simplification