diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7a84afd4f83..bf9b9f9ed5f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -667,16 +667,6 @@ true]]]> true]]]> - - - - - - - - - ]]> - diff --git a/src/Persisters/Collection/ManyToManyPersister.php b/src/Persisters/Collection/ManyToManyPersister.php index 7cf993d5997..5a037ad46f9 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); @@ -238,7 +242,8 @@ public function loadCriteria(PersistentCollection $collection, Criteria $criteri $paramTypes[] = PersisterHelper::getTypeOfColumn($value, $ownerMetadata, $this->em); } - $parameters = $this->expandCriteriaParameters($criteria); + $parameters = $this->expandCriteriaParameters($criteria); + $paramsValues = []; foreach ($parameters as $parameter) { [$name, $value, $operator] = $parameter; @@ -249,11 +254,13 @@ 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; + $paramsValues[] = $this->getValues($value); $paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0]; } } + $params = array_merge($params, ...$paramsValues); + $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform); 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..abfaaf1d5b3 --- /dev/null +++ b/src/Persisters/Traits/ResolveValuesHelper.php @@ -0,0 +1,72 @@ + + */ + private function getValues(mixed $value): array + { + if (is_array($value)) { + $newValues = []; + + foreach ($value as $itemValue) { + $newValues[] = $this->getValues($itemValue); + } + + return [array_merge(...$newValues)]; + } + + 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) { + $newValues = []; + + foreach ($class->getIdentifierValues($value) as $innerValue) { + $newValues[] = $this->getValues($innerValue); + } + + return array_merge(...$newValues); + } + + 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..e7d3a0eb176 --- /dev/null +++ b/tests/Tests/Models/Enums/BookCategory.php @@ -0,0 +1,43 @@ +books = new ArrayCollection(); + } + + public function addBook(Book $book): void + { + $this->books->add($book); + } + + public function getBooks(): Collection + { + return $this->books; + } +} 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 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..e6d3f94c59c 100644 --- a/tests/Tests/ORM/Functional/EnumTest.php +++ b/tests/Tests/ORM/Functional/EnumTest.php @@ -4,6 +4,8 @@ namespace Doctrine\Tests\ORM\Functional; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Driver\AttributeDriver; @@ -12,9 +14,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; @@ -74,7 +80,6 @@ public function testEnumHydrationObjectHydrator(): void $this->_em->persist($card2); $this->_em->flush(); - unset($card1, $card2); $this->_em->clear(); /** @var list $foundCards */ @@ -402,7 +407,6 @@ public function testFindByEnum(): void $this->_em->persist($card2); $this->_em->flush(); - unset($card1, $card2); $this->_em->clear(); /** @var list $foundCards */ @@ -516,4 +520,152 @@ public function testEnumWithDefault(): void self::assertSame(Suit::Hearts, $card->suit); } + + public function testEnumLazyCollectionMatchingWithOneToMany(): 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; + + $this->_em->clear(); + + $library = $this->_em->find(Library::class, $libraryId); + + $redBooks = $library->books->matching( + Criteria::create()->andWhere(Criteria::expr()->eq('bookColor', BookColor::RED)), + ); + + $this->assertCount(1, $redBooks); + } + + public function testEnumInitializedCollectionMatchingWithOneToMany(): 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; + + $this->_em->clear(); + + $library = $this->_em->find(Library::class, $libraryId); + $this->assertInstanceOf(Library::class, $library); + + // Load books collection first + $this->assertCount(2, $library->getBooks()); + + $redBooks = $library->books->matching( + Criteria::create()->andWhere(Criteria::expr()->eq('bookColor', BookColor::RED)), + ); + + $this->assertCount(1, $redBooks); + } + + public function testEnumLazyCollectionMatchingWithManyToMany(): 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(); + + $thrillerCategory = $this->_em->find(BookCategory::class, $thrillerCategoryId); + + $redBooks = $thrillerCategory->books->matching( + Criteria::create()->andWhere(Criteria::expr()->eq('bookColor', BookColor::RED)), + ); + + $this->assertCount(1, $redBooks); + } + + public function testEnumInitializedCollectionMatchingWithManyToMany(): 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(); + + $thrillerCategory = $this->_em->find(BookCategory::class, $thrillerCategoryId); + $this->assertInstanceOf(BookCategory::class, $thrillerCategory); + + // Load books collection first + $this->assertCount(2, $thrillerCategory->getBooks()); + + $redBooks = $thrillerCategory->books->matching( + Criteria::create()->andWhere(Criteria::expr()->eq('bookColor', BookColor::RED)), + ); + + $this->assertCount(1, $redBooks); + } }