wip: email uniqueness validation

This commit is contained in:
Björn Fromme committed 2025-10-23 09:29:27 +02:00
1 parent 60176d19f4
commit 3e54a58b9b
11 files changed
+549 -783

No files matched your search

-39
View File
@@ -196,43 +196,4 @@ class BookingDto
}
}
#[Assert\Callback(groups: ['booking_create', 'booking_edit'])]
public function validateEmailUniqueness(ExecutionContextInterface $context): void
{
// Build map of email addresses to participant indices (adults only)
$emailMap = [];
foreach ($this->participants as $index => $participant) {
// Skip children (they can share email addresses)
if (true === $participant->isChild()) {
continue;
}
// Skip null or empty emails (handled by other validators)
if (null === $participant->email || '' === trim($participant->email)) {
continue;
}
// Normalize email for case-insensitive comparison
$normalizedEmail = strtolower(trim($participant->email));
// Add participant index to the email map
if (false === isset($emailMap[$normalizedEmail])) {
$emailMap[$normalizedEmail] = [];
}
$emailMap[$normalizedEmail][] = $index;
}
// Add validation errors for duplicate emails
foreach ($emailMap as $email => $indices) {
// Only add violations if email is used by multiple adult participants
if (1 < count($indices)) {
foreach ($indices as $index) {
$context->buildViolation('Diese E-Mail Adresse wird bereits von einem anderen erwachsenen Teilnehmer verwendet')
->atPath(sprintf('participants[%d].email', $index))
->addViolation();
}
}
}
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Form\Model;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* Wrapper DTO for editing a participant within a booking context.
*
* This wrapper enables clean validation of participant email uniqueness
* without requiring back-references or form coupling. It contains both
* the participant being edited and the full booking context needed for
* cross-participant validation.
*/
class ParticipantEditDto
{
public function __construct(
#[Assert\Valid]
public readonly ParticipantDto $participant,
public readonly BookingDto $bookingContext,
) {
}
/**
* Validates that adult participants have unique email addresses.
*
* Children (under 16) are exempt from this validation and can share
* email addresses with adults. Email comparison is case-insensitive
* and whitespace is normalized.
*/
#[Assert\Callback(groups: ['booking_create', 'booking_edit'])]
public function validateEmailUniqueness(ExecutionContextInterface $context): void
{
// Skip validation for children (under 16)
if (true === $this->participant->isChild()) {
return;
}
// Skip if email is null or empty (handled by @Email and @NotBlank constraints)
if (null === $this->participant->email || '' === trim($this->participant->email)) {
return;
}
// Normalize current participant's email for comparison
$normalizedEmail = strtolower(trim($this->participant->email));
// Check against all adult participants in booking context
foreach ($this->bookingContext->participants as $index => $otherParticipant) {
// Skip self-comparison (same participant index)
if ($index === $this->participant->index) {
continue;
}
// Skip children (they can share email addresses)
if (true === $otherParticipant->isChild()) {
continue;
}
// Skip null or empty emails
if (null === $otherParticipant->email || '' === trim($otherParticipant->email)) {
continue;
}
// Compare normalized emails
$otherNormalizedEmail = strtolower(trim($otherParticipant->email));
if ($normalizedEmail === $otherNormalizedEmail) {
// Add violation to participant.email path
$context->buildViolation('Diese E-Mail Adresse wird bereits von einem anderen Teilnehmer verwendet')
->atPath('participant.email')
->addViolation();
// Stop after first duplicate found (no need to report multiple times)
return;
}
}
}
}