247 lines
10 KiB
PHP
247 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Validator\Constraints;
|
|
|
|
use App\Form\Model\BookingDto;
|
|
use App\Form\Model\ParticipantDto;
|
|
use App\Form\Model\ParticipantEditDto;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\Validator\Constraint;
|
|
use Symfony\Component\Validator\ConstraintValidator;
|
|
|
|
/**
|
|
* Validates required fields with mode-aware and mutability-aware rules.
|
|
*
|
|
* Validation Strategy:
|
|
* - Create mode: STRICT (all fields required + format validation)
|
|
* - Edit mode + applicant immutable: STRICT (all fields required + format validation)
|
|
* - Edit mode + applicant mutable: RELAXED (only format validation, fields optional)
|
|
*
|
|
* The applicant's (index 0) mutability flag determines validation strictness for ALL participants.
|
|
* This ensures consistent validation across the entire booking based on whether the booking
|
|
* can be modified freely or has restrictions imposed by the booking system.
|
|
*
|
|
* Strict Validation (Create + Edit Immutable):
|
|
* - Personal data: firstName, lastName, dateOfBirth, email, address (all subfields) - REQUIRED
|
|
* - Services: skiPass, transportationOutbound, transportationInbound, insurance - REQUIRED
|
|
* - Format constraints (Email, date formats) also apply
|
|
*
|
|
* Relaxed Validation (Edit Mutable):
|
|
* - All fields OPTIONAL (can be null/empty)
|
|
* - Format constraints still apply IF values are provided
|
|
* - Allows partial updates without forcing complete data re-entry
|
|
*/
|
|
class BookingValidator extends ConstraintValidator
|
|
{
|
|
public function __construct(private readonly LoggerInterface $logger)
|
|
{
|
|
}
|
|
|
|
public function validate(mixed $value, Constraint $constraint): void
|
|
{
|
|
$this->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();
|
|
}
|
|
}
|
|
}
|