@@ -361,6 +361,146 @@ this option to specify one or more fields to only ignore ``null`` values on them
361
361
database, you might see insertion errors when your application attempts to
362
362
persist entities that the ``UniqueEntity `` constraint considers valid.
363
363
364
+ ``comparator ``
365
+ ~~~~~~~~~~~~~~
366
+
367
+ **type **: ``callable `` **default **: ``null ``
368
+
369
+ .. versionadded :: 7.4
370
+
371
+ The ``comparator `` option was introduced in Symfony 7.4.
372
+
373
+ The default strategy to check for equality of the found entity (if any) and
374
+ the current validation subject, when using the `entityClass `_ option in
375
+ combination with the `identifierFieldNames `_ option, is to compare each
376
+ mapping in `identifierFieldNames `_ using strict equality (``=== ``). Optionally
377
+ after **casting ** each to string, if they implement ``\Stringable ``.
378
+
379
+ This works in most cases, but fails if either side of the mapping entry is
380
+ not ``\Stringable `` and doesn't refer to the same value or object. This can
381
+ happen if, for example, the referred-to value in the found entity is a value
382
+ object or an association mapped as part of the entity identity.
383
+
384
+ To still be able to check for equality, you can supply a ``callable `` which
385
+ will be used instead of the default strategy.
386
+
387
+ .. configuration-block ::
388
+
389
+ .. code-block :: php-attributes
390
+
391
+ // src/Form/Data/BlogPostTranslationFormData.php
392
+ namespace App\Form\Data;
393
+
394
+ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
395
+ use Symfony\Component\Uid\Uuid;
396
+ use Symfony\Component\Validator\Constraints\NotBlank;
397
+ use App\Entity\BlogPostTranslation;
398
+
399
+ #[UniqueEntity(
400
+ fields: ['locale', 'slug'],
401
+ message: 'This slug is already used by another post in this language.',
402
+ entityClass: BlogPostTranslation::class,
403
+ comparator: [BlogPostTranslationFormData::class, 'compare'],
404
+ errorPath: 'slug'
405
+ )]
406
+ class BlogPostTranslationFormData
407
+ {
408
+ public function __construct(
409
+ public readonly Uuid $postId,
410
+ public readonly string $locale,
411
+
412
+ #[NotBlank]
413
+ public ?string $slug = null,
414
+ ) {}
415
+
416
+ public static function compare(self $subject, BlogPostTranslation $foundEntity): bool
417
+ {
418
+ return $foundEntity->getPost()->getId()->equals($this->postId)
419
+ && $foundEntity->getLocale() === $this->locale;
420
+ }
421
+ }
422
+
423
+ .. code-block :: yaml
424
+
425
+ # config/validator/validation.yaml
426
+ App\Form\Data\BlogPostTranslationFormData :
427
+ constraints :
428
+ - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity :
429
+ fields : [locale, slug]
430
+ message : ' This slug is already used by another post in this language.'
431
+ entityClass : App\Entity\BlogPostTranslation,
432
+ comparator : [App\Form\Data\BlogPostTranslationFormData, compare]
433
+ errorPath : slug
434
+
435
+ .. code-block :: xml
436
+
437
+ <!-- config/validator/validation.xml -->
438
+ <?xml version =" 1.0" encoding =" UTF-8" ?>
439
+ <constraint-mapping xmlns =" http://symfony.com/schema/dic/constraint-mapping"
440
+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
441
+ xsi : schemaLocation =" http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd" >
442
+
443
+ <class name =" App\Form\Data\BlogPostTranslationFormData" >
444
+ <constraint name =" Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity" >
445
+ <option name =" fields" >
446
+ <value >locale</value >
447
+ <value >slug</value >
448
+ </option >
449
+ <option name =" message" >This slug is already used by another post in this language.</option >
450
+ <option name =" entityClass" >App\Entity\BlogPostTranslation</option >
451
+ <option name =" comparator" >
452
+ <value >App\Form\Data\BlogPostTranslationFormData</value >
453
+ <value >compare</value >
454
+ </option >
455
+ <option name =" errorPath" >slug</option >
456
+ </constraint >
457
+ </class >
458
+ </constraint-mapping >
459
+
460
+ .. code-block :: php
461
+
462
+ // src/Form/Data/BlogPostTranslationFormData.php
463
+ namespace App\Form\Data;
464
+
465
+ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
466
+ use Symfony\Component\Uid\Uuid;
467
+ use Symfony\Component\Validator\Constraints\NotBlank;
468
+ use App\Entity\BlogPostTranslation;
469
+
470
+ class BlogPostTranslationFormData
471
+ {
472
+ public function __construct(
473
+ public readonly Uuid $postId,
474
+ public readonly string $locale,
475
+
476
+ #[NotBlank]
477
+ public ?string $slug = null,
478
+ ) {}
479
+
480
+ public static function loadValidatorMetadata(ClassMetadata $metadata): void
481
+ {
482
+ $metadata->addConstraint(UniqueEntity(
483
+ fields: ['locale', 'slug'],
484
+ message: 'This slug is already used by another post in this language.',
485
+ entityClass: BlogPostTranslation::class,
486
+ comparator: [self::class, 'compare'],
487
+ errorPath: 'slug'
488
+ ));
489
+ }
490
+
491
+ public static function compare(self $subject, BlogPostTranslation $foundEntity): bool
492
+ {
493
+ return $foundEntity->getPost()->getId()->equals($this->postId)
494
+ && $foundEntity->getLocale() === $this->locale;
495
+ }
496
+ }
497
+
498
+
499
+ .. warning ::
500
+
501
+ This option cannot be used in conjunction with the `identifierFieldNames `_
502
+ option.
503
+
364
504
``message ``
365
505
~~~~~~~~~~~
366
506
0 commit comments