@@ -27,6 +27,8 @@ protected function init(): void
2727}
2828class Model_Item extends Model
2929{
30+ use ModelSoftDeleteTrait;
31+
3032 public $ table = 'item ' ;
3133
3234 protected function init (): void
@@ -36,6 +38,8 @@ protected function init(): void
3638 $ this ->addField ('name ' );
3739 $ this ->hasOne ('parent_item_id ' , ['model ' => [self ::class]])
3840 ->addTitle ();
41+
42+ $ this ->initSoftDelete ();
3943 }
4044}
4145class Model_Item2 extends Model
@@ -71,6 +75,83 @@ protected function init(): void
7175 }
7276}
7377
78+ trait ModelSoftDeleteTrait
79+ {
80+ protected function initSoftDelete (): void
81+ {
82+ $ this ->addField ('is_deleted ' , ['type ' => 'boolean ' , 'nullable ' => false , 'default ' => false ]);
83+ $ this ->addCondition ('is_deleted ' , false );
84+ $ this ->onHook (Model::HOOK_BEFORE_DELETE , function (Model $ entity ) {
85+ $ softDeleteController = new ControllerSoftDelete ();
86+ $ softDeleteController ->softDelete ($ entity );
87+
88+ $ entity ->hook (Model::HOOK_AFTER_DELETE );
89+ $ entity ->breakHook (false ); // this will cancel original Model::delete()
90+ });
91+ }
92+ }
93+
94+ class ControllerSoftDelete
95+ {
96+ protected function init (): void
97+ {
98+ // example broken for clone "Object cannot be cloned with hook bound to a different object than this"
99+ // TODO remove this code from docs, hard to fix, controller is not meant to be added this way to model
100+ throw new \Error ();
101+ }
102+
103+ /**
104+ * @return mixed
105+ */
106+ public function invokeCallbackWithoutUndeletedCondition (Model $ model , \Closure $ callback )
107+ {
108+ $ model ->getField ('is_deleted ' ); // assert field exists
109+
110+ $ scopeElementsOrig = $ model ->scope ()->elements ;
111+ try {
112+ foreach ($ model ->scope ()->elements as $ k => $ v ) {
113+ if ($ v instanceof Model \Scope \Condition && $ v ->key === 'is_deleted ' && $ v ->operator === '= ' && $ v ->value === false ) {
114+ unset($ model ->scope ()->elements [$ k ]);
115+ }
116+ }
117+
118+ return $ callback ();
119+ } finally {
120+ $ model ->scope ()->elements = $ scopeElementsOrig ;
121+ }
122+ }
123+
124+ public function softDelete (Model $ entity ): void
125+ {
126+ $ entity ->assertIsLoaded ();
127+
128+ $ this ->invokeCallbackWithoutUndeletedCondition ($ entity ->getModel (), function () use ($ entity ): void {
129+ if ($ entity ->hook ('beforeSoftDelete ' ) === false ) {
130+ return ;
131+ }
132+
133+ $ entity ->saveAndUnload (['is_deleted ' => true ]);
134+
135+ $ entity ->hook ('afterSoftDelete ' );
136+ });
137+ }
138+
139+ public function restore (Model $ entity ): void
140+ {
141+ $ entity ->assertIsLoaded ();
142+
143+ $ this ->invokeCallbackWithoutUndeletedCondition ($ entity ->getModel (), function () use ($ entity ): void {
144+ if ($ entity ->hook ('beforeRestore ' ) === false ) {
145+ return ;
146+ }
147+
148+ $ entity ->saveAndUnload (['is_deleted ' => false ]);
149+
150+ $ entity ->hook ('afterRestore ' );
151+ });
152+ }
153+ }
154+
74155class RandomTest extends TestCase
75156{
76157 public function testRate (): void
@@ -87,6 +168,46 @@ public function testRate(): void
87168 self ::assertSame (2 , $ m ->executeCountQuery ());
88169 }
89170
171+ public function testSoftDelete (): void
172+ {
173+ $ m = new Model_Item ($ this ->db );
174+ $ this ->createMigrator ($ m )->dropIfExists ()->create ();
175+
176+ $ m ->insert (['name ' => 'John ' ]);
177+ $ m ->insert (['name ' => 'Michael ' ]);
178+
179+ $ softDeleteController = new ControllerSoftDelete ();
180+
181+ $ entity = $ m ->loadBy ('name ' , 'Michael ' );
182+ $ softDeleteController ->softDelete ($ entity );
183+ static ::assertSame ([
184+ 'item ' => [
185+ 1 => ['id ' => 1 , 'name ' => 'John ' , 'parent_item_id ' => null , 'is_deleted ' => '0 ' ],
186+ 2 => ['id ' => 2 , 'name ' => 'Michael ' , 'parent_item_id ' => null , 'is_deleted ' => '1 ' ],
187+ ],
188+ ], $ this ->getDb ());
189+
190+ $ entity = $ softDeleteController ->invokeCallbackWithoutUndeletedCondition ($ m , function () use ($ m ) {
191+ return $ m ->loadBy ('name ' , 'Michael ' );
192+ });
193+ $ softDeleteController ->restore ($ entity );
194+ static ::assertSame ([
195+ 'item ' => [
196+ 1 => ['id ' => 1 , 'name ' => 'John ' , 'parent_item_id ' => null , 'is_deleted ' => '0 ' ],
197+ 2 => ['id ' => 2 , 'name ' => 'Michael ' , 'parent_item_id ' => null , 'is_deleted ' => '0 ' ],
198+ ],
199+ ], $ this ->getDb ());
200+
201+ $ entity = $ m ->loadBy ('name ' , 'Michael ' );
202+ $ entity ->delete ();
203+ static ::assertSame ([
204+ 'item ' => [
205+ 1 => ['id ' => 1 , 'name ' => 'John ' , 'parent_item_id ' => null , 'is_deleted ' => '0 ' ],
206+ 2 => ['id ' => 2 , 'name ' => 'Michael ' , 'parent_item_id ' => null , 'is_deleted ' => '1 ' ],
207+ ],
208+ ], $ this ->getDb ());
209+ }
210+
90211 public function testTitleImport (): void
91212 {
92213 $ this ->setDb ([
@@ -205,16 +326,16 @@ public function testSameTable(): void
205326 {
206327 $ this ->setDb ([
207328 'item ' => [
208- 1 => ['id ' => 1 , 'name ' => 'John ' , 'parent_item_id ' => 1 ],
209- ['id ' => 2 , 'name ' => 'Sue ' , 'parent_item_id ' => 1 ],
210- ['id ' => 3 , 'name ' => 'Smith ' , 'parent_item_id ' => 2 ],
329+ 1 => ['id ' => 1 , 'name ' => 'John ' , 'parent_item_id ' => 1 , ' is_deleted ' => false ],
330+ ['id ' => 2 , 'name ' => 'Sue ' , 'parent_item_id ' => 1 , ' is_deleted ' => false ],
331+ ['id ' => 3 , 'name ' => 'Smith ' , 'parent_item_id ' => 2 , ' is_deleted ' => false ],
211332 ],
212333 ]);
213334
214335 $ m = new Model_Item ($ this ->db , ['table ' => 'item ' ]);
215336
216337 self ::assertSame (
217- ['id ' => 3 , 'name ' => 'Smith ' , 'parent_item_id ' => 2 , 'parent_item ' => 'Sue ' ],
338+ ['id ' => 3 , 'name ' => 'Smith ' , 'parent_item_id ' => 2 , 'parent_item ' => 'Sue ' , ' is_deleted ' => false ],
218339 $ m ->load (3 )->get ()
219340 );
220341 }
@@ -350,8 +471,8 @@ public function testGetTitle(): void
350471 {
351472 $ this ->setDb ([
352473 'item ' => [
353- 1 => ['id ' => 1 , 'name ' => 'John ' , 'parent_item_id ' => 1 ],
354- ['id ' => 2 , 'name ' => 'Sue ' , 'parent_item_id ' => 1 ],
474+ 1 => ['id ' => 1 , 'name ' => 'John ' , 'parent_item_id ' => 1 , ' is_deleted ' => false ],
475+ ['id ' => 2 , 'name ' => 'Sue ' , 'parent_item_id ' => 1 , ' is_deleted ' => false ],
355476 ],
356477 ]);
357478
0 commit comments