feat: improved mutability checks for personal data in edit or create mode
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
#[\Attribute]
|
||||
class ApplicantAddress extends Constraint
|
||||
{
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
class ApplicantAddressValidator extends ConstraintValidator
|
||||
{
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
/** @var ParticipantDto $participant */
|
||||
$participant = $value;
|
||||
|
||||
// Only validate for the first participant (applicant in create mode)
|
||||
if (false === $participant->isApplicant()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure address object exists
|
||||
if (null === $participant->address) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required address fields
|
||||
if (null === $participant->address->street || '' === trim($participant->address->street)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.street')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (null === $participant->address->postCode || '' === trim($participant->address->postCode)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.postCode')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (null === $participant->address->city || '' === trim($participant->address->city)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.city')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (null === $participant->address->country || '' === trim($participant->address->country)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.country')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
// Mobile/phone is mandatory for applicant in create mode
|
||||
if (null === $participant->mobile || '' === trim($participant->mobile)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('mobile')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Validates required fields based on booking mode and applicant mutability.
|
||||
*
|
||||
* This constraint implements mode-aware validation that differentiates between:
|
||||
* - Create mode: All fields mandatory
|
||||
* - Edit mode (immutable): All fields mandatory
|
||||
* - Edit mode (mutable): Only format validation, fields optional
|
||||
*
|
||||
* The applicant's (index 0) mutability flag controls validation for the entire booking.
|
||||
*/
|
||||
#[\Attribute]
|
||||
class Booking extends Constraint
|
||||
{
|
||||
public string $message = 'Bitte prüfe deine Angaben.';
|
||||
|
||||
public function getTargets(): array|string
|
||||
{
|
||||
return static::CLASS_CONSTRAINT;
|
||||
|
||||
@@ -1,39 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Model\BookingUpdate;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Model\ParticipantEditDto;
|
||||
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 ApiClient $apiClient)
|
||||
{
|
||||
}
|
||||
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
/** @var BookingDto $booking */
|
||||
$bookingData = $value;
|
||||
|
||||
$result = $this->apiClient->updateBooking($bookingData);
|
||||
|
||||
if ($result instanceof Notification) {
|
||||
$this->context
|
||||
->buildViolation($result->message)
|
||||
->addViolation()
|
||||
;
|
||||
if (!$value instanceof ParticipantEditDto) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($result instanceof BookingUpdate && false === $result->valid) {
|
||||
$this->context
|
||||
->buildViolation($constraint->message)
|
||||
->addViolation()
|
||||
;
|
||||
$participant = $value->participant;
|
||||
$bookingContext = $value->bookingContext;
|
||||
|
||||
// Determine if strict validation applies
|
||||
if (true === $this->shouldApplyStrictValidation($bookingContext)) {
|
||||
$this->enforceStrictValidation($participant);
|
||||
}
|
||||
|
||||
// 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
|
||||
*/
|
||||
private function enforceStrictValidation(ParticipantDto $participant): void
|
||||
{
|
||||
// Skip validation for canceled participants
|
||||
if (true === $participant->isCanceled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Personal data validation
|
||||
$this->validateRequired($participant->firstName, 'participant.firstName', 'Bitte angeben');
|
||||
$this->validateRequired($participant->lastName, 'participant.lastName', 'Bitte angeben');
|
||||
$this->validateRequired($participant->email, 'participant.email', 'Bitte angeben');
|
||||
|
||||
if (null === $participant->dateOfBirth) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('participant.dateOfBirth')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
// Address validation only for applicant (index 0)
|
||||
if (0 === $participant->index) {
|
||||
if (null !== $participant->address) {
|
||||
$this->validateRequired($participant->address->street, 'participant.address.street', 'Bitte angeben');
|
||||
$this->validateRequired($participant->address->postCode, 'participant.address.postCode', 'Bitte angeben');
|
||||
$this->validateRequired($participant->address->city, 'participant.address.city', 'Bitte angeben');
|
||||
$this->validateRequired($participant->address->country, 'participant.address.country', 'Bitte angeben');
|
||||
} else {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('participant.address')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
// Service validation
|
||||
if (null === $participant->skiPass) {
|
||||
$this->context->buildViolation('Bitte auswählen')
|
||||
->atPath('participant.skiPass')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $participant->transportationOutbound) {
|
||||
$this->context->buildViolation('Bitte auswählen')
|
||||
->atPath('participant.transportationOutbound')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $participant->transportationInbound) {
|
||||
$this->context->buildViolation('Bitte auswählen')
|
||||
->atPath('participant.transportationInbound')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $participant->insurance) {
|
||||
$this->context->buildViolation('Bitte auswählen')
|
||||
->atPath('participant.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->context->buildViolation($message)
|
||||
->atPath($path)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ class ParticipantValidator extends ConstraintValidator
|
||||
$participant = $value;
|
||||
|
||||
$this->assertBodyMeasurementsValid($participant);
|
||||
$this->assertTransportationSelected($participant);
|
||||
$this->assertPickupSelected($participant);
|
||||
}
|
||||
|
||||
@@ -34,23 +33,6 @@ class ParticipantValidator extends ConstraintValidator
|
||||
}
|
||||
}
|
||||
|
||||
public function assertTransportationSelected(ParticipantDto $participant): void
|
||||
{
|
||||
// no transportation services required for canceled participants
|
||||
if (true === $participant->isCanceled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (['transportationOutbound', 'transportationInbound'] as $property) {
|
||||
if (null === $participant->{$property}) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath($property)
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function assertPickupSelected(ParticipantDto $participant): void
|
||||
{
|
||||
// Check if either outbound or inbound transportation is bus
|
||||
|
||||
Reference in New Issue
Block a user