Files
archived-orm/tests/Tests/ORM/Functional/ManyToManyBasicAssociationTest.php
2025-08-27 00:02:52 +02:00

626 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\Models\CMS\CmsGroup;
use Doctrine\Tests\Models\CMS\CmsTag;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function assert;
use function class_exists;
use function get_class;
/**
* Basic many-to-many association tests.
* ("Working with associations")
*/
class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
$this->useModelSet('cms');
parent::setUp();
}
public function testUnsetManyToMany(): void
{
$user = $this->addCmsUserGblancoWithGroups(1);
// inverse side
// owning side!
unset($user->groups[0]->users[0], $user->groups[0]);
$this->_em->flush();
// Check that the link in the association table has been deleted
$this->assertGblancoGroupCountIs(0);
}
public function testBasicManyToManyJoin(): void
{
$user = $this->addCmsUserGblancoWithGroups(1);
$this->_em->clear();
self::assertEquals(0, $this->_em->getUnitOfWork()->size());
$query = $this->_em->createQuery('select u, g from Doctrine\Tests\Models\CMS\CmsUser u join u.groups g');
$result = $query->getResult();
self::assertEquals(2, $this->_em->getUnitOfWork()->size());
self::assertInstanceOf(CmsUser::class, $result[0]);
self::assertEquals('Guilherme', $result[0]->name);
self::assertEquals(1, $result[0]->getGroups()->count());
$groups = $result[0]->getGroups();
self::assertEquals('Developers_0', $groups[0]->getName());
self::assertEquals(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($result[0]));
self::assertEquals(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($groups[0]));
self::assertInstanceOf(PersistentCollection::class, $groups);
self::assertInstanceOf(PersistentCollection::class, $groups[0]->getUsers());
$groups[0]->getUsers()->clear();
$groups->clear();
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery('select u, g from Doctrine\Tests\Models\CMS\CmsUser u join u.groups g');
self::assertCount(0, $query->getResult());
}
public function testManyToManyAddRemove(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$this->_em->clear();
$uRep = $this->_em->getRepository($user::class);
// Get user
$user = $uRep->findOneById($user->getId());
self::assertNotNull($user, 'Has to return exactly one entry.');
self::assertFalse($user->getGroups()->isInitialized());
// Check groups
self::assertEquals(2, $user->getGroups()->count());
self::assertTrue($user->getGroups()->isInitialized());
// Remove first group
unset($user->groups[0]);
//$user->getGroups()->remove(0);
$this->_em->flush();
self::assertFalse($user->getGroups()->isDirty());
$this->_em->clear();
// Reload same user
$user2 = $uRep->findOneById($user->getId());
// Check groups
self::assertEquals(1, $user2->getGroups()->count());
}
public function testManyToManyInverseSideIgnored(): void
{
$user = $this->addCmsUserGblancoWithGroups(0);
$group = new CmsGroup();
$group->name = 'Humans';
// modify directly, addUser() would also (properly) set the owning side
$group->users[] = $user;
$this->_em->persist($user);
$this->_em->persist($group);
$this->_em->flush();
$this->_em->clear();
// Association should not exist
$user2 = $this->_em->find($user::class, $user->getId());
self::assertNotNull($user2, 'Has to return exactly one entry.');
self::assertEquals(0, $user2->getGroups()->count());
}
public function testManyToManyCollectionClearing(): void
{
$user = $this->addCmsUserGblancoWithGroups($groupCount = 10);
// Check that there are indeed 10 links in the association table
$this->assertGblancoGroupCountIs($groupCount);
$user->groups->clear();
$this->getQueryLog()->reset()->enable();
$this->_em->flush();
// Deletions of entire collections happen in a single query
$this->removeTransactionCommandsFromQueryLog();
self::assertQueryCount(1);
// Check that the links in the association table have been deleted
$this->assertGblancoGroupCountIs(0);
}
public function testManyToManyCollectionClearAndAdd(): void
{
$user = $this->addCmsUserGblancoWithGroups($groupCount = 10);
$groups = $user->groups->toArray();
$user->groups->clear();
foreach ($groups as $group) {
$user->groups[] = $group;
}
self::assertInstanceOf(PersistentCollection::class, $user->groups);
self::assertTrue($user->groups->isDirty());
self::assertCount($groupCount, $user->groups, 'There should be 10 groups in the collection.');
$this->_em->flush();
$this->assertGblancoGroupCountIs($groupCount);
}
public function assertGblancoGroupCountIs(int $expectedGroupCount): void
{
$countDql = "SELECT count(g.id) FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.groups g WHERE u.username = 'gblanco'";
self::assertEquals(
$expectedGroupCount,
$this->_em->createQuery($countDql)->getSingleScalarResult(),
"Failed to verify that CmsUser with username 'gblanco' has a group count of 10 with a DQL count query.",
);
}
public function testRetrieveManyToManyAndAddMore(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$group = new CmsGroup();
$group->name = 'Developers_Fresh';
$this->_em->persist($group);
$this->_em->flush();
$this->_em->clear();
$freshUser = $this->_em->find(CmsUser::class, $user->getId());
assert($freshUser instanceof CmsUser);
$newGroup = new CmsGroup();
$newGroup->setName('12Monkeys');
$freshUser->addGroup($newGroup);
self::assertFalse($freshUser->groups->isInitialized(), 'CmsUser::groups Collection has to be uninitialized for this test.');
$this->_em->flush();
self::assertFalse($freshUser->groups->isInitialized(), 'CmsUser::groups Collection has to be uninitialized for this test.');
self::assertCount(3, $freshUser->getGroups());
self::assertCount(3, $freshUser->getGroups()->getSnapshot(), 'Snapshot of CmsUser::groups should contain 3 entries.');
$this->_em->clear();
$freshUser = $this->_em->find(CmsUser::class, $user->getId());
self::assertCount(3, $freshUser->getGroups());
}
#[Group('DDC-130')]
public function testRemoveUserWithManyGroups(): void
{
$user = $this->addCmsUserGblancoWithGroups(10);
$userId = $user->getId();
$this->_em->remove($user);
$this->getQueryLog()->reset()->enable();
$this->_em->flush();
// This takes three queries: One to delete all user -> group join table rows for the user,
// one to delete all user -> tags join table rows for the user, and a final one to delete the user itself.
$this->removeTransactionCommandsFromQueryLog();
self::assertQueryCount(3);
$newUser = $this->_em->find($user::class, $userId);
self::assertNull($newUser);
}
#[Group('DDC-130')]
public function testRemoveGroupWithUser(): void
{
$user = $this->addCmsUserGblancoWithGroups(5);
$anotherUser = new CmsUser();
$anotherUser->username = 'joe_doe';
$anotherUser->name = 'Joe Doe';
$anotherUser->status = 'QA Engineer';
foreach ($user->getGroups() as $group) {
$anotherUser->addGroup($group);
}
$this->_em->persist($anotherUser);
$this->_em->flush();
foreach ($user->getGroups() as $group) {
$this->_em->remove($group);
}
$this->getQueryLog()->reset()->enable();
$this->_em->flush();
// This takes 5 * 2 queries for each group to be removed, one to remove all join table rows
// for the CmsGroup -> CmsUser inverse side association (for both users at once),
// and one for the group itself.
$this->removeTransactionCommandsFromQueryLog();
self::assertQueryCount(10);
// Changes to in-memory collection have been made and flushed
self::assertCount(0, $user->getGroups());
self::assertFalse($user->getGroups()->isDirty());
$this->_em->clear();
// Changes have been made to the database
$newUser = $this->_em->find($user::class, $user->getId());
self::assertCount(0, $newUser->getGroups());
}
public function testDereferenceCollectionDelete(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$user->groups = null;
$this->getQueryLog()->reset()->enable();
$this->_em->flush();
// It takes one query to remove all join table rows for the user at once
$this->removeTransactionCommandsFromQueryLog();
self::assertQueryCount(1);
$this->_em->clear();
$newUser = $this->_em->find($user::class, $user->getId());
self::assertCount(0, $newUser->getGroups());
}
#[Group('DDC-839')]
public function testWorkWithDqlHydratedEmptyCollection(): void
{
$user = $this->addCmsUserGblancoWithGroups(0);
$group = new CmsGroup();
$group->name = 'Developers0';
$this->_em->persist($group);
$this->_em->flush();
$this->_em->clear();
$newUser = $this->_em->createQuery('SELECT u, g FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN u.groups g WHERE u.id = ?1')
->setParameter(1, $user->getId())
->getSingleResult();
self::assertCount(0, $newUser->groups);
self::assertInstanceOf(AssociationMapping::class, $newUser->groups->getMapping());
$newUser->addGroup($group);
$this->_em->flush();
$this->_em->clear();
$newUser = $this->_em->find($user::class, $user->getId());
self::assertCount(1, $newUser->groups);
}
public function addCmsUserGblancoWithGroups(int $groupCount = 1): CmsUser
{
$user = new CmsUser();
$user->name = 'Guilherme';
$user->username = 'gblanco';
$user->status = 'developer';
for ($i = 0; $i < $groupCount; ++$i) {
$group = new CmsGroup();
$group->name = 'Developers_' . $i;
$user->addGroup($group);
}
$this->_em->persist($user);
$this->_em->flush();
self::assertNotNull($user->getId(), "User 'gblanco' should have an ID assigned after the persist()/flush() operation.");
return $user;
}
#[Group('DDC-978')]
public function testClearAndResetCollection(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$group1 = new CmsGroup();
$group1->name = 'Developers_New1';
$group2 = new CmsGroup();
$group2->name = 'Developers_New2';
$this->_em->persist($group1);
$this->_em->persist($group2);
$this->_em->flush();
$this->_em->clear();
$user = $this->_em->find($user::class, $user->id);
$coll = new ArrayCollection([$group1, $group2]);
$user->groups = $coll;
$this->_em->flush();
self::assertInstanceOf(
PersistentCollection::class,
$user->groups,
'UnitOfWork should have replaced ArrayCollection with PersistentCollection.',
);
$this->_em->flush();
$this->_em->clear();
$user = $this->_em->find($user::class, $user->id);
self::assertCount(2, $user->groups);
self::assertEquals('Developers_New1', $user->groups[0]->name);
self::assertEquals('Developers_New2', $user->groups[1]->name);
}
#[Group('DDC-733')]
public function testInitializePersistentCollection(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$this->_em->clear();
$user = $this->_em->find($user::class, $user->id);
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$this->_em->getUnitOfWork()->initializeObject($user->groups);
self::assertTrue($user->groups->isInitialized(), 'Collection should be initialized after calling UnitOfWork::initializeObject()');
}
#[Group('DDC-1189')]
#[Group('DDC-956')]
public function testClearBeforeLazyLoad(): void
{
$user = $this->addCmsUserGblancoWithGroups(4);
$this->_em->clear();
$user = $this->_em->find($user::class, $user->id);
$user->groups->clear();
self::assertCount(0, $user->groups);
$this->_em->flush();
$user = $this->_em->find($user::class, $user->id);
self::assertCount(0, $user->groups);
}
#[Group('DDC-3952')]
public function testManyToManyOrderByIsNotIgnored(): void
{
$user = $this->addCmsUserGblancoWithGroups(1);
$group1 = new CmsGroup();
$group2 = new CmsGroup();
$group3 = new CmsGroup();
$group1->name = 'C';
$group2->name = 'A';
$group3->name = 'B';
$user->addGroup($group1);
$user->addGroup($group2);
$user->addGroup($group3);
$this->_em->persist($user);
$this->_em->flush();
$this->_em->clear();
$user = $this->_em->find($user::class, $user->id);
$criteria = Criteria::create()
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
self::assertEquals(
['A', 'B', 'C', 'Developers_0'],
$user
->getGroups()
->matching($criteria)
->map(static fn (CmsGroup $group) => $group->getName())
->toArray(),
);
}
#[Group('DDC-3952')]
public function testManyToManyOrderByHonorsFieldNameColumnNameAliases(): void
{
$user = new CmsUser();
$user->name = 'Guilherme';
$user->username = 'gblanco';
$user->status = 'developer';
$tag1 = new CmsTag();
$tag2 = new CmsTag();
$tag3 = new CmsTag();
$tag1->name = 'C';
$tag2->name = 'A';
$tag3->name = 'B';
$user->addTag($tag1);
$user->addTag($tag2);
$user->addTag($tag3);
$this->_em->persist($user);
$this->_em->flush();
$this->_em->clear();
$user = $this->_em->find($user::class, $user->id);
$criteria = Criteria::create()
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
self::assertEquals(
['A', 'B', 'C'],
$user
->getTags()
->matching($criteria)
->map(static fn (CmsTag $tag) => $tag->getName())
->toArray(),
);
}
public function testMatchingWithLimit(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$this->_em->clear();
$user = $this->_em->find($user::class, $user->id);
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create()->setMaxResults(1);
$result = $groups->matching($criteria);
self::assertCount(1, $result);
self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection');
}
public function testMatchingWithOffset(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$this->_em->clear();
$user = $this->_em->find($user::class, $user->id);
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create()->setFirstResult(1);
$result = $groups->matching($criteria);
self::assertCount(1, $result);
$firstGroup = $result->first();
self::assertEquals('Developers_1', $firstGroup->name);
self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection');
}
public function testMatchingWithLimitAndOffset(): void
{
$user = $this->addCmsUserGblancoWithGroups(5);
$this->_em->clear();
$user = $this->_em->find($user::class, $user->id);
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create()->setFirstResult(1)->setMaxResults(3);
$result = $groups->matching($criteria);
self::assertCount(3, $result);
$firstGroup = $result->first();
self::assertEquals('Developers_1', $firstGroup->name);
$lastGroup = $result->last();
self::assertEquals('Developers_3', $lastGroup->name);
self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection');
}
public function testMatching(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$this->_em->clear();
$user = $this->_em->find($user::class, $user->id);
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create()->where(Criteria::expr()->eq('name', (string) 'Developers_0'));
$result = $groups->matching($criteria);
self::assertCount(1, $result);
$firstGroup = $result->first();
self::assertEquals('Developers_0', $firstGroup->name);
self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection');
}
public function testMatchingWithInCondition(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$this->_em->clear();
$user = $this->_em->find(get_class($user), $user->id);
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create()->where(Criteria::expr()->in('name', ['Developers_1']));
$result = $groups->matching($criteria);
self::assertCount(1, $result);
self::assertEquals('Developers_1', $result[0]->name);
self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection');
}
public function testMatchingWithNotInCondition(): void
{
$user = $this->addCmsUserGblancoWithGroups(2);
$this->_em->clear();
$user = $this->_em->find(get_class($user), $user->id);
$groups = $user->groups;
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
$criteria = Criteria::create()->where(Criteria::expr()->notIn('name', ['Developers_0']));
$result = $groups->matching($criteria);
self::assertCount(1, $result);
self::assertEquals('Developers_1', $result[0]->name);
self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection');
}
private function removeTransactionCommandsFromQueryLog(): void
{
$log = $this->getQueryLog();
foreach ($log->queries as $key => $entry) {
if ($entry['sql'] === '"START TRANSACTION"' || $entry['sql'] === '"COMMIT"') {
unset($log->queries[$key]);
}
}
}
}