From 81beb5ef77f851f561339036fabcd7e57b15af69 Mon Sep 17 00:00:00 2001 From: Unay Santisteban Date: Tue, 30 Jan 2024 17:30:58 +1000 Subject: [PATCH] Refactor/Interface improvement. --- .github/workflows/documentation.yml | 48 ++++++++++++++++++ README.md | 7 +-- src/Contracts/CriteriaSource.php | 9 +++- src/Criteria.php | 44 ++++++++-------- src/Filter.php | 35 +++++++------ src/FilterGroup.php | 47 +++++++---------- src/Operator.php | 27 +++++++--- src/Order.php | 10 ++-- src/OrderType.php | 10 ++-- src/Page.php | 27 ++++++---- tests/CriteriaTest.php | 57 +++++++++++---------- tests/FilterGroupTest.php | 10 +--- wiki/Laravel-Implementation.md | 79 +++++++++++++++++++++++++++++ 13 files changed, 274 insertions(+), 136 deletions(-) create mode 100644 .github/workflows/documentation.yml create mode 100644 wiki/Laravel-Implementation.md diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..2c129e5 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,48 @@ +name: Documentation + +on: + push: + branches: [ 'main' ] + +jobs: + publish: + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: [ '8.2' ] + + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring + coverage: xdebug + tools: composer:v2 + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ~/.composer/cache + key: php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: php-${{ matrix.php-version }}-composer- + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Install Dependencies + run: composer install --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + - name: Copy Files + run: cp README.md wiki/Home.md + + - name: Publish documentation to Wiki + uses: SwiftDocOrg/github-wiki-publish-action@v1 + with: + path: "wiki/" + env: + GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} diff --git a/README.md b/README.md index 067fa78..e18f793 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Criteria (a.k.a Filter) +# Criteria [![Test](https://github.com/ComplexHeart/php-criteria/actions/workflows/test.yml/badge.svg)](https://github.com/ComplexHeart/php-criteria/actions/workflows/test.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ComplexHeart_php-criteria&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ComplexHeart_php-criteria) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ComplexHeart_php-criteria&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ComplexHeart_php-criteria) -Small implementation of a filter criteria pattern in PHP for Complex Heart SDK. Compose several filters using fluent +Small implementation of a criteria pattern in PHP for Complex Heart SDK. Compose several filters using fluent interface. ## Installation @@ -58,7 +58,6 @@ A `FilterGroup` is a set of filters or conditions that must match all together ( (`OR`), just add more `FilterGroup`. ```php - // Match articles with given term in title, or in tagline, or in content. $criteria = Criteria::default() ->withFilterGroup(FilterGroup::create()->addFilterContains('title', $term)) @@ -68,3 +67,5 @@ $criteria = Criteria::default() ->withOrderType(Order::TYPE_ASC) ->withPageNumber(3); ``` + + diff --git a/src/Contracts/CriteriaSource.php b/src/Contracts/CriteriaSource.php index ff1e2e1..a3427d0 100644 --- a/src/Contracts/CriteriaSource.php +++ b/src/Contracts/CriteriaSource.php @@ -50,9 +50,16 @@ public function orderBy(): string; public function pageLimit(): int; /** - * Provides the offset, by default should be 0. + * Provides the offset, by default should be 0. This value will be discarded + * if page number returns a value > 0. * * @return int */ public function pageOffset(): int; + + /** + * Provides the page number. If value > 0, will be used to compute the offset. + * @return int + */ + public function pageNumber(): int; } diff --git a/src/Criteria.php b/src/Criteria.php index 3964a2e..1376fa6 100644 --- a/src/Criteria.php +++ b/src/Criteria.php @@ -68,6 +68,15 @@ public static function create(array $groups, Order $order, Page $page): self return new self(groups: $groups, order: $order, page: $page); } + public static function default(): self + { + return self::create( + groups: [], + order: Order::none(), + page: Page::default() + ); + } + /** * Creates a new instance of Criteria from the given data source. * @@ -76,21 +85,18 @@ public static function create(array $groups, Order $order, Page $page): self */ public static function fromSource(CriteriaSource $source): self { - return Criteria::create( + return self::create( groups: map( - fn(array $g): FilterGroup => FilterGroup::createFromArray($g), + fn(array $g): FilterGroup => FilterGroup::fromArray($g), $source->filterGroups() ), order: Order::create($source->orderBy(), OrderType::make($source->orderType())), - page: Page::create($source->pageLimit(), $source->pageOffset()) + page: $source->pageNumber() > 0 + ? Page::number($source->pageNumber(), $source->pageLimit()) + : Page::create($source->pageLimit(), $source->pageOffset()) ); } - public static function default(): self - { - return self::create(groups: [], order: Order::none(), page: Page::create()); - } - /** * Returns a new instance of the criteria with the given FilterGroups. * @@ -114,9 +120,7 @@ public function withFilterGroups(array $groups): self */ public function withFilterGroup(FilterGroup|Closure $group): self { - if (is_callable($group)) { - $group = $group(new FilterGroup()); - } + $group = $group instanceof FilterGroup ? $group : $group(FilterGroup::create()); // push single FilterGroup into an array. $group = is_array($group) ? $group : [$group]; @@ -157,21 +161,21 @@ public function withPage(Page $page): self return self::create(groups: $this->groups, order: $this->order, page: $page); } - public function withPageOffset(int $offset): self + public function withPageLimit(int $limit): self { return self::create( groups: $this->groups, order: $this->order, - page: Page::create($this->pageLimit(), $offset) + page: Page::create($limit, $this->pageOffset()) ); } - public function withPageLimit(int $limit): self + public function withPageOffset(int $offset): self { return self::create( groups: $this->groups, order: $this->order, - page: Page::create($limit, $this->pageOffset()) + page: Page::create($this->pageLimit(), $offset) ); } @@ -180,7 +184,7 @@ public function withPageNumber(int $number, int $size = null): self return self::create( groups: $this->groups, order: $this->order, - page: Page::number($number, is_null($size) ? $this->page->limit() : $size) + page: Page::number($number, is_null($size) ? $this->pageLimit() : $size) ); } @@ -214,14 +218,14 @@ public function page(): Page return $this->page; } - public function pageOffset(): int + public function pageLimit(): int { - return $this->page->offset(); + return $this->page->limit(); } - public function pageLimit(): int + public function pageOffset(): int { - return $this->page->limit(); + return $this->page->offset(); } public function __toString(): string diff --git a/src/Filter.php b/src/Filter.php index 130e262..4ae7781 100644 --- a/src/Filter.php +++ b/src/Filter.php @@ -38,7 +38,6 @@ public function __construct( * @param string $field * @param Operator $operator * @param mixed $value - * * @return Filter */ public static function create(string $field, Operator $operator, mixed $value): self @@ -48,9 +47,9 @@ public static function create(string $field, Operator $operator, mixed $value): /** * @param array|array $filter - * @return self + * @return Filter */ - public static function createFromArray(array $filter): self + public static function fromArray(array $filter): self { // check if the array is indexed or associative. $isIndexed = fn($source): bool => ([] !== $source) && array_keys($source) === range(0, count($source) - 1); @@ -58,72 +57,72 @@ public static function createFromArray(array $filter): self return ($isIndexed($filter)) ? self::create( "$filter[0]", - Operator::make("$filter[1]"), + Operator::create("$filter[1]"), $filter[2] ) : self::create( "{$filter['field']}", - Operator::make("{$filter['operator']}"), + Operator::create("{$filter['operator']}"), "{$filter['value']}" ); } - public static function createEqual(string $field, mixed $value): self + public static function equal(string $field, mixed $value): self { return self::create($field, Operator::EQUAL, $value); } - public static function createNotEqual(string $field, mixed $value): self + public static function notEqual(string $field, mixed $value): self { return self::create($field, Operator::NOT_LIKE, $value); } - public static function createGreaterThan(string $field, mixed $value): self + public static function greaterThan(string $field, mixed $value): self { return self::create($field, Operator::GT, $value); } - public static function createGreaterOrEqualThan(string $field, mixed $value): self + public static function greaterOrEqualThan(string $field, mixed $value): self { return self::create($field, Operator::GTE, $value); } - public static function createLessThan(string $field, mixed $value): self + public static function lessThan(string $field, mixed $value): self { return self::create($field, Operator::LT, $value); } - public static function createLessOrEqualThan(string $field, mixed $value): self + public static function lessOrEqualThan(string $field, mixed $value): self { return self::create($field, Operator::LTE, $value); } - public static function createIn(string $field, mixed $value): self + public static function in(string $field, mixed $value): self { return self::create($field, Operator::IN, $value); } - public static function createNotIn(string $field, mixed $value): self + public static function notIn(string $field, mixed $value): self { return self::create($field, Operator::NOT_IN, $value); } - public static function createLike(string $field, mixed $value): self + public static function like(string $field, mixed $value): self { return self::create($field, Operator::LIKE, $value); } - public static function createNotLike(string $field, mixed $value): self + public static function notLike(string $field, mixed $value): self { return self::create($field, Operator::NOT_LIKE, $value); } - public static function createContains(string $field, mixed $value): self + public static function contains(string $field, mixed $value): self { return self::create($field, Operator::CONTAINS, $value); } - public static function createNotContains(string $field, mixed $value): self + public static function notContains(string $field, mixed $value): self { return self::create($field, Operator::NOT_CONTAINS, $value); } @@ -165,7 +164,7 @@ public function __toString(): string $this->field(), $this->operator()->value, is_array($this->value()) - ? implode('|', $this->value()) + ? implode(',', $this->value()) : $this->value() ); } diff --git a/src/FilterGroup.php b/src/FilterGroup.php index 182e0bd..c6605c8 100644 --- a/src/FilterGroup.php +++ b/src/FilterGroup.php @@ -45,10 +45,10 @@ public static function create(Filter ...$filters): self * @param array> $filters * @return FilterGroup */ - public static function createFromArray(array $filters): self + public static function fromArray(array $filters): self { return self::create( - ...map(fn(array $filter): Filter => Filter::createFromArray($filter), $filters) + ...map(fn(array $filter): Filter => Filter::fromArray($filter), $filters) ); } @@ -56,8 +56,7 @@ public static function createFromArray(array $filters): self * Add new filter to the FilterGroup. * * @param Filter $new - * - * @return self + * @return FilterGroup */ public function addFilter(Filter $new): self { @@ -72,84 +71,72 @@ public function addFilter(Filter $new): self public function addFilterEqual(string $field, mixed $value): self { - $this->addFilter(Filter::createEqual($field, $value)); - return $this; + return $this->addFilter(Filter::equal($field, $value)); } public function addFilterNotEqual(string $field, mixed $value): self { - $this->addFilter(Filter::createNotEqual($field, $value)); - return $this; + return $this->addFilter(Filter::notEqual($field, $value)); } public function addFilterGreaterThan(string $field, mixed $value): self { - $this->addFilter(Filter::createGreaterThan($field, $value)); - return $this; + return $this->addFilter(Filter::greaterThan($field, $value)); } public function addFilterGreaterOrEqualThan(string $field, mixed $value): self { - $this->addFilter(Filter::createGreaterOrEqualThan($field, $value)); - return $this; + return $this->addFilter(Filter::greaterOrEqualThan($field, $value)); } public function addFilterLessThan(string $field, mixed $value): self { - $this->addFilter(Filter::createLessThan($field, $value)); - return $this; + return $this->addFilter(Filter::lessThan($field, $value)); } public function addFilterLessOrEqualThan(string $field, mixed $value): self { - $this->addFilter(Filter::createLessOrEqualThan($field, $value)); - return $this; + return $this->addFilter(Filter::lessOrEqualThan($field, $value)); } /** * @param string $field * @param array $value - * @return $this + * @return FilterGroup */ public function addFilterIn(string $field, array $value): self { - $this->addFilter(Filter::createIn($field, $value)); - return $this; + return $this->addFilter(Filter::in($field, $value)); } /** * @param string $field * @param array $value - * @return $this + * @return FilterGroup */ public function addFilterNotIn(string $field, array $value): self { - $this->addFilter(Filter::createNotIn($field, $value)); - return $this; + return $this->addFilter(Filter::notIn($field, $value)); } public function addFilterLike(string $field, string $value): self { - $this->addFilter(Filter::createLike($field, $value)); - return $this; + return $this->addFilter(Filter::like($field, $value)); } public function addFilterNotLike(string $field, string $value): self { - $this->addFilter(Filter::createNotLike($field, $value)); - return $this; + return $this->addFilter(Filter::notLike($field, $value)); } public function addFilterContains(string $field, string $value): self { - $this->addFilter(Filter::createContains($field, $value)); - return $this; + return $this->addFilter(Filter::contains($field, $value)); } public function addFilterNotContains(string $field, string $value): self { - $this->addFilter(Filter::createNotContains($field, $value)); - return $this; + return $this->addFilter(Filter::notContains($field, $value)); } /** diff --git a/src/Operator.php b/src/Operator.php index 53fee34..2fa40da 100644 --- a/src/Operator.php +++ b/src/Operator.php @@ -18,15 +18,26 @@ enum Operator: string case GTE = '>='; case LT = '<'; case LTE = '<='; - case IN = 'in'; - case NOT_IN = 'notIn'; - case LIKE = 'like'; - case NOT_LIKE = 'notLike'; - case CONTAINS = 'contains'; - case NOT_CONTAINS = 'notContains'; + case IN = 'IN'; + case NOT_IN = 'NOT IN'; + case LIKE = 'LIKE'; + case NOT_LIKE = 'NOT LIKE'; + case CONTAINS = 'CONTAINS'; + case NOT_CONTAINS = 'NOT CONTAINS'; - public static function make(string $value): self + public static function create(string $value): self { - return self::from($value); + return match ($value) { + 'eq' => self::EQUAL, + 'neq', 'ne' => self::NOT_EQUAL, + 'gt' => self::GT, + 'gte', 'ge' => self::GTE, + 'lt' => self::LT, + 'lte', 'le' => self::LTE, + 'nin', 'out' => self::NOT_IN, + 'nlike' => self::NOT_LIKE, + 'ncontains' => self::NOT_CONTAINS, + default => self::from($value) + }; } } diff --git a/src/Order.php b/src/Order.php index 40a2e8a..5399cad 100644 --- a/src/Order.php +++ b/src/Order.php @@ -35,17 +35,17 @@ protected function invariantOrderByValueMustContainOnlyAlphanumericalCharacters( return preg_match('/\w*/', $this->by) === 1; } - public static function create(string $by, OrderType $type = OrderType::ASC): self + public static function create(string $by, OrderType $type): self { return new self($by, $type); } - public static function createAscBy(string $by): self + public static function asc(string $by): self { - return self::create($by); + return self::create($by, OrderType::ASC); } - public static function createDescBy(string $by): self + public static function desc(string $by): self { return self::create($by, OrderType::DESC); } @@ -82,6 +82,6 @@ public function isRandom(): bool public function __toString(): string { - return sprintf('%s.%s', $this->by(), $this->type()->value); + return sprintf('%s %s', $this->by(), $this->type()->value); } } diff --git a/src/OrderType.php b/src/OrderType.php index 86e9c39..31fdeef 100644 --- a/src/OrderType.php +++ b/src/OrderType.php @@ -12,13 +12,13 @@ */ enum OrderType: string { - case ASC = 'asc'; - case DESC = 'desc'; - case NONE = 'none'; - case RANDOM = 'random'; + case ASC = 'ASC'; + case DESC = 'DESC'; + case NONE = 'NONE'; + case RANDOM = 'RANDOM'; public static function make(string $value): self { - return self::from($value); + return self::from(strtoupper($value)); } } diff --git a/src/Page.php b/src/Page.php index d41953c..953d599 100644 --- a/src/Page.php +++ b/src/Page.php @@ -27,30 +27,35 @@ final class Page implements ValueObject * @param int $offset */ public function __construct( - private readonly int $limit = self::DEFAULT_LIMIT, - private readonly int $offset = self::DEFAULT_OFFSET, + private readonly int $limit, + private readonly int $offset, ) { $this->check(); } - public static function create(int $limit = self::DEFAULT_LIMIT, int $offset = self::DEFAULT_OFFSET): self + protected function invariantLimitValueMustBePositive(): bool { - return new self($limit, $offset); + return $this->limit >= 0; } - public static function number(int $number, int $size = self::DEFAULT_LIMIT): self + protected function invariantOffsetValueMustBePositive(): bool { - return self::create($size, ($size * $number) - $size); + return $this->offset >= 0; } - protected function invariantLimitValueMustBePositive(): bool + public static function create(int $limit, int $offset): self { - return $this->limit >= 0; + return new self($limit, $offset); } - protected function invariantOffsetValueMustBePositive(): bool + public static function default(): self { - return $this->offset >= 0; + return self::create(self::DEFAULT_LIMIT, self::DEFAULT_OFFSET); + } + + public static function number(int $number, int $size = self::DEFAULT_LIMIT): self + { + return self::create($size, ($size * $number) - $size); } public function limit(): int @@ -65,6 +70,6 @@ public function offset(): int public function __toString(): string { - return sprintf('%s.%s', $this->limit, $this->offset); + return sprintf('%s %s', $this->limit, $this->offset); } } diff --git a/tests/CriteriaTest.php b/tests/CriteriaTest.php index 6d117a4..b0e6a63 100644 --- a/tests/CriteriaTest.php +++ b/tests/CriteriaTest.php @@ -25,7 +25,7 @@ public function filterGroups(): array public function orderType(): string { - return 'asc'; + return 'ASC'; } public function orderBy(): string @@ -42,14 +42,19 @@ public function pageOffset(): int { return 0; } + + public function pageNumber(): int + { + return 3; + } }); expect($c->groups())->toHaveCount(1) ->and($c->groups()[0])->toHaveCount(2) ->and($c->orderBy())->toBe('name') - ->and($c->orderType())->toBe('asc') + ->and($c->orderType())->toBe('ASC') ->and($c->pageLimit())->toBe(25) - ->and($c->pageOffset())->toBe(0); + ->and($c->pageOffset())->toBe(50); }); test('Criteria should throw exception for invalid filter groups.', function () { @@ -62,14 +67,14 @@ public function pageOffset(): int test('Criteria should change complete and partially the criteria order parameter.', function () { $c = Criteria::default() - ->withOrder(Order::createDescBy('name')); + ->withOrder(Order::desc('name')); expect($c->orderBy())->toBe('name') - ->and($c->orderType())->toBe('desc') + ->and($c->orderType())->toBe('DESC') ->and($c->order()->isNone())->toBeFalse(); - $c = $c->withOrder(Order::createAscBy('name')); - expect($c->orderType())->toBe('asc'); + $c = $c->withOrder(Order::asc('name')); + expect($c->orderType())->toBe('ASC'); $c = $c->withOrder(Order::random()); expect($c->order()->isRandom())->toBeTrue(); @@ -80,35 +85,33 @@ public function pageOffset(): int $c = $c->withOrderBy('surname'); expect($c->orderBy())->toBe('surname'); - $c = $c->withOrderType('asc'); - expect($c->orderType())->toBe('asc') - ->and($c->order())->toBeInstanceOf(Order::class); + $c = $c->withOrderType('ASC'); + expect($c->orderType())->toBe('ASC'); }); test('Criteria should change complete and partially the criteria page parameter.', function () { $c = Criteria::default() - ->withPage(Page::create(100, 50)); + ->withPage(Page::create(100, 100)); - expect($c->pageLimit())->toBe(100) - ->and($c->pageOffset())->toBe(50); + expect($c->page()->limit())->toBe(100) + ->and($c->page()->offset())->toBe(100); $c = $c->withPageLimit(42); expect($c->pageLimit())->toBe(42); $c = $c->withPageOffset(10); - expect($c->pageOffset())->toBe(10) - ->and($c->page())->toBeInstanceOf(Page::class); + expect($c->pageOffset())->toBe(10); }); test('Criteria should change the complete filter groups.', function () { $c = Criteria::default() ->withFilterGroups([ - FilterGroup::createFromArray([['field', '=', 'one']]) + FilterGroup::fromArray([['name', '=', 'Vicent']]) ]) ->withFilterGroups([ - FilterGroup::createFromArray([['field', '=', 'two']]), - FilterGroup::createFromArray([['field', '=', 'three']]), - FilterGroup::createFromArray([['field', '=', 'four']]), + FilterGroup::fromArray([['name', '=', 'Jules']]), + FilterGroup::fromArray([['name', '=', 'Marcellus']]), + FilterGroup::fromArray([['name', '=', 'Butch']]), ]); expect($c->groups())->toHaveCount(3); @@ -116,11 +119,11 @@ public function pageOffset(): int test('Criteria should add or filter group to criteria object.', function () { $c = Criteria::default() - ->withFilterGroup(fn(FilterGroup $g): FilterGroup => $g + ->withFilterGroup(FilterGroup::create() ->addFilterEqual('name', 'Vincent') ->addFilterEqual('status', 'deceased') ) - ->withFilterGroup(fn(FilterGroup $g): FilterGroup => $g + ->withFilterGroup(FilterGroup::create() ->addFilterEqual('name', 'Jules') ->addFilterEqual('deceased', 'alive') ); @@ -134,22 +137,22 @@ public function pageOffset(): int test('Criteria should be correctly serialized to string.', function () { $c = Criteria::default() - ->withFilterGroup(fn(FilterGroup $group): FilterGroup => $group + ->withFilterGroup(FilterGroup::create() ->addFilterEqual('name', 'Vincent') ->addFilterGreaterOrEqualThan('age', '35') ) ->withPageLimit(100) ->withPageOffset(0) ->withOrderBy('name') - ->withOrderType('asc'); + ->withOrderType('ASC'); - expect($c->__toString())->toBe('name.=.Vincent+age.>=.35#name.asc#100.0'); + expect($c->__toString())->toBe('name.=.Vincent+age.>=.35#name ASC#100 0'); }); -test('Criteria should configure limit and offset using page number', function () { +test('Criteria should configure limit and offset using page number.', function () { $c = Criteria::default() ->withPageNumber(3, 25); - expect($c->page()->limit())->toBe(25) - ->and($c->page()->offset())->toBe(50); + expect($c->pageLimit())->toBe(25) + ->and($c->pageOffset())->toBe(50); }); diff --git a/tests/FilterGroupTest.php b/tests/FilterGroupTest.php index 53d1078..0cec7dc 100644 --- a/tests/FilterGroupTest.php +++ b/tests/FilterGroupTest.php @@ -6,14 +6,8 @@ use ComplexHeart\Domain\Criteria\FilterGroup; use ComplexHeart\Domain\Criteria\Operator; -test('FilterGroup should only accept Filter instances.', function () { - expect(FilterGroup::create()) - ->toBeInstanceOf(FilterGroup::class) - ->toHaveCount(0); -}); - test('FilterGroup should be created from primitive array of values.', function () { - expect(FilterGroup::createFromArray([['field', '=', 'value']])) + expect(FilterGroup::fromArray([['field', '=', 'value']])) ->toHaveCount(1); }); @@ -23,7 +17,7 @@ ['field', '=', 'value'], ]; - $g = FilterGroup::createFromArray($filters) + $g = FilterGroup::fromArray($filters) ->addFilter(Filter::create('field', Operator::EQUAL, 'value')) ->addFilter(Filter::create('name', Operator::EQUAL, 'Vega')); diff --git a/wiki/Laravel-Implementation.md b/wiki/Laravel-Implementation.md new file mode 100644 index 0000000..74fa0d9 --- /dev/null +++ b/wiki/Laravel-Implementation.md @@ -0,0 +1,79 @@ +## HTTP Request as Criteria source + +Let's see a brief example of how to implement a `CriteriaSource` along with **Laravel** HTTP Request. + +First, create a [Form Request](https://laravel.com/docs/10.x/validation#creating-form-requests) to implement +the `CriteriaSource` interface. + +```bash +php artisan make:request SearchUserRequest +``` + +Next, implement the interface: + +```php +namespace LaravelMade\Http\Requests; + +use ComplexHeart\Domain\Criteria\Contracts\CriteriaSource; +use Illuminate\Foundation\Http\FormRequest; + +class SearchUserRequest extends FormRequest implements CriteriaSource +{ + public function filterGroups(): array + { + // get the filter groups from the request. + // you can also return N groups of filters (OR). + return [$this->input('filters', [])]; + } + + public function orderType(): string + { + return $this->input('order', 'none'); + } + + public function orderBy(): string + { + return $this->input('orderBy', ''); + } + + public function pageLimit(): int + { + return $this->input('limit', 25); + } + + public function pageOffset(): int + { + return $this->input('offset', 0); + } + + public function pageNumber(): int + { + return $this->input('page', 0); + } +} +``` + +Done, now you only need to call the `fromSource` method of the `Criteria` object. + +```php +Route::get('users', function (SearchUserRequest $request): JsonResponse { + $criteria = Criteria::fromSource($request); + + // use criteria to fetch the users. +}); +``` + +Additionally, you can add rules to the `FormRequest` object to ensure the `Criteria` is properly instantiated. If the +`Criteria` object cannot be instantiated a `CriteriaError` will be thrown. + +```php +Route::get('users', function (SearchUserRequest $request): JsonResponse { + try { + $criteria = Criteria::fromSource($request); + } catch (CriteriaError $e) { + // handle the Criteria error + } + + // use criteria to fetch the users. +}); +```