diff --git a/src/DataMapper/DataMapper.php b/src/DataMapper/DataMapper.php index 695b7cd6..c3a12821 100644 --- a/src/DataMapper/DataMapper.php +++ b/src/DataMapper/DataMapper.php @@ -260,12 +260,16 @@ public function where( $model = new $this->schema->entityClass; + $query = $this->prepareValueQuery($query); + + $query = $this->insertFilterForSoftDelete($query); + return new $cursorClass( $this->schema, $this->getCollection(), 'find', [ - $this->prepareValueQuery($query), + $query, [ 'projection' => $this->prepareProjection($projection), 'eagerLoads' => $model->with ?? [], @@ -665,4 +669,21 @@ function ($value) { return $filtered; } + + private function insertFilterForSoftDelete(array $query): array + { + return array_merge( + $query, + [ + '$or' => [ + [ + 'deleted_at' => null, + ], + [ + 'deleted_at' => ['$exists' => false], + ], + ], + ] + ); + } } diff --git a/src/Model/SoftDeletesTrait.php b/src/Model/SoftDeletesTrait.php new file mode 100644 index 00000000..0d01d394 --- /dev/null +++ b/src/Model/SoftDeletesTrait.php @@ -0,0 +1,55 @@ +{$this->getDeletedAtColumn()}); + } + + public function restore(): bool + { + $collumn = $this->getDeletedAtColumn(); + if (!$collumn = $this->{$collumn}) { + return false; + } + + $this->{$collumn} = null; + + return $this->execute('save'); + } + + public function forceDelete(): bool + { + $this->forceDelete = true; + + return $this->execute('delete'); + } + + private function getDeletedAtColumn(): string + { + return defined( + static::class . '::DELETED_AT' + ) + ? static::DELETED_AT + : 'deleted_at'; + } + + private function getFiltredQuery(): array + { + return [ + '$or' => [ + [ + 'deleted_at' => null, + ], + [ + 'deleted_at' => ['$exists' => false], + ], + ], + ]; + } +} diff --git a/tests/Unit/Model/SoftDeletesTraitTest.php b/tests/Unit/Model/SoftDeletesTraitTest.php new file mode 100644 index 00000000..bf8404b9 --- /dev/null +++ b/tests/Unit/Model/SoftDeletesTraitTest.php @@ -0,0 +1,73 @@ +makeProduct(); + $product->deleted_at = $date; + + // Actions + $actual = $product->isTrashed(); + + // Assertions + $this->assertTrue($actual); + } + + public function testShouldRestoreProduct(): void + { + // Set + $product = $this->makeProduct(); + $date = new UTCDateTime(new DateTime('today')); + $product->deleted_at = $date; + $dataMapper = $this->instance( + DataMapper::class, + m::mock(DataMapper::class) + ); + + // Expectations + $dataMapper->expects() + ->setSchema(m::type(DynamicSchema::class)); + + $dataMapper->expects() + ->save(m::type(Product::class), m::type('array')) + ->andReturnTrue(); + + // Actions + $actual = $product->restore(); + + // Assertions + $this->assertTrue($actual); + } + + public function testShouldNotRestoreProduct(): void + { + // Set + $product = $this->makeProduct(); + + // Actions + $actual = $product->restore(); + + // Assertions + $this->assertFalse($actual); + } + + private function makeProduct(): Product + { + return new class extends Product { + use SoftDeletesTrait; + }; + } +}