From 5904fd772aed20facd059c33641bf2b1c054a67f Mon Sep 17 00:00:00 2001 From: Unay Santisteban Date: Tue, 9 Jan 2024 17:12:39 +1000 Subject: [PATCH] Feature/Add build from source. --- src/Contracts/CriteriaSource.php | 58 +++++++++++++++++++++ src/Criteria.php | 86 ++++++++++++++++++++++++++------ src/Errors/CriteriaError.php | 59 ++++++++++++++++++++++ src/FilterGroup.php | 30 +++++------ src/Order.php | 4 +- tests/CriteriaTest.php | 51 +++++++++++++++++++ 6 files changed, 256 insertions(+), 32 deletions(-) create mode 100644 src/Contracts/CriteriaSource.php create mode 100644 src/Errors/CriteriaError.php diff --git a/src/Contracts/CriteriaSource.php b/src/Contracts/CriteriaSource.php new file mode 100644 index 0000000..ff1e2e1 --- /dev/null +++ b/src/Contracts/CriteriaSource.php @@ -0,0 +1,58 @@ + + * @package ComplexHeart\Domain\Criteria\Contracts + */ +interface CriteriaSource +{ + /** + * Provides the list of filter groups. Each filter group is a list of + * filters. A filter is the combination of field or attribute, operator + * and value: + * + * [ + * [ + * ["field" => "title", "operator" => "like", "value" => "to hero"], + * ["field" => "tag", "operator" => "in", "value" => ["beginner", "intermediate"]], + * ], + * ]; + * + * @return array>> + */ + public function filterGroups(): array; + + /** + * One of: asc, desc, none or random. + * + * @return string + */ + public function orderType(): string; + + /** + * The field or attribute to order by. + * + * @return string + */ + public function orderBy(): string; + + /** + * Provides the size of a page. + * + * @return int + */ + public function pageLimit(): int; + + /** + * Provides the offset, by default should be 0. + * + * @return int + */ + public function pageOffset(): int; +} diff --git a/src/Criteria.php b/src/Criteria.php index 1e596cf..995bbcb 100644 --- a/src/Criteria.php +++ b/src/Criteria.php @@ -6,6 +6,8 @@ use Closure; use ComplexHeart\Domain\Contracts\Model\ValueObject; +use ComplexHeart\Domain\Criteria\Contracts\CriteriaSource; +use ComplexHeart\Domain\Criteria\Errors\CriteriaError; use ComplexHeart\Domain\Model\IsValueObject; use function Lambdish\Phunctional\map; @@ -32,6 +34,27 @@ public function __construct( private readonly Order $order, private readonly Page $page, ) { + $this->check(); + } + + protected function invariantGroupsMustBeArrayOfFilterGroup(): bool + { + foreach ($this->groups as $group) { + if (!($group instanceof FilterGroup)) { + return false; + } + } + + return true; + } + + /** + * @param array $violations + * @return void + */ + protected function invariantHandler(array $violations): void + { + throw CriteriaError::create('Unable to create criteria object.', $violations); } /** @@ -42,12 +65,30 @@ public function __construct( */ public static function create(array $groups, Order $order, Page $page): self { - return new self($groups, $order, $page); + return new self(groups: $groups, order: $order, page: $page); + } + + /** + * Creates a new instance of Criteria from the given data source. + * + * @param CriteriaSource $source + * @return Criteria + */ + public static function fromSource(CriteriaSource $source): self + { + return Criteria::create( + groups: map( + fn(array $g): FilterGroup => FilterGroup::createFromArray($g), + $source->filterGroups() + ), + order: Order::create($source->orderBy(), OrderType::make($source->orderType())), + page: Page::create($source->pageLimit(), $source->pageOffset()) + ); } public static function default(): self { - return self::create([], Order::none(), Page::create()); + return self::create(groups: [], order: Order::none(), page: Page::create()); } /** @@ -58,7 +99,11 @@ public static function default(): self */ public function withFilterGroups(array $groups): self { - return self::create($groups, $this->order, $this->page); + return self::create( + groups: $groups, + order: $this->order, + page: $this->page + ); } /** @@ -73,50 +118,61 @@ public function withFilterGroup(FilterGroup|Closure $group): self $group = $group(new FilterGroup()); } - return $this->withFilterGroups(array_merge($this->groups, [$group])); + // push single FilterGroup into an array. + $group = is_array($group) ? $group : [$group]; + + return $this->withFilterGroups(groups: array_merge($this->groups, $group)); } public function withOrder(Order $order): self { - return self::create($this->groups, $order, $this->page); + return self::create(groups: $this->groups, order: $order, page: $this->page); } public function withOrderRandom(): self { - return self::create($this->groups, Order::random(), $this->page); + return self::create(groups: $this->groups, order: Order::random(), page: $this->page); } public function withOrderBy(string $field): self { - return self::create($this->groups, Order::create($field, $this->order->type()), $this->page); + return self::create( + groups: $this->groups, + order: Order::create($field, $this->order->type()), + page: $this->page + ); } public function withOrderType(string $type): self { return self::create( - $this->groups, - Order::create($this->orderBy(), OrderType::make($type)), - $this->page + groups: $this->groups, + order: Order::create($this->orderBy(), OrderType::make($type)), + page: $this->page ); } public function withPage(Page $page): self { - return self::create($this->groups, $this->order, $page); + return self::create(groups: $this->groups, order: $this->order, page: $page); } public function withPageOffset(int $offset): self { return self::create( - $this->groups, - $this->order, - Page::create($this->pageLimit(), $offset) + groups: $this->groups, + order: $this->order, + page: Page::create($this->pageLimit(), $offset) ); } public function withPageLimit(int $limit): self { - return self::create($this->groups, $this->order, Page::create($limit, $this->pageOffset())); + return self::create( + groups: $this->groups, + order: $this->order, + page: Page::create($limit, $this->pageOffset()) + ); } /** diff --git a/src/Errors/CriteriaError.php b/src/Errors/CriteriaError.php new file mode 100644 index 0000000..69394c8 --- /dev/null +++ b/src/Errors/CriteriaError.php @@ -0,0 +1,59 @@ + + * @package ComplexHeart\Domain\Criteria\Errors + */ +class CriteriaError extends Error +{ + /** + * List of invariant violations. + * + * @var array + */ + private array $violations; + + /** + * @param string $message + * @param int $code + * @param Throwable|null $previous + * @param array $violations + */ + public function __construct(string $message = "", int $code = 0, Throwable $previous = null, array $violations = []) + { + parent::__construct($message, $code, $previous); + + $this->violations = $violations; + } + + /** + * @param string $message + * @param array $violations + * @param int $code + * @param Throwable|null $previous + * @return self + */ + public static function create(string $message, array $violations, int $code = 0, Throwable $previous = null): self + { + return new self($message, $code, $previous, $violations); + } + + /** + * Returns the list of invariant violations. + * + * @return array + */ + public function violations(): array + { + return $this->violations; + } +} diff --git a/src/FilterGroup.php b/src/FilterGroup.php index 3e7f491..182e0bd 100644 --- a/src/FilterGroup.php +++ b/src/FilterGroup.php @@ -36,7 +36,7 @@ public function __construct(array $items = []) * @param Filter ...$filters * @return FilterGroup */ - public static function create(Filter ...$filters): FilterGroup + public static function create(Filter ...$filters): self { return new self(array_values($filters)); } @@ -45,7 +45,7 @@ public static function create(Filter ...$filters): FilterGroup * @param array> $filters * @return FilterGroup */ - public static function createFromArray(array $filters): FilterGroup + public static function createFromArray(array $filters): self { return self::create( ...map(fn(array $filter): Filter => Filter::createFromArray($filter), $filters) @@ -59,7 +59,7 @@ public static function createFromArray(array $filters): FilterGroup * * @return self */ - public function addFilter(Filter $new): FilterGroup + public function addFilter(Filter $new): self { if ($this->filter(fn(Filter $filter): bool => $filter->equals($new))->count() > 0) { return $this; @@ -70,37 +70,37 @@ public function addFilter(Filter $new): FilterGroup return $this; } - public function addFilterEqual(string $field, mixed $value): FilterGroup + public function addFilterEqual(string $field, mixed $value): self { $this->addFilter(Filter::createEqual($field, $value)); return $this; } - public function addFilterNotEqual(string $field, mixed $value): FilterGroup + public function addFilterNotEqual(string $field, mixed $value): self { $this->addFilter(Filter::createNotEqual($field, $value)); return $this; } - public function addFilterGreaterThan(string $field, mixed $value): FilterGroup + public function addFilterGreaterThan(string $field, mixed $value): self { $this->addFilter(Filter::createGreaterThan($field, $value)); return $this; } - public function addFilterGreaterOrEqualThan(string $field, mixed $value): FilterGroup + public function addFilterGreaterOrEqualThan(string $field, mixed $value): self { $this->addFilter(Filter::createGreaterOrEqualThan($field, $value)); return $this; } - public function addFilterLessThan(string $field, mixed $value): FilterGroup + public function addFilterLessThan(string $field, mixed $value): self { $this->addFilter(Filter::createLessThan($field, $value)); return $this; } - public function addFilterLessOrEqualThan(string $field, mixed $value): FilterGroup + public function addFilterLessOrEqualThan(string $field, mixed $value): self { $this->addFilter(Filter::createLessOrEqualThan($field, $value)); return $this; @@ -111,7 +111,7 @@ public function addFilterLessOrEqualThan(string $field, mixed $value): FilterGro * @param array $value * @return $this */ - public function addFilterIn(string $field, array $value): FilterGroup + public function addFilterIn(string $field, array $value): self { $this->addFilter(Filter::createIn($field, $value)); return $this; @@ -122,31 +122,31 @@ public function addFilterIn(string $field, array $value): FilterGroup * @param array $value * @return $this */ - public function addFilterNotIn(string $field, array $value): FilterGroup + public function addFilterNotIn(string $field, array $value): self { $this->addFilter(Filter::createNotIn($field, $value)); return $this; } - public function addFilterLike(string $field, string $value): FilterGroup + public function addFilterLike(string $field, string $value): self { $this->addFilter(Filter::createLike($field, $value)); return $this; } - public function addFilterNotLike(string $field, string $value): FilterGroup + public function addFilterNotLike(string $field, string $value): self { $this->addFilter(Filter::createNotLike($field, $value)); return $this; } - public function addFilterContains(string $field, string $value): FilterGroup + public function addFilterContains(string $field, string $value): self { $this->addFilter(Filter::createContains($field, $value)); return $this; } - public function addFilterNotContains(string $field, string $value): FilterGroup + public function addFilterNotContains(string $field, string $value): self { $this->addFilter(Filter::createNotContains($field, $value)); return $this; diff --git a/src/Order.php b/src/Order.php index 497f9de..40a2e8a 100644 --- a/src/Order.php +++ b/src/Order.php @@ -50,12 +50,12 @@ public static function createDescBy(string $by): self return self::create($by, OrderType::DESC); } - public static function none(): Order + public static function none(): self { return self::create('', OrderType::NONE); } - public static function random(): Order + public static function random(): self { return self::create('', OrderType::RANDOM); } diff --git a/tests/CriteriaTest.php b/tests/CriteriaTest.php index 3493a6f..d0c2e4f 100644 --- a/tests/CriteriaTest.php +++ b/tests/CriteriaTest.php @@ -4,11 +4,62 @@ namespace ComplexHeart\Test\Domain\Criteria; +use ComplexHeart\Domain\Criteria\Contracts\CriteriaSource; use ComplexHeart\Domain\Criteria\Criteria; +use ComplexHeart\Domain\Criteria\Errors\CriteriaError; use ComplexHeart\Domain\Criteria\FilterGroup; use ComplexHeart\Domain\Criteria\Order; use ComplexHeart\Domain\Criteria\Page; +test('Criteria should be successfully created from source.', function () { + $c = Criteria::fromSource(new class implements CriteriaSource { + public function filterGroups(): array + { + return [ + [ + ['field' => 'name', 'operator' => '=', 'value' => 'Jules'], + ['field' => 'surname', 'operator' => '=', 'value' => 'Winnfield'], + ] + ]; + } + + public function orderType(): string + { + return 'asc'; + } + + public function orderBy(): string + { + return 'name'; + } + + public function pageLimit(): int + { + return 25; + } + + public function pageOffset(): int + { + return 0; + } + }); + + expect($c->groups())->toHaveCount(1) + ->and($c->groups()[0])->toHaveCount(2) + ->and($c->orderBy())->toBe('name') + ->and($c->orderType())->toBe('asc') + ->and($c->pageLimit())->toBe(25) + ->and($c->pageOffset())->toBe(0); +}); + +test('Criteria should throw exception for invalid filter groups.', function () { + try { + Criteria::default()->withFilterGroup(fn() => [1, 2, 3]); + } catch (CriteriaError $e) { + expect($e->violations())->toHaveCount(1); + } +}); + test('Criteria should change complete and partially the criteria order parameter.', function () { $c = Criteria::default() ->withOrder(Order::createDescBy('name'));