Skip to content

fix(symfony): reject duplicate operation names instead of silently dropping operations#8232

Merged
soyuka merged 6 commits into
api-platform:4.3from
soyuka:fix/same-path-different-methods-8175
Jun 3, 2026
Merged

fix(symfony): reject duplicate operation names instead of silently dropping operations#8232
soyuka merged 6 commits into
api-platform:4.3from
soyuka:fix/same-path-different-methods-8175

Conversation

@soyuka

@soyuka soyuka commented Jun 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Two operations declared on the same resource with the same explicit name silently dropped one of them because the operation name is the unique key in the operations collection (and the Symfony route name).
  • The attribute and XML/YAML metadata factories now detect the collision up front and throw RuntimeException with an actionable message instead of letting the second declaration overwrite the first.

Reproduction

Two custom operations sharing uriTemplate: '/forms/{id}/submit{._format}' and name: '_api_/forms/{id}/submit{._format}' but differing by method (POST and PATCH) — only the second-declared operation survived, the other 405'd.

Test plan

  • Added failing test covering the bug.
  • Test passes after the fix.
  • Related test classes still pass locally.

Fixes #8175

soyuka added 6 commits June 3, 2026 11:15
…opping operations

Two operations declared on the same resource with an identical explicit `name`
silently overwrote each other in the operations collection, leaving only the
last one alive. The reporter hit this with two custom operations sharing the
URI template `/forms/{id}/submit{._format}` and the same `name`, differing only
by HTTP method (POST and PATCH): the first declaration would 405.

The operation name is the unique key in the operations collection and is also
used verbatim as the Symfony route name, so duplicates can never both work.
The attribute and XML/YAML factories now detect a collision at metadata-build
time and throw `RuntimeException` with a message pointing to the duplicate
name and how to fix it (either drop the duplicate `name` so the framework
disambiguates by method, or set distinct names).

Fixes api-platform#8175
…ad of throwing

The previous commit threw a RuntimeException whenever two operations resolved to
the same key in the operations collection. That broke legitimate "override an
earlier declaration" patterns used across the test suite (e.g. OverriddenOperationDummy
re-declares Get with custom serialization groups, producing the same default key
_api_OverriddenOperationDummy_get twice). The throw fired during cache warmup and
took out every PHPUnit job.

Restore the pre-regression behavior selectively:

- Same key + same HTTP method: leave untouched. This is the override pattern;
  the later declaration wins, matching how Operations::add() already behaves.
- Same key + different HTTP method (the issue api-platform#8175 case): the colliding entry
  is auto-suffixed with its method (e.g. _api_/forms/{id}/submit{._format}_patch)
  so both operations survive in the collection and Symfony route registration.

The disambiguation lives on OperationDefaultsTrait so both the attributes/concerns
factories (via MetadataCollectionFactoryTrait) and the XML/YAML extractor factory
share one implementation. Tests previously asserting the throw now assert that
both operations are present with the expected methods.
The disambiguateOperationName calls added two lines to MetadataCollectionFactoryTrait,
shifting the existing shortName-duplicate trigger_deprecation from line 252 to 254.
PHPUnit baselines key on the line number; out-of-sync baselines stop ignoring the
deprecation, so --fail-on-deprecation jobs (no-deprecations, Symfony dev, metadata)
fail on a pre-existing, intentionally-baselined message.
…ures

The fixture declared two parameterless `new Get()` operations on the same
resource, which resolved to identical default operation names. In 4.2.0+ the
second declaration silently overwrote the first; with the duplicate-name
guard added in 271664c this now throws, breaking cache warmup.

The first `new Get()` was dead — the second one (with overridden_operation_dummy_get
groups) is what every test exercises. Removing it is the same fix users
hitting api-platform#8175 need to apply to their own resources.

Also bump the shortName-deduplication baseline from line 252 to 254 to
match the new trigger_deprecation position once the throw guard's
assertOperationNameIsUnique calls have shifted the surrounding code.
@soyuka soyuka merged commit 46c25c7 into api-platform:4.3 Jun 3, 2026
107 of 108 checks passed
@rvanlaak

rvanlaak commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

This could also have been the reason why in the Symfony Profiler the tabs on an entity's resources (when two Resource attributes were defined - e.g. one for users, one for admins) would not allow clicking the entity's second resource. Will try to verify that later this week.

@soyuka

soyuka commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

Sure let me know !

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.

2 participants