From 7f65194f536391d8095eb137a5c2bf600b761966 Mon Sep 17 00:00:00 2001 From: Romain Monteil Date: Wed, 9 Jul 2025 14:10:22 +0200 Subject: [PATCH] [DoctrineBridge][Validator] Improve documentation for UniqueEntity constraint --- reference/constraints/UniqueEntity.rst | 422 +++++++++++++++++++++++-- 1 file changed, 391 insertions(+), 31 deletions(-) diff --git a/reference/constraints/UniqueEntity.rst b/reference/constraints/UniqueEntity.rst index 0ab2c0a8cbd..060c5ab1cf8 100644 --- a/reference/constraints/UniqueEntity.rst +++ b/reference/constraints/UniqueEntity.rst @@ -36,10 +36,7 @@ between all of the rows in your user table: namespace App\Entity; use Doctrine\ORM\Mapping as ORM; - - // DON'T forget the following use statement!!! use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; - use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] @@ -84,14 +81,14 @@ between all of the rows in your user table: // src/Entity/User.php namespace App\Entity; - // DON'T forget the following use statement!!! + use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; - use Symfony\Component\Validator\Constraints as Assert; class User { - // ... + #[ORM\Column(name: 'email', type: 'string', length: 255, unique: true)] + protected string $email; public static function loadValidatorMetadata(ClassMetadata $metadata): void { @@ -103,29 +100,6 @@ between all of the rows in your user table: } } - // src/Form/Type/UserType.php - namespace App\Form\Type; - - // ... - // DON'T forget the following use statement!!! - use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; - - class UserType extends AbstractType - { - // ... - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - // ... - 'data_class' => User::class, - 'constraints' => [ - new UniqueEntity(fields: ['email']), - ], - ]); - } - } - .. warning:: This constraint doesn't provide any protection against `race conditions`_. @@ -139,6 +113,116 @@ between all of the rows in your user table: that haven't been persisted as entities yet. You'll need to create your own validator to handle that case. +Using a PHP class +----------------- + +This constraint can also check **uniqueness on any PHP class** and not only on +Doctrine entities. Consider the following Doctrine entity:: + + // src/Entity/User.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + + #[ORM\Entity] + class User + { + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + public int $id; + + #[ORM\Column] + public string $email; + + // ... + } + +Instead of adding the ``UniqueEntity`` constraint to it, you can now check +for uniqueness in other ways. +For example, in a DTO that creates User entities, you can now define the +following using the `entityClass`_ option: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Dto/UserDto.php + namespace App\Dto; + + use App\Entity\User; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Validator\Constraints as Assert; + + #[UniqueEntity( + fields: 'email', + entityClass: User::class, + )] + class UserDto + { + public ?int $id = null, + + #[Assert\Email] + public ?string $email = null; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Dto\UserDto: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: + fields: email + entityClass: App\Entity\User + properties: + email: + - Email: ~ + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Dto/UserDto.php + namespace App\Dto; + + use App\Entity\User; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Validator\Constraints as Assert; + + class UserDto + { + public ?int $id = null; + + public ?string $email = null; + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new UniqueEntity( + fields: 'email', + entityClass: User::class, + )); + + $metadata->addPropertyConstraint('email', new Assert\Email()); + } + } + Options ------- @@ -269,9 +353,97 @@ the combination value is unique (e.g. two users could have the same email, as long as they don't have the same name also). If you need to require two fields to be individually unique (e.g. a unique -``email`` and a unique ``username``), you use two ``UniqueEntity`` entries, +``email`` and a unique ``username``), you should use two ``UniqueEntity`` entries, each with a single field. +When `using a PHP class`_, the names of the unique fields may differ +from the one defined in the entity. In this case you can map them using +a key-value mapping: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Dto/UserDto.php + namespace App\Dto; + + use App\Entity\User; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Validator\Constraints as Assert; + + #[UniqueEntity( + // 'userIdentifier' is the field name in the PHP class and + // 'email' is the field name in the Doctrine entity + fields: ['userIdentifier' => 'email'], + entityClass: User::class, + )] + class UserDto + { + // ... + + public ?string $userIdentifier = null; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Dto\UserDto: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: + # 'userIdentifier' is the field name in the PHP class and + # 'email' is the field name in the Doctrine entity + fields: {'userIdentifier': 'email'} + entityClass: App\Entity\User + # ... + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // src/Dto/UserDto.php + namespace App\Dto; + + use App\Entity\User; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + + class UserDto + { + public string $userIdentifier; + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new UniqueEntity( + // 'userIdentifier' is the field name in the PHP class and + // 'email' is the field name in the Doctrine entity + fields: ['userIdentifier' => 'email'], + entityClass: User::class, + )); + + // ... + } + } + .. include:: /reference/constraints/_groups-option.rst.inc ``ignoreNull`` @@ -357,10 +529,198 @@ this option to specify one or more fields to only ignore ``null`` values on them .. warning:: - If you ``ignoreNull`` on fields that are part of a unique index in your + If you set ``ignoreNull`` on fields that are part of a unique index in your database, you might see insertion errors when your application attempts to persist entities that the ``UniqueEntity`` constraint considers valid. +``identifierFieldNames`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``array`` **default**: ``[]`` + +The ``identifierFieldNames`` option allows you to specify the field +(or list of fields) used to identify a Doctrine entity, set by the +`entityClass`_ option, when you use a PHP class to update it. + +This way, the entity will be excluded from the list of potential matches +returned by the constraint. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Dto/UserDto.php + namespace App\Dto; + + use App\Entity\User; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + + #[UniqueEntity( + fields: 'email', + entityClass: User::class, + // 'id' is the name of the Doctrine entity field used as the primary key + identifierFieldNames: ['id'] + )] + class UserDto + { + public int $id; + + public string $email; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Dto\UserDto: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: + fields: 'email' + entityClass: 'App\Entity\User' + # 'id' is the name of the Doctrine entity field used as the primary key + identifierFieldNames: ['id'] + properties: + # ... + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Dto/UserDto.php + namespace App\Dto; + + use App\Entity\User; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + + class UserDto + { + public int $id; + + public string $email; + + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addConstraint(new UniqueEntity([ + 'fields' => 'email', + 'entityClass' => User::class, + // 'id' is the name of the Doctrine entity field used as the primary key + 'identifierFieldNames' => ['id'] + ])); + + // ... + } + } + +When `using a PHP class`_ to update an entity, the name of the identifier field +may differ from the one defined in the entity. In this case you can map them using +a key-value mapping: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Dto/UserDto.php + namespace App\Dto; + + use App\Entity\User; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + + #[UniqueEntity( + fields: 'email', + entityClass: User::class, + // 'uid' is the field name in the PHP class and + // 'id' is the field name used as the primary key in the Doctrine entity + identifierFieldNames: ['uid' => 'id'] + )] + class UserDto + { + public int $uid; + + public string $email; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Dto\UserDto: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: + fields: 'email' + entityClass: 'App\Entity\User' + # 'uid' is the field name in the PHP class and + # 'id' is the field name used as the primary key in the Doctrine entity + identifierFieldNames: {'uid': 'id'} + properties: + # ... + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // src/Dto/UserDto.php + namespace App\Dto; + + use App\Entity\User; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + + class UserDto + { + public int $uid; + + public string $email; + + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addConstraint(new UniqueEntity([ + 'fields' => 'email', + 'entityClass' => User::class, + // 'uid' is the field name in the PHP class and + // 'id' is the field name used as the primary key in the Doctrine entity + 'identifierFieldNames' => ['uid' => 'id'] + ])); + + // ... + } + } + ``message`` ~~~~~~~~~~~