logger->info('BookingValidator called', [ 'value_type' => get_class($value), 'is_ParticipantEditDto' => $value instanceof ParticipantEditDto, 'is_ParticipantDto' => $value instanceof ParticipantDto, ]); $propertyPathPrefix = ''; if ($value instanceof ParticipantEditDto) { $this->logger->info('Validating ParticipantEditDto (individual form)'); $participant = $value->participant; $bookingContext = $value->bookingContext; $propertyPathPrefix = 'participant.'; } elseif ($value instanceof ParticipantDto) { $this->logger->info('Validating ParticipantDto (cards view)', [ 'participant_index' => $value->index ?? 'unknown', ]); $participant = $value; // Try to get booking context from validation context $root = $this->context->getRoot(); $this->logger->info('Context root type', [ 'root_type' => is_object($root) ? get_class($root) : gettype($root), ]); if ($root instanceof BookingDto) { $bookingContext = $root; $this->logger->info('Successfully got BookingDto from root'); } else { // Root is likely a Form object - try to get data from it if (method_exists($root, 'getData')) { $bookingContext = $root->getData(); $this->logger->info('Got data from Form object', [ 'data_type' => is_object($bookingContext) ? get_class($bookingContext) : gettype($bookingContext), ]); } else { $this->logger->warning('Cannot validate ParticipantDto - root has no getData method', [ 'root_type' => is_object($root) ? get_class($root) : gettype($root), ]); return; } if (!$bookingContext instanceof BookingDto) { $this->logger->warning('Cannot validate ParticipantDto - data is not BookingDto', [ 'data_type' => is_object($bookingContext) ? get_class($bookingContext) : gettype($bookingContext), ]); return; } } } else { $this->logger->info('BookingValidator skipping - unsupported type'); return; } // Determine if strict validation applies $shouldValidate = $this->shouldApplyStrictValidation($bookingContext); $this->logger->info('Validation decision', [ 'should_validate' => $shouldValidate, 'mode' => $bookingContext->getMode(), 'participant_index' => $participant->index ?? 'unknown', ]); if (true === $shouldValidate) { $this->enforceStrictValidation($participant, $propertyPathPrefix); } // Relaxed validation: no required checks, format validation handled by existing constraints } /** * Determines whether strict validation should be applied. * * Strict validation applies when: * 1. In create mode (new booking), OR * 2. In edit mode AND applicant is not mutable (booking has restrictions) * * @param BookingDto $bookingContext The booking context * * @return bool True if strict validation should apply */ private function shouldApplyStrictValidation(BookingDto $bookingContext): bool { $mode = $bookingContext->getMode(); // Create mode: always strict if (BookingDto::MODE_CREATE === $mode) { return true; } // Edit mode: check applicant mutability $applicant = $bookingContext->participants[0] ?? null; if (null === $applicant) { return true; // Fail-safe: if no applicant, apply strict validation } // Strict validation if applicant is not mutable return false === $applicant->mutable; } /** * Enforces strict validation: all required fields must be filled. * * Validates: * - Personal data: firstName, lastName, dateOfBirth, email * - Address: street, postCode, city, country * - Services: skiPass, transportationOutbound, transportationInbound, insurance * * @param ParticipantDto $participant The participant to validate * @param string $propertyPathPrefix Prefix for property paths ('participant.' for wrapped DTO, '' for direct) */ private function enforceStrictValidation(ParticipantDto $participant, string $propertyPathPrefix = 'participant.'): void { // Skip validation for canceled participants if (true === $participant->isCanceled()) { $this->logger->info('Skipping validation for canceled participant', [ 'participant_index' => $participant->index, ]); return; } $this->logger->info('Enforcing strict validation', [ 'participant_index' => $participant->index, 'firstName' => $participant->firstName ?? 'null', 'lastName' => $participant->lastName ?? 'null', 'email' => $participant->email ?? 'null', 'dateOfBirth' => $participant->dateOfBirth?->format('Y-m-d') ?? 'null', 'skiPass' => $participant->skiPass?->id ?? 'null', 'insurance' => $participant->insurance?->id ?? 'null', ]); // Personal data validation $this->validateRequired($participant->firstName, $propertyPathPrefix.'firstName', 'Bitte angeben'); $this->validateRequired($participant->lastName, $propertyPathPrefix.'lastName', 'Bitte angeben'); $this->validateRequired($participant->email, $propertyPathPrefix.'email', 'Bitte angeben'); if (null === $participant->dateOfBirth) { $this->context->buildViolation('Bitte angeben') ->atPath($propertyPathPrefix.'dateOfBirth') ->addViolation(); } // Address validation only for applicant (index 0) if (0 === $participant->index) { if (null !== $participant->address) { $this->validateRequired($participant->address->street, $propertyPathPrefix.'address.street', 'Bitte angeben'); $this->validateRequired($participant->address->postCode, $propertyPathPrefix.'address.postCode', 'Bitte angeben'); $this->validateRequired($participant->address->city, $propertyPathPrefix.'address.city', 'Bitte angeben'); $this->validateRequired($participant->address->country, $propertyPathPrefix.'address.country', 'Bitte angeben'); } else { $this->context->buildViolation('Bitte angeben') ->atPath($propertyPathPrefix.'address') ->addViolation(); } } // Service validation if (null === $participant->skiPass) { $this->context->buildViolation('Bitte auswählen') ->atPath($propertyPathPrefix.'skiPass') ->addViolation(); } if (null === $participant->transportationOutbound) { $this->context->buildViolation('Bitte auswählen') ->atPath($propertyPathPrefix.'transportationOutbound') ->addViolation(); } if (null === $participant->transportationInbound) { $this->context->buildViolation('Bitte auswählen') ->atPath($propertyPathPrefix.'transportationInbound') ->addViolation(); } if (null === $participant->insurance) { $this->context->buildViolation('Bitte auswählen') ->atPath($propertyPathPrefix.'insurance') ->addViolation(); } } /** * Validates that a field is not empty (null or blank string). * * @param mixed $value The field value to check * @param string $path The property path for the violation * @param string $message The violation message */ private function validateRequired(mixed $value, string $path, string $message): void { if (null === $value || '' === trim((string) $value)) { $this->logger->info('Adding violation for required field', [ 'path' => $path, 'message' => $message, 'value' => $value ?? 'null', ]); $this->context->buildViolation($message) ->atPath($path) ->addViolation(); } } }