From 11852a63d8e1cf5e3897d25f28e39075afcd8ca6 Mon Sep 17 00:00:00 2001 From: Romain Monteil Date: Wed, 9 Jul 2025 14:10:22 +0200 Subject: [PATCH 1/3] [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`` ~~~~~~~~~~~ From a7cfb4fca59d7780df477ceabb4bf02f7531802d Mon Sep 17 00:00:00 2001 From: Romain Monteil Date: Fri, 1 Aug 2025 01:59:38 +0200 Subject: [PATCH 2/3] Add suggestions --- reference/constraints/UniqueEntity.rst | 36 +++++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/reference/constraints/UniqueEntity.rst b/reference/constraints/UniqueEntity.rst index 060c5ab1cf8..758ed3cc295 100644 --- a/reference/constraints/UniqueEntity.rst +++ b/reference/constraints/UniqueEntity.rst @@ -100,6 +100,27 @@ between all of the rows in your user table: } } + // src/Form/Type/UserType.php + namespace App\Form\Type; + + 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`_. @@ -116,8 +137,9 @@ between all of the rows in your user table: 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:: +This constraint can also check **uniqueness on any PHP class** that is mapped to +a Doctrine entity, and not only on Doctrine entities. Consider the following +Doctrine entity:: // src/Entity/User.php namespace App\Entity; @@ -160,8 +182,6 @@ following using the `entityClass`_ option: )] class UserDto { - public ?int $id = null, - #[Assert\Email] public ?string $email = null; } @@ -208,8 +228,6 @@ following using the `entityClass`_ option: class UserDto { - public ?int $id = null; - public ?string $email = null; public static function loadValidatorMetadata(ClassMetadata $metadata): void @@ -353,7 +371,7 @@ 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 should use two ``UniqueEntity`` entries, +``email`` and a unique ``username``), you must use two ``UniqueEntity`` entries, each with a single field. When `using a PHP class`_, the names of the unique fields may differ @@ -721,6 +739,10 @@ a key-value mapping: } } +.. note:: + + This option has no effect when the constraint is applied to an entity. + ``message`` ~~~~~~~~~~~ From 14be95089370ccb1e2110f6a43a9fd9758226261 Mon Sep 17 00:00:00 2001 From: Romain Monteil Date: Fri, 1 Aug 2025 17:36:47 +0200 Subject: [PATCH 3/3] Add versionadded --- reference/constraints/UniqueEntity.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/reference/constraints/UniqueEntity.rst b/reference/constraints/UniqueEntity.rst index 758ed3cc295..7d6742b4de4 100644 --- a/reference/constraints/UniqueEntity.rst +++ b/reference/constraints/UniqueEntity.rst @@ -556,6 +556,10 @@ this option to specify one or more fields to only ignore ``null`` values on them **type**: ``array`` **default**: ``[]`` +.. versionadded:: 7.1 + + The ``identifierFieldNames`` option was introduced in Symfony 7.1. + 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.