1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
|
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use function is_a;
class GH10566Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH10566A::class,
GH10566B::class,
GH10566C::class,
);
}
#[DataProvider('provideEntityClasses')]
public function testInsertion(string $startEntityClass): void
{
$a = new GH10566A();
$b = new GH10566B();
$c = new GH10566C();
$a->other = $b;
$b->other = $c;
$c->other = $a;
foreach ([$a, $b, $c] as $candidate) {
if (is_a($candidate, $startEntityClass)) {
$this->_em->persist($candidate);
}
}
// Since all associations are nullable, the ORM has no problem finding an insert order,
// it can always schedule "deferred updates" to fill missing foreign key values.
$this->_em->flush();
self::assertNotNull($a->id);
self::assertNotNull($b->id);
self::assertNotNull($c->id);
}
#[DataProvider('provideEntityClasses')]
public function testRemoval(string $startEntityClass): void
{
$a = new GH10566A();
$b = new GH10566B();
$c = new GH10566C();
$a->other = $b;
$b->other = $c;
$c->other = $a;
$this->_em->persist($a);
$this->_em->flush();
$aId = $a->id;
$bId = $b->id;
$cId = $c->id;
// In the removal case, the ORM currently does not schedule "extra updates"
// to break association cycles before entities are removed. So, we must not
// look at "nullable" for associations to find a delete commit order.
//
// To make it work, the user needs to have a database-level "ON DELETE SET NULL"
// on an association. That's where the cycle can be broken. Commit order computation
// for the removal case needs to look at this property.
//
// In this example, only A -> B can be used to break the cycle. So, regardless which
// entity we start with, the ORM-level cascade will always remove all three entities,
// and the order of database deletes always has to be (can only be) from B, then C, then A.
foreach ([$a, $b, $c] as $candidate) {
if (is_a($candidate, $startEntityClass)) {
$this->_em->remove($candidate);
}
}
$this->_em->flush();
self::assertFalse($this->_em->getConnection()->fetchOne('SELECT id FROM gh10566_a WHERE id = ?', [$aId]));
self::assertFalse($this->_em->getConnection()->fetchOne('SELECT id FROM gh10566_b WHERE id = ?', [$bId]));
self::assertFalse($this->_em->getConnection()->fetchOne('SELECT id FROM gh10566_c WHERE id = ?', [$cId]));
}
/** @return Generator<array{class-string}> */
public static function provideEntityClasses(): Generator
{
yield [GH10566A::class];
yield [GH10566B::class];
yield [GH10566C::class];
}
}
#[ORM\Entity]
#[ORM\Table(name: 'gh10566_a')]
class GH10566A
{
/** @var int */
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
public $id;
/** @var GH10566B */
#[ORM\OneToOne(targetEntity: GH10566B::class, cascade: ['all'])]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
public $other;
}
#[ORM\Entity]
#[ORM\Table(name: 'gh10566_b')]
class GH10566B
{
/** @var int */
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
public $id;
/** @var GH10566C */
#[ORM\OneToOne(targetEntity: GH10566C::class, cascade: ['all'])]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
public $other;
}
#[ORM\Entity]
#[ORM\Table(name: 'gh10566_c')]
class GH10566C
{
/** @var int */
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
public $id;
/** @var GH10566A */
#[ORM\OneToOne(targetEntity: GH10566A::class, cascade: ['all'])]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
public $other;
}
|