Skip to content

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

Open
audain-dg wants to merge 118 commits into
api-platform:4.3from
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 118 commits into
api-platform:4.3from
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.

soyuka and others added 30 commits March 5, 2026 11:20
…rm#7817)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* test: phpunit exception not restored

* test(symfony): track exception handler stack to fix risky tests

| Q             | A
| ------------- | ---
| Branch?       | main
| Tickets       | ∅
| License       | MIT
| Doc PR        | ∅

ApiTestCase now snapshots the exception handler stack via #[Before]/#[After]
hooks (works even when subclasses override setUp without parent::setUp()).
Bump symfony/http-kernel to ^6.4.13 to skip the ErrorListener handler leak
and drop the AppKernel restore_exception_handler() workarounds.
…atform#7957)

* test(jsonld): migrate trivial behat features to ApiTestCase

Replaces 9 jsonld behat features with ApiTestCase functional tests
backed by isolated static-provider fixtures (no Doctrine entities,
except PropertyCollectionIriOnlyTest which keeps the existing entities
to preserve Link/uriVariables semantics). Covers disable_id_generation,
no_output, getter_setter_renaming, interface_as_resource,
interface_dto_output, max_depth, json_serializable, iri_only and drops
the debug-only inheritance scenarios.

* test(jsonld): migrate context/non-resource and abs/net URL features

Replaces context.feature (entrypoint, resource, embed-relation,
extended jsonldContext), non_resource.feature (genid, sparse fieldsets,
DateTime, plain object, non-resource relation), absolute_url.feature,
network_path.feature and hydra/absolute.feature (paged hydra:view).
Per-operation urlGenerationStrategy replaces the legacy app-level
configuration; new fixtures use static providers and ArrayPaginator.

* test(hydra): migrate entrypoint, error and item_uri_template features

Replaces the entrypoint, error and item_uri_template hydra behat
features with ApiTestCase functional tests. Error scenarios use
ValidationException + BadRequestHttpException via dedicated processors;
item_uri_template covers cars/brands plus the existing issue5662 and
CollectionReferencingItem fixtures registered through SetupClassResources.

* test(hydra): migrate collection, docs and input_output features

Replaces the remaining hydra/jsonld behat features:
* hydra/collection.feature: pagination, filters, partial pagination,
  no-prefix mode and cursor pagination (cursor variant uses SoMany).
* hydra/docs.feature: simplified to assert vocabulary, supportedClass,
  property/operation surface and deprecations on a dedicated fixture.
* jsonld/input_output.feature: custom input/output DTO, output: false,
  input: false, full input/output cycle.

Drops legacy @V3 / messenger / DataTransformer scenarios that no
longer reflect 4.x semantics.

* test(jsonld): close coverage gaps left by behat migration

* abs/network URL tests now run against http://example.com via base_uri
  override, matching the original behat host.
* HydraDocsTest exercises the original assertions on a richer fixture:
  subClassOf via types, range/maxCardinality on relations, schema:FindAction
  type on GET, per-method hydra:title and description, deprecation
  propagating from a deprecated resource into the entrypoint.
* InputOutputDtoTest restores the three legacy @V3 collection-DTO scenarios
  (genid for resource without id, custom itemUriTemplate, identifier-bearing
  resource still falling back to genid when no item operation exists).
* New EntityClassWithDateTimeTest covers JSON-LD on a resource backed by
  a Doctrine entity through stateOptions.
* HydraCollectionTest now uses a partial-only paginator implementation,
  asserting that hydra:view drops first/last and keeps next/previous.
* RenamedGetterSetterTest restores the exact @id assertion (collection URI
  fallback when the resource has no identifier).

* chore(tests): remove fixtures orphaned by jsonld behat migration

EntityWithRenamedGetterAndSetter, EntityWithDtoOutput,
JsonldContextDummy and MaxDepthEagerDummy (entity + document)
were only referenced by the deleted behat features. Other
fixtures touched by the migration (IriOnlyDummy, AbsoluteUrlDummy,
NetworkPathDummy, MaxDepthDummy, ContainNonResource, PlainObjectDummy,
NonRelationResource, DisableIdGeneration) are still referenced from
DoctrineContext, the JSON Schema command test, the jsonapi feature
suite or HAL tests, so they stay.

