Skip to content

fix(symfony): throw on route name collisions between resource classes - #8502

Open
audain-dg wants to merge 1 commit into
api-platform:mainfrom
audain-dg:fix/route-name-collision-across-resources-main
Open

fix(symfony): throw on route name collisions between resource classes#8502
audain-dg wants to merge 1 commit into
api-platform:mainfrom
audain-dg:fix/route-name-collision-across-resources-main

Conversation

@audain-dg

Copy link
Copy Markdown
Q A
Branch? main
Tickets n/a
License MIT
Doc PR n/a

What

Operation names double as Symfony route names, and ApiLoader registers them with RouteCollection::add(), which silently replaces an existing entry. Two resource classes exposing the same URI template and method therefore end up with one route only, owned by whichever class was discovered last, with no error or warning. The winner can flip on cache:clear when the discovery order changes.

MetadataCollectionFactoryTrait::assertOperationNameIsUnique() (4.3) already rejects a duplicate name within one class. This PR extends the guarantee across classes:

  • two exposed operations from different classes under one name now throw a RuntimeException naming both classes, and pointing to routeName as the supported way to share a route on purpose;
  • a NotExposed placeholder never wins over an exposed operation of another class, whatever the discovery order, and never throws (a DTO and an entity sharing a shortName keep working).

Why it matters

The test application itself had four such collisions, each silently dropping a route:

Operation name Loser Winner
_api_/user-actions_get_collection Issue7916\UserActionResource (ORM) UserActionResourceOdm
_api_/companies/{companyId}/employees/{id}_get DummyResourceWithComplexConstructor Employee
_api_/books/{id}{._format}_get IriFilterRelationsTest\Book (NotExposed) Entity\Book
_api_/people/{id}{._format}_get Issue5438\Person (NotExposed) Entity\Person

Nobody noticed because functional tests restrict resources per test class. In a real application the same thing happens when two resources (an ORM/ODM pair, a DTO and its entity, two projections of one entity) share a URI template.

The first two fixtures now use distinct URI templates; the last two are covered by the placeholder rule.

Notes

  • Targets main because a silent overwrite becomes an exception. Happy to retarget 4.3 if you consider it a plain bug fix.
  • ApiLoaderTest::testApiLoader was asserting RelatedDummyEntity::class on every route, i.e. it was asserting the overwrite: both classes returned the same metadata collection. The helper now gives the second class an empty collection by default.
  • Sharing a route on purpose is unchanged: a resource declaring routeName creates no route (ApiLoader skips it) and is not affected. Covered by a new test.

@audain-dg
audain-dg force-pushed the fix/route-name-collision-across-resources-main branch from c3192bb to 41d495f Compare September 5, 2026 08:52
Operation names double as Symfony route names, and `ApiLoader` registered
them with `RouteCollection::add()`, which silently replaces an existing
entry. Two resource classes exposing the same URI template and method
therefore produced one route only, owned by whichever class was discovered
last, without any error or warning.

`MetadataCollectionFactoryTrait::assertOperationNameIsUnique()` already
rejects a duplicate name within one class; this extends the guarantee
across classes:

- two exposed operations from different classes under one name throw a
  `RuntimeException` naming both classes and pointing to `routeName` as
  the way to share a route on purpose;
- a `NotExposed` placeholder never wins over an exposed operation of
  another class, whatever the discovery order, and never throws.

The test application had four such collisions, each dropping a route
silently: `Issue7916\UserActionResource` vs its ODM twin,
`DummyResourceWithComplexConstructor` vs `Employee`, and two `NotExposed`
placeholders shadowing `Book` and `Person`. The first two now use distinct
URI templates, the last two are handled by the placeholder rule.
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