From bcceaa2e9dfca2777aebaba1059a707d38117bc4 Mon Sep 17 00:00:00 2001 From: Stephan Six Date: Tue, 29 Jul 2025 13:09:35 +0200 Subject: [PATCH 1/2] [WCM] [Validator] Added documentation for the new `comparator` option --- reference/constraints/UniqueEntity.rst | 140 +++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/reference/constraints/UniqueEntity.rst b/reference/constraints/UniqueEntity.rst index 0ab2c0a8cbd..9e9bb27119d 100644 --- a/reference/constraints/UniqueEntity.rst +++ b/reference/constraints/UniqueEntity.rst @@ -361,6 +361,146 @@ this option to specify one or more fields to only ignore ``null`` values on them database, you might see insertion errors when your application attempts to persist entities that the ``UniqueEntity`` constraint considers valid. +``comparator`` +~~~~~~~~~~~~~~ + +**type**: ``callable`` **default**: ``null`` + +.. versionadded:: 7.4 + + The ``comparator`` option was introduced in Symfony 7.4. + +The default strategy to check for equality of the found entity (if any) and +the current validation subject, when using the `entityClass`_ option in +combination with the `identifierFieldNames`_ option, is to compare each +mapping in `identifierFieldNames`_ using strict equality (``===``). Optionally +after **casting** each to string, if they implement ``\Stringable``. + +This works in most cases, but fails if either side of the mapping entry is +not ``\Stringable`` and doesn't refer to the same value or object. This can +happen if, for example, the referred-to value in the found entity is a value +object or an association mapped as part of the entity identity. + +To still be able to check for equality, you can supply a ``callable`` which +will be used instead of the default strategy. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Form/Data/BlogPostTranslationFormData.php + namespace App\Form\Data; + + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Uid\Uuid; + use Symfony\Component\Validator\Constraints\NotBlank; + use App\Entity\BlogPostTranslation; + + #[UniqueEntity( + fields: ['locale', 'slug'], + message: 'This slug is already used by another post in this language.', + entityClass: BlogPostTranslation::class, + comparator: [BlogPostTranslationFormData::class, 'compare'], + errorPath: 'slug' + )] + class BlogPostTranslationFormData + { + public function __construct( + public readonly Uuid $postId, + public readonly string $locale, + + #[NotBlank] + public ?string $slug = null, + ) {} + + public static function compare(self $subject, BlogPostTranslation $foundEntity): bool + { + return $foundEntity->getPost()->getId()->equals($this->postId) + && $foundEntity->getLocale() === $this->locale; + } + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Form\Data\BlogPostTranslationFormData: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: + fields: [locale, slug] + message: 'This slug is already used by another post in this language.' + entityClass: App\Entity\BlogPostTranslation, + comparator: [App\Form\Data\BlogPostTranslationFormData, compare] + errorPath: slug + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Form/Data/BlogPostTranslationFormData.php + namespace App\Form\Data; + + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Uid\Uuid; + use Symfony\Component\Validator\Constraints\NotBlank; + use App\Entity\BlogPostTranslation; + + class BlogPostTranslationFormData + { + public function __construct( + public readonly Uuid $postId, + public readonly string $locale, + + #[NotBlank] + public ?string $slug = null, + ) {} + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(UniqueEntity( + fields: ['locale', 'slug'], + message: 'This slug is already used by another post in this language.', + entityClass: BlogPostTranslation::class, + comparator: [self::class, 'compare'], + errorPath: 'slug' + )); + } + + public static function compare(self $subject, BlogPostTranslation $foundEntity): bool + { + return $foundEntity->getPost()->getId()->equals($this->postId) + && $foundEntity->getLocale() === $this->locale; + } + } + + +.. warning:: + + This option cannot be used in conjunction with the `identifierFieldNames`_ + option. + ``message`` ~~~~~~~~~~~ From 78f8a3bd6592132465496805614f1d3bf87f9cf5 Mon Sep 17 00:00:00 2001 From: Stephan Six Date: Tue, 29 Jul 2025 15:41:29 +0200 Subject: [PATCH 2/2] [WCM] [Validator] Fixed use statements order --- reference/constraints/UniqueEntity.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/constraints/UniqueEntity.rst b/reference/constraints/UniqueEntity.rst index 9e9bb27119d..fd496c09889 100644 --- a/reference/constraints/UniqueEntity.rst +++ b/reference/constraints/UniqueEntity.rst @@ -391,10 +391,10 @@ will be used instead of the default strategy. // src/Form/Data/BlogPostTranslationFormData.php namespace App\Form\Data; + use App\Entity\BlogPostTranslation; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Uid\Uuid; use Symfony\Component\Validator\Constraints\NotBlank; - use App\Entity\BlogPostTranslation; #[UniqueEntity( fields: ['locale', 'slug'], @@ -462,10 +462,10 @@ will be used instead of the default strategy. // src/Form/Data/BlogPostTranslationFormData.php namespace App\Form\Data; + use App\Entity\BlogPostTranslation; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Uid\Uuid; use Symfony\Component\Validator\Constraints\NotBlank; - use App\Entity\BlogPostTranslation; class BlogPostTranslationFormData {