Skip to content

Commit 1bbda5e

Browse files
Support deleting related model when saving a model
1 parent fd6a653 commit 1bbda5e

3 files changed

Lines changed: 79 additions & 0 deletions

File tree

library/Notifications/Common/EntityManager.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,15 @@ protected function saveGraph(Model $model): void
198198

199199
$this->saveLinks($relation, $model, $targets);
200200
}
201+
202+
// 5. Deletions queued on this model via Model::deleteOnSave(). A child dropped from a hasMany
203+
// relation is not removed by the cascade above, so it is deleted explicitly here — within the
204+
// same transaction, and only for entries the caller asked to delete (never a whole relation).
205+
foreach ($model->getPendingDeletions() as $deletion) {
206+
$this->delete($deletion);
207+
}
208+
209+
$model->clearPendingDeletions();
201210
}
202211

203212
/**

library/Notifications/Common/Model.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ abstract class Model extends \ipl\Orm\Model
3030
*/
3131
private bool $resolvingProperty = false;
3232

33+
/**
34+
* Related models queued to be deleted the next time this model is persisted
35+
*
36+
* @var list<Model>
37+
*/
38+
private array $pendingDeletions = [];
39+
3340
/**
3441
* Get whether this entity is new, i.e. not yet persisted to the database
3542
*
@@ -94,6 +101,42 @@ public function markClean(): static
94101
return $this;
95102
}
96103

104+
/**
105+
* Queue a related model to be deleted the next time this model is persisted
106+
*
107+
* @param Model $model
108+
*
109+
* @return $this
110+
*/
111+
public function deleteOnSave(Model $model): static
112+
{
113+
$this->pendingDeletions[] = $model;
114+
115+
return $this;
116+
}
117+
118+
/**
119+
* Get the related models queued for deletion via {@see deleteOnSave()}
120+
*
121+
* @return list<Model>
122+
*/
123+
public function getPendingDeletions(): array
124+
{
125+
return $this->pendingDeletions;
126+
}
127+
128+
/**
129+
* Forget any models queued for deletion via {@see deleteOnSave()}
130+
*
131+
* @return $this
132+
*/
133+
public function clearPendingDeletions(): static
134+
{
135+
$this->pendingDeletions = [];
136+
137+
return $this;
138+
}
139+
97140
protected function getProperty(string $key): mixed
98141
{
99142
$wasResolving = $this->resolvingProperty;

test/php/library/Notifications/Common/EntityManagerTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,33 @@ public function testHasManyCascadeCopiesParentKeyIntoChildren()
173173
);
174174
}
175175

176+
public function testDeleteOnSaveRemovesAChildDroppedFromAHasManyRelation()
177+
{
178+
$workshop = new Workshop();
179+
$workshop->name = 'Acme';
180+
181+
$spanner = new Gadget();
182+
$spanner->name = 'Spanner';
183+
$wrench = new Gadget();
184+
$wrench->name = 'Wrench';
185+
$workshop->gadgets = [$spanner, $wrench];
186+
187+
$this->em()->save($workshop);
188+
189+
$workshop->gadgets = [$spanner];
190+
$workshop->deleteOnSave($wrench);
191+
192+
$this->em()->save($workshop);
193+
194+
$this->assertSame(
195+
[['name' => 'Spanner', 'workshop_id' => $workshop->id]],
196+
$this->rows('SELECT name, workshop_id FROM gadget ORDER BY id'),
197+
'Only the queued child is deleted; its siblings are untouched'
198+
);
199+
$this->assertTrue($wrench->isNew(), 'The deleted model is marked new again');
200+
$this->assertSame([], $workshop->getPendingDeletions(), 'The queue is cleared after save');
201+
}
202+
176203
public function testBelongsToCascadeSavesParentFirst()
177204
{
178205
$gadget = new Gadget();

0 commit comments

Comments
 (0)