diff --git a/src/DataMapper/DataMapper.php b/src/DataMapper/DataMapper.php index c2e0f9ac..4877266f 100644 --- a/src/DataMapper/DataMapper.php +++ b/src/DataMapper/DataMapper.php @@ -4,7 +4,6 @@ use DateTime; use InvalidArgumentException; -use MongoDB\BSON\ObjectId; use MongoDB\BSON\UTCDateTime; use MongoDB\Collection; use Mongolid\Container\Container; @@ -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 @@ -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 ?? [], @@ -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)] ); @@ -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. * @@ -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); diff --git a/src/Model/SoftDeletesTrait.php b/src/Model/SoftDeletesTrait.php index 8c4a8806..5178b139 100644 --- a/src/Model/SoftDeletesTrait.php +++ b/src/Model/SoftDeletesTrait.php @@ -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; @@ -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' diff --git a/src/Util/QueryBuilder.php b/src/Util/QueryBuilder.php new file mode 100644 index 00000000..ff40cace --- /dev/null +++ b/src/Util/QueryBuilder.php @@ -0,0 +1,109 @@ + $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; + } +} diff --git a/src/Util/SoftDeleteQueries.php b/src/Util/SoftDeleteQueries.php deleted file mode 100644 index d4e78221..00000000 --- a/src/Util/SoftDeleteQueries.php +++ /dev/null @@ -1,36 +0,0 @@ - [ - [ - $field => null, - ], - [ - $field => ['$exists' => false], - ], - ], - ] - ); - } - - public static function getDeletedAtColumn(ModelInterface $model): string - { - return defined( - $model::class . '::DELETED_AT' - ) - ? $model::DELETED_AT - : 'deleted_at'; - } -} diff --git a/tests/Integration/PersisteModelWithSoftDeleteTest.php b/tests/Integration/PersisteModelWithSoftDeleteTest.php index c4ee5749..3f3305d3 100644 --- a/tests/Integration/PersisteModelWithSoftDeleteTest.php +++ b/tests/Integration/PersisteModelWithSoftDeleteTest.php @@ -21,7 +21,7 @@ public function testFindNotDeletedProduct(): void // Actions $result = ProductWithSoftDelete::where()->first(); - // Assertion + // Assertions $this->assertEquals($product, $result); } @@ -33,10 +33,30 @@ public function testCannotFindDeletedProduct(): void // Actions $result = ProductWithSoftDelete::where()->first(); - // Assertion + // Assertions $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(); + + // Assertions + $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 @@ -45,7 +65,7 @@ public function testFindDeletedProductWithFirst(): void // Actions $result = ProductWithSoftDelete::first($this->_id); - // Assertion + // Assertions $this->assertEquals($product, $result); } @@ -57,7 +77,7 @@ public function testCannotFindDeletedProductWithFirst(): void // Actions $result = ProductWithSoftDelete::first($this->_id); - // Assertion + // Assertions $this->assertNull($result); } @@ -70,7 +90,7 @@ public function testRestoreDeletedProduct(): void $isRestored = $product->restore(); $result = ProductWithSoftDelete::first($this->_id); - // Assertion + // Assertions $this->assertTrue($isRestored); $this->assertEquals($product, $result); } @@ -84,7 +104,7 @@ public function testCannotRestoreAlreadyRestoredProduct(): void $isRestored = $product->restore(); $result = ProductWithSoftDelete::first($this->_id); - // Assertion + // Assertions $this->assertFalse($isRestored); $this->assertEquals($product, $result); } @@ -98,7 +118,7 @@ public function testExecuteSoftDeleteOnProduct(): void $isDeleted = $product->delete(); $result = ProductWithSoftDelete::first($this->_id); - // Assertion + // Assertions $this->assertTrue($isDeleted); $this->assertNull($result); $this->assertInstanceOf(UTCDateTime::class, $product->deleted_at); @@ -113,7 +133,7 @@ public function testCannotExecuteSoftDeleteOnProduct(): void $isDeleted = $product->delete(); $result = ProductWithSoftDelete::first($this->_id); - // Assertion + // Assertions $this->assertTrue($isDeleted); $this->assertNull($result); $this->assertNull($result->deleted_at ?? null); diff --git a/tests/Unit/DataMapper/DataMapperTest.php b/tests/Unit/DataMapper/DataMapperTest.php index ab76391b..1aeef3e9 100644 --- a/tests/Unit/DataMapper/DataMapperTest.php +++ b/tests/Unit/DataMapper/DataMapperTest.php @@ -4,7 +4,6 @@ use InvalidArgumentException; use Mockery as m; -use MongoDB\BSON\ObjectID; use MongoDB\Client; use MongoDB\Collection; use MongoDB\Database; @@ -747,7 +746,7 @@ public function testShouldGetNullIfFirstCantFindAnything() { // Arrange $connection = m::mock(Connection::class); - $mapper = m::mock(DataMapper::class.'[prepareValueQuery,getCollection]', [$connection]); + $mapper = m::mock(DataMapper::class.'[getCollection]', [$connection]); $schema = m::mock(Schema::class); $collection = m::mock(Collection::class); @@ -770,10 +769,6 @@ public function testShouldGetNullIfFirstCantFindAnything() $mapper->shouldAllowMockingProtectedMethods(); // Expect - $mapper->shouldReceive('prepareValueQuery') - ->once() - ->with($query) - ->andReturn($preparedQuery); $mapper->shouldReceive('getCollection') ->once() @@ -821,11 +816,6 @@ public function testShouldGetFirstProjectingFields() $mapper->shouldAllowMockingProtectedMethods(); // Expect - $mapper->shouldReceive('prepareValueQuery') - ->once() - ->with($query) - ->andReturn($preparedQuery); - $mapper->shouldReceive('getCollection') ->once() ->andReturn($collection); @@ -978,22 +968,6 @@ public function testShouldGetRawCollection() $this->assertEquals($collection, $result); } - /** - * @dataProvider queryValueScenarios - */ - public function testShouldPrepareQueryValue($value, $expectation) - { - // Arrange - $connection = m::mock(Connection::class); - $mapper = new DataMapper($connection); - - // Act - $result = $this->callProtected($mapper, 'prepareValueQuery', [$value]); - - // Assert - $this->assertMongoQueryEquals($expectation, $result); - } - /** * @dataProvider getProjections */ @@ -1083,48 +1057,6 @@ public function eventsToBailOperations() ]; } - public function queryValueScenarios() - { - return [ - 'An array' => [ - 'value' => ['age' => ['$gt' => 25]], - 'expectation' => ['age' => ['$gt' => 25]], - ], - // ------------------------ - 'An ObjectId string' => [ - 'value' => '507f1f77bcf86cd799439011', - 'expectation' => ['_id' => new ObjectID('507f1f77bcf86cd799439011')], - ], - // ------------------------ - 'An ObjectId string within a query' => [ - 'value' => ['_id' => '507f1f77bcf86cd799439011'], - 'expectation' => ['_id' => new ObjectID('507f1f77bcf86cd799439011')], - ], - // ------------------------ - 'Other type of _id, sequence for example' => [ - 'value' => 7, - 'expectation' => ['_id' => 7], - ], - // ------------------------ - 'Series of string _ids as the $in parameter' => [ - 'value' => ['_id' => ['$in' => ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012']]], - 'expectation' => [ - '_id' => [ - '$in' => [ - new ObjectID('507f1f77bcf86cd799439011'), - new ObjectID('507f1f77bcf86cd799439012'), - ], - ], - ], - ], - // ------------------------ - 'Series of string _ids as the $in parameter' => [ - 'value' => ['_id' => ['$nin' => ['507f1f77bcf86cd799439011']]], - 'expectation' => ['_id' => ['$nin' => [new ObjectID('507f1f77bcf86cd799439011')]]], - ], - ]; - } - public function getWriteConcernVariations() { return [ diff --git a/tests/Unit/Util/QueryBuilderTest.php b/tests/Unit/Util/QueryBuilderTest.php new file mode 100644 index 00000000..723c22f3 --- /dev/null +++ b/tests/Unit/Util/QueryBuilderTest.php @@ -0,0 +1,73 @@ +assertMongoQueryEquals($expectation, $result); + } + + public function queryValueScenarios(): array + { + return [ + 'An array' => [ + 'value' => ['age' => ['$gt' => 25]], + 'expectation' => ['age' => ['$gt' => 25]], + ], + 'An ObjectId string' => [ + 'value' => '507f1f77bcf86cd799439011', + 'expectation' => [ + '_id' => new ObjectID( + '507f1f77bcf86cd799439011' + ), + ], + ], + 'An ObjectId string within a query' => [ + 'value' => ['_id' => '507f1f77bcf86cd799439011'], + 'expectation' => [ + '_id' => new ObjectID( + '507f1f77bcf86cd799439011' + ), + ], + ], + 'Other type of _id, sequence for example' => [ + 'value' => 7, + 'expectation' => ['_id' => 7], + ], + 'Series of string _ids as the $in parameter' => [ + 'value' => ['_id' => ['$in' => ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012']]], + 'expectation' => [ + '_id' => [ + '$in' => [ + new ObjectID('507f1f77bcf86cd799439011'), + new ObjectID('507f1f77bcf86cd799439012'), + ], + ], + ], + ], + 'Series of string _ids as the $in parameter' => [ + 'value' => ['_id' => ['$nin' => ['507f1f77bcf86cd799439011']]], + 'expectation' => [ + '_id' => [ + '$nin' => [new ObjectID( + '507f1f77bcf86cd799439011' + ), + ], + ], + ], + ], + ]; + } +}