API Platform version(s) affected: 4.3.10 (regression — works fine on 4.3.3)
Description
A GraphQL item query crashes with a Doctrine MappingException when the
queried resource has a public property typed as a resource class but not mapped
as a Doctrine association (e.g. a transient/runtime-only helper, or a self
reference).
LinkFactory::createLinksFromRelations() builds a relation Link for any
property whose native type resolves to a resource class — it does not check
whether that property is an actual Doctrine association. For a GraphQL root item
query, Doctrine\Common\State\LinksHandlerTrait::getLinks() keeps links whose
toClass === resourceClass (self references). The ORM
Doctrine\Orm\State\LinksHandlerTrait::handleLinks() then calls
ClassMetadata::getAssociationMapping($link->getFromProperty()) without a
hasAssociation() guard, which throws:
No mapping found for field 'relatedButNotMapped' on class 'App\Entity\Foo'.
So the metadata layer decides the property is a relation purely from its PHP
type, while the ORM state layer assumes that relation is a mapped association.
The two assumptions are inconsistent, and the mismatch surfaces as an unhandled
exception.
This worked on 4.3.3 and broke after upgrading to 4.3.10 with no changelog entry
describing the change. Prime suspects are #8236 / #8239 (4.3.9, circular reference
/ nested resource link handling) and #8237 (item query provider dispatch), but I
have not bisected the exact patch release.
How to reproduce
Minimal resource — a public getter returning the resource type, not a Doctrine
association:
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Query;
use Doctrine\ORM\Mapping as ORM;
#[ApiResource(graphQlOperations: [new Query()])]
#[ORM\Entity]
class Foo
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
public ?int $id = null;
// NOT persisted, NOT a Doctrine association — just a transient resource-typed property
private ?Foo $relatedButNotMapped = null;
public function getRelatedButNotMapped(): ?Foo
{
return $this->relatedButNotMapped;
}
public function setRelatedButNotMapped(?Foo $foo): void
{
$this->relatedButNotMapped = $foo;
}
}
Run a GraphQL item query:
{
foo(id: "/foos/1") {
id
}
}
Possible Solution
Guard the association lookup in
Doctrine\Orm\State\LinksHandlerTrait::handleLinks(): skip (or raise a clear,
actionable error) when the link's fromProperty is not an actual association:
if ($link->getFromProperty() && !$link->getToProperty()) {
if (!$fromClassMetadata->hasAssociation($link->getFromProperty())) {
continue; // resource-typed but not a Doctrine association — nothing to join on
}
$associationMapping = $fromClassMetadata->getAssociationMapping($link->getFromProperty());
// ...
}
Alternatively, LinkFactory::createLinksFromRelations() could skip non-readable
properties, or there could be a documented, first-class way to exclude a
resource-typed property from relation-link generation (currently
#[ApiProperty(readable: false, writable: false)] and #[Ignore] do not
remove the property from the name collection used by the link factory — the only
working workaround is a PropertyMetadataFactory decorator clearing the
nativeType).
Additional Context
PHP 8.5, Symfony 7, Doctrine ORM 3 / DBAL 4, PostgreSQL.
Relevant source (monorepo paths):
src/Metadata/Resource/Factory/LinkFactory.php → createLinksFromRelations() (no association check)
src/Doctrine/Common/State/LinksHandlerTrait.php → getLinks() (keeps toClass === resourceClass self references for GraphQL root item queries)
src/Doctrine/Orm/State/LinksHandlerTrait.php → handleLinks() (unguarded getAssociationMapping())
API Platform version(s) affected: 4.3.10 (regression — works fine on 4.3.3)
Description
A GraphQL item query crashes with a Doctrine
MappingExceptionwhen thequeried resource has a public property typed as a resource class but not mapped
as a Doctrine association (e.g. a transient/runtime-only helper, or a self
reference).
LinkFactory::createLinksFromRelations()builds a relationLinkfor anyproperty whose native type resolves to a resource class — it does not check
whether that property is an actual Doctrine association. For a GraphQL root item
query,
Doctrine\Common\State\LinksHandlerTrait::getLinks()keeps links whosetoClass === resourceClass(self references). The ORMDoctrine\Orm\State\LinksHandlerTrait::handleLinks()then callsClassMetadata::getAssociationMapping($link->getFromProperty())without ahasAssociation()guard, which throws:No mapping found for field 'relatedButNotMapped' on class 'App\Entity\Foo'.
So the metadata layer decides the property is a relation purely from its PHP
type, while the ORM state layer assumes that relation is a mapped association.
The two assumptions are inconsistent, and the mismatch surfaces as an unhandled
exception.
This worked on 4.3.3 and broke after upgrading to 4.3.10 with no changelog entry
describing the change. Prime suspects are #8236 / #8239 (4.3.9, circular reference
/ nested resource link handling) and #8237 (item query provider dispatch), but I
have not bisected the exact patch release.
How to reproduce
Minimal resource — a public getter returning the resource type, not a Doctrine
association: