├── 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 | Build Status 7 | Code Coverage 8 | Latest Version on Packagist 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 | --------------------------------------------------------------------------------