Skip to content

fix: stop reporting an edited record as its own duplicate - #25

Merged
66Ton99 merged 1 commit into
Svaroh:mainfrom
Web20:fix/unique-entity-ignores-edited-record
Aug 28, 2026
Merged

fix: stop reporting an edited record as its own duplicate#25
66Ton99 merged 1 commit into
Svaroh:mainfrom
Web20:fix/unique-entity-ignores-edited-record

Conversation

@66Ton99

@66Ton99 66Ton99 commented Aug 27, 2026

Copy link
Copy Markdown

Problem

Found while running a single page CRUD against the bundle: every edit of a record whose entity carries a UniqueEntity constraint is blocked in the browser, even when the value was not changed.

An edit form submits the value the record already holds, so checkUniqueEntityAction finds that record and answers false. The browser shows "This value is already used." and refuses to send the form. Symfony's own UniqueEntityValidator does not do this — it compares the found entity with the one it is validating and skips it — so the client refuses a submit the server would have accepted.

Reproduced end to end: a Product with #[UniqueEntity(fields: ['sku'])], opened from an edit route, name changed, SKU untouched. The only request made was the uniqueness check; the form POST never went out and the field showed the duplicate message.

Change

The browser already sends the identifier of the object the form is bound to as entityIdForm\Constraint\UniqueEntity fills it from getId(), constraints/UniqueEntity.js puts it in the request, and src/Resources/doc/3_9.md already documents it and tells custom controllers to use it to exclude the edited entity. The default controller now does the same: the value is free when every record holding it is the one that identifier names.

The identifier of a match is read from Doctrine's class metadata when a manager knows the class, and falls back to getId(); a composite key, or a match whose identifier cannot be read, is never taken for the edited record. A request with no entityId — a create form — behaves exactly as before.

toList() was added because a constraint may declare a repositoryMethod that answers with a single entity or with null rather than a list; the previous empty() check accepted all three shapes and this keeps that.

Trust

entityId is the one part of the lookup the request decides, so a caller can ask "is this value free if we ignore record N?" and get true for a value record N holds. It changes an answer, never data, and the server side validator is unaffected. The route remains the unauthenticated existence check 3_9.md already documents and asks applications to restrict; this does not widen what it can be asked about — the field combination and repository method still have to match a declared constraint. Noted in 3_9.md and in the design risks in AGENTS.md.

Tests

Four tests in Tests/Controller/AjaxControllerTest.php: the edited record is not its own duplicate, another record holding the value still is, an existing value with no entityId still is, and a match whose identifier cannot be read still is. The first fails on main.

Verified in the Nix shell on PHP 8.5.6: composer test 99 tests / 272 assertions, composer phpstan no errors, composer coverage 95.85% lines.

🤖 Generated with Claude Code

@66Ton99

66Ton99 commented Aug 28, 2026

Copy link
Copy Markdown
Author

The two red jobs here are not from this change — it touches PHP only. main itself fails one Jest test: registerForm() now sweeps the registrations whose markup has left the document, and the removeDetachedForms test builds its two registrations in a way that sweep passes over. Every open pull request is red for the same reason.

#26 repairs it and is green on all six jobs. Re-running the checks here after that lands should turn this one green; nothing in this branch needs to change.

A form that edits a record submits the value that record already holds,
so the lookup found the record itself and the browser reported "already
used" for every edit of an entity carrying a UniqueEntity constraint.
The submit was blocked client side even though Symfony's own validator,
which skips the object it is validating, would have accepted it.

The browser already sends the identifier of the object the form is bound
to as "entityId", and the documentation already told custom controllers
to use it. The default controller now does the same: a value is free
when every record holding it is the one that identifier names.

That identifier is the one part of the lookup the request decides, so a
caller can ask for an answer that ignores one record. It changes an
answer, never data, and the server side validator is unaffected; the
route stays the existence oracle documented in 3_9.
@66Ton99
66Ton99 force-pushed the fix/unique-entity-ignores-edited-record branch from a0a9dc2 to 062bf05 Compare August 28, 2026 06:40
@66Ton99
66Ton99 merged commit 0be9c2f into Svaroh:main Aug 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant