Skip to content

Commit bcceaa2

Browse files
author
Stephan Six
committed
[WCM] [Validator] Added documentation for the new comparator option
1 parent dac15b6 commit bcceaa2

File tree

1 file changed

+140
-0
lines changed

1 file changed

+140
-0
lines changed

reference/constraints/UniqueEntity.rst

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,146 @@ this option to specify one or more fields to only ignore ``null`` values on them
361361
database, you might see insertion errors when your application attempts to
362362
persist entities that the ``UniqueEntity`` constraint considers valid.
363363

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+
364504
``message``
365505
~~~~~~~~~~~
366506

0 commit comments

Comments
 (0)