├── tests
├── Unit
│ ├── .gitignore
│ ├── AbstractFilterTest.php
│ └── AbstractFilterableRepositoryTest.php
├── Example
│ ├── Entities
│ │ ├── PeopleEntityList.php
│ │ └── PeopleEntity.php
│ ├── Filters
│ │ └── OldPeopleFilter.php
│ └── PeopleRepository.php
├── TestCase.php
└── Feature
│ └── PeopleRepositoryTest.php
├── .gitignore
├── src
├── FilterClassNotFound.php
├── IncorrectFilterException.php
├── NoApplyFilterRuleException.php
├── AbstractFilter.php
└── AbstractFilterableRepository.php
├── .github
└── workflows
│ ├── php-cs-fixer.yml
│ └── tests.yml
├── .php-cs-fixer.php
├── phpunit.xml
├── composer.json
└── readme.md
/tests/Unit/.gitignore:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | tests/reports/
3 | composer.lock
4 | .php-cs-fixer.cache
5 |
--------------------------------------------------------------------------------
/tests/Example/Entities/PeopleEntityList.php:
--------------------------------------------------------------------------------
1 | where('age', '>=', $this->age);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/php-cs-fixer.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Check & fix styling
3 |
4 | on: [push]
5 |
6 | jobs:
7 | php-cs-fixer:
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Checkout code
12 | uses: actions/checkout@v2
13 | with:
14 | ref: ${{ github.head_ref }}
15 |
16 | - name: Run PHP CS Fixer
17 | uses: docker://oskarstark/php-cs-fixer-ga
18 | with:
19 | args: --config=.php-cs-fixer.php --allow-risky=yes
20 |
21 | - name: Commit changes
22 | uses: stefanzweifel/git-auto-commit-action@v4
23 | with:
24 | commit_message: Fix styling
25 |
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | setRiskyAllowed(true)
10 | ->setRules([
11 | '@PSR2' => true,
12 | '@Symfony:risky' => true,
13 | '@PHP71Migration:risky' => true,
14 | '@PHP73Migration' => true,
15 | 'blank_line_after_opening_tag' => true,
16 | 'method_argument_space' => false,
17 | ])
18 | ->setFinder((new PhpCsFixer\Finder())
19 | ->in(
20 | array_map(
21 | static fn (string $dir) => __DIR__ . '/' . $dir,
22 | $dirs,
23 | )
24 | ),
25 | );
26 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | php: [7.4, 8.0, 8.1]
18 |
19 | steps:
20 | - uses: actions/checkout@v1
21 |
22 | - name: Set PHP version
23 | uses: shivammathur/setup-php@v2
24 | with:
25 | php-version: ${{ matrix.php }}
26 |
27 | - name: Install composer dependencies
28 | run: composer install --quiet --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
29 |
30 | - name: Execute tests
31 | run: vendor/bin/phpunit
32 |
--------------------------------------------------------------------------------
/src/AbstractFilter.php:
--------------------------------------------------------------------------------
1 | setAttributes($args);
14 | }
15 |
16 | protected function setAttributes(array $attributes): void
17 | {
18 | foreach (get_class_vars(static::class) as $key => $value) {
19 | if (isset($attributes[$key])) {
20 | $this->$key = $attributes[$key];
21 | }
22 | }
23 | }
24 |
25 | public static function isApplicable(... $args): bool
26 | {
27 | return true;
28 | }
29 |
30 | /**
31 | * @throws NoApplyFilterRuleException
32 | */
33 | public function apply(Builder $queryBuilder): void
34 | {
35 | throw new NoApplyFilterRuleException(self::class);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/Unit/AbstractFilterTest.php:
--------------------------------------------------------------------------------
1 | expectException(NoApplyFilterRuleException::class);
17 |
18 | $filter->apply($this->app['db']->query());
19 | }
20 |
21 | public function testCanFeedPropertiesByConstruct(): void
22 | {
23 | $filter = new class extends AbstractFilter {
24 | public string $foo;
25 | };
26 |
27 | $filterInstance = new $filter(['foo' => 'hey', 'bar' => 'nope']);
28 |
29 | $this->assertSame('hey', $filterInstance->foo);
30 | $this->expectException(Exception::class);
31 | $this->assertNull($filterInstance->bar);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ./tests/Unit
11 |
12 |
13 | ./tests/Feature
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | src/
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
20 | $table->string('firstname');
21 | $table->string('lastname');
22 | $table->integer('age');
23 | $table->string('gender');
24 | });
25 | });
26 | }
27 |
28 | public function instantiatePeopleRepository(): PeopleRepository
29 | {
30 | return new PeopleRepository($this->app['db']);
31 | }
32 |
33 | public function createPeople(array $peoples): void
34 | {
35 | $this->app['db']
36 | ->table('people')
37 | ->insert($peoples);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "devmakerlab/laravel-filters",
3 | "type": "library",
4 | "license": "WTFPL",
5 | "autoload": {
6 | "psr-4": {
7 | "DevMakerLab\\LaravelFilters\\": "src/"
8 | }
9 | },
10 | "autoload-dev": {
11 | "psr-4": {
12 | "Tests\\": "tests/",
13 | "Tests\\Example\\": "tests/Example"
14 | }
15 | },
16 | "authors": [
17 | {
18 | "name": "Valentin Leguy",
19 | "email": "leguy.val@gmail.com"
20 | },
21 | {
22 | "name": "Anthony Meinder",
23 | "email": "anthony@meinder.dev"
24 | }
25 | ],
26 | "require": {
27 | "php": "^7.4|^8.0",
28 | "illuminate/database": "^7.0|^8.0|^9.0|^10.0"
29 | },
30 | "require-dev": {
31 | "symfony/var-dumper": "^5.3|^6.0|^7.0",
32 | "phpunit/phpunit": "^10.1",
33 | "orchestra/testbench": "^6.19|^7.0|^8.2",
34 | "devmakerlab/entities": "^3.1"
35 | },
36 | "scripts": {
37 | "post-autoload-dump": [
38 | "@php ./vendor/bin/testbench package:discover --ansi"
39 | ]
40 | },
41 | "minimum-stability": "stable"
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Feature/PeopleRepositoryTest.php:
--------------------------------------------------------------------------------
1 | instantiatePeopleRepository();
18 |
19 | $this->createPeople([
20 | [
21 | 'firstname' => 'Snoop',
22 | 'lastname' => 'Dogg',
23 | 'gender' => 'male',
24 | 'age' => 49,
25 | ],
26 | [
27 | 'firstname' => 'Danielle',
28 | 'lastname' => 'Studio',
29 | 'gender' => 'female',
30 | 'age' => 67,
31 | ],
32 | ]);
33 |
34 | $people = $peopleRepository
35 | ->addFilter(OldPeopleFilter::class)
36 | ->limit(1)
37 | ->get(['age' => 60]);
38 |
39 | $this->assertInstanceOf(PeopleEntityList::class, $people);
40 | $this->assertCount(1, $people);
41 | $this->assertInstanceOf(\Tests\Example\Entities\PeopleEntity::class, $people[0]);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/Example/PeopleRepository.php:
--------------------------------------------------------------------------------
1 | databaseManager = $databaseManager;
18 | }
19 |
20 | public function get(array $args): PeopleEntityList
21 | {
22 | $queryBuilder = $this->databaseManager->table('people')
23 | ->select(['firstname', 'lastname', 'age', 'gender']);
24 |
25 | $this->applyFilters($queryBuilder, $args);
26 |
27 | $people = $queryBuilder->get();
28 |
29 | return $this->transform($people);
30 | }
31 |
32 | public function transform(\Illuminate\Support\Collection $people): PeopleEntityList
33 | {
34 | $people->transform(function ($person) {
35 | return new PeopleEntity([
36 | 'name' => sprintf('%s %s', $person->lastname, $person->firstname),
37 | 'age' => $person->age,
38 | 'gender' => $person->gender,
39 | ]);
40 | });
41 |
42 | return new PeopleEntityList($people->toArray());
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/AbstractFilterableRepository.php:
--------------------------------------------------------------------------------
1 | filters[] = $filter;
30 |
31 | return $this;
32 | }
33 |
34 | public function resetFilters(): self
35 | {
36 | $this->filters = [];
37 |
38 | return $this;
39 | }
40 |
41 | public function limit(int $limit): self
42 | {
43 | $this->limit = $limit;
44 |
45 | return $this;
46 | }
47 |
48 | public function resetLimit(): self
49 | {
50 | $this->limit = null;
51 |
52 | return $this;
53 | }
54 |
55 | public function applyFilters(Builder &$builder, array $args): self
56 | {
57 | foreach ($this->filters as $filter) {
58 | $neededArgs = $this->extractNeededArgs($filter, $args);
59 |
60 | if ($filter::isApplicable($neededArgs)) {
61 | $filterInstance = new $filter($neededArgs);
62 | $filterInstance->apply($builder);
63 | }
64 | }
65 |
66 | if ($this->limit) {
67 | $builder->limit($this->limit);
68 | }
69 |
70 | $this->resetFilters();
71 | $this->resetLimit();
72 |
73 | return $this;
74 | }
75 |
76 | private function extractNeededArgs(string $class, array $args): array
77 | {
78 | return array_intersect_key($args, array_flip(array_keys(get_class_vars($class))));
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/tests/Unit/AbstractFilterableRepositoryTest.php:
--------------------------------------------------------------------------------
1 | expectException(FilterClassNotFound::class);
19 | $abstractRepository->addFilter(FooFilter::class);
20 | }
21 |
22 | public function testExceptionThrownWhenAddingFilterWhichIsNotExtendingAbstractFilter(): void
23 | {
24 | $abstractRepository = new class extends AbstractFilterableRepository {
25 | };
26 | $filter = new class {
27 | };
28 |
29 | $this->expectException(IncorrectFilterException::class);
30 | $abstractRepository->addFilter($filter::class);
31 | }
32 |
33 | public function testCanAddFilter(): void
34 | {
35 | $abstractRepository = new class extends AbstractFilterableRepository {
36 | public function getFilters(): array
37 | {
38 | return $this->filters;
39 | }
40 | };
41 |
42 | $filter = new class extends AbstractFilter {
43 | };
44 |
45 | $abstractRepository->addFilter($filter::class);
46 |
47 | $this->assertCount(1, $abstractRepository->getFilters());
48 | $this->assertSame($filter::class, $abstractRepository->getFilters()[0]);
49 | }
50 |
51 | public function testCanResetFilters(): void
52 | {
53 | $abstractRepository = new class extends AbstractFilterableRepository {
54 | public function getFilters(): array
55 | {
56 | return $this->filters;
57 | }
58 | };
59 |
60 | $filter = new class extends AbstractFilter {
61 | };
62 |
63 | $abstractRepository->addFilter($filter::class);
64 | $this->assertCount(1, $abstractRepository->getFilters());
65 |
66 | $abstractRepository->resetFilters();
67 | $this->assertCount(0, $abstractRepository->getFilters());
68 | }
69 |
70 | public function testCanSpecifyLimit(): void
71 | {
72 | $abstractRepository = new class extends AbstractFilterableRepository {
73 | public function getLimit(): ?int
74 | {
75 | return $this->limit;
76 | }
77 | };
78 |
79 | $this->assertNull($abstractRepository->getLimit());
80 |
81 | $abstractRepository->limit(10);
82 | $this->assertSame(10, $abstractRepository->getLimit());
83 | }
84 |
85 | public function testCanResetLimit(): void
86 | {
87 | $abstractRepository = new class extends AbstractFilterableRepository {
88 | public function getLimit(): ?int
89 | {
90 | return $this->limit;
91 | }
92 | };
93 |
94 | $this->assertNull($abstractRepository->getLimit());
95 |
96 | $abstractRepository->limit(10);
97 | $this->assertSame(10, $abstractRepository->getLimit());
98 |
99 | $abstractRepository->resetLimit();
100 | $this->assertNull($abstractRepository->getLimit());
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | # DevMakerLab/Laravel-Filters
14 |
15 | Need some filters? This package is based on the Repository Design Pattern to let you create specific queries easily.
16 |
17 | * [Installation](#installation)
18 | * [Usage](#usage)
19 | * [Example](#example)
20 |
21 | ## Installation
22 | ⚠️ Requires >= PHP 7.4 ⚠️
23 | ```shell
24 | composer require devmakerlab/laravel-filters
25 | ```
26 |
27 | ## Usage
28 |
29 | This package offers an abstract class `AbstractFilterableRepository` which needs to be extended to implement the features of this package.
30 |
31 |
32 | PeopleService.php
33 | ```php
34 | addFilter(OldPeopleFilter::class)
40 | ->get(['age' => 60]);
41 | ```
42 |
43 | OldPeopleFilter.php
44 | ```php
45 | where('age', '>=', $this->age);
59 | }
60 | }
61 | ```
62 |
63 | PeopleRepository.php
64 | ```php
65 | databaseManager = $databaseManager;
78 | }
79 |
80 | public function get(array $args): array
81 | {
82 | $queryBuilder = $this->databaseManager->table('people')
83 | ->select(['firstname', 'lastname', 'age', 'gender']);
84 |
85 | $this->applyFilters($queryBuilder, $args);
86 |
87 | $people = $queryBuilder->get();
88 |
89 | return $this->transform($people);
90 | }
91 |
92 | public function transform(Collection $people): array
93 | {
94 | $people->transform(function ($person) {
95 | return [
96 | 'name' => sprintf('%s %s', $person->lastname, $person->firstname),
97 | 'age' => $person->age,
98 | 'gender' => $person->gender,
99 | ];
100 | });
101 |
102 | return $people->toArray();
103 | }
104 | }
105 | ```
106 |
107 | ## Example
108 |
109 | [Usage Example](https://github.com/devmakerlab/laravel-filters/tree/master/tests/Example) of DevMakerLab/Laravel-Filters package.
110 |
--------------------------------------------------------------------------------