Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add native soft-delete support #1054

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 127 additions & 6 deletions tests/RandomTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ protected function init(): void
}
class Model_Item extends Model
{
use ModelSoftDeleteTrait;

public $table = 'item';

#[\Override]
Expand All @@ -38,6 +40,8 @@ protected function init(): void
$this->addField('name');
$this->hasOne('parent_item_id', ['model' => [self::class]])
->addTitle();

$this->initSoftDelete();
}
}
class Model_Item2 extends Model
Expand Down Expand Up @@ -75,6 +79,83 @@ protected function init(): void
}
}

trait ModelSoftDeleteTrait
{
protected function initSoftDelete(): void
{
$this->addField('is_deleted', ['type' => 'boolean', 'nullable' => false, 'default' => false]);
$this->addCondition('is_deleted', false);
$this->onHook(Model::HOOK_BEFORE_DELETE, function (Model $entity) {
$softDeleteController = new ControllerSoftDelete();
$softDeleteController->softDelete($entity);

$entity->hook(Model::HOOK_AFTER_DELETE);
$entity->breakHook(false); // this will cancel original Model::delete()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an alternative could be to replace the persistence during update so the delete hook does not need to be broken

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both solutions are internal so we can go ahead in this way and add this feature.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, such changes can have effect on overrided delete method or user hooks, very important decision that needs literaly hours of very fine reasoning to decide this correctly.

}, [], 100);
}
}

class ControllerSoftDelete
{
protected function init(): void
{
// example broken for clone "Object cannot be cloned with hook bound to a different object than this"
// TODO remove this code from docs, hard to fix, controller is not meant to be added this way to model
throw new \Error();
}

/**
* @return mixed
*/
public function invokeCallbackWithoutUndeletedCondition(Model $model, \Closure $callback)
{
$model->getField('is_deleted'); // assert field exists

$scopeElementsOrig = $model->scope()->elements;
try {
foreach ($model->scope()->elements as $k => $v) {
if ($v instanceof Model\Scope\Condition && $v->key === 'is_deleted' && $v->operator === '=' && $v->value === false) {
unset($model->scope()->elements[$k]);
}
}

return $callback();
} finally {
$model->scope()->elements = $scopeElementsOrig;
}
}

public function softDelete(Model $entity): void
{
$entity->assertIsLoaded();

$this->invokeCallbackWithoutUndeletedCondition($entity->getModel(), function () use ($entity): void {
if ($entity->hook('beforeSoftDelete') === false) {
return;
}

$entity->saveAndUnload(['is_deleted' => true]);

$entity->hook('afterSoftDelete');
});
}

public function restore(Model $entity): void
{
$entity->assertIsLoaded();

$this->invokeCallbackWithoutUndeletedCondition($entity->getModel(), function () use ($entity): void {
if ($entity->hook('beforeRestore') === false) {
return;
}

$entity->saveAndUnload(['is_deleted' => false]);

$entity->hook('afterRestore');
});
}
}

class RandomTest extends TestCase
{
public function testRate(): void
Expand All @@ -91,6 +172,46 @@ public function testRate(): void
self::assertSame(2, $m->executeCountQuery());
}

public function testSoftDelete(): void
{
$m = new Model_Item($this->db);
$this->createMigrator($m)->dropIfExists()->create();

$m->insert(['name' => 'John']);
$m->insert(['name' => 'Michael']);

$softDeleteController = new ControllerSoftDelete();

$entity = $m->loadBy('name', 'Michael');
$softDeleteController->softDelete($entity);
static::assertSame([
'item' => [
1 => ['id' => 1, 'name' => 'John', 'parent_item_id' => null, 'is_deleted' => '0'],
2 => ['id' => 2, 'name' => 'Michael', 'parent_item_id' => null, 'is_deleted' => '1'],
],
], $this->getDb());

$entity = $softDeleteController->invokeCallbackWithoutUndeletedCondition($m, function () use ($m) {
return $m->loadBy('name', 'Michael');
});
$softDeleteController->restore($entity);
static::assertSame([
'item' => [
1 => ['id' => 1, 'name' => 'John', 'parent_item_id' => null, 'is_deleted' => '0'],
2 => ['id' => 2, 'name' => 'Michael', 'parent_item_id' => null, 'is_deleted' => '0'],
],
], $this->getDb());

$entity = $m->loadBy('name', 'Michael');
$entity->delete();
static::assertSame([
'item' => [
1 => ['id' => 1, 'name' => 'John', 'parent_item_id' => null, 'is_deleted' => '0'],
2 => ['id' => 2, 'name' => 'Michael', 'parent_item_id' => null, 'is_deleted' => '1'],
],
], $this->getDb());
}

public function testTitleImport(): void
{
$this->setDb([
Expand Down Expand Up @@ -209,16 +330,16 @@ public function testSameTable(): void
{
$this->setDb([
'item' => [
1 => ['id' => 1, 'name' => 'John', 'parent_item_id' => 1],
['id' => 2, 'name' => 'Sue', 'parent_item_id' => 1],
['id' => 3, 'name' => 'Smith', 'parent_item_id' => 2],
1 => ['id' => 1, 'name' => 'John', 'parent_item_id' => 1, 'is_deleted' => false],
['id' => 2, 'name' => 'Sue', 'parent_item_id' => 1, 'is_deleted' => false],
['id' => 3, 'name' => 'Smith', 'parent_item_id' => 2, 'is_deleted' => false],
],
]);

$m = new Model_Item($this->db, ['table' => 'item']);

self::assertSame(
['id' => 3, 'name' => 'Smith', 'parent_item_id' => 2, 'parent_item' => 'Sue'],
['id' => 3, 'name' => 'Smith', 'parent_item_id' => 2, 'parent_item' => 'Sue', 'is_deleted' => false],
$m->load(3)->get()
);
}
Expand Down Expand Up @@ -354,8 +475,8 @@ public function testGetTitle(): void
{
$this->setDb([
'item' => [
1 => ['id' => 1, 'name' => 'John', 'parent_item_id' => 1],
['id' => 2, 'name' => 'Sue', 'parent_item_id' => 1],
1 => ['id' => 1, 'name' => 'John', 'parent_item_id' => 1, 'is_deleted' => false],
['id' => 2, 'name' => 'Sue', 'parent_item_id' => 1, 'is_deleted' => false],
],
]);

Expand Down
Loading