Skip to content

fix(metadata): resolve uri variable identifiers from metadata - #8578

Merged
soyuka merged 3 commits into
api-platform:4.4from
soyuka:fix/uri-variable-identifier-inference-8167
Sep 25, 2026
Merged

soyuka merged 3 commits into
api-platform:4.4from
soyuka:fix/uri-variable-identifier-inference-8167

Conversation

@soyuka

@soyuka soyuka commented Sep 24, 2026

Copy link
Copy Markdown
Member

Summary

UriTemplateResourceMetadataCollectionFactory::configureUriVariables() falls back to a
"guess" when the number of uri template variables doesn't match the resolved uri
variables. That guess used property_exists($operation->getClass(), $variable) to decide
whether the missing variable is a real identifier or should default to 'id'.

Eloquent models expose their columns as magic attributes through __get/__set, never as
declared PHP properties, so property_exists() is always false for them - even when the
model has a custom $primaryKey (e.g. a UUID column). The guess silently picked the literal
string 'id' instead of the real identifier, which made route/IRI generation fail.

The fix replaces the property_exists() guess with the link factory's own identifier
resolution (LinkFactoryInterface::completeLink()), already used a few lines above in the
same method. It resolves identifiers via getIdentifiersFromResourceClass(), which reads
declared property metadata (isIdentifier()) instead of raw PHP property existence - on
Laravel this correctly resolves a custom $primaryKey such as 'uuid'. The previous
guess is kept as a fallback for the (rare) case where completeLink() still can't resolve
any identifier, so no existing behavior regresses.

Reproduction

A Laravel Eloquent model using a non-id string primary key (e.g. $primaryKey = 'uuid')
with an explicit Get(uriTemplate: '/buildings/{uuid}') operation and no explicit
uriVariables could get its identifier silently guessed as 'id' when the uri variables
had to be rebuilt from scratch (e.g. stale/legacy-loaded uri variables that no longer match
the current uri template), instead of being resolved from metadata. Route/IRI generation
then throws Unable to generate an IRI for the item of type "...".

Test plan

  • Added UriTemplateResourceMetadataCollectionFactoryTest::testResolvesMissingUriVariableIdentifierFromMetadataInsteadOfGuessingId, which fails with the old property_exists() guess (resolves to 'id') and passes with the fix (resolves to 'uuid').
  • Added Laravel fixtures (Building model/migration/factory + UuidPrimaryKeyIriTest) matching the reported example (UUID primary key, explicit CRUD operations, JSON:API collection request) to guard against regressions on that shape.
  • Existing UriTemplateResourceMetadataCollectionFactoryTest::testCreate still passes.

Fixes #8167

property_exists() cannot see Eloquent magic attributes (columns are
exposed through __get, not declared properties), so the uri variable
"guess" fallback silently picked the literal 'id' instead of the real
identifier. Consult the link factory's own identifier resolution
(completeLink(), which reads isIdentifier() from property metadata)
before falling back to the old guess, so Laravel models with a custom
$primaryKey (e.g. a UUID) get their real identifier instead of 'id'.

Fixes api-platform#8167
LinkFactory::getIdentifiersFromResourceClass() cached an empty
identifiers array forever once computed. On a long-lived worker
(e.g. Octane), or when routes register before migrations run, a
resource's identifiers can resolve empty on the first call and stay
poisoned for the life of the process, even once the underlying table
becomes available. Mirrors ModelMetadata::getAttributes()'s guard for
the same race.

Fixes api-platform#8571
@soyuka
soyuka merged commit 332cb1b into api-platform:4.4 Sep 25, 2026
115 of 117 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