Skip to content

Commit cc87e60

Browse files
HugoSEIGLEKocal
authored andcommitted
[Autocomplete] Fix handling of associated properties in DQL joins
1 parent 89c7fa9 commit cc87e60

File tree

9 files changed

+411
-2
lines changed

9 files changed

+411
-2
lines changed

src/Autocomplete/src/Doctrine/EntitySearchUtil.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public function addSearchClause(QueryBuilder $queryBuilder, string $query, strin
4747
];
4848

4949
$entitiesAlreadyJoined = [];
50+
$aliasAlreadyUsed = [];
5051
$searchableProperties = empty($searchableProperties) ? $entityMetadata->getAllPropertyNames() : $searchableProperties;
5152
$expressions = [];
5253
foreach ($searchableProperties as $propertyName) {
@@ -68,10 +69,18 @@ public function addSearchClause(QueryBuilder $queryBuilder, string $query, strin
6869
$associatedEntityAlias = SearchEscaper::escapeDqlAlias($associatedEntityName);
6970
$associatedPropertyName = $associatedProperties[$i + 1];
7071

71-
if (!\in_array($associatedEntityName, $entitiesAlreadyJoined, true)) {
72+
$associatedParentName = null;
73+
if (\array_key_exists($i - 1, $associatedProperties) && $queryBuilder->getRootAliases()[0] !== $associatedProperties[$i - 1]) {
74+
$associatedParentName = $associatedProperties[$i - 1];
75+
}
76+
77+
$associatedEntityAlias = $associatedParentName ? $associatedParentName.'_'.$associatedEntityAlias : $associatedEntityAlias;
78+
79+
if (!\in_array($associatedEntityName, $entitiesAlreadyJoined, true) || !\in_array($associatedEntityAlias, $aliasAlreadyUsed, true)) {
7280
$parentEntityName = 0 === $i ? $queryBuilder->getRootAliases()[0] : $associatedProperties[$i - 1];
7381
$queryBuilder->leftJoin($parentEntityName.'.'.$associatedEntityName, $associatedEntityAlias);
7482
$entitiesAlreadyJoined[] = $associatedEntityName;
83+
$aliasAlreadyUsed[] = $associatedEntityAlias;
7584
}
7685

7786
if ($i < $numAssociatedProperties - 2) {

src/Autocomplete/tests/Fixtures/Entity/Category.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@ class Category
3232
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Product::class)]
3333
private Collection $products;
3434

35+
#[ORM\ManyToMany(targetEntity: CategoryTag::class, mappedBy: 'categories')]
36+
private Collection $tags;
37+
3538
public function __construct()
3639
{
3740
$this->products = new ArrayCollection();
41+
$this->tags = new ArrayCollection();
3842
}
3943

4044
public function getId(): ?int
@@ -96,6 +100,31 @@ public function removeProduct(Product $product): self
96100
return $this;
97101
}
98102

103+
/**
104+
* @return Collection<int, CategoryTag>
105+
*/
106+
public function getTags(): Collection
107+
{
108+
return $this->tags;
109+
}
110+
111+
public function addTag(CategoryTag $tag): self
112+
{
113+
if (!$this->tags->contains($tag)) {
114+
$this->tags[] = $tag;
115+
$tag->addCategory($this);
116+
}
117+
118+
return $this;
119+
}
120+
121+
public function removeTag(CategoryTag $tag): self
122+
{
123+
$this->tags->removeElement($tag);
124+
125+
return $this;
126+
}
127+
99128
public function __toString(): string
100129
{
101130
return $this->getName();
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\UX\Autocomplete\Tests\Fixtures\Entity;
13+
14+
use Doctrine\Common\Collections\ArrayCollection;
15+
use Doctrine\Common\Collections\Collection;
16+
use Doctrine\ORM\Mapping as ORM;
17+
18+
#[ORM\Entity()]
19+
class CategoryTag
20+
{
21+
#[ORM\Id]
22+
#[ORM\GeneratedValue]
23+
#[ORM\Column()]
24+
private ?int $id = null;
25+
26+
#[ORM\Column()]
27+
private ?string $name = null;
28+
29+
#[ORM\ManyToMany(targetEntity: Category::class, inversedBy: 'tags')]
30+
#[ORM\JoinTable(name: 'category_tag')]
31+
private Collection $categories;
32+
33+
public function __construct()
34+
{
35+
$this->categories = new ArrayCollection();
36+
}
37+
38+
public function getId(): ?int
39+
{
40+
return $this->id;
41+
}
42+
43+
public function getName(): ?string
44+
{
45+
return $this->name;
46+
}
47+
48+
public function setName(string $name): self
49+
{
50+
$this->name = $name;
51+
52+
return $this;
53+
}
54+
55+
/**
56+
* @return Collection<int, Category>
57+
*/
58+
public function getCategories(): Collection
59+
{
60+
return $this->categories;
61+
}
62+
63+
public function addCategory(Category $category): self
64+
{
65+
if (!$this->categories->contains($category)) {
66+
$this->categories[] = $category;
67+
}
68+
69+
return $this;
70+
}
71+
72+
public function removeCategory(Category $category): self
73+
{
74+
$this->categories->removeElement($category);
75+
76+
return $this;
77+
}
78+
}

src/Autocomplete/tests/Fixtures/Entity/Product.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,16 @@ class Product
4040
#[ORM\JoinColumn(nullable: false)]
4141
private ?Category $category = null;
4242

43-
#[Orm\OneToMany(targetEntity: Ingredient::class, mappedBy: 'product')]
43+
#[ORM\OneToMany(targetEntity: Ingredient::class, mappedBy: 'product')]
4444
private Collection $ingredients;
4545

46+
#[ORM\ManyToMany(targetEntity: ProductTag::class, mappedBy: 'products')]
47+
private Collection $tags;
48+
4649
public function __construct()
4750
{
4851
$this->ingredients = new ArrayCollection();
52+
$this->tags = new ArrayCollection();
4953
}
5054

5155
public function getId(): ?int
@@ -142,4 +146,31 @@ public function removeIngredient(Ingredient $ingredient): self
142146

143147
return $this;
144148
}
149+
150+
/**
151+
* @return Collection<int, ProductTag>
152+
*/
153+
public function getTags(): Collection
154+
{
155+
return $this->tags;
156+
}
157+
158+
public function addTag(ProductTag $tag): self
159+
{
160+
if (!$this->tags->contains($tag)) {
161+
$this->tags[] = $tag;
162+
$tag->addProduct($this);
163+
}
164+
165+
return $this;
166+
}
167+
168+
public function removeTag(ProductTag $tag): self
169+
{
170+
if ($this->tags->removeElement($tag)) {
171+
$tag->removeProduct($this);
172+
}
173+
174+
return $this;
175+
}
145176
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\UX\Autocomplete\Tests\Fixtures\Entity;
13+
14+
use Doctrine\Common\Collections\ArrayCollection;
15+
use Doctrine\Common\Collections\Collection;
16+
use Doctrine\ORM\Mapping as ORM;
17+
18+
#[ORM\Entity()]
19+
class ProductTag
20+
{
21+
#[ORM\Id]
22+
#[ORM\GeneratedValue]
23+
#[ORM\Column()]
24+
private ?int $id = null;
25+
26+
#[ORM\Column()]
27+
private ?string $name = null;
28+
29+
#[ORM\ManyToMany(targetEntity: Product::class, inversedBy: 'tags')]
30+
#[ORM\JoinTable(name: 'product_tag')]
31+
private Collection $products;
32+
33+
public function __construct()
34+
{
35+
$this->products = new ArrayCollection();
36+
}
37+
38+
public function getId(): ?int
39+
{
40+
return $this->id;
41+
}
42+
43+
public function getName(): ?string
44+
{
45+
return $this->name;
46+
}
47+
48+
public function setName(string $name): self
49+
{
50+
$this->name = $name;
51+
52+
return $this;
53+
}
54+
55+
/**
56+
* @return Collection<int, Product>
57+
*/
58+
public function getProducts(): Collection
59+
{
60+
return $this->products;
61+
}
62+
63+
public function addProduct(Product $product): self
64+
{
65+
if (!$this->products->contains($product)) {
66+
$this->products[] = $product;
67+
}
68+
69+
return $this;
70+
}
71+
72+
public function removeProduct(Product $product): self
73+
{
74+
$this->products->removeElement($product);
75+
76+
return $this;
77+
}
78+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\UX\Autocomplete\Tests\Fixtures\Factory;
13+
14+
use Doctrine\ORM\EntityRepository;
15+
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\CategoryTag;
16+
use Zenstruck\Foundry\ModelFactory;
17+
use Zenstruck\Foundry\Proxy;
18+
use Zenstruck\Foundry\RepositoryProxy;
19+
20+
/**
21+
* @extends ModelFactory<CategoryTag>
22+
*
23+
* @method static CategoryTag|Proxy createOne(array $attributes = [])
24+
* @method static CategoryTag[]|Proxy[] createMany(int $number, array|callable $attributes = [])
25+
* @method static CategoryTag|Proxy find(object|array|mixed $criteria)
26+
* @method static CategoryTag|Proxy findOrCreate(array $attributes)
27+
* @method static CategoryTag|Proxy first(string $sortedField = 'id')
28+
* @method static CategoryTag|Proxy last(string $sortedField = 'id')
29+
* @method static CategoryTag|Proxy random(array $attributes = [])
30+
* @method static CategoryTag|Proxy randomOrCreate(array $attributes = [])
31+
* @method static CategoryTag[]|Proxy[] all()
32+
* @method static CategoryTag[]|Proxy[] findBy(array $attributes)
33+
* @method static CategoryTag[]|Proxy[] randomSet(int $number, array $attributes = [])
34+
* @method static CategoryTag[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
35+
* @method static EntityRepository|RepositoryProxy repository()
36+
* @method CategoryTag|Proxy create(array|callable $attributes = [])
37+
*/
38+
final class CategoryTagFactory extends ModelFactory
39+
{
40+
protected function getDefaults(): array
41+
{
42+
return [
43+
'name' => self::faker()->word(),
44+
];
45+
}
46+
47+
protected function initialize(): self
48+
{
49+
return $this;
50+
}
51+
52+
protected static function getClass(): string
53+
{
54+
return CategoryTag::class;
55+
}
56+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\UX\Autocomplete\Tests\Fixtures\Factory;
13+
14+
use Doctrine\ORM\EntityRepository;
15+
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\ProductTag;
16+
use Zenstruck\Foundry\ModelFactory;
17+
use Zenstruck\Foundry\Proxy;
18+
use Zenstruck\Foundry\RepositoryProxy;
19+
20+
/**
21+
* @extends ModelFactory<ProductTag>
22+
*
23+
* @method static ProductTag|Proxy createOne(array $attributes = [])
24+
* @method static ProductTag[]|Proxy[] createMany(int $number, array|callable $attributes = [])
25+
* @method static ProductTag|Proxy find(object|array|mixed $criteria)
26+
* @method static ProductTag|Proxy findOrCreate(array $attributes)
27+
* @method static ProductTag|Proxy first(string $sortedField = 'id')
28+
* @method static ProductTag|Proxy last(string $sortedField = 'id')
29+
* @method static ProductTag|Proxy random(array $attributes = [])
30+
* @method static ProductTag|Proxy randomOrCreate(array $attributes = [])
31+
* @method static ProductTag[]|Proxy[] all()
32+
* @method static ProductTag[]|Proxy[] findBy(array $attributes)
33+
* @method static ProductTag[]|Proxy[] randomSet(int $number, array $attributes = [])
34+
* @method static ProductTag[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
35+
* @method static EntityRepository|RepositoryProxy repository()
36+
* @method ProductTag|Proxy create(array|callable $attributes = [])
37+
*/
38+
final class ProductTagFactory extends ModelFactory
39+
{
40+
protected function getDefaults(): array
41+
{
42+
return [
43+
'name' => self::faker()->word(),
44+
];
45+
}
46+
47+
protected function initialize(): self
48+
{
49+
return $this;
50+
}
51+
52+
protected static function getClass(): string
53+
{
54+
return ProductTag::class;
55+
}
56+
}

0 commit comments

Comments
 (0)