From d38e93357bc52f771b654ec0e88515ba2444cb41 Mon Sep 17 00:00:00 2001 From: Valentin SALMERON Date: Fri, 21 Jun 2024 08:55:25 +0200 Subject: [PATCH] Fix issue #11481 --- .../Collection/ManyToManyPersister.php | 6 +- .../Entity/BasicEntityPersister.php | 61 +----------- src/Persisters/Traits/ResolveValuesHelper.php | 74 +++++++++++++++ tests/Tests/Models/Enums/Book.php | 52 ++++++++++ tests/Tests/Models/Enums/BookCategory.php | 52 ++++++++++ tests/Tests/Models/Enums/BookColor.php | 11 +++ tests/Tests/Models/Enums/Library.php | 50 ++++++++++ tests/Tests/ORM/Functional/EnumTest.php | 95 +++++++++++++++++++ 8 files changed, 341 insertions(+), 60 deletions(-) create mode 100644 src/Persisters/Traits/ResolveValuesHelper.php create mode 100644 tests/Tests/Models/Enums/Book.php create mode 100644 tests/Tests/Models/Enums/BookCategory.php create mode 100644 tests/Tests/Models/Enums/BookColor.php create mode 100644 tests/Tests/Models/Enums/Library.php diff --git a/src/Persisters/Collection/ManyToManyPersister.php b/src/Persisters/Collection/ManyToManyPersister.php index 7cf993d5997..c9d6b3ec454 100644 --- a/src/Persisters/Collection/ManyToManyPersister.php +++ b/src/Persisters/Collection/ManyToManyPersister.php @@ -15,10 +15,12 @@ use Doctrine\ORM\Mapping\ManyToManyAssociationMapping; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Persisters\SqlValueVisitor; +use Doctrine\ORM\Persisters\Traits\ResolveValuesHelper; use Doctrine\ORM\Query; use Doctrine\ORM\Utility\PersisterHelper; use function array_fill; +use function array_merge; use function array_pop; use function assert; use function count; @@ -32,6 +34,8 @@ */ class ManyToManyPersister extends AbstractCollectionPersister { + use ResolveValuesHelper; + public function delete(PersistentCollection $collection): void { $mapping = $this->getMapping($collection); @@ -249,7 +253,7 @@ public function loadCriteria(PersistentCollection $collection, Criteria $criteri $whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT'); } else { $whereClauses[] = sprintf('te.%s %s ?', $field, $operator); - $params[] = $value; + $params = array_merge($params, $this->getValues($value)); $paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0]; } } diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index 377e03ce274..399abccee81 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -4,7 +4,6 @@ namespace Doctrine\ORM\Persisters\Entity; -use BackedEnum; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Expr\Comparison; use Doctrine\Common\Collections\Order; @@ -31,7 +30,7 @@ use Doctrine\ORM\Persisters\Exception\UnrecognizedField; use Doctrine\ORM\Persisters\SqlExpressionVisitor; use Doctrine\ORM\Persisters\SqlValueVisitor; -use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; +use Doctrine\ORM\Persisters\Traits\ResolveValuesHelper; use Doctrine\ORM\Query; use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\Query\ResultSetMapping; @@ -53,7 +52,6 @@ use function count; use function implode; use function is_array; -use function is_object; use function reset; use function spl_object_id; use function sprintf; @@ -99,6 +97,7 @@ class BasicEntityPersister implements EntityPersister { use LockSqlHelper; + use ResolveValuesHelper; /** @var array */ private static array $comparisonMap = [ @@ -1912,62 +1911,6 @@ private function getArrayBindingType(ParameterType|int|string $type): ArrayParam }; } - /** - * Retrieves the parameters that identifies a value. - * - * @return mixed[] - */ - private function getValues(mixed $value): array - { - if (is_array($value)) { - $newValue = []; - - foreach ($value as $itemValue) { - $newValue = array_merge($newValue, $this->getValues($itemValue)); - } - - return [$newValue]; - } - - return $this->getIndividualValue($value); - } - - /** - * Retrieves an individual parameter value. - * - * @psalm-return list - */ - private function getIndividualValue(mixed $value): array - { - if (! is_object($value)) { - return [$value]; - } - - if ($value instanceof BackedEnum) { - return [$value->value]; - } - - $valueClass = DefaultProxyClassNameResolver::getClass($value); - - if ($this->em->getMetadataFactory()->isTransient($valueClass)) { - return [$value]; - } - - $class = $this->em->getClassMetadata($valueClass); - - if ($class->isIdentifierComposite) { - $newValue = []; - - foreach ($class->getIdentifierValues($value) as $innerValue) { - $newValue = array_merge($newValue, $this->getValues($innerValue)); - } - - return $newValue; - } - - return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)]; - } - public function exists(object $entity, Criteria|null $extraConditions = null): bool { $criteria = $this->class->getIdentifierValues($entity); diff --git a/src/Persisters/Traits/ResolveValuesHelper.php b/src/Persisters/Traits/ResolveValuesHelper.php new file mode 100644 index 00000000000..edef7f04829 --- /dev/null +++ b/src/Persisters/Traits/ResolveValuesHelper.php @@ -0,0 +1,74 @@ +getValues($itemValue)); + } + + return [$newValue]; + } + + return $this->getIndividualValue($value); + } + + /** + * Retrieves an individual parameter value. + * + * @psalm-return list + */ + private function getIndividualValue(mixed $value): array + { + if (! is_object($value)) { + return [$value]; + } + + if ($value instanceof BackedEnum) { + return [$value->value]; + } + + $valueClass = DefaultProxyClassNameResolver::getClass($value); + + if ($this->em->getMetadataFactory()->isTransient($valueClass)) { + return [$value]; + } + + $class = $this->em->getClassMetadata($valueClass); + + if ($class->isIdentifierComposite) { + $newValue = []; + + foreach ($class->getIdentifierValues($value) as $innerValue) { + $newValue = array_merge($newValue, $this->getValues($innerValue)); + } + + return $newValue; + } + + return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)]; + } +} diff --git a/tests/Tests/Models/Enums/Book.php b/tests/Tests/Models/Enums/Book.php new file mode 100644 index 00000000000..ec7f960927a --- /dev/null +++ b/tests/Tests/Models/Enums/Book.php @@ -0,0 +1,52 @@ +categories = new ArrayCollection(); + } + + public function setLibrary(Library $library): void + { + $this->library = $library; + } + + public function addCategory(BookCategory $bookCategory): void + { + $this->categories->add($bookCategory); + $bookCategory->addBook($this); + } +} diff --git a/tests/Tests/Models/Enums/BookCategory.php b/tests/Tests/Models/Enums/BookCategory.php new file mode 100644 index 00000000000..4096a8a0b1c --- /dev/null +++ b/tests/Tests/Models/Enums/BookCategory.php @@ -0,0 +1,52 @@ +books = new ArrayCollection(); + } + + public function addBook(Book $book): void + { + $this->books->add($book); + } + + public function getBooks(): Collection + { + return $this->books; + } + + public function getBooksWithColor(BookColor $bookColor): Collection + { + $criteria = Criteria::create() + ->andWhere(Criteria::expr()->eq('bookColor', $bookColor)); + + return $this->books->matching($criteria); + } +} diff --git a/tests/Tests/Models/Enums/BookColor.php b/tests/Tests/Models/Enums/BookColor.php new file mode 100644 index 00000000000..66c3e8664e0 --- /dev/null +++ b/tests/Tests/Models/Enums/BookColor.php @@ -0,0 +1,11 @@ +books = new ArrayCollection(); + } + + public function getBooksWithColor(BookColor $bookColor): Collection + { + $criteria = Criteria::create() + ->andWhere(Criteria::expr()->eq('bookColor', $bookColor)); + + return $this->books->matching($criteria); + } + + public function getBooks(): Collection + { + return $this->books; + } + + public function addBook(Book $book): void + { + $this->books->add($book); + $book->setLibrary($this); + } +} diff --git a/tests/Tests/ORM/Functional/EnumTest.php b/tests/Tests/ORM/Functional/EnumTest.php index 75ccac8b506..7bf11511dba 100644 --- a/tests/Tests/ORM/Functional/EnumTest.php +++ b/tests/Tests/ORM/Functional/EnumTest.php @@ -12,9 +12,13 @@ use Doctrine\ORM\Tools\SchemaTool; use Doctrine\Tests\Models\DataTransferObjects\DtoWithArrayOfEnums; use Doctrine\Tests\Models\DataTransferObjects\DtoWithEnum; +use Doctrine\Tests\Models\Enums\Book; +use Doctrine\Tests\Models\Enums\BookCategory; +use Doctrine\Tests\Models\Enums\BookColor; use Doctrine\Tests\Models\Enums\Card; use Doctrine\Tests\Models\Enums\CardWithDefault; use Doctrine\Tests\Models\Enums\CardWithNullable; +use Doctrine\Tests\Models\Enums\Library; use Doctrine\Tests\Models\Enums\Product; use Doctrine\Tests\Models\Enums\Quantity; use Doctrine\Tests\Models\Enums\Scale; @@ -516,4 +520,95 @@ public function testEnumWithDefault(): void self::assertSame(Suit::Hearts, $card->suit); } + + public function testEnumCollectionMatchingWithOneToMany(): void + { + $this->setUpEntitySchema([Book::class, Library::class]); + + $redBook = new Book(); + $redBook->bookColor = BookColor::RED; + + $blueBook = new Book(); + $blueBook->bookColor = BookColor::BLUE; + + $library = new Library(); + $library->addBook($blueBook); + $library->addBook($redBook); + + $this->_em->persist($library); + $this->_em->persist($blueBook); + $this->_em->persist($redBook); + + $this->_em->flush(); + $libraryId = $library->id; + + unset($library, $redBook, $blueBook); + + $this->_em->clear(); + + // Case 1: load collection first, then use matching() + + $library = $this->_em->find(Library::class, $libraryId); + $this->assertInstanceOf(Library::class, $library); + + // Load books collection first + $this->assertCount(2, $library->getBooks()); + + $this->assertCount(1, $library->getBooksWithColor(BookColor::RED)); + + $this->_em->clear(); + + // Case 2: use matching() with uninitialized collection + + $library = $this->_em->find(Library::class, $libraryId); + $this->assertCount(1, $library->getBooksWithColor(BookColor::RED)); + } + + public function testEnumCollectionMatchingWithManyToMany(): void + { + $this->setUpEntitySchema([Book::class, BookCategory::class, Library::class]); + + $thrillerCategory = new BookCategory(); + $thrillerCategory->name = 'thriller'; + + $fantasyCategory = new BookCategory(); + $fantasyCategory->name = 'fantasy'; + + $redBook = new Book(); + $redBook->addCategory($fantasyCategory); + $redBook->addCategory($thrillerCategory); + $redBook->bookColor = BookColor::RED; + + $blueBook = new Book(); + $blueBook->addCategory($thrillerCategory); + $blueBook->bookColor = BookColor::BLUE; + + $this->_em->persist($thrillerCategory); + $this->_em->persist($fantasyCategory); + $this->_em->persist($blueBook); + $this->_em->persist($redBook); + + $this->_em->flush(); + $thrillerCategoryId = $thrillerCategory->id; + + $this->_em->clear(); + unset($thrillerCategory, $fantasyCategory, $blueBook, $redBook); + + // Case 1: load collection first, then use matching() + + $thrillerCategory = $this->_em->find(BookCategory::class, $thrillerCategoryId); + $this->assertInstanceOf(BookCategory::class, $thrillerCategory); + + // Load books collection first + $this->assertCount(2, $thrillerCategory->getBooks()); + + $this->assertCount(1, $thrillerCategory->getBooksWithColor(BookColor::RED)); + + $this->_em->clear(); + + // Case 2: use matching() with uninitialized collection + + $thrillerCategory = $this->_em->find(BookCategory::class, $thrillerCategoryId); + $this->assertCount(1, $thrillerCategory->getBooksWithColor(BookColor::RED)); + } }