Skip to content

Latest commit

 

History

History
111 lines (89 loc) · 4.21 KB

File metadata and controls

111 lines (89 loc) · 4.21 KB

3.9 Checking entity uniqueness

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 magic findByX method for an arbitrary column, nor any other method of a custom repository.
  • ignoreNull and entityClass — taken from the constraint. A boolean or a list of field names is accepted for ignoreNull, 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.