Skip to content

Commit cdd1213

Browse files
committed
fix(doctrine): use PHP property name in DQL for modern filters with name converter
Modern parameter filters (ExactFilter, SortFilter, ComparisonFilter, PartialSearchFilter) used Parameter::getProperty() verbatim to build DQL/aggregation queries. When a name converter is configured, ParameterResourceMetadataCollectionFactory::setDefaults() normalizes that property (e.g. createdAt -> created_at) for the public/OpenAPI name, but Doctrine still knows the entity field by its PHP name, causing a 500 "has no field or association named created_at" for any multi-word property. Preserve the original PHP property name as a `query_property` extra property before normalizing, and prefer it over the converted property in both NestedPropertyHelperTrait implementations (ORM and ODM), which is the single chokepoint every modern filter routes through for flat (non-nested) properties. Nested properties already skip normalization via nested_properties_info and are unaffected. Follow-up to #7877, which fixed the equivalent issue for legacy AbstractFilter-based filters. Fixes #8380
1 parent f9d3706 commit cdd1213

6 files changed

Lines changed: 206 additions & 3 deletions

File tree

src/Doctrine/Odm/NestedPropertyHelperTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ protected function addNestedParameterLookups(string $property, Builder $aggregat
3939
$nestedInfo = ($extraProperties['nested_properties_info'] ?? []) ? reset($extraProperties['nested_properties_info']) : null;
4040

4141
if (!$nestedInfo) {
42-
return $property;
42+
return $extraProperties['query_property'] ?? $property;
4343
}
4444

4545
$odmSegments = $nestedInfo['odm_segments'] ?? [];

src/Doctrine/Orm/NestedPropertyHelperTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ protected function addNestedParameterJoins(
4343
$nestedInfo = ($extraProperties['nested_properties_info'] ?? []) ? reset($extraProperties['nested_properties_info']) : null;
4444

4545
if (!$nestedInfo) {
46-
return [$alias, $property];
46+
return [$alias, $extraProperties['query_property'] ?? $property];
4747
}
4848

4949
$relationClasses = $nestedInfo['relation_classes'] ?? [];

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,10 @@ private function setDefaults(string $key, Parameter $parameter, ?object $filter,
405405
// Skip name conversion if we already have nested property info
406406
$paramExtraProps = $parameter->getExtraProperties();
407407
if (!isset($paramExtraProps['nested_properties_info'])) {
408-
$parameter = $parameter->withProperty($this->nameConverter->normalize($property));
408+
// Keep the original (non name-converted) property to build the query while the public property matches the API naming convention.
409+
$parameter = $parameter
410+
->withExtraProperties([...$paramExtraProps, 'query_property' => $property])
411+
->withProperty($this->nameConverter->normalize($property));
409412
}
410413
}
411414

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Document;
15+
16+
use ApiPlatform\Doctrine\Odm\Filter\ExactFilter;
17+
use ApiPlatform\Doctrine\Odm\Filter\SortFilter;
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Metadata\GetCollection;
20+
use ApiPlatform\Metadata\QueryParameter;
21+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
22+
23+
/**
24+
* Tests that modern parameter filters (ExactFilter, SortFilter) work with
25+
* QueryParameter on a multi-word property when a nameConverter is configured
26+
* (issue #8380).
27+
*/
28+
#[ApiResource(
29+
operations: [
30+
new GetCollection(
31+
parameters: [
32+
'nameConverted' => new QueryParameter(
33+
filter: new ExactFilter(),
34+
property: 'nameConverted',
35+
),
36+
'orderNameConverted' => new QueryParameter(
37+
filter: new SortFilter(),
38+
property: 'nameConverted',
39+
),
40+
],
41+
),
42+
],
43+
)]
44+
#[ODM\Document]
45+
class ConvertedFilterParameter
46+
{
47+
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
48+
private ?int $id = null;
49+
50+
#[ODM\Field(type: 'string')]
51+
public string $nameConverted = '';
52+
53+
public function getId(): ?int
54+
{
55+
return $this->id;
56+
}
57+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
17+
use ApiPlatform\Doctrine\Orm\Filter\SortFilter;
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Metadata\GetCollection;
20+
use ApiPlatform\Metadata\QueryParameter;
21+
use Doctrine\ORM\Mapping as ORM;
22+
23+
/**
24+
* Tests that modern parameter filters (ExactFilter, SortFilter) work with
25+
* QueryParameter on a multi-word property when a nameConverter is configured
26+
* (issue #8380).
27+
*/
28+
#[ApiResource(
29+
operations: [
30+
new GetCollection(
31+
parameters: [
32+
'nameConverted' => new QueryParameter(
33+
filter: new ExactFilter(),
34+
property: 'nameConverted',
35+
),
36+
'orderNameConverted' => new QueryParameter(
37+
filter: new SortFilter(),
38+
property: 'nameConverted',
39+
),
40+
],
41+
),
42+
],
43+
)]
44+
#[ORM\Entity]
45+
class ConvertedFilterParameter
46+
{
47+
#[ORM\Column(type: 'integer', nullable: true)]
48+
#[ORM\Id]
49+
#[ORM\GeneratedValue(strategy: 'AUTO')]
50+
private ?int $id = null;
51+
52+
#[ORM\Column(type: 'string')]
53+
public string $nameConverted = '';
54+
55+
public function getId(): ?int
56+
{
57+
return $this->id;
58+
}
59+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Functional\Parameters;
15+
16+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedFilterParameter as ConvertedFilterParameterDocument;
18+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedFilterParameter;
19+
use ApiPlatform\Tests\RecreateSchemaTrait;
20+
use ApiPlatform\Tests\SetupClassResourcesTrait;
21+
22+
/**
23+
* Tests that modern parameter filters (ExactFilter, SortFilter) work with
24+
* QueryParameter on a multi-word property when a nameConverter is configured.
25+
*
26+
* @see https://github.com/api-platform/core/issues/8380
27+
*/
28+
final class NameConverterModernFilterTest extends ApiTestCase
29+
{
30+
use RecreateSchemaTrait;
31+
use SetupClassResourcesTrait;
32+
33+
protected static ?bool $alwaysBootKernel = false;
34+
35+
/**
36+
* @return class-string[]
37+
*/
38+
public static function getResources(): array
39+
{
40+
return [ConvertedFilterParameter::class];
41+
}
42+
43+
protected function setUp(): void
44+
{
45+
$entityClass = $this->isMongoDB() ? ConvertedFilterParameterDocument::class : ConvertedFilterParameter::class;
46+
47+
$this->recreateSchema([$entityClass]);
48+
$this->loadFixtures($entityClass);
49+
}
50+
51+
public function testExactFilterWithNameConverter(): void
52+
{
53+
$response = self::createClient()->request('GET', '/converted_filter_parameters?nameConverted=bar');
54+
$this->assertResponseIsSuccessful();
55+
$members = $response->toArray()['hydra:member'];
56+
$this->assertCount(1, $members);
57+
$this->assertSame('bar', $members[0]['name_converted']);
58+
}
59+
60+
public function testSortFilterWithNameConverter(): void
61+
{
62+
$response = self::createClient()->request('GET', '/converted_filter_parameters?orderNameConverted=desc');
63+
$this->assertResponseIsSuccessful();
64+
$members = $response->toArray()['hydra:member'];
65+
$names = array_map(static fn (array $m): string => $m['name_converted'], $members);
66+
$this->assertSame(['foo', 'baz', 'bar'], $names);
67+
}
68+
69+
/**
70+
* @param class-string $entityClass
71+
*/
72+
private function loadFixtures(string $entityClass): void
73+
{
74+
$manager = $this->getManager();
75+
76+
foreach (['bar', 'baz', 'foo'] as $name) {
77+
$entity = new $entityClass();
78+
$entity->nameConverted = $name;
79+
$manager->persist($entity);
80+
}
81+
82+
$manager->flush();
83+
}
84+
}

0 commit comments

Comments
 (0)