* chore(tests): fix references orphaned by jsonld behat migration

DtoOutput was defined inside EntityWithDtoOutput.php (deleted in
fbfe043); src/JsonSchema/Tests/DefinitionNameFactoryTest.php still
imports it as a class-string for definition-name generation. Re-adds a
minimal class. Also narrows InterfaceTaxonImpl::getCode() to non-nullable
string (covariant) which phpstan now catches.

* test(jsonld): fix CI failures from behat migration

Two fixes for the JsonLd functional tests added yesterday:

* Consolidate dual #[ApiResource] declarations sharing the same
  shortName on AbsoluteUrlChild, NetworkPathResource and UriTemplateCar
  into a single attribute with all operations. Avoids the 4.2-deprecated
  duplicate-shortName path that was failing the no-deprecation and
  Symfony-dev PHPUnit jobs.
* Skip MongoDB-only paths in ItemUriTemplateHydraTest and the
  UserResource branch of InputOutputDtoTest where the underlying
  fixtures live in the Entity namespace and aren't loaded under the
  Document kernel; skip InputOutputDtoTest::testCreateNoInputResource
  in event-listener mode where PlaceholderAction cannot resolve $data
  for input:false POST.

* test(hal): migrate behat features to ApiTestCase

Replaces the eleven hal behat features with ApiTestCase functional tests
under tests/Functional/Hal, mirroring the jsonld/hydra migration done
yesterday. Most use static-provider fixtures; PropertyCollectionIriOnly
and TableInheritance keep the existing entity fixtures (still referenced
by features/main/table_inheritance.feature). MaxDepthEagerDummy was
already deleted in fbfe043 — this restores coverage for that scenario
under HalMaxDepth.

With features/hal, features/jsonld and features/hydra all gone, the
ld-api-hal-hydra shard reduced to features/jsonapi alone; renames the
shard accordingly.

* cs: enforce yoda style in JsonLd CollectionPagedResource

* chore(tests): move root JSON-LD tests into JsonLd/ subdir

* HydraTest -> JsonLd/HydraHideFromDocsTest (descriptive name reflects what
  the single test asserts: hiding hydra:supportedClass and operations)
* LinkedDataPlatformTest -> JsonLd/LinkedDataPlatformTest (LDP is JSON-LD)
* ItemUriTemplateTest -> JsonLd/ItemUriTemplateNotFoundTest (rename
  testIssue6718 to testNotFoundOnInvalidItemUriTemplateRelation)

* chore(tests): split monolithic JsonLdTest into per-feature files

Replace the 9-method root JsonLdTest.php with 6 focused JsonLd/* test
files, one per feature:

* InputDtoIriDenormalizationTest (input DTO with IRI relation)
* ContextOutputTest (output DTO @context shape, ignored properties)
* GenIdFalseTest (genId:false at resource and nested levels)
* PolymorphicResourceCollectionTest (per-item @type in collections)
* ItemUriTemplateCollectionTest (itemUriTemplate as @id, with stateOptions)
* MultiResourceContextTest (correct shortName per ApiResource variant)

Each file scopes its setUp/recreateSchema to only the entities it uses,
and replaces issue-numbered method names with descriptive ones.

* chore(tests): split monolithic JsonApiTest into JsonApi/ subdir

Replace the 8-method root JsonApiTest.php with 4 focused JsonApi/* test
files, mirroring the JsonLd/ and Hal/ subdir layout:

* JsonApi/ErrorTest (ErrorResource rendered in vnd.api+json)
* JsonApi/IdentifierModeTest (4 use_iri_as_id:false tests sharing the
  bootJsonApiKernel helper)
* JsonApi/IriModeTest (default use_iri_as_id:true mode)
* JsonApi/InputDtoTest (POST with input DTOs, preserved attributes and
  required constructor args)

Each file scopes its fixtures to only what its tests use.

* test(security): migrate authorization behat features to ApiTestCase

Replaces features/authorization/{deny,legacy_deny}.feature with functional
tests under tests/Functional/Authorization, preserving full scenario
coverage. Each @link_security scenario lives in its own test so the
positive and negative paths are asserted independently.

Drops features/authorization from the misc behat shard; the
SecuredDummy-with-related-dummies fixture step stays since
features/graphql/authorization.feature still uses it.

* test(jsonapi): migrate behat features to ApiTestCase

Replaces 13 jsonapi behat features with ApiTestCase functional tests
backed by static-provider fixtures (CollectionUriTemplateTest reuses
the existing PropertyCollectionIriOnly entities to preserve Link
semantics). Covers errors, absolute/network URLs, item/collection
URI templates, circular refs, non-resource handling, DTO output,
ordering, filtering with sparse fieldsets, pagination, entrypoint,
CRUD with relationships, and include= related-resources resolution.

* test(jsonapi): back CRUD and inclusion tests with Doctrine persistence

CrudTest now uses Dummy/RelatedDummy/ThirdLevel/RelationEmbedder
entities through recreateSchema/persist/flush instead of static
processors that fabricated ids, matching the authorization migration
pattern. RelatedResourcesInclusionTest grows from 5 partial scenarios
to all 15 original behat scenarios (many-to-one, many-to-many, dedup,
path-based, collection variants) backed by DummyProperty/DummyGroup/
FourthLevel/RelatedOwningDummy. The 6 now-unused static JsonApi
fixtures are dropped.

* chore(ci): fix jsonapi migration follow-ups

* Drop empty `jsonapi` behat shard from CI matrix (features/jsonapi
  was deleted in 0bf7af5).
* PHPStan: replace `is_int($offset)` always-true narrowing with an
  upfront page-range guard in PaginationDummy.
* CS: prefix `array_slice` with `\` in OrderingTest per project style.

* test(security): skip DenyTest ORM-id-bound scenarios on MongoDB

Seven DenyTest cases assume the freshly-seeded SecuredDummy gets id=1
on every run. The MongoDB ODM INCREMENT strategy keeps its counter
outside the dropped document collection, so subsequent test runs
re-target a non-existent id and the API returns 404 instead of the
expected 403 / 200. Skip them on MongoDB to match the existing
isMongoDB() pattern used by the link-security tests in this file.

Also baseline the multi-shortName deprecation triggered by
RelatedLinkedDummy (used only by DenyTest now) so the
phpunit-no-deprecations CI job stays green.

* test(security): modernize DenyTest fixtures, drop INCREMENT id assumptions

* RelatedLinkedDummy (Entity + Document) gives each #[ApiResource] a
  distinct shortName so the multi-shortName 4.2 deprecation no longer
  fires. The baseline entry added in 8f0cb09 is reverted.
* DenyTest / LegacyDenyTest no longer hard-code /secured_dummies/1:
  POSTs now return the IRI which subsequent GET/PUT calls consume.
  seedLinkedDummy switches to API POST and returns the created ids,
  so the helper works on both ORM and ODM. This removes seven
  isMongoDB() skips and the failures from the MongoDB CI job (the
  ODM INCREMENT counter survives collection drops and never gives id=1
  twice in a row).

Verified locally against both APP_ENV=test and APP_ENV=mongodb.

* test(hal,jsonld): close remaining behat migration coverage gaps

Add PUT/PATCH on HAL relation embedder, PUT on HAL max-depth resource,
JSON-LD messenger and DataTransformerInitializer scenarios. Restore the
PropertyFilter on JsonLdNonResourceContainer (lost during migration) and
tighten the sparse-fieldset assertion so excluded keys are checked.

* test(jsonld): cover Issue5438 inheritance IRIs

The deleted features/jsonld/inheritance.feature only had
"print last JSON response" steps. Replace with a real assertion
that confirms each collection member uses its concrete subtype's
URI template (Contractor5438 → /contractor_5438/{id}, Employee5438
→ /employee_5438/{id}) — the original behavior fixed in api-platform#5449.
…atform#7971)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Behat→PHPUnit migration scaffolder under `tools/` was a one-shot
helper; it has no place in the published source tree.

`doctrine/mongodb-odm` and `doctrine/mongodb-odm-bundle` are installed
on demand by the `mongodb` CI job (and likewise on contributor
machines), not via require-dev — the lines added in api-platform#8202 made them
load unconditionally and bloated the install footprint for everyone
else.
Probot stale does not understand GitHub issue types. Replace the
legacy `.github/stale.yml` with an `actions/github-script` workflow
that exempts issues whose type is `Bug` or `Feature` and keeps the
prior 60/7 day window plus exempt labels (RFC, Hacktoberfest,
EU-FOSSA Hackathon).
soyuka and others added 23 commits July 12, 2026 15:19
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Ousama Ben Younes <benyounes.ousama@gmail.com>
# Conflicts:
#	.github/workflows/ci.yml
#	composer.json
#	src/GraphQl/composer.json
#	src/Mcp/composer.json
#	src/Serializer/composer.json
#	src/Symfony/composer.json
#	tests/Functional/Parameters/DoctrineTest.php
# Conflicts:
#	.github/workflows/ci.yml
#	.github/workflows/commitlint.yml
#	.github/workflows/guides.yml
#	.github/workflows/release.yml
#	CHANGELOG.md
#	composer.json
#	src/Mcp/composer.json
#	tests/Functional/JsonLd/SerializableItemDataProviderTest.php
The 4.3 -> 4.4 merge 7085b6d re-added 121 lines to DoctrineTest.php that api-platform#8344
had removed when SearchFilterParameter moved under Legacy/. The three methods
reference a fixture class and a route that only exist on 4.3, breaking every
PHPUnit job and PHPStan. Legacy/SearchFilterParameterLegacyTest.php already
covers them.
api-platform/serializer 4.4.0-alpha.1 predates api-platform#8397, so --prefer-lowest pulled
symfony/serializer 6.4, which keys circular references by spl_object_hash and
broke testNormalizeCircularReference. Also drop the EventStreamResponse probe:
it tested http-foundation to guess a serializer behaviour, and Symfony >= 7.3 is
the only supported range.
# Conflicts:
#	tests/Functional/Parameters/Legacy/SearchFilterParameterLegacyTest.php
…-platform#8490)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexis Lefebvre <alexislefebvre+github@gmail.com>
Closes api-platform#7361
# Conflicts:
#	CHANGELOG.md
#	src/Mcp/composer.json
The Doctrine (ORM, ODM, Common) and JSON:API components ship filters
implementing OpenApiParameterFilterInterface that instantiate
ApiPlatform\OpenApi\Model\Parameter, and none of them depends on
api-platform/openapi. ParameterResourceMetadataCollectionFactory only
guards that call with class_exists() as of v4.4.0-alpha.4, so an older
metadata makes them fatal on an install without the OpenApi component.
# Conflicts:
#	CHANGELOG.md
#	composer.json
#	src/Doctrine/Common/composer.json
#	src/Doctrine/Odm/composer.json
#	src/Doctrine/Orm/composer.json
#	src/JsonApi/composer.json
#	src/JsonSchema/Tests/SchemaFactoryTest.php
#	src/Mcp/composer.json
The Doctrine (ORM, ODM, Common) and JSON:API components ship filters
implementing OpenApiParameterFilterInterface that instantiate
ApiPlatform\OpenApi\Model\Parameter, and none of them depends on
api-platform/openapi. ParameterResourceMetadataCollectionFactory only
guards that call with class_exists() as of v5.0.0-alpha.3, so an older
metadata makes them fatal on an install without the OpenApi component.
@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.
@alexisLefebvre

Copy link
Copy Markdown
Contributor

It looks like the rebase had issues, there are too many commits, unrelated to the PR.

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.

9 participants