If you use Symfony's Doctrine UniqueEntity constraint, import the default
bundle route as shown in the README.
The default controller answers only the lookups your application opted into. A
request names an entity class, a field combination and a repository method, and
the controller looks that combination up in the validation metadata of the named
class. It replies only when a UniqueEntity constraint there declares exactly
that field combination and that repository method; otherwise it returns
400 Bad Request without touching Doctrine. The remaining options are read from
the constraint, never from the request:
repositoryMethod— the method the constraint declares is the one called, so a request cannot reach a Doctrine magicfindByXmethod for an arbitrary column, nor any other method of a custom repository.ignoreNullandentityClass— taken from the constraint. A boolean or a list of field names is accepted forignoreNull, as in Symfony.- criteria values — must be scalar or
null. An array value is refused, so a request cannot widen the lookup into an extra condition.
A form that edits a record submits the value that record already holds, so the
record matches itself. The controller answers that the value is free when every
record holding it is the one named by entityId, the way Symfony's own
validator skips the object it is validating. Without that, a UniqueEntity
constraint would report every edit of an existing record as a duplicate.
entityId comes from the request, so a caller can ask for an answer that
ignores one record of its choosing. Nothing is created or changed by the answer,
and the server side validator still refuses a submit that is really a duplicate;
the route remains the existence check described below either way.
Two things this does not do, because only your application can:
- For the field combinations you did declare unique, the route is still a public existence check: anybody who can reach it can ask whether a given email is already registered. Restrict it in your firewall if that answer is sensitive.
- The route is not rate limited. Add throttling in the host application if you need it.
Note that a UniqueEntity passed inline through a form's constraints option is
not part of the class validation metadata, so the endpoint cannot see it and will
refuse the lookup. Declare UniqueEntity on the entity class, or use the custom
controller below.
A refused lookup leaves the form usable: the browser reports no uniqueness error of its own, the submit that was waiting for the answer goes through, and the server side validates the constraint as it always did.
To customize server-side uniqueness validation, create your own controller and point the bundle config to your route:
# config/packages/svaroh_js_form_validator.yaml
svaroh_js_form_validator:
routing:
check_unique_entity: app_custom_unique_controller# config/routes/svaroh_js_form_validator_custom.yaml
app_custom_unique_controller:
path: /custom_unique_controller
controller: App\Controller\CustomUniqueController::index<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
final class CustomUniqueController
{
public function index(Request $request): JsonResponse
{
$data = $request->request->all();
// Add your uniqueness lookup here.
$isUnique = true;
return new JsonResponse($isUnique);
}
}The request data has this shape:
$data = [
'message' => 'This value is already used.',
'service' => 'doctrine.orm.validator.unique',
'repositoryMethod' => 'findBy',
'fields' => ['email'],
'ignoreNull' => '1',
'groups' => ['Default', 'User'],
'entityName' => 'App\Entity\User',
'entityId' => 15,
'data' => [
'email' => 'john_doe@example.com',
],
];When the form is bound to an object that has a getId() method, entityId
contains the current object id. The default controller uses it to exclude the
edited record from its lookup, and a custom controller should do the same.
Return true when the value is unique and false when it is already used.