@ -0,0 +1,118 @@
API Platform version(s) affected: 4.3.3
Description
When using stateOptions with entityClass to map an API Resource to a Doctrine entity, nested property filters (PartialSearchFilter, OrFilter, FreeTextQueryFilter) fail to generate JOINs for relations that point to an entity which is not directly declared as an #[ApiResource].
The root cause is in ParameterResourceMetadataCollectionFactory::getProperties(). When traversing a dotted property path (e.g. user.name), the method calls getClassNameFromProperty() which internally calls isResourceClass() on the relation's target class. If the target entity class is not registered as an API resource (even if another API Resource references it via stateOptions), isResourceClass() returns false, getClassNameFromProperty() returns null, and the traversal stops — meaning nested_properties_info is never populated.
Without nested_properties_info, the NestedPropertyHelperTrait::addNestedParameterJoins() returns early without adding any JOIN, producing invalid DQL like WHERE LOWER(o.user.name) LIKE ... instead of JOIN o.user u WHERE LOWER(u.name) LIKE ....
How to reproduce
// Entity (not declared as ApiResource)
#[ORM\Entity]
class User implements UserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $email = null;
// getters/setters...
}
// Entity with relation to User
#[ORM\Entity]
class UserAction
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 50)]
private string $action;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
private User $user;
// getters/setters...
}
// API Resource using stateOptions
#[ApiResource(
operations: [
new GetCollection(
uriTemplate: '/user-actions',
parameters: [
'user' => new QueryParameter(
filter: new OrFilter(new PartialSearchFilter()),
properties: ['user.name', 'user.email'],
),
],
),
],
stateOptions: new Options(entityClass: UserAction::class),
)]
class UserActionResource
{
public ?int $id = null;
public ?string $action = null;
// ...
}
// Separate API Resource for User exists with stateOptions
#[ApiResource(
stateOptions: new Options(entityClass: User::class),
)]
class UserResource
{
public ?int $id = null;
public ?string $name = null;
// ...
}
Requesting GET /user-actions?user=john produces:
[Semantical Error] Class UserAction has no field or association named user.name
Generated DQL:
SELECT o FROM UserAction o WHERE LOWER(o.user.name) LIKE LOWER(:p1)
Expected DQL:
SELECT o FROM UserAction o JOIN o.user u WHERE LOWER(u.name) LIKE LOWER(:p1)
Workaround
Add #[ApiResource(operations: [])] on the target entity to register it as a resource class without exposing any routes:
#[ApiResource(operations: [])]
#[ORM\Entity]
class User implements UserInterface
{
// ...
}
Additional Context
- The
filterClass parameter on QueryParameter does not help because it only affects the starting class of the traversal, not the isResourceClass check on the relation target.
- This issue affects all nested property filters:
PartialSearchFilter, ExactFilter, OrFilter, FreeTextQueryFilter.
- Discovered while using the ObjectMapper pattern (separate API Resource class +
stateOptions(entityClass: ...)) which is the recommended approach in API Platform 4.x.
@ -0,0 +1,118 @@
API Platform version(s) affected: 4.3.3
Description
When using
stateOptionswithentityClassto map an API Resource to a Doctrine entity, nested property filters (PartialSearchFilter,OrFilter,FreeTextQueryFilter) fail to generate JOINs for relations that point to an entity which is not directly declared as an#[ApiResource].The root cause is in
ParameterResourceMetadataCollectionFactory::getProperties(). When traversing a dotted property path (e.g.user.name), the method callsgetClassNameFromProperty()which internally callsisResourceClass()on the relation's target class. If the target entity class is not registered as an API resource (even if another API Resource references it viastateOptions),isResourceClass()returnsfalse,getClassNameFromProperty()returnsnull, and the traversal stops — meaningnested_properties_infois never populated.Without
nested_properties_info, theNestedPropertyHelperTrait::addNestedParameterJoins()returns early without adding any JOIN, producing invalid DQL likeWHERE LOWER(o.user.name) LIKE ...instead ofJOIN o.user u WHERE LOWER(u.name) LIKE ....How to reproduce
Requesting
GET /user-actions?user=johnproduces:Generated DQL:
Expected DQL:
Workaround
Add
#[ApiResource(operations: [])]on the target entity to register it as a resource class without exposing any routes:Additional Context
filterClassparameter onQueryParameterdoes not help because it only affects the starting class of the traversal, not theisResourceClasscheck on the relation target.PartialSearchFilter,ExactFilter,OrFilter,FreeTextQueryFilter.stateOptions(entityClass: ...)) which is the recommended approach in API Platform 4.x.