Skip to content

ExactFilter not working with Doctrine embeddables #8390

Description

@emmanuel-nz

API Platform version(s) affected: 4.3.3

Description
When using the ExactFilter on a nested entity containing a Doctrine embeddable, to filter and only get records matching one of the embeddable properties, I've got the following error: [Semantical Error] [...] Error: Class [...] has no field or association named [...].

For example:
I've got a Foo entity with a $bars property which is a 1-n association with a Bar entity.
The Bar entity has a Baz embeddable, which "groups" several properties, like a $qux property.

I want Foo records where qux is equal to 1.
The url will look like this: /foos?bars.baz.qux=1.
If I hadn't an embeddable, and my entity was "flat", it would be equivalent to this: /foos?bars.bazQux=1.

When I query /foos?bars.baz.qux=1, I've got the following error (which is thrown by Doctrine btw):
[Semantical Error] line 0, col 1210 near 'qux = :bars_baz_qux_p3)': Error: Class Foo has no field or association named bars.baz.qux.

When I looked at the generated DQL, I noticed it uses the wrong alias: it uses the alias of the Foo entity instead of the Bar entity.
That's why Doctrine raises an exception, it looks for a bars.baz.qux field on the Foo entity, instead of looking for a baz.qux field on the Bar entity.

SELECT o_a3
FROM Foo o_a3
INNER JOIN o_a3.bars bars_a4
WHERE o_a3.bars.baz.qux = :bars_baz_qux_p3

Here, instead of o_a3.bars.baz.qux, it should be bars_a4.baz.qux.

How to reproduce
The embeddable:

<?php

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Embeddable]
class Baz
{
    #[ORM\Column(nullable: true)]
    public ?int $qux = null;

    #[ORM\Column(nullable: true)]
    public ?string $anotherProp = null;
}

The nested entity:

<?php

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;

#[ORM\Entity]
class Bar
{
    #[ORM\Id]
    #[ORM\Column(type: 'uuid', unique: true)]
    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
    public ?Uuid $id = null;

    #[ORM\Embedded(class: Baz::class)]
    public Baz $baz;
}

The main resource/entity:

<?php

use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;

#[ORM\Entity]
#[ApiResource(
    operations: [
        new GetCollection(
            parameters: [
                'bar.baz.qux' => new QueryParameter(
                    filter: new ExactFilter(),
                ),
            ],
        )
    ]
)]
class Foo
{
    #[ORM\Id]
    #[ORM\Column(type: 'uuid', unique: true)]
    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
    public ?Uuid $id = null;

    /** @var Collection<int, Bar> */
    #[ORM\OneToMany(targetEntity: Bar::class)]
    public Collection $bars;

    public function __construct()
    {
        $this->bars = new ArrayCollection();
    }
}

Possible Solution
The problem probably lies in the lines 57 & 58 of the ApiPlatform\Doctrine\Orm\Filter\ExactFilter class.

$queryBuilder
    ->{$context['whereClause'] ?? 'andWhere'}(\sprintf('%s.%s %s :%s', $alias, $property, $operator, $parameterName));

I guess one solution could be to read the doctrine metadata to detect whether the property is part of an embeddable, get the name of the parent entity, and figure out the alias used in the JOIN.
Easy to say I guess...

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions