Skip to content

Commit fe8e313

Browse files
authored
Merge pull request #10747 from wtfzdotnet/feature/fix-one-to-many-custom-id-orphan-removal
2 parents 1adb5c0 + 3c0d140 commit fe8e313

File tree

3 files changed

+205
-9
lines changed

3 files changed

+205
-9
lines changed

lib/Doctrine/ORM/Persisters/Collection/OneToManyPersister.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use function array_values;
1717
use function assert;
1818
use function implode;
19+
use function is_int;
1920
use function is_string;
2021

2122
/**
@@ -174,16 +175,22 @@ private function deleteEntityCollection(PersistentCollection $collection): int
174175
$targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
175176
$columns = [];
176177
$parameters = [];
178+
$types = [];
177179

178180
foreach ($targetClass->associationMappings[$mapping['mappedBy']]['joinColumns'] as $joinColumn) {
179181
$columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
180182
$parameters[] = $identifier[$sourceClass->getFieldForColumn($joinColumn['referencedColumnName'])];
183+
$types[] = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $sourceClass, $this->em);
181184
}
182185

183186
$statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
184187
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
185188

186-
return $this->conn->executeStatement($statement, $parameters);
189+
$numAffected = $this->conn->executeStatement($statement, $parameters, $types);
190+
191+
assert(is_int($numAffected));
192+
193+
return $numAffected;
187194
}
188195

189196
/**
@@ -247,6 +254,8 @@ private function deleteJoinedEntityCollection(PersistentCollection $collection):
247254

248255
$this->conn->executeStatement($statement);
249256

257+
assert(is_int($numDeleted));
258+
250259
return $numDeleted;
251260
}
252261
}

psalm-baseline.xml

-8
Original file line numberDiff line numberDiff line change
@@ -1244,14 +1244,6 @@
12441244
$mapping['indexBy'] => $index,
12451245
]]]></code>
12461246
</InvalidArrayOffset>
1247-
<InvalidReturnStatement>
1248-
<code>$numDeleted</code>
1249-
<code><![CDATA[$this->conn->executeStatement($statement, $parameters)]]></code>
1250-
</InvalidReturnStatement>
1251-
<InvalidReturnType>
1252-
<code>int</code>
1253-
<code>int</code>
1254-
</InvalidReturnType>
12551247
<PossiblyNullArgument>
12561248
<code><![CDATA[$collection->getOwner()]]></code>
12571249
<code><![CDATA[$collection->getOwner()]]></code>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional;
6+
7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Common\Collections\Collection;
9+
use Doctrine\DBAL\Platforms\AbstractPlatform;
10+
use Doctrine\DBAL\Types\Type as DBALType;
11+
use Doctrine\ORM\Mapping as ORM;
12+
use Doctrine\ORM\Mapping\Column;
13+
use Doctrine\ORM\Mapping\Entity;
14+
use Doctrine\ORM\Mapping\Id;
15+
use Doctrine\ORM\Mapping\Table;
16+
use Doctrine\Tests\DbalTypes\CustomIdObject;
17+
use Doctrine\Tests\OrmFunctionalTestCase;
18+
19+
use function method_exists;
20+
use function str_replace;
21+
22+
/**
23+
* Functional tests for asserting that orphaned children in a OneToMany relationship get removed with a custom identifier
24+
*
25+
* @group GH10747
26+
*/
27+
final class GH10747Test extends OrmFunctionalTestCase
28+
{
29+
protected function setUp(): void
30+
{
31+
parent::setUp();
32+
33+
if (! DBALType::hasType(GH10747CustomIdObjectHashType::class)) {
34+
DBALType::addType(GH10747CustomIdObjectHashType::class, GH10747CustomIdObjectHashType::class);
35+
}
36+
37+
$this->setUpEntitySchema([GH10747Article::class, GH10747Credit::class]);
38+
}
39+
40+
public function testOrphanedOneToManyDeletesCollection(): void
41+
{
42+
$object = new GH10747Article(
43+
new CustomIdObject('article')
44+
);
45+
46+
$creditOne = new GH10747Credit(
47+
$object,
48+
'credit1'
49+
);
50+
51+
$creditTwo = new GH10747Credit(
52+
$object,
53+
'credit2'
54+
);
55+
56+
$object->setCredits(new ArrayCollection([$creditOne, $creditTwo]));
57+
58+
$this->_em->persist($object);
59+
$this->_em->persist($creditOne);
60+
$this->_em->persist($creditTwo);
61+
$this->_em->flush();
62+
63+
$id = $object->id;
64+
65+
$object2 = $this->_em->find(GH10747Article::class, $id);
66+
67+
$creditThree = new GH10747Credit(
68+
$object2,
69+
'credit3'
70+
);
71+
72+
$object2->setCredits(new ArrayCollection([$creditThree]));
73+
74+
$this->_em->persist($object2);
75+
$this->_em->persist($creditThree);
76+
$this->_em->flush();
77+
78+
$currentDatabaseCredits = $this->_em->createQueryBuilder()
79+
->select('c.id')
80+
->from(GH10747Credit::class, 'c')
81+
->getQuery()
82+
->execute();
83+
84+
self::assertCount(1, $currentDatabaseCredits);
85+
}
86+
}
87+
88+
/**
89+
* @Entity
90+
* @Table
91+
*/
92+
class GH10747Article
93+
{
94+
/**
95+
* @Id
96+
* @Column(type="Doctrine\Tests\ORM\Functional\GH10747CustomIdObjectHashType")
97+
* @var CustomIdObject
98+
*/
99+
public $id;
100+
101+
/**
102+
* @ORM\OneToMany(targetEntity="GH10747Credit", mappedBy="article", orphanRemoval=true)
103+
*
104+
* @var Collection<int, GH10747Credit>
105+
*/
106+
public $credits;
107+
108+
public function __construct(CustomIdObject $id)
109+
{
110+
$this->id = $id;
111+
$this->credits = new ArrayCollection();
112+
}
113+
114+
public function setCredits(Collection $credits): void
115+
{
116+
$this->credits = $credits;
117+
}
118+
119+
/** @return Collection<int, GH10747Credit> */
120+
public function getCredits(): Collection
121+
{
122+
return $this->credits;
123+
}
124+
}
125+
126+
/**
127+
* @Entity
128+
* @Table
129+
*/
130+
class GH10747Credit
131+
{
132+
/**
133+
* @ORM\Column(type="integer")
134+
* @ORM\GeneratedValue()
135+
*
136+
* @Id()
137+
* @var int|null
138+
*/
139+
public $id = null;
140+
141+
/** @var string */
142+
public $name;
143+
144+
/**
145+
* @ORM\ManyToOne(targetEntity="GH10747Article", inversedBy="credits")
146+
*
147+
* @var GH10747Article
148+
*/
149+
public $article;
150+
151+
public function __construct(GH10747Article $article, string $name)
152+
{
153+
$this->article = $article;
154+
$this->name = $name;
155+
}
156+
}
157+
158+
class GH10747CustomIdObjectHashType extends DBALType
159+
{
160+
/**
161+
* {@inheritDoc}
162+
*/
163+
public function convertToDatabaseValue($value, AbstractPlatform $platform)
164+
{
165+
return $value->id . '_test';
166+
}
167+
168+
/**
169+
* {@inheritDoc}
170+
*/
171+
public function convertToPHPValue($value, AbstractPlatform $platform)
172+
{
173+
return new CustomIdObject(str_replace('_test', '', $value));
174+
}
175+
176+
/**
177+
* {@inheritDoc}
178+
*/
179+
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
180+
{
181+
if (method_exists($platform, 'getStringTypeDeclarationSQL')) {
182+
return $platform->getStringTypeDeclarationSQL($fieldDeclaration);
183+
}
184+
185+
return $platform->getVarcharTypeDeclarationSQL($fieldDeclaration);
186+
}
187+
188+
/**
189+
* {@inheritDoc}
190+
*/
191+
public function getName()
192+
{
193+
return self::class;
194+
}
195+
}

0 commit comments

Comments
 (0)