File tree Expand file tree Collapse file tree
library/Notifications/Common
test/php/library/Notifications/Common Expand file tree Collapse file tree Original file line number Diff line number Diff 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 /**
Original file line number Diff line number Diff 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 ;
Original file line number Diff line number Diff 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 ();
You can’t perform that action at this time.
0 commit comments