Skip to content

Commit 96a2b2d

Browse files
authored
Merge pull request #2051 from sryabov/fix-existsfilter-onetoone-inverse
Fix ExistsFilter for inverse side of OneToOne association
2 parents 7c38133 + e166e1b commit 96a2b2d

File tree

10 files changed

+304
-4
lines changed

10 files changed

+304
-4
lines changed

features/main/content_negotiation.feature

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Feature: Content Negotiation support
1818
And the response should be equal to
1919
"""
2020
<?xml version="1.0"?>
21-
<response><description/><dummy/><dummyBoolean/><dummyDate/><dummyFloat/><dummyPrice/><relatedDummy/><relatedDummies/><jsonData/><arrayData/><name_converted/><id>1</id><name>XML!</name><alias/><foo/></response>
21+
<response><description/><dummy/><dummyBoolean/><dummyDate/><dummyFloat/><dummyPrice/><relatedDummy/><relatedDummies/><jsonData/><arrayData/><name_converted/><relatedOwnedDummy/><relatedOwningDummy/><id>1</id><name>XML!</name><alias/><foo/></response>
2222
"""
2323

2424
Scenario: Retrieve a collection in XML
@@ -29,7 +29,7 @@ Feature: Content Negotiation support
2929
And the response should be equal to
3030
"""
3131
<?xml version="1.0"?>
32-
<response><item key="0"><description/><dummy/><dummyBoolean/><dummyDate/><dummyFloat/><dummyPrice/><relatedDummy/><relatedDummies/><jsonData/><arrayData/><name_converted/><id>1</id><name>XML!</name><alias/><foo/></item></response>
32+
<response><item key="0"><description/><dummy/><dummyBoolean/><dummyDate/><dummyFloat/><dummyPrice/><relatedDummy/><relatedDummies/><jsonData/><arrayData/><name_converted/><relatedOwnedDummy/><relatedOwningDummy/><id>1</id><name>XML!</name><alias/><foo/></item></response>
3333
"""
3434

3535
Scenario: Retrieve a collection in XML using the .xml URL
@@ -39,7 +39,7 @@ Feature: Content Negotiation support
3939
And the response should be equal to
4040
"""
4141
<?xml version="1.0"?>
42-
<response><item key="0"><description/><dummy/><dummyBoolean/><dummyDate/><dummyFloat/><dummyPrice/><relatedDummy/><relatedDummies/><jsonData/><arrayData/><name_converted/><id>1</id><name>XML!</name><alias/><foo/></item></response>
42+
<response><item key="0"><description/><dummy/><dummyBoolean/><dummyDate/><dummyFloat/><dummyPrice/><relatedDummy/><relatedDummies/><jsonData/><arrayData/><name_converted/><relatedOwnedDummy/><relatedOwningDummy/><id>1</id><name>XML!</name><alias/><foo/></item></response>
4343
"""
4444

4545
Scenario: Retrieve a collection in JSON
@@ -63,6 +63,8 @@ Feature: Content Negotiation support
6363
"jsonData": [],
6464
"arrayData": [],
6565
"name_converted": null,
66+
"relatedOwnedDummy": null,
67+
"relatedOwningDummy": null,
6668
"id": 1,
6769
"name": "XML!",
6870
"alias": null,
@@ -83,7 +85,7 @@ Feature: Content Negotiation support
8385
And the response should be equal to
8486
"""
8587
<?xml version="1.0"?>
86-
<response><description/><dummy/><dummyBoolean/><dummyDate/><dummyFloat/><dummyPrice/><relatedDummy/><relatedDummies/><jsonData/><arrayData/><name_converted/><id>2</id><name>Sent in JSON</name><alias/><foo/></response>
88+
<response><description/><dummy/><dummyBoolean/><dummyDate/><dummyFloat/><dummyPrice/><relatedDummy/><relatedDummies/><jsonData/><arrayData/><name_converted/><relatedOwnedDummy/><relatedOwningDummy/><id>2</id><name>Sent in JSON</name><alias/><foo/></response>
8789
"""
8890

8991
Scenario: Requesting the same format in the Accept header and in the URL should work

features/main/crud.feature

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Feature: Create-Retrieve-Update-Delete
4444
},
4545
"arrayData": [],
4646
"name_converted": null,
47+
"relatedOwnedDummy": null,
48+
"relatedOwningDummy": null,
4749
"id": 1,
4850
"name": "My Dummy",
4951
"alias": null,
@@ -78,6 +80,8 @@ Feature: Create-Retrieve-Update-Delete
7880
},
7981
"arrayData": [],
8082
"name_converted": null,
83+
"relatedOwnedDummy": null,
84+
"relatedOwningDummy": null,
8185
"id": 1,
8286
"name": "My Dummy",
8387
"alias": null,
@@ -120,6 +124,8 @@ Feature: Create-Retrieve-Update-Delete
120124
},
121125
"arrayData": [],
122126
"name_converted": null,
127+
"relatedOwnedDummy": null,
128+
"relatedOwningDummy": null,
123129
"id": 1,
124130
"name": "My Dummy",
125131
"alias": null,
@@ -444,6 +450,8 @@ Feature: Create-Retrieve-Update-Delete
444450
],
445451
"arrayData": [],
446452
"name_converted": null,
453+
"relatedOwnedDummy": null,
454+
"relatedOwningDummy": null,
447455
"id": 1,
448456
"name": "A nice dummy",
449457
"alias": null,
@@ -481,6 +489,8 @@ Feature: Create-Retrieve-Update-Delete
481489
],
482490
"arrayData": [],
483491
"name_converted": null,
492+
"relatedOwnedDummy": null,
493+
"relatedOwningDummy": null,
484494
"id": 1,
485495
"name": "A nice dummy",
486496
"alias": null,

features/main/relation.feature

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ Feature: Relations support
162162
"jsonData": [],
163163
"arrayData": [],
164164
"name_converted": null,
165+
"relatedOwnedDummy": null,
166+
"relatedOwningDummy": null,
165167
"id": 1,
166168
"name": "Dummy with relations",
167169
"alias": null,
@@ -544,6 +546,8 @@ Feature: Relations support
544546
"jsonData":[],
545547
"arrayData":[],
546548
"name_converted":null,
549+
"relatedOwnedDummy": null,
550+
"relatedOwningDummy": null,
547551
"id":2,
548552
"name":"Dummy with plain relations",
549553
"alias":null,

features/security/strong_typing.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Feature: Handle properly invalid data submitted to the API
3333
"jsonData": [],
3434
"arrayData": [],
3535
"name_converted": null,
36+
"relatedOwnedDummy": null,
37+
"relatedOwningDummy": null,
3638
"id": 1,
3739
"name": "Not existing",
3840
"alias": null,

features/security/unknown_attributes.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Feature: Ignore unknown attributes
3434
"jsonData": [],
3535
"arrayData": [],
3636
"name_converted": null,
37+
"relatedOwnedDummy": null,
38+
"relatedOwningDummy": null,
3739
"id": 1,
3840
"name": "Not existing",
3941
"alias": null,

src/Bridge/Doctrine/Orm/Filter/ExistsFilter.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313

1414
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Filter;
1515

16+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
1617
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
1718
use ApiPlatform\Core\Exception\InvalidArgumentException;
1819
use Doctrine\ORM\Mapping\ClassMetadataInfo;
20+
use Doctrine\ORM\Query\Expr\Join;
1921
use Doctrine\ORM\QueryBuilder;
2022

2123
/**
@@ -110,6 +112,15 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
110112
return;
111113
}
112114

115+
if ($metadata->isAssociationInverseSide($field)) {
116+
$alias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $field, Join::LEFT_JOIN);
117+
118+
$queryBuilder
119+
->andWhere(sprintf('%s %s NULL', $alias, $value ? 'IS NOT' : 'IS'));
120+
121+
return;
122+
}
123+
113124
$queryBuilder
114125
->andWhere(sprintf('%s.%s %s NULL', $alias, $field, $value ? 'IS NOT' : 'IS'));
115126

tests/Bridge/Doctrine/Orm/Filter/ExistsFilterTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,54 @@ public function provideApplyTestData(): array
265265
],
266266
sprintf('SELECT o FROM %s o WHERE o.description IS NOT NULL AND o.relatedDummy IS NULL', Dummy::class),
267267
],
268+
269+
'related owned association does not exist' => [
270+
[
271+
'relatedOwnedDummy' => null,
272+
],
273+
[
274+
'relatedOwnedDummy' => [
275+
'exists' => '0',
276+
],
277+
],
278+
sprintf('SELECT o FROM %s o LEFT JOIN o.relatedOwnedDummy relatedOwnedDummy_a1 WHERE relatedOwnedDummy_a1 IS NULL', Dummy::class),
279+
],
280+
281+
'related owned association exists' => [
282+
[
283+
'relatedOwnedDummy' => null,
284+
],
285+
[
286+
'relatedOwnedDummy' => [
287+
'exists' => '1',
288+
],
289+
],
290+
sprintf('SELECT o FROM %s o LEFT JOIN o.relatedOwnedDummy relatedOwnedDummy_a1 WHERE relatedOwnedDummy_a1 IS NOT NULL', Dummy::class),
291+
],
292+
293+
'related owning association does not exist' => [
294+
[
295+
'relatedOwningDummy' => null,
296+
],
297+
[
298+
'relatedOwningDummy' => [
299+
'exists' => '0',
300+
],
301+
],
302+
sprintf('SELECT o FROM %s o WHERE o.relatedOwningDummy IS NULL', Dummy::class),
303+
],
304+
305+
'related owning association exists' => [
306+
[
307+
'relatedOwningDummy' => null,
308+
],
309+
[
310+
'relatedOwningDummy' => [
311+
'exists' => '1',
312+
],
313+
],
314+
sprintf('SELECT o FROM %s o WHERE o.relatedOwningDummy IS NOT NULL', Dummy::class),
315+
],
268316
];
269317
}
270318
}

tests/Fixtures/TestBundle/Entity/Dummy.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,20 @@ class Dummy
152152
*/
153153
public $nameConverted;
154154

155+
/**
156+
* @var RelatedOwnedDummy
157+
*
158+
* @ORM\OneToOne(targetEntity="RelatedOwnedDummy", cascade={"persist"}, mappedBy="owningDummy")
159+
*/
160+
public $relatedOwnedDummy;
161+
162+
/**
163+
* @var RelatedOwningDummy
164+
*
165+
* @ORM\OneToOne(targetEntity="RelatedOwningDummy", cascade={"persist"}, inversedBy="ownedDummy")
166+
*/
167+
public $relatedOwningDummy;
168+
155169
public static function staticMethod()
156170
{
157171
}
@@ -274,6 +288,30 @@ public function addRelatedDummy(RelatedDummy $relatedDummy)
274288
$this->relatedDummies->add($relatedDummy);
275289
}
276290

291+
public function getRelatedOwnedDummy()
292+
{
293+
return $this->relatedOwnedDummy;
294+
}
295+
296+
public function setRelatedOwnedDummy(RelatedOwnedDummy $relatedOwnedDummy)
297+
{
298+
$this->relatedOwnedDummy = $relatedOwnedDummy;
299+
300+
if ($this !== $this->relatedOwnedDummy->getOwningDummy()) {
301+
$this->relatedOwnedDummy->setOwningDummy($this);
302+
}
303+
}
304+
305+
public function getRelatedOwningDummy()
306+
{
307+
return $this->relatedOwningDummy;
308+
}
309+
310+
public function setRelatedOwningDummy(RelatedOwningDummy $relatedOwningDummy)
311+
{
312+
$this->relatedOwningDummy = $relatedOwningDummy;
313+
}
314+
277315
/**
278316
* @return bool
279317
*/
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Core\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiResource;
17+
use Doctrine\ORM\Mapping as ORM;
18+
19+
/**
20+
* Related Owned Dummy.
21+
*
22+
* @author Sergey V. Ryabov <[email protected]>
23+
*
24+
* @ApiResource(iri="https://schema.org/Product")
25+
* @ORM\Entity
26+
*/
27+
class RelatedOwnedDummy
28+
{
29+
/**
30+
* @ORM\Column(type="integer")
31+
* @ORM\Id
32+
* @ORM\GeneratedValue(strategy="AUTO")
33+
*/
34+
private $id;
35+
36+
/**
37+
* @var string A name
38+
*
39+
* @ORM\Column(nullable=true)
40+
*/
41+
public $name;
42+
43+
/**
44+
* @var Dummy
45+
*
46+
* @ORM\OneToOne(targetEntity="Dummy", cascade={"persist"}, inversedBy="relatedOwnedDummy")
47+
* @ORM\JoinColumn(nullable=false)
48+
*/
49+
public $owningDummy;
50+
51+
public function getId()
52+
{
53+
return $this->id;
54+
}
55+
56+
public function setId($id)
57+
{
58+
$this->id = $id;
59+
}
60+
61+
public function setName($name)
62+
{
63+
$this->name = $name;
64+
}
65+
66+
public function getName()
67+
{
68+
return $this->name;
69+
}
70+
71+
/**
72+
* Get owning dummy.
73+
*
74+
* @return Dummy
75+
*/
76+
public function getOwningDummy()
77+
{
78+
return $this->owningDummy;
79+
}
80+
81+
/**
82+
* Set owning dummy.
83+
*
84+
* @param Dummy $owningDummy the value to set
85+
*/
86+
public function setOwningDummy(Dummy $owningDummy)
87+
{
88+
$this->owningDummy = $owningDummy;
89+
}
90+
}

0 commit comments

Comments
 (0)