Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Autocomplete] Fix handling of associated properties in DQL joins #2377

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/Autocomplete/src/Doctrine/EntitySearchUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function addSearchClause(QueryBuilder $queryBuilder, string $query, strin
];

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

if (!\in_array($associatedEntityName, $entitiesAlreadyJoined, true)) {
$associatedParentName = null;
if (\array_key_exists($i - 1, $associatedProperties) && $queryBuilder->getRootAliases()[0] !== $associatedProperties[$i - 1]) {
$associatedParentName = $associatedProperties[$i - 1];
}

$associatedEntityAlias = $associatedParentName ? $associatedParentName.'_'.$associatedEntityAlias : $associatedEntityAlias;

if (!\in_array($associatedEntityName, $entitiesAlreadyJoined, true) || !\in_array($associatedEntityAlias, $aliasAlreadyUsed, true)) {
$parentEntityName = 0 === $i ? $queryBuilder->getRootAliases()[0] : $associatedProperties[$i - 1];
$queryBuilder->leftJoin($parentEntityName.'.'.$associatedEntityName, $associatedEntityAlias);
$entitiesAlreadyJoined[] = $associatedEntityName;
$aliasAlreadyUsed[] = $associatedEntityAlias;
}

if ($i < $numAssociatedProperties - 2) {
Expand Down
29 changes: 29 additions & 0 deletions src/Autocomplete/tests/Fixtures/Entity/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ class Category
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Product::class)]
private Collection $products;

#[ORM\ManyToMany(targetEntity: CategoryTag::class, mappedBy: 'categories')]
private Collection $tags;

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

public function getId(): ?int
Expand Down Expand Up @@ -96,6 +100,31 @@ public function removeProduct(Product $product): self
return $this;
}

/**
* @return Collection<int, CategoryTag>
*/
public function getTags(): Collection
{
return $this->tags;
}

public function addTag(CategoryTag $tag): self
{
if (!$this->tags->contains($tag)) {
$this->tags[] = $tag;
$tag->addCategory($this);
}

return $this;
}

public function removeTag(CategoryTag $tag): self
{
$this->tags->removeElement($tag);

return $this;
}

public function __toString(): string
{
return $this->getName();
Expand Down
78 changes: 78 additions & 0 deletions src/Autocomplete/tests/Fixtures/Entity/CategoryTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Autocomplete\Tests\Fixtures\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity()]
class CategoryTag
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column()]
private ?int $id = null;

#[ORM\Column()]
private ?string $name = null;

#[ORM\ManyToMany(targetEntity: Category::class, inversedBy: 'tags')]
#[ORM\JoinTable(name: 'category_tag')]
private Collection $categories;

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

public function getId(): ?int
{
return $this->id;
}

public function getName(): ?string
{
return $this->name;
}

public function setName(string $name): self
{
$this->name = $name;

return $this;
}

/**
* @return Collection<int, Category>
*/
public function getCategories(): Collection
{
return $this->categories;
}

public function addCategory(Category $category): self
{
if (!$this->categories->contains($category)) {
$this->categories[] = $category;
}

return $this;
}

public function removeCategory(Category $category): self
{
$this->categories->removeElement($category);

return $this;
}
}
33 changes: 32 additions & 1 deletion src/Autocomplete/tests/Fixtures/Entity/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ class Product
#[ORM\JoinColumn(nullable: false)]
private ?Category $category = null;

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

#[ORM\ManyToMany(targetEntity: ProductTag::class, mappedBy: 'products')]
private Collection $tags;

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

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

return $this;
}

/**
* @return Collection<int, ProductTag>
*/
public function getTags(): Collection
{
return $this->tags;
}

public function addTag(ProductTag $tag): self
{
if (!$this->tags->contains($tag)) {
$this->tags[] = $tag;
$tag->addProduct($this);
}

return $this;
}

public function removeTag(ProductTag $tag): self
{
if ($this->tags->removeElement($tag)) {
$tag->removeProduct($this);
}

return $this;
}
}
78 changes: 78 additions & 0 deletions src/Autocomplete/tests/Fixtures/Entity/ProductTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Autocomplete\Tests\Fixtures\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity()]
class ProductTag
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column()]
private ?int $id = null;

#[ORM\Column()]
private ?string $name = null;

#[ORM\ManyToMany(targetEntity: Product::class, inversedBy: 'tags')]
#[ORM\JoinTable(name: 'product_tag')]
private Collection $products;

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

public function getId(): ?int
{
return $this->id;
}

public function getName(): ?string
{
return $this->name;
}

public function setName(string $name): self
{
$this->name = $name;

return $this;
}

/**
* @return Collection<int, Product>
*/
public function getProducts(): Collection
{
return $this->products;
}

public function addProduct(Product $product): self
{
if (!$this->products->contains($product)) {
$this->products[] = $product;
}

return $this;
}

public function removeProduct(Product $product): self
{
$this->products->removeElement($product);

return $this;
}
}
56 changes: 56 additions & 0 deletions src/Autocomplete/tests/Fixtures/Factory/CategoryTagFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Autocomplete\Tests\Fixtures\Factory;

use Doctrine\ORM\EntityRepository;
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\CategoryTag;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
use Zenstruck\Foundry\RepositoryProxy;

/**
* @extends ModelFactory<CategoryTag>
*
* @method static CategoryTag|Proxy createOne(array $attributes = [])
* @method static CategoryTag[]|Proxy[] createMany(int $number, array|callable $attributes = [])
* @method static CategoryTag|Proxy find(object|array|mixed $criteria)
* @method static CategoryTag|Proxy findOrCreate(array $attributes)
* @method static CategoryTag|Proxy first(string $sortedField = 'id')
* @method static CategoryTag|Proxy last(string $sortedField = 'id')
* @method static CategoryTag|Proxy random(array $attributes = [])
* @method static CategoryTag|Proxy randomOrCreate(array $attributes = [])
* @method static CategoryTag[]|Proxy[] all()
* @method static CategoryTag[]|Proxy[] findBy(array $attributes)
* @method static CategoryTag[]|Proxy[] randomSet(int $number, array $attributes = [])
* @method static CategoryTag[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
* @method static EntityRepository|RepositoryProxy repository()
* @method CategoryTag|Proxy create(array|callable $attributes = [])
*/
final class CategoryTagFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
'name' => self::faker()->word(),
];
}

protected function initialize(): self
{
return $this;
}

protected static function getClass(): string
{
return CategoryTag::class;
}
}
56 changes: 56 additions & 0 deletions src/Autocomplete/tests/Fixtures/Factory/ProductTagFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Autocomplete\Tests\Fixtures\Factory;

use Doctrine\ORM\EntityRepository;
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\ProductTag;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
use Zenstruck\Foundry\RepositoryProxy;

/**
* @extends ModelFactory<ProductTag>
*
* @method static ProductTag|Proxy createOne(array $attributes = [])
* @method static ProductTag[]|Proxy[] createMany(int $number, array|callable $attributes = [])
* @method static ProductTag|Proxy find(object|array|mixed $criteria)
* @method static ProductTag|Proxy findOrCreate(array $attributes)
* @method static ProductTag|Proxy first(string $sortedField = 'id')
* @method static ProductTag|Proxy last(string $sortedField = 'id')
* @method static ProductTag|Proxy random(array $attributes = [])
* @method static ProductTag|Proxy randomOrCreate(array $attributes = [])
* @method static ProductTag[]|Proxy[] all()
* @method static ProductTag[]|Proxy[] findBy(array $attributes)
* @method static ProductTag[]|Proxy[] randomSet(int $number, array $attributes = [])
* @method static ProductTag[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
* @method static EntityRepository|RepositoryProxy repository()
* @method ProductTag|Proxy create(array|callable $attributes = [])
*/
final class ProductTagFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
'name' => self::faker()->word(),
];
}

protected function initialize(): self
{
return $this;
}

protected static function getClass(): string
{
return ProductTag::class;
}
}
Loading