Skip to content

Commit

Permalink
feat: add withTrashed
Browse files Browse the repository at this point in the history
  • Loading branch information
JoaoFerrazfs committed Aug 16, 2023
1 parent 1e3d3cd commit dd461b0
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 172 deletions.
68 changes: 4 additions & 64 deletions src/DataMapper/DataMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use DateTime;
use InvalidArgumentException;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Collection;
use Mongolid\Container\Container;
Expand All @@ -17,9 +16,8 @@
use Mongolid\Model\ModelInterface;
use Mongolid\Schema\HasSchemaInterface;
use Mongolid\Schema\Schema;
use Mongolid\Util\ObjectIdUtils;
use Mongolid\Connection\Connection;
Use Mongolid\Util\SoftDeleteQueries;
Use Mongolid\Util\QueryBuilder;

/**
* The DataMapper class will abstract how an Entity is persisted and retrieved
Expand Down Expand Up @@ -267,14 +265,13 @@ public function where(
$cursorClass = $cacheable ? SchemaCacheableCursor::class : SchemaCursor::class;

$model = new $this->schema->entityClass;
$query = $this->prepareValueQuery($query);

return new $cursorClass(
$this->schema,
$this->getCollection(),
'find',
[
SoftDeleteQueries::insertFilterForSoftDelete($query, $model),
QueryBuilder::resolveQuery($query, $model),
[
'projection' => $this->prepareProjection($projection),
'eagerLoads' => $model->with ?? [],
Expand Down Expand Up @@ -312,10 +309,9 @@ public function first(
}

$model = new $this->schema->entityClass;
$query = $this->prepareValueQuery($query);

$document = $this->getCollection()->findOne(
SoftDeleteQueries::insertFilterForSoftDelete($query, $model),
QueryBuilder::resolveQuery($query, $model),
['projection' => $this->prepareProjection($projection)]
);

Expand Down Expand Up @@ -406,62 +402,6 @@ public function getCollection(): Collection
return $collection;
}

/**
* 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 document
*
* @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;
}

/**
* Retrieves an EntityAssembler instance.
*
Expand Down Expand Up @@ -680,7 +620,7 @@ function ($value) {

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

return $this->update($entity, $options);
Expand Down
22 changes: 19 additions & 3 deletions src/Model/SoftDeletesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@

namespace Mongolid\Model;

use Mongolid\Cursor\CursorInterface;
use Mongolid\Util\QueryBuilder;

trait SoftDeletesTrait
{
public $enabledSoftDeletes = true;

public function isTrashed(): bool
{
return !is_null($this->{$this->getDeletedAtColumn()});
return !is_null($this->{ self::getDeletedAtColumn()});
}

public function restore(): bool
{
$collumn = $this->getDeletedAtColumn();
$collumn = self::getDeletedAtColumn();

if (!$this->{$collumn}) {
return false;
Expand All @@ -31,7 +34,20 @@ public function forceDelete(): bool
return $this->execute('delete');
}

private function getDeletedAtColumn(): string
public static function withTrashed(
array $query = [],
array $projection = [],
bool $useCache = false
): CursorInterface {
$query = QueryBuilder::prepareValueQuery($query);
$query = array_merge($query, [
'withTrashed' => true,
]);

return parent::where($query, $projection, $useCache);
}

private static function getDeletedAtColumn(): string
{
return defined(
static::class . '::DELETED_AT'
Expand Down
109 changes: 109 additions & 0 deletions src/Util/QueryBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace Mongolid\Util;

use MongoDB\BSON\ObjectId;
use Mongolid\Model\ModelInterface;

class QueryBuilder
{
public static function resolveQuery($query, ModelInterface $model): array
{
$query = self::prepareValueQuery($query);

return self::addSoftDeleteFilterIfRequired($query, $model);
}

public static function getDeletedAtColumn(ModelInterface $model): string
{
return defined(
$model::class . '::DELETED_AT'
)
? $model::DELETED_AT
: 'deleted_at';
}

/**
* 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 document
*
* @return array Query for the given _id
*/
public static 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'] = self::prepareArrayFieldOfQuery($value['_id']);
}

return $value;
}

private static function addSoftDeleteFilterIfRequired(array $query, ModelInterface $model): array
{
$field = self::getDeletedAtColumn($model);

if (isset($query['withTrashed'])) {
unset($query['withTrashed']);

return $query;
}

return array_merge(
$query,
[
'$or' => [
[
$field => null,
],
[
$field => ['$exists' => false],
],
],
]
);
}

/**
* 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
*/
private static 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;
}
}
36 changes: 0 additions & 36 deletions src/Util/SoftDeleteQueries.php

This file was deleted.

20 changes: 20 additions & 0 deletions tests/Integration/PersisteModelWithSoftDeleteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ public function testCannotFindDeletedProduct(): void
$this->assertNull($result);
}

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

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

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

public function testFindDeletedProductWithFirst(): void
{
// Set
Expand Down
Loading

0 comments on commit dd461b0

Please sign in to comment.