diff --git a/src/Collection/Filter/Filter.php b/src/Collection/Filter/Filter.php new file mode 100644 index 00000000..78592579 --- /dev/null +++ b/src/Collection/Filter/Filter.php @@ -0,0 +1,53 @@ +find = new FindFilter(); + $this->order = new OrderFilter(); + } + + public function find(): FindFilter + { + return $this->find; + } + + public function order(): OrderFilter + { + return $this->order; + } + + public function limit(int $count, ?int $offset = null): void + { + $this->limitCount = $count; + $this->limitOffset = $offset; + } + + /** + * @return array{int|null, int|null} + */ + public function getLimit(): array + { + return [ + $this->limitCount, + $this->limitOffset, + ]; + } + +} diff --git a/src/Collection/Filter/FindFilter.php b/src/Collection/Filter/FindFilter.php new file mode 100644 index 00000000..3d4c6786 --- /dev/null +++ b/src/Collection/Filter/FindFilter.php @@ -0,0 +1,179 @@ + */ + private $conditions = []; + + /** @var string */ + private $logicalOperator; + + public function __construct(string $logicalOperator = ICollection::AND) + { + $this->logicalOperator = $logicalOperator; + } + + /** + * @param mixed $value + */ + public function equal(string $property, $value): void + { + $this->operator('', $property, $value); + } + + /** + * @param mixed $value + */ + public function notEqual(string $property, $value): void + { + $this->operator('!=', $property, $value); + } + + /** + * @param mixed $number + */ + public function greater(string $property, $number): void + { + $this->operator('>', $property, $number); + } + + /** + * @param mixed $number + */ + public function greaterOrEqual(string $property, $number): void + { + $this->operator('>=', $property, $number); + } + + /** + * @param mixed $number + */ + public function lower(string $property, $number): void + { + $this->operator('<', $property, $number); + } + + /** + * @param mixed $number + */ + public function lowerOrEqual(string $property, $number): void + { + $this->operator('<=', $property, $number); + } + + public function like(string $property, LikeExpression $expression): void + { + $this->operator('~', $property, $expression); + } + + /** + * @param mixed $value + */ + public function operator(string $operator, string $property, $value): void + { + $this->raw([ + "{$property}{$operator}" => $value, + ]); + } + + /** + * @param class-string $function + * @param string|array $expression + * @param mixed $values + */ + public function function(string $function, $expression, ...$values): void + { + $this->raw($this->createFunction($function, $expression, ...$values)); + } + + /** + * @param class-string $function + * @param string|array $expression + * @param mixed $values + * @return array + */ + public function createFunction(string $function, $expression, ...$values): array + { + return array_merge([$function, $expression], $values); + } + + /** + * @param array $condition + */ + public function raw(array $condition): void + { + $this->conditions[] = $condition; + } + + /** + * @param Closure(FindFilter): void $conditions + */ + public function and(Closure $conditions): void + { + $this->logicalOperator($conditions, ICollection::AND); + } + + /** + * @param Closure(FindFilter): void $conditions + */ + public function or(Closure $conditions): void + { + $this->logicalOperator($conditions, ICollection::OR); + } + + /** + * @param Closure(FindFilter): void $conditions + */ + private function logicalOperator(Closure $conditions, string $operator): void + { + $find = new FindFilter($operator); + + $conditions($find); + + $raw = $find->getConditions(); + + if ($raw === []) { + return; + } + + $this->raw($raw); + } + + /** + * @return array + */ + public function getConditions(): array + { + // No conditions, empty result + $count = count($this->conditions); + if ($count === 0) { + return []; + } + + // Only condition is inner logical operator, optimize it + if ($count === 1) { + $key = $this->conditions[0][0] ?? null; + if ($key === ICollection::AND || $key === ICollection::OR) { + return $this->conditions[0]; + } + } + + $conditions = $this->conditions; + array_unshift($conditions, $this->logicalOperator); + + return $conditions; + } + +} diff --git a/src/Collection/Filter/OrderFilter.php b/src/Collection/Filter/OrderFilter.php new file mode 100644 index 00000000..cf77a080 --- /dev/null +++ b/src/Collection/Filter/OrderFilter.php @@ -0,0 +1,61 @@ +, 1:string}> */ + private $order = []; + + public function property(string $property, string $direction = ICollection::ASC): void + { + $this->raw($property, $direction); + } + + /** + * @param class-string $function + * @param string|array $expression + * @param mixed $values + */ + public function function(string $function, $expression, string $direction = ICollection::ASC, ...$values): void + { + $this->raw( + $this->createFunction($function, $expression, ...$values), + $direction, + ); + } + + /** + * @param class-string $function + * @param string|array $expression + * @param mixed $values + * @return array + */ + public function createFunction(string $function, $expression, ...$values): array + { + return array_merge([$function, $expression], $values); + } + + /** + * @param string|array $expression + */ + public function raw($expression, string $direction = ICollection::ASC): void + { + $this->order[] = [$expression, $direction]; + } + + /** + * @return array, 1:string}> + */ + public function getOrder(): array + { + return $this->order; + } + +} diff --git a/src/Repository/Repository.php b/src/Repository/Repository.php index 6efacdc8..78e89729 100644 --- a/src/Repository/Repository.php +++ b/src/Repository/Repository.php @@ -11,6 +11,8 @@ use Nextras\Orm\Collection\ArrayCollection; +use Nextras\Orm\Collection\Filter\Filter; +use Nextras\Orm\Collection\Filter\FindFilter; use Nextras\Orm\Collection\Functions\AvgAggregateFunction; use Nextras\Orm\Collection\Functions\CompareEqualsFunction; use Nextras\Orm\Collection\Functions\CompareGreaterThanEqualsFunction; @@ -249,6 +251,22 @@ public function getByIdChecked($id): IEntity } + public function getByFilter(FindFilter $find): ?IEntity + { + return $this->getBy($find->getConditions()); + } + + + public function getByFilterChecked(FindFilter $find): IEntity + { + $entity = $this->getBy($find->getConditions()); + if ($entity === null) { + throw new NoResultException(); + } + return $entity; + } + + /** {@inheritdoc} */ public function findAll(): ICollection { @@ -278,6 +296,28 @@ public function findById($ids): ICollection } + /** + * @phpstan-return ICollection + */ + public function findByFilter(Filter $filter): ICollection + { + $conditions = $filter->find()->getConditions(); + $collection = $conditions === [] ? $this->findAll() : $this->findBy($conditions); + + $order = $filter->order()->getOrder(); + foreach ($order as [$expression, $direction]) { + $collection = $collection->orderBy($expression, $direction); + } + + [$limitCount, $limitOffset] = $filter->getLimit(); + if ($limitCount !== null) { + $collection = $collection->limitBy($limitCount, $limitOffset); + } + + return $collection; + } + + /** {@inheritdoc} */ public function findByIds(array $ids): ICollection { @@ -347,6 +387,12 @@ protected function createCollectionFunction(string $name) } + public function createFilter(): Filter + { + return new Filter(); + } + + /** {@inheritdoc} */ public function attach(IEntity $entity): void { diff --git a/tests/cases/unit/Collection/Filter/FilterTest.phpt b/tests/cases/unit/Collection/Filter/FilterTest.phpt new file mode 100644 index 00000000..acf83329 --- /dev/null +++ b/tests/cases/unit/Collection/Filter/FilterTest.phpt @@ -0,0 +1,45 @@ +find()->getConditions()); + Assert::same([], $filter->order()->getOrder()); + Assert::same([null, null], $filter->getLimit()); + } + + public function testLimit(): void + { + $filter = new Filter(); + + Assert::same([null, null], $filter->getLimit()); + + $filter->limit(10, 5); + Assert::same([10, 5], $filter->getLimit()); + + $filter->limit(10); + Assert::same([10, null], $filter->getLimit()); + } + +} + + +$test = new FilterTest(); +$test->run(); diff --git a/tests/cases/unit/Collection/Filter/FindFilterTest.phpt b/tests/cases/unit/Collection/Filter/FindFilterTest.phpt new file mode 100644 index 00000000..d6ed0f79 --- /dev/null +++ b/tests/cases/unit/Collection/Filter/FindFilterTest.phpt @@ -0,0 +1,212 @@ +getConditions()); + } + + public function testOperators(): void + { + $find = new FindFilter(); + + $find->equal('equal', 'v'); + $find->notEqual('notEqual', 'v'); + $find->greater('greater', 1); + $find->greaterOrEqual('greaterOrEqual', 1); + $find->lower('lower', 1); + $find->lowerOrEqual('lowerOrEqual', 1); + $find->like('like', $like = LikeExpression::startsWith('l')); + $find->operator('??', 'operator', 'v'); + + Assert::same( + [ + ICollection::AND, + ['equal' => 'v'], + ['notEqual!=' => 'v'], + ['greater>' => 1], + ['greaterOrEqual>=' => 1], + ['lower<' => 1], + ['lowerOrEqual<=' => 1], + ['like~' => $like], + ['operator??' => 'v'], + ], + $find->getConditions() + ); + } + + public function testFunctions(): void + { + $find = new FindFilter(); + + $f1 = $find->createFunction(CountAggregateFunction::class, 'property'); + $f2 = $find->createFunction(CompareEqualsFunction::class, 'property', 'val1', 'val2', 'val3'); + + Assert::same( + [ + CountAggregateFunction::class, + 'property', + ], + $f1 + ); + + Assert::same( + [ + CompareEqualsFunction::class, + 'property', + 'val1', + 'val2', + 'val3', + ], + $f2 + ); + + $find->function(CompareEqualsFunction::class, 'property'); + $find->function(CompareGreaterThanFunction::class, $f1); + $find->function(SumAggregateFunction::class, 'property', 'val1', 'val2'); + + Assert::same( + [ + ICollection::AND, + [ + CompareEqualsFunction::class, + 'property', + ], + [ + CompareGreaterThanFunction::class, + [ + CountAggregateFunction::class, + 'property', + ], + ], + [ + SumAggregateFunction::class, + 'property', + 'val1', + 'val2', + ], + ], + $find->getConditions() + ); + } + + public function testRaw(): void + { + $find = new FindFilter(); + + $find->raw(['raw', 'array']); + + Assert::same( + [ + ICollection::AND, + ['raw', 'array'], + ], + $find->getConditions() + ); + } + + public function testOr(): void + { + $find = new FindFilter(); + + $find->or(static function (FindFilter $scope): void { + $scope->equal('property', '2b'); + $scope->notEqual('property', '2b'); + }); + + Assert::same( + [ + ICollection::OR, + ['property' => '2b'], + ['property!=' => '2b'], + ], + $find->getConditions() + ); + } + + public function testAnd(): void + { + $find = new FindFilter(); + + $find->and(static function (FindFilter $scope): void { + $scope->greaterOrEqual('property', 10); + $scope->lowerOrEqual('property', 20); + }); + + Assert::same( + [ + ICollection::AND, + ['property>=' => 10], + ['property<=' => 20], + ], + $find->getConditions(), + ); + } + + public function testLogicalOperators(): void + { + $find = new FindFilter(); + + $find->and(static function (FindFilter $scope): void { + $scope->greaterOrEqual('property1', 10); + $scope->lowerOrEqual('property1', 20); + }); + + $find->or(static function (FindFilter $scope): void { + $scope->equal('property2', '2b'); + $scope->notEqual('property2', '2b'); + }); + + $find->and(static function (FindFilter $scope): void { + // Empty, does not render + }); + + $find->or(static function (FindFilter $scope): void { + // Empty, does not render + }); + + Assert::same( + [ + ICollection::AND, + [ + ICollection::AND, + ['property1>=' => 10], + ['property1<=' => 20], + ], + [ + ICollection::OR, + ['property2' => '2b'], + ['property2!=' => '2b'], + ], + ], + $find->getConditions() + ); + } + +} + +$test = new FindFilterTest(); +$test->run(); diff --git a/tests/cases/unit/Collection/Filter/OrderFilterTest.phpt b/tests/cases/unit/Collection/Filter/OrderFilterTest.phpt new file mode 100644 index 00000000..f9608e29 --- /dev/null +++ b/tests/cases/unit/Collection/Filter/OrderFilterTest.phpt @@ -0,0 +1,134 @@ +getOrder() + ); + } + + public function testProperties(): void + { + $order = new OrderFilter(); + + $order->property('property1'); + $order->property('property2', ICollection::DESC); + + Assert::same( + [ + ['property1', ICollection::ASC], + ['property2', ICollection::DESC], + ], + $order->getOrder() + ); + } + + public function testFunction(): void + { + $order = new OrderFilter(); + + $f1 = $order->createFunction(CountAggregateFunction::class, 'property'); + $f2 = $order->createFunction(CompareEqualsFunction::class, 'property', 'val1', 'val2', 'val3'); + + Assert::same( + [ + CountAggregateFunction::class, + 'property', + ], + $f1 + ); + + Assert::same( + [ + CompareEqualsFunction::class, + 'property', + 'val1', + 'val2', + 'val3', + ], + $f2 + ); + + $order->function(CompareEqualsFunction::class, 'property'); + $order->function(CompareGreaterThanFunction::class, $f1); + $order->function(SumAggregateFunction::class, 'property', ICollection::DESC, 'val1', 'val2'); + + Assert::same( + [ + [ + [ + CompareEqualsFunction::class, + 'property', + ], + ICollection::ASC, + ], + [ + [ + CompareGreaterThanFunction::class, + [ + CountAggregateFunction::class, + 'property', + ], + ], + ICollection::ASC, + ], + [ + [ + SumAggregateFunction::class, + 'property', + 'val1', + 'val2', + ], + ICollection::DESC, + ], + ], + $order->getOrder() + ); + } + + public function testRaw(): void + { + $order = new OrderFilter(); + + $order->raw('property1'); + $order->raw('property2', ICollection::DESC); + $order->raw(['raw', 'array']); + + Assert::same( + [ + ['property1', ICollection::ASC], + ['property2', ICollection::DESC], + [['raw', 'array'], ICollection::ASC], + ], + $order->getOrder() + ); + } + +} + +$test = new OrderFilterTest(); +$test->run();