Skip to content

Commit

Permalink
feat: add softDelete for new models
Browse files Browse the repository at this point in the history
  • Loading branch information
JoaoFerrazfs committed Aug 17, 2023
1 parent ec9ab6a commit e938b12
Show file tree
Hide file tree
Showing 16 changed files with 362 additions and 219 deletions.
5 changes: 3 additions & 2 deletions src/Model/SoftDeletesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
trait SoftDeletesTrait
{
public bool $enabledSoftDeletes = true;
public bool $forceDelete = false;

public function isTrashed(): bool
{
Expand All @@ -18,11 +19,11 @@ public function restore(): bool
{
$collumn = self::getDeletedAtColumn();

if (!$this->{$collumn}) {
if (!$this->isTrashed()) {
return false;
}

$this->{$collumn} = null;
unset($this->{$collumn});

return $this->execute('save');
}
Expand Down
75 changes: 17 additions & 58 deletions src/Query/Builder.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<?php
namespace Mongolid\Query;

use DateTime;
use InvalidArgumentException;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
use Mongolid\Connection\Connection;
use Mongolid\Container\Container;
use Mongolid\Cursor\CacheableCursor;
Expand All @@ -13,6 +15,7 @@
use Mongolid\Model\Exception\ModelNotFoundException;
use Mongolid\Model\ModelInterface;
use Mongolid\Util\ObjectIdUtils;
use Mongolid\Util\QueryBuilder;

/**
* This class will abstract how a Model is persisted and retrieved
Expand Down Expand Up @@ -171,6 +174,10 @@ public function update(ModelInterface $model, array $options = []): bool
*/
public function delete(ModelInterface $model, array $options = []): bool
{
if (($model->enabledSoftDeletes ?? false) && !($model->forceDelete ?? false)) {
return $this->executeSoftDelete($model, $options);
}

if (false === $this->fireEvent('deleting', $model, true)) {
return false;
}
Expand Down Expand Up @@ -207,7 +214,7 @@ public function where(ModelInterface $model, $query = [], array $projection = []
$model->getCollection(),
'find',
[
$this->prepareValueQuery($query),
QueryBuilder::resolveQuery($query, $model),
[
'projection' => $this->prepareProjection($projection),
'eagerLoads' => $model->with ?? [],
Expand Down Expand Up @@ -247,7 +254,7 @@ public function first(ModelInterface $model, $query = [], array $projection = []
}

return $model->getCollection()->findOne(
$this->prepareValueQuery($query),
QueryBuilder::resolveQuery($query, $model),
['projection' => $this->prepareProjection($projection)],
);
}
Expand All @@ -274,62 +281,6 @@ public function firstOrFail(ModelInterface $model, $query = [], array $projectio
throw (new ModelNotFoundException())->setModel(get_class($model));
}

/**
* Transforms a value that is not an array into an MongoDB query (array).
* This method will take care of converting a single value into a query for
* an _id, including when a objectId is passed as a string.
*
* @param mixed $value the _id of the model
*
* @return array Query for the given _id
*/
protected function prepareValueQuery($value): array
{
if (!is_array($value)) {
$value = ['_id' => $value];
}

if (isset($value['_id']) &&
is_string($value['_id']) &&
ObjectIdUtils::isObjectId($value['_id'])
) {
$value['_id'] = new ObjectId($value['_id']);
}

if (isset($value['_id']) &&
is_array($value['_id'])
) {
$value['_id'] = $this->prepareArrayFieldOfQuery($value['_id']);
}

return $value;
}

/**
* Prepares an embedded array of an query. It will convert string ObjectIds
* in operators into actual objects.
*
* @param array $value array that will be treated
*
* @return array prepared array
*/
protected function prepareArrayFieldOfQuery(array $value): array
{
foreach (['$in', '$nin'] as $operator) {
if (isset($value[$operator]) &&
is_array($value[$operator])
) {
foreach ($value[$operator] as $index => $id) {
if (ObjectIdUtils::isObjectId($id)) {
$value[$operator][$index] = new ObjectId($id);
}
}
}
}

return $value;
}

/**
* Triggers an event. May return if that event had success.
*
Expand Down Expand Up @@ -476,4 +427,12 @@ private function getUpdateData($model, array $data): array

return $changes;
}

private function executeSoftDelete(ModelInterface $entity, $options): bool
{
$deletedAtCoullum = QueryBuilder::getDeletedAtColumn($entity);
$entity->$deletedAtCoullum = new UTCDateTime(new DateTime('now'));

return $this->update($entity, $options);
}
}
2 changes: 1 addition & 1 deletion tests/Integration/EagerLoadingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use MongoDB\BSON\ObjectId;
use Mongolid\Container\Container;
use Mongolid\Tests\Stubs\Price;
use Mongolid\Tests\Stubs\Product;
use Mongolid\Tests\Stubs\Legacy\Product;
use Mongolid\Tests\Stubs\ReferencedUser;
use Mongolid\Tests\Stubs\Shop;
use Mongolid\Tests\Stubs\Sku;
Expand Down
174 changes: 174 additions & 0 deletions tests/Integration/PersisteLegacyModelWithSoftDeleteTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php

namespace Mongolid\Tests\Integration;

use DateTime;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
use Mongolid\Model\ModelInterface;
use Mongolid\Tests\Stubs\Legacy\ProductWithSoftDelete;
use Mongolid\Tests\Stubs\Legacy\Product;

final class PersisteLegacyModelWithSoftDeleteTest extends IntegrationTestCase
{
private ObjectId $_id;

public function testShouldFindNotDeletedProduct(): void
{
// Set
$product = $this->persiteProduct();

// Actions
$actualWhereResult = ProductWithSoftDelete::where()->first();
$actualFirstResult = ProductWithSoftDelete::first($this->_id);

// Assertions
$this->assertEquals($product, $actualWhereResult);
$this->assertEquals($product, $actualFirstResult);
}

public function testCannotFindDeletedProduct(): void
{
// Set
$this->persiteProduct(true);

// Actions
$actualWhereResult = ProductWithSoftDelete::where()->first();
$actualFirstResult = ProductWithSoftDelete::first($this->_id);

// Assertions
$this->assertNull($actualWhereResult);
$this->assertNull($actualFirstResult);
}

public function testShouldFindATrashedProduct(): void
{
// Set
$this->persiteProduct(true);
$this->_id = new ObjectId('5bcb310783a7fcdf1bf1a123');
$this->persiteProduct();

// Actions
$result = ProductWithSoftDelete::withTrashed();
$resultArray = $result->toArray();

// Assertions
$this->assertSame(2, $result->count());
$this->assertInstanceOf(
UTCDateTime::class,
$resultArray[0]['deleted_at']
);
$this->assertNull($resultArray[1]['deleted_at'] ?? null);
}

public function testRestoreDeletedProduct(): void
{
// Set
$product = $this->persiteProduct();
$product->delete();

// Actions
$isRestored = $product->restore();
$result = ProductWithSoftDelete::first($this->_id);

// Assertions
$this->assertTrue($isRestored);
$this->assertEquals($product, $result);
}

public function testCannotRestoreAlreadyRestoredProduct(): void
{
// Set
$product = $this->persiteProduct(isRestored: true);

// Actions
$isRestored = $product->restore();
$result = ProductWithSoftDelete::first($this->_id);

// Assertions
$this->assertFalse($isRestored);
$this->assertEquals($product, $result);
}

public function testExecuteSoftDeleteOnProduct(): void
{
// Set
$product = $this->persiteProduct();

// Actions
$isDeleted = $product->delete();
$result = ProductWithSoftDelete::first($this->_id);

// Assertions
$this->assertTrue($isDeleted);
$this->assertNull($result);
$this->assertEquals(
$product,
ProductWithSoftDelete::withTrashed()->first()
);
}

public function testExecuteForceDeleteOnProduct(): void
{
// Set
$product = $this->persiteProduct();
$this->_id = new ObjectId('5bcb310783a7fcdf1bf1a123');
$product2 = $this->persiteProduct();

// Actions
$isDeleted = $product->forceDelete();
$result = ProductWithSoftDelete::withTrashed();

// Assertions
$this->assertTrue($isDeleted);
$this->assertSame(1, $result->count());
$this->assertEquals($product2, $result->first());
}

public function testCannotExecuteSoftDeleteOnProduct(): void
{
// Set
$product = $this->persiteProduct(model:Product::class);

// Actions
$isDeleted = $product->delete();
$result = ProductWithSoftDelete::first($this->_id);

// Assertions
$this->assertTrue($isDeleted);
$this->assertNull($result);
$this->assertNull($result->deleted_at ?? null);
}

protected function setUp(): void
{
parent::setUp();

$this->_id = new ObjectId('5bcb310783a7fcdf1bf1a672');
}

private function persiteProduct(
bool $softDeleted = false,
bool $isRestored = false,
string $model = ProductWithSoftDelete::class
): ModelInterface {
$product = new $model();
$product->_id = $this->_id;
$product->short_name = 'Furadeira de Impacto Bosch com Chave de Mandril ';
$product->name = 'Furadeira de Impacto Bosch com Chave de Mandril e Acessórios 550W 1/2 GSB 550 RE 127V (110V)';

if ($softDeleted) {
$date = new UTCDateTime(new DateTime('today'));

$product->deleted_at = $date;
}

if ($isRestored) {
$product->deleted_at = null;
}

$product->save();

return $product;
}
}
Loading

0 comments on commit e938b12

Please sign in to comment.