From d13b7a0f8217cd635dfc66b221f9cd45134dd293 Mon Sep 17 00:00:00 2001 From: JoaoFerrazfs Date: Fri, 25 Aug 2023 11:27:28 -0300 Subject: [PATCH] refact: improve queryBuilder --- src/DataMapper/DataMapper.php | 28 +++- src/Model/SoftDeleteTrait.php | 29 ++-- src/Query/Builder.php | 27 ++-- .../QueryBuilder.php => Query/Resolver.php} | 39 ++--- .../PersistLegacyModelWithSoftDeleteTest.php | 2 - tests/Unit/Model/SoftDeletesTraitTest.php | 134 +++++++++++++++--- tests/Unit/Util/QueryBuilderTest.php | 67 +++------ 7 files changed, 212 insertions(+), 114 deletions(-) rename src/{Util/QueryBuilder.php => Query/Resolver.php} (73%) diff --git a/src/DataMapper/DataMapper.php b/src/DataMapper/DataMapper.php index 0e05c49c..8424118a 100644 --- a/src/DataMapper/DataMapper.php +++ b/src/DataMapper/DataMapper.php @@ -4,6 +4,7 @@ use InvalidArgumentException; use MongoDB\Collection; +use Mongolid\Connection\Connection; use Mongolid\Container\Container; use Mongolid\Cursor\CursorInterface; use Mongolid\Cursor\EagerLoadedCursor; @@ -12,10 +13,9 @@ use Mongolid\Event\EventTriggerService; use Mongolid\Model\Exception\ModelNotFoundException; use Mongolid\Model\ModelInterface; +use Mongolid\Query\Resolver; use Mongolid\Schema\HasSchemaInterface; use Mongolid\Schema\Schema; -use Mongolid\Connection\Connection; -Use Mongolid\Util\QueryBuilder; /** * The DataMapper class will abstract how an Entity is persisted and retrieved @@ -25,7 +25,7 @@ */ class DataMapper implements HasSchemaInterface { - public bool $withTrashed = false; + private bool $ignoreSoftDelete = false; /** * Name of the schema class to be used. @@ -261,9 +261,11 @@ public function where( $model = new $this->schema->entityClass; - $query = $this->withTrashed ? - QueryBuilder::prepareValueForQueryCompatibility($query) : - QueryBuilder::prepareValueForSoftDeleteCompatibility($query, $model); + $query = Resolver::resolveQuery( + $query, + $model, + $this->ignoreSoftDelete + ); return new $cursorClass( $this->schema, @@ -309,8 +311,13 @@ public function first( $model = new $this->schema->entityClass; + $query = Resolver::resolveQuery( + $query, + $model, + ); + $document = $this->getCollection()->findOne( - QueryBuilder::prepareValueForSoftDeleteCompatibility($query, $model), + $query, ['projection' => $this->prepareProjection($projection)] ); @@ -347,6 +354,13 @@ public function firstOrFail( throw (new ModelNotFoundException())->setModel($this->schema->entityClass); } + public function withoutSoftDelete(): self + { + $this->ignoreSoftDelete = true; + + return $this; + } + /** * Parses an object with SchemaMapper and the given Schema. * diff --git a/src/Model/SoftDeleteTrait.php b/src/Model/SoftDeleteTrait.php index aeaa478f..50329eb1 100644 --- a/src/Model/SoftDeleteTrait.php +++ b/src/Model/SoftDeleteTrait.php @@ -5,7 +5,7 @@ use DateTime; use MongoDB\BSON\UTCDateTime; use Mongolid\Cursor\CursorInterface; -use Mongolid\Util\QueryBuilder; +use Mongolid\Query\Resolver; trait SoftDeleteTrait { @@ -13,12 +13,12 @@ trait SoftDeleteTrait public function isTrashed(): bool { - return !is_null($this->{QueryBuilder::getDeletedAtColumn($this)}); + return !is_null($this->{Resolver::getDeletedAtColumn($this)}); } public function restore(): bool { - $collumn = QueryBuilder::getDeletedAtColumn($this); + $collumn = Resolver::getDeletedAtColumn($this); if (!$this->isTrashed()) { return false; @@ -36,7 +36,7 @@ public function forceDelete(): bool public function executeSoftDelete(): bool { - $deletedAtColumn = QueryBuilder::getDeletedAtColumn($this); + $deletedAtColumn = Resolver::getDeletedAtColumn($this); $this->$deletedAtColumn = new UTCDateTime(new DateTime('now')); return $this->update(); @@ -52,25 +52,16 @@ public static function withTrashed( private static function searchWithDataMapper(mixed $query, array $projection, bool $useCache): CursorInterface { - $mapper = self::getDataMapperInstance(); - - $mapper->withTrashed = true; - - return $mapper->where($query, $projection, $useCache); + return self::getDataMapperInstance() + ->withoutSoftDelete() + ->where($query, $projection, $useCache); } private static function searchWithBuilder(mixed $query, array $projection, bool $useCache): CursorInterface { - $mapper = self::getBuilderInstance(); - - $mapper->withTrashed = true; - - return $mapper->where( - new static(), - $query, - $projection, - $useCache - ); + return self::getBuilderInstance() + ->withoutSoftDelete() + ->where(new static(), $query, $projection, $useCache); } private static function performSearch(mixed $query, array $projection, bool $useCache): CursorInterface diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 75fe5c94..9e633362 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -11,8 +11,6 @@ use Mongolid\Event\EventTriggerService; 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 @@ -20,7 +18,7 @@ */ class Builder { - public bool $withTrashed = false; + private bool $ignoreSoftDelete = false; /** * Connection that is going to be used to interact with the database. @@ -205,10 +203,11 @@ public function where(ModelInterface $model, $query = [], array $projection = [] { $cursor = $useCache ? CacheableCursor::class : Cursor::class; - $query = $this->withTrashed ? - QueryBuilder::prepareValueForQueryCompatibility($query) : - QueryBuilder::prepareValueForSoftDeleteCompatibility($query, $model); - + $query = Resolver::resolveQuery( + $query, + $model, + $this->ignoreSoftDelete + ); return new $cursor( $model->getCollection(), 'find', @@ -252,8 +251,13 @@ public function first(ModelInterface $model, $query = [], array $projection = [] return $this->where($model, $query, $projection, $useCache)->first(); } + $query = Resolver::resolveQuery( + $query, + $model, + ); + return $model->getCollection()->findOne( - QueryBuilder::prepareValueForSoftDeleteCompatibility($query, $model), + $query, ['projection' => $this->prepareProjection($projection)], ); } @@ -280,6 +284,13 @@ public function firstOrFail(ModelInterface $model, $query = [], array $projectio throw (new ModelNotFoundException())->setModel(get_class($model)); } + public function withoutSoftDelete(): self + { + $this->ignoreSoftDelete = true; + + return $this; + } + /** * Triggers an event. May return if that event had success. * diff --git a/src/Util/QueryBuilder.php b/src/Query/Resolver.php similarity index 73% rename from src/Util/QueryBuilder.php rename to src/Query/Resolver.php index b3fa3cea..75e543fd 100644 --- a/src/Util/QueryBuilder.php +++ b/src/Query/Resolver.php @@ -1,15 +1,24 @@ isSoftDeleteEnabled ?? false) { + return $query; + } + + if ($ignoreSoftDelete) { + return $query; + } return self::addSoftDeleteFilterIfRequired($query, $model); } @@ -23,7 +32,7 @@ public static function getDeletedAtColumn(ModelInterface $model): string : 'deleted_at'; } - public static function prepareValueForQueryCompatibility(mixed $query): array + private static function prepareIdForQueryCompatibility(mixed $query): array { if (!is_array($query)) { $query = ['_id' => $query]; @@ -49,18 +58,14 @@ public static function prepareValueForQueryCompatibility(mixed $query): array private static function addSoftDeleteFilterIfRequired(array $query, ModelInterface $model): array { - if ($model->isSoftDeleteEnabled) { - $field = self::getDeletedAtColumn($model); - - return array_merge( - $query, - [ - $field => ['$exists' => false], - ] - ); - } - - return $query; + $field = self::getDeletedAtColumn($model); + + return array_merge( + $query, + [ + $field => ['$exists' => false], + ] + ); } private static function convertStringIdsToObjectIds(array $query): array diff --git a/tests/Integration/PersistLegacyModelWithSoftDeleteTest.php b/tests/Integration/PersistLegacyModelWithSoftDeleteTest.php index afebadb6..531dbd8d 100644 --- a/tests/Integration/PersistLegacyModelWithSoftDeleteTest.php +++ b/tests/Integration/PersistLegacyModelWithSoftDeleteTest.php @@ -10,8 +10,6 @@ final class PersistLegacyModelWithSoftDeleteTest extends IntegrationTestCase { - private ObjectId $_id; - public function testShouldFindUndeletedProduct(): void { // Set diff --git a/tests/Unit/Model/SoftDeletesTraitTest.php b/tests/Unit/Model/SoftDeletesTraitTest.php index 424996c9..1ce46a8f 100644 --- a/tests/Unit/Model/SoftDeletesTraitTest.php +++ b/tests/Unit/Model/SoftDeletesTraitTest.php @@ -7,10 +7,13 @@ use MongoDB\BSON\UTCDateTime; use Mongolid\Cursor\CursorInterface; use Mongolid\DataMapper\DataMapper; +use Mongolid\Query\Builder; use Mongolid\Schema\DynamicSchema; use Mongolid\TestCase; -use Mongolid\Tests\Stubs\Legacy\ProductWithSoftDelete; -use Mongolid\Tests\Stubs\Legacy\Product; +use Mongolid\Tests\Stubs\Legacy\ProductWithSoftDelete as LegacyProductWithSoftDelete; +use Mongolid\Tests\Stubs\Legacy\Product as LegacyProduct; +use Mongolid\Tests\Stubs\Product; +use Mongolid\Tests\Stubs\ProductWithSoftDelete; class SoftDeletesTraitTest extends TestCase { @@ -23,7 +26,7 @@ public function testShouldReturnStatusOfSoftDelete( bool $isFillable = true ): void { // Set - $product = new ProductWithSoftDelete(); + $product = new LegacyProductWithSoftDelete(); if ($isFillable) { $product->deleted_at = $date; @@ -58,7 +61,7 @@ public function getSoftDeleteStatus(): array public function testShouldRestoreProduct(): void { // Set - $product = new ProductWithSoftDelete(); + $product = new LegacyProductWithSoftDelete(); $date = new UTCDateTime(new DateTime('today')); $product->deleted_at = $date; @@ -72,7 +75,7 @@ public function testShouldRestoreProduct(): void ->setSchema(m::type(DynamicSchema::class)); $dataMapper->expects() - ->update(m::type(Product::class), m::type('array')) + ->update(m::type(LegacyProduct::class), m::type('array')) ->andReturnTrue(); // Actions @@ -85,7 +88,7 @@ public function testShouldRestoreProduct(): void public function testShouldNotRestoreProduct(): void { // Set - $product = new ProductWithSoftDelete(); + $product = new LegacyProductWithSoftDelete(); // Actions $actual = $product->restore(); @@ -94,10 +97,10 @@ public function testShouldNotRestoreProduct(): void $this->assertFalse($actual); } - public function testShouldForceDeleteOnProduct(): void + public function testShouldForceDeleteForLegacyModel(): void { // Set - $product = new ProductWithSoftDelete(); + $product = new LegacyProductWithSoftDelete(); $dataMapper = $this->instance( DataMapper::class, @@ -109,7 +112,7 @@ public function testShouldForceDeleteOnProduct(): void ->setSchema(m::type(DynamicSchema::class)); $dataMapper->expects() - ->delete(m::type(Product::class), m::type('array')) + ->delete(m::type(LegacyProduct::class), m::type('array')) ->andReturnTrue(); // Actions @@ -119,10 +122,10 @@ public function testShouldForceDeleteOnProduct(): void $this->assertTrue($actual); } - public function testShouldExecuteSoftDeleteProduct(): void + public function testShouldExecuteSoftDeleteForLegacyModel(): void { // Set - $product = new ProductWithSoftDelete(); + $product = new LegacyProductWithSoftDelete(); $dataMapper = $this->instance( DataMapper::class, @@ -134,7 +137,7 @@ public function testShouldExecuteSoftDeleteProduct(): void ->setSchema(m::type(DynamicSchema::class)); $dataMapper->expects() - ->update(m::type(Product::class), m::type('array')) + ->update(m::type(LegacyProduct::class), m::type('array')) ->andReturnTrue(); // Actions @@ -144,10 +147,10 @@ public function testShouldExecuteSoftDeleteProduct(): void $this->assertTrue($actual); } - public function testShouldNotExecuteSoftDelete(): void + public function testShouldNotExecuteSoftForLegacyModel(): void { // Set - $product = new Product(); + $product = new LegacyProduct(); $dataMapper = $this->instance( DataMapper::class, @@ -159,7 +162,7 @@ public function testShouldNotExecuteSoftDelete(): void ->setSchema(m::type(DynamicSchema::class)); $dataMapper->expects() - ->delete(m::type(Product::class), m::type('array')) + ->delete(m::type(LegacyProduct::class), m::type('array')) ->andReturnTrue(); // Actions @@ -169,10 +172,10 @@ public function testShouldNotExecuteSoftDelete(): void $this->assertTrue($actual); } - public function testShouldFindWithTrashedProducts(): void + public function testShouldFindWithTrashedForLegacyModel(): void { // Set - $product = new ProductWithSoftDelete(); + $product = new LegacyProductWithSoftDelete(); $cursor = m::mock(CursorInterface::class); $dataMapper = $this->instance( @@ -188,6 +191,103 @@ public function testShouldFindWithTrashedProducts(): void ->where('123', [], false) ->andReturn($cursor); + $dataMapper->expects() + ->withoutSoftDelete() + ->andReturnSelf(); + + // Actions + $actual = $product->withTrashed('123'); + + // Assertions + $this->assertInstanceOf(CursorInterface::class, $actual); + } + + public function testShouldForceDeleteForModel(): void + { + // Set + $product = new ProductWithSoftDelete(); + + $builder = $this->instance( + Builder::class, + m::mock(Builder::class) + ); + + // Expectations + $builder->expects() + ->delete(m::type(Product::class), m::type('array')) + ->andReturnTrue(); + + // Actions + $actual = $product->forceDelete(); + + // Assertions + $this->assertTrue($actual); + } + + public function testShouldExecuteSoftDeleteForModel(): void + { + // Set + $product = new ProductWithSoftDelete(); + + $builder = $this->instance( + Builder::class, + m::mock(Builder::class) + ); + + // Expectations + $builder->expects() + ->update(m::type(Product::class), m::type('array')) + ->andReturnTrue(); + + // Actions + $actual = $product->delete(); + + // Assertions + $this->assertTrue($actual); + } + + public function testShouldNotExecuteSoftForModel(): void + { + // Set + $product = new Product(); + + $builder = $this->instance( + Builder::class, + m::mock(Builder::class) + ); + + // Expectations + $builder->expects() + ->delete(m::type(Product::class), m::type('array')) + ->andReturnTrue(); + + // Actions + $actual = $product->delete(); + + // Assertions + $this->assertTrue($actual); + } + + public function testShouldFindWithTrashedForModel(): void + { + // Set + $product = new ProductWithSoftDelete(); + $cursor = m::mock(CursorInterface::class); + + $builder = $this->instance( + Builder::class, + m::mock(Builder::class) + ); + + // Expectations + $builder->expects() + ->where(m::type(ProductWithSoftDelete::class), '123', [], false) + ->andReturn($cursor); + + $builder->expects() + ->withoutSoftDelete() + ->andReturnSelf(); + // Actions $actual = $product->withTrashed('123'); diff --git a/tests/Unit/Util/QueryBuilderTest.php b/tests/Unit/Util/QueryBuilderTest.php index 19841e6a..e0c3a127 100644 --- a/tests/Unit/Util/QueryBuilderTest.php +++ b/tests/Unit/Util/QueryBuilderTest.php @@ -5,6 +5,7 @@ use Mockery as m; use MongoDB\BSON\ObjectId; use Mongolid\Model\ModelInterface; +use Mongolid\Query\Resolver; use Mongolid\TestCase; use Mongolid\Tests\Stubs\Legacy\ProductWithSoftDelete; @@ -14,11 +15,14 @@ final class QueryBuilderTest extends TestCase * @dataProvider queryValueScenarios */ public function testShouldPrepareQueryValue( - mixed $value, + mixed $query, + bool $isSoftDeleteEnabled, array $expectation ): void { // Actions - $result = QueryBuilder::prepareValueForQueryCompatibility($value); + $model = m::mock(ModelInterface::class); + $model->isSoftDeleteEnabled = $isSoftDeleteEnabled; + $result = Resolver::resolveQuery($query, $model, false); // Assertions $this->assertMongoQueryEquals($expectation, $result); @@ -28,77 +32,52 @@ public function queryValueScenarios(): array { return [ 'An array' => [ - 'value' => ['age' => ['$gt' => 25]], + 'query' => ['age' => ['$gt' => 25]], + 'isSoftDeleteEnabled' => false, 'expectation' => ['age' => ['$gt' => 25]], ], 'An ObjectId string' => [ - 'value' => '507f1f77bcf86cd799439011', + 'query' => '507f1f77bcf86cd799439011', + 'isSoftDeleteEnabled' => false, 'expectation' => [ - '_id' => new ObjectID( + '_id' => new ObjectId( '507f1f77bcf86cd799439011' ), ], ], 'An ObjectId string within a query' => [ - 'value' => ['_id' => '507f1f77bcf86cd799439011'], + 'query' => ['_id' => '507f1f77bcf86cd799439011'], + 'isSoftDeleteEnabled' => false, 'expectation' => [ - '_id' => new ObjectID( + '_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']]], + 'query' => ['_id' => ['$in' => ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012']]], + 'isSoftDeleteEnabled' => false, 'expectation' => [ '_id' => [ '$in' => [ - new ObjectID('507f1f77bcf86cd799439011'), - new ObjectID('507f1f77bcf86cd799439012'), + new ObjectId('507f1f77bcf86cd799439011'), + new ObjectId('507f1f77bcf86cd799439012'), ], ], ], ], - 'Series of string _ids as the $in parameter' => [ - 'value' => ['_id' => ['$nin' => ['507f1f77bcf86cd799439011']]], + 'Series of string _ids as the $nin parameter' => [ + 'query' => ['_id' => ['$nin' => ['507f1f77bcf86cd799439011']]], + 'isSoftDeleteEnabled' => false, 'expectation' => [ '_id' => [ - '$nin' => [new ObjectID( + '$nin' => [new ObjectId( '507f1f77bcf86cd799439011' ), ], ], ], ], - ]; - } - - /** - * @dataProvider getQuery - */ - public function testShouldPrepareValueForSoftDeleteCompatibility( - mixed $query, - bool $isSoftDeleteEnabled, - array $expected - ): void { - // Set - $model = m::mock(ModelInterface::class); - $model->isSoftDeleteEnabled = $isSoftDeleteEnabled; - - // Actions - $actual = QueryBuilder::prepareValueForSoftDeleteCompatibility($query, $model); - - // Assertions - $this->assertSame($expected, $actual); - } - - public function getQuery(): array - { - $objectId = new ObjectId('64e8963b1de34f08a40502e0'); - return [ 'When query is a string and softDelete is enabled' => [ 'query' => '123', 'isSoftDeleteEnabled' => true, @@ -156,7 +135,7 @@ public function testShouldGetDeleteAtColumn(bool $isDefault, string $expected): $model = $this->buildProduct($isDefault); // Actions - $actual = QueryBuilder::getDeletedAtColumn($model); + $actual = Resolver::getDeletedAtColumn($model); // Assertions $this->assertSame($expected, $actual);