Skip to content

Commit

Permalink
[Autocomplete] Fix handling of associated properties in DQL joins
Browse files Browse the repository at this point in the history
  • Loading branch information
HugoSEIGLE authored and Kocal committed Nov 28, 2024
1 parent 89c7fa9 commit cc87e60
Show file tree
Hide file tree
Showing 9 changed files with 411 additions and 2 deletions.
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

0 comments on commit cc87e60

Please sign in to comment.