diff --git a/src/Form/Model/BookingDto.php b/src/Form/Model/BookingDto.php index 59c4060..536f47b 100644 --- a/src/Form/Model/BookingDto.php +++ b/src/Form/Model/BookingDto.php @@ -378,6 +378,13 @@ class BookingDto $this->$key = $value; } + foreach ($this->participants as $participant) { + if ($participant instanceof ParticipantDto) { + // Temporary cleanup for older serialized participants restored from session. + $participant->normalizeBodyDimensions(); + } + } + // Old sessions (before c373a989) still carry a full Travel object; // new sessions carry only the int ID. Handle both formats. // TODO: Remove Travel instance branch once all pre-deploy sessions have expired diff --git a/src/Form/Model/ParticipantDto.php b/src/Form/Model/ParticipantDto.php index f37d70c..e3e1b0f 100644 --- a/src/Form/Model/ParticipantDto.php +++ b/src/Form/Model/ParticipantDto.php @@ -360,6 +360,61 @@ class ParticipantDto return $ageThreshold > $age; } + /** + * Normalizes body-dimension fields to canonical string values. + * + * Keeps plain integers and digit-only strings, converting them to a + * normalized string representation. Anything else is treated as legacy + * garbage and reset to null so form hydration can continue safely. + */ + public function normalizeBodyDimensions(): void + { + $this->height = $this->normalizeBodyDimensionValue($this->height); + $this->weight = $this->normalizeBodyDimensionValue($this->weight); + $this->shoeSize = $this->normalizeBodyDimensionValue($this->shoeSize); + } + + /** + * Restores the DTO from session/serialization data. + * + * @param array $data + */ + public function __unserialize(array $data): void + { + foreach ($data as $key => $value) { + if (false === property_exists($this, $key)) { + continue; + } + + $this->$key = $value; + } + + if (false === isset($this->address)) { + $this->address = new Address(); + } + + // TODO: Remove after all legacy drafts/session snapshots with body-dimension strings have expired. + $this->normalizeBodyDimensions(); + } + + private function normalizeBodyDimensionValue(mixed $value): ?string + { + if (true === is_int($value)) { + return (string) $value; + } + + if (false === is_string($value)) { + return null; + } + + $value = trim($value); + if ('' === $value || false === ctype_digit($value)) { + return null; + } + + return (string) (int) $value; + } + /** * Validates that the applicant (index 0) has a complete address. * diff --git a/src/Service/BookingEditDraftMerger.php b/src/Service/BookingEditDraftMerger.php index f586ff9..cdc2c5f 100644 --- a/src/Service/BookingEditDraftMerger.php +++ b/src/Service/BookingEditDraftMerger.php @@ -46,6 +46,8 @@ class BookingEditDraftMerger // Body dimensions if (true === isset($data['bodyDimensions']) && true === is_array($data['bodyDimensions'])) { $this->applyBodyDimensions($participant, $data['bodyDimensions']); + // TODO: Drop this once all legacy drafts no longer contain non-numeric body-dimension values. + $participant->normalizeBodyDimensions(); } // Room assignment diff --git a/tests/Form/Model/BookingDtoTest.php b/tests/Form/Model/BookingDtoTest.php index f599b4a..148ad4e 100644 --- a/tests/Form/Model/BookingDtoTest.php +++ b/tests/Form/Model/BookingDtoTest.php @@ -6,6 +6,7 @@ namespace App\Tests\Form\Model; use App\BusProNet\Model\Travel; use App\Form\Model\BookingDto; +use App\Form\Model\ParticipantDto; use PHPUnit\Framework\TestCase; /** @@ -63,4 +64,27 @@ class BookingDtoTest extends TestCase $this->assertSame($travel, $dto->travel); $this->assertSame(42, $dto->travel->id); } + + public function testUnserializeNormalizesLegacyBodyDimensionsOnParticipants(): void + { + $travel = new Travel(); + $travel->id = 42; + $travel->dateFrom = new \DateTimeImmutable('2025-06-01'); + $travel->dateTo = new \DateTimeImmutable('2025-06-08'); + + $participant = new ParticipantDto(); + $participant->height = '149-157'; + $participant->weight = '75'; + $participant->shoeSize = '043'; + + $dto = new BookingDto($travel, 1); + $dto->participants = [$participant]; + + $restored = unserialize(serialize($dto)); + + $this->assertInstanceOf(BookingDto::class, $restored); + $this->assertNull($restored->participants[0]->height); + $this->assertSame('75', $restored->participants[0]->weight); + $this->assertSame('43', $restored->participants[0]->shoeSize); + } } diff --git a/tests/Service/BookingEditDraftManagerMutabilityTest.php b/tests/Service/BookingEditDraftManagerMutabilityTest.php index f8635ae..e2ae26e 100644 --- a/tests/Service/BookingEditDraftManagerMutabilityTest.php +++ b/tests/Service/BookingEditDraftManagerMutabilityTest.php @@ -422,6 +422,66 @@ class BookingEditDraftManagerMutabilityTest extends TestCase $this->assertSame('Updated', $mutableParticipant->firstName); } + public function testDraftNormalizesLegacyBodyDimensionsToNull(): void + { + $participant = new ParticipantDto(); + $participant->height = null; + $participant->weight = null; + $participant->shoeSize = null; + + $travel = $this->createTravel(); + $dto = $this->createBookingDto($travel, [$participant]); + + $draft = $this->createDraft([ + 'participants' => [ + 0 => [ + 'bodyDimensions' => [ + 'height' => '149-157', + 'weight' => '195+', + 'shoeSize' => 'abc', + ], + ], + ], + ]); + + $applied = $this->service->applyDraftToDto($draft, $dto, $travel); + + $this->assertTrue($applied); + $this->assertNull($participant->height); + $this->assertNull($participant->weight); + $this->assertNull($participant->shoeSize); + } + + public function testDraftNormalizesNumericBodyDimensionsToCanonicalStrings(): void + { + $participant = new ParticipantDto(); + $participant->height = null; + $participant->weight = null; + $participant->shoeSize = null; + + $travel = $this->createTravel(); + $dto = $this->createBookingDto($travel, [$participant]); + + $draft = $this->createDraft([ + 'participants' => [ + 0 => [ + 'bodyDimensions' => [ + 'height' => '180', + 'weight' => '75', + 'shoeSize' => '043', + ], + ], + ], + ]); + + $applied = $this->service->applyDraftToDto($draft, $dto, $travel); + + $this->assertTrue($applied); + $this->assertSame('180', $participant->height); + $this->assertSame('75', $participant->weight); + $this->assertSame('43', $participant->shoeSize); + } + // --- Helpers --- private function createService(int $id, string $label = 'Test', bool $isTransportation = false): Service