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

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

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
66Ton99force-pushed the fix/unique-entity-ignores-edited-record branch from a0a9dc2 to 062bf05CompareAugust 28, 2026 06:40
@66Ton99
66Ton99 merged commit 0be9c2f into Svaroh:mainAug 28, 2026
6 checks passed
Sign up for freeto 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

@66Ton99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

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

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
66Ton99force-pushed the fix/unique-entity-ignores-edited-record branch from a0a9dc2 to 062bf05CompareAugust 28, 2026 06:40
@66Ton99
66Ton99 merged commit 0be9c2f into Svaroh:mainAug 28, 2026
6 checks passed
Sign up for freeto 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

@66Ton99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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

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
66Ton99force-pushed the fix/unique-entity-ignores-edited-record branch from a0a9dc2 to 062bf05CompareAugust 28, 2026 06:40
@66Ton99
66Ton99 merged commit 0be9c2f into Svaroh:mainAug 28, 2026
6 checks passed
Sign up for freeto 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

@66Ton99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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

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
66Ton99force-pushed the fix/unique-entity-ignores-edited-record branch from a0a9dc2 to 062bf05CompareAugust 28, 2026 06:40
@66Ton99
66Ton99 merged commit 0be9c2f into Svaroh:mainAug 28, 2026
6 checks passed
Sign up for freeto 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

@66Ton99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

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

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
66Ton99force-pushed the fix/unique-entity-ignores-edited-record branch from a0a9dc2 to 062bf05CompareAugust 28, 2026 06:40
@66Ton99
66Ton99 merged commit 0be9c2f into Svaroh:mainAug 28, 2026
6 checks passed
Sign up for freeto 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

@66Ton99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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

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
66Ton99force-pushed the fix/unique-entity-ignores-edited-record branch from a0a9dc2 to 062bf05CompareAugust 28, 2026 06:40
@66Ton99
66Ton99 merged commit 0be9c2f into Svaroh:mainAug 28, 2026
6 checks passed
Sign up for freeto 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

@66Ton99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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

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
66Ton99force-pushed the fix/unique-entity-ignores-edited-record branch from a0a9dc2 to 062bf05CompareAugust 28, 2026 06:40
@66Ton99
66Ton99 merged commit 0be9c2f into Svaroh:mainAug 28, 2026
6 checks passed
Sign up for freeto 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

@66Ton99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

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

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
66Ton99force-pushed the fix/unique-entity-ignores-edited-record branch from a0a9dc2 to 062bf05CompareAugust 28, 2026 06:40
@66Ton99
66Ton99 merged commit 0be9c2f into Svaroh:mainAug 28, 2026
6 checks passed
Sign up for freeto 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

@66Ton99