├── .php-cs-fixer.dist.php ├── .php-cs.dist.php ├── LICENSE.md ├── README.md ├── composer.json ├── config └── pipeline-query-collection.php ├── database └── factories │ ├── RelatedModelFactory.php │ └── TestModelFactory.php ├── phpunit.xml.dist.bak ├── pint.json └── src ├── BaseFilter.php ├── BasePipe.php ├── BaseRangeFilter.php ├── BaseSort.php ├── BitwiseFilter.php ├── BooleanFilter.php ├── Concerns ├── Filterable.php └── Sortable.php ├── Contracts ├── CanFilterContract.php └── CanSortContract.php ├── DateFromFilter.php ├── DateToFilter.php ├── Enums ├── MotionEnum.php ├── TrashOptionEnum.php └── WildcardPositionEnum.php ├── ExactFilter.php ├── FieldsRelativeFilter.php ├── PipelineQueryCollectionServiceProvider.php ├── RangeFromFilter.php ├── RangeToFilter.php ├── RelationFilter.php ├── RelativeFilter.php ├── ScopeFilter.php ├── Sort.php ├── SortAscending.php ├── SortDescending.php └── TrashFilter.php /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /.php-cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) l3aro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A query database collection for use with Laravel Pipeline 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/l3aro/pipeline-query-collection.svg?style=flat-square)](https://packagist.org/packages/l3aro/pipeline-query-collection) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/l3aro/pipeline-query-collection/run-tests?label=tests)](https://github.com/l3aro/pipeline-query-collection/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/l3aro/pipeline-query-collection.svg?style=flat-square)](https://packagist.org/packages/l3aro/pipeline-query-collection) 6 | 7 | This package contains a collection of class that can be used with Laravel Pipeline. Let's see below queries: 8 | 9 | ```php 10 | // users?name=Baro&is_admin=1&created_at_from=2022-06-01&created_at_to=2022-06-31 11 | $users = User::query() 12 | ->when($request->name ?? null, function($query, $name) { 13 | $query->where('name', 'like', "%$name%"); 14 | }) 15 | ->when($request->is_admin ?? null, function($query, $isAdmin) { 16 | $query->where('is_admin', $isAdmin ? 1 : 0); 17 | }) 18 | ->when($request->created_at_from ?? null, function($query, $date) { 19 | $query->where('created_at', '>=', $date); 20 | }) 21 | ->when($request->created_at_to ?? null, function($query, $date) { 22 | $query->where('created_at', '<=', $date); 23 | }) 24 | ->get(); 25 | ``` 26 | 27 | As you all can see, it's obviously that filter conditions will continue to grow as well as the duplication of same filter for other queries. We can use Laravel Pipeline combine with some pre-made queries to refactor this 28 | 29 | ```php 30 | use Baro\PipelineQueryCollection; 31 | 32 | // users?name=Baro&is_admin=1&created_at_from=2022-06-01&created_at_to=2022-06-31 33 | $users = Users::query()->filter([ 34 | PipelineQueryCollection\RelativeFilter::make('name'), 35 | PipelineQueryCollection\BooleanFilter::make('is_admin'), 36 | PipelineQueryCollection\DateFromFilter::make('created_at'), 37 | PipelineQueryCollection\DateToFilter::make('created_at'), 38 | ]) 39 | ->get(); 40 | ``` 41 | 42 | ## Table of Contents 43 | 44 | - [A query database collection for use with Laravel Pipeline](#a-query-database-collection-for-use-with-laravel-pipeline) 45 | - [Table of Contents](#table-of-contents) 46 | - [Installation](#installation) 47 | - [Usage](#usage) 48 | - [Preparing your model](#preparing-your-model) 49 | - [Feature](#feature) 50 | - [Bitwise filter](#bitwise-filter) 51 | - [Boolean filter](#boolean-filter) 52 | - [Date From filter](#date-from-filter) 53 | - [Range Filter](#range-filter) 54 | - [Date To filter](#date-to-filter) 55 | - [Exact filter](#exact-filter) 56 | - [Relation filter](#relation-filter) 57 | - [Relative filter](#relative-filter) 58 | - [Scope filter](#scope-filter) 59 | - [Trash filter](#trash-filter) 60 | - [Sort](#sort) 61 | - [Detector](#detector) 62 | - [Custom search column](#custom-search-column) 63 | - [Custom search value](#custom-search-value) 64 | - [Extend filter](#extend-filter) 65 | - [Testing](#testing) 66 | - [Contributing](#contributing) 67 | - [Security Vulnerabilities](#security-vulnerabilities) 68 | - [Credits](#credits) 69 | - [License](#license) 70 | 71 | ## Installation 72 | 73 | Install the package via composer: 74 | 75 | ```bash 76 | composer require l3aro/pipeline-query-collection 77 | ``` 78 | 79 | Optionally, you can publish the config file with: 80 | 81 | ```bash 82 | php artisan vendor:publish --tag="pipeline-query-collection-config" 83 | ``` 84 | 85 | This is the contents of the published config file: 86 | 87 | ```php 88 | return [ 89 | // key to detect param to filter 90 | 'detect_key' => env('PIPELINE_QUERY_COLLECTION_DETECT_KEY', ''), 91 | 92 | // type of postfix for date filters 93 | 'date_from_postfix' => env('PIPELINE_QUERY_COLLECTION_DATE_FROM_POSTFIX', 'from'), 94 | 'date_to_postfix' => env('PIPELINE_QUERY_COLLECTION_DATE_TO_POSTFIX', 'to'), 95 | 96 | // default motion for date filters 97 | 'date_motion' => env('PIPELINE_QUERY_COLLECTION_DATE_MOTION', 'find'), 98 | ]; 99 | ``` 100 | 101 | ## Usage 102 | 103 | ### Preparing your model 104 | 105 | To use this collection with a model, you should implement the following interface and trait: 106 | 107 | ```php 108 | namespace App\Models; 109 | 110 | use Illuminate\Database\Eloquent\Model; 111 | use Baro\PipelineQueryCollection\Concerns\Filterable; 112 | use Baro\PipelineQueryCollection\Contracts\CanFilterContract; 113 | 114 | class YourModel extends Model implements CanFilterContract 115 | { 116 | use Filterable; 117 | 118 | public function getFilters(): array 119 | { 120 | return [ 121 | // the filter and sorting that your model need 122 | ]; 123 | } 124 | } 125 | ``` 126 | 127 | After setup your model, you can use scope filter on your model like this 128 | 129 | ```php 130 | YourModel::query()->filter()->get(); 131 | ``` 132 | 133 | You can also override the predefined filter lists in your model like this 134 | 135 | ```php 136 | YourModel::query()->filter([ 137 | // the custom filter and sorting that your model need 138 | ]) 139 | ->paginate(); 140 | ``` 141 | 142 | ### Feature 143 | 144 | Here the use all filter and sort in the collection 145 | 146 | #### Bitwise filter 147 | 148 | ```php 149 | use Baro\PipelineQueryCollection\BitwiseFilter; 150 | 151 | // users?permission[0]=2&permission[1]=4 152 | User::query()->filter([ 153 | BitwiseFilter::make('permission'), // where permission & 6 = 6 154 | ]); 155 | ``` 156 | 157 | #### Boolean filter 158 | 159 | ```php 160 | use Baro\PipelineQueryCollection\BooleanFilter; 161 | 162 | // users?is_admin=1 163 | User::query()->filter([ 164 | BooleanFilter::make('is_admin'), // where is_admin = 1 165 | ]); 166 | ``` 167 | 168 | #### Date From filter 169 | 170 | ```php 171 | use Baro\PipelineQueryCollection\DateFromFilter; 172 | use Baro\PipelineQueryCollection\Enums\MotionEnum; 173 | 174 | // users?updated_at_from=2022-05-31 175 | User::query()->filter([ 176 | DateFromFilter::make('updated_at'), // where updated_at >= 2022-05-31 177 | DateFromFilter::make('updated_at', MotionEnum::TILL), // where updated_at > 2022-05-31 178 | // you can config default motion behavior and the postfix `from` in the config file 179 | ]); 180 | ``` 181 | 182 | #### Date To filter 183 | 184 | ```php 185 | use Baro\PipelineQueryCollection\DateToFilter; 186 | use Baro\PipelineQueryCollection\Enums\MotionEnum; 187 | 188 | // users?updated_at_to=2022-05-31 189 | User::query()->filter([ 190 | DateToFilter::make('updated_at'), // where updated_at <= 2022-05-31 191 | DateToFilter::make('updated_at', MotionEnum::TILL), // where updated_at < 2022-05-31 192 | // you can config default motion behavior and the postfix `to` in the config file 193 | ]); 194 | ``` 195 | 196 | # Range Filter 197 | 198 | ```php 199 | use Baro\PipelineQueryCollection\RangeFromFilter; 200 | use Baro\PipelineQueryCollection\RangeToFilter; 201 | 202 | // Example: products?price_from=100&price_to=500 203 | Product::query()->filter([ 204 | RangeFromFilter::make('price'), // Adds where price >= 100 205 | RangeToFilter::make('price'), // Adds where price <= 500 206 | ]); 207 | 208 | // Example: clients?age_min=18&age_max=65 209 | Client::query()->filter([ 210 | RangeFromFilter::make('age')->setPostFix('min'), // Adds where age >= 18 211 | RangeToFilter::make('age')->setPostFix('max'), // Adds where age <= 65 212 | ]); 213 | ``` 214 | 215 | #### Exact filter 216 | 217 | ```php 218 | use Baro\PipelineQueryCollection\ExactFilter; 219 | 220 | // users?id=4 221 | User::query()->filter([ 222 | ExactFilter::make('id'), // where id = 4 223 | ]); 224 | ``` 225 | 226 | #### Relation filter 227 | 228 | ```php 229 | use Baro\PipelineQueryCollection\RelationFilter; 230 | 231 | // users?roles_id[0]=1&roles_id[1]=4 232 | User::query()->filter([ 233 | RelationFilter::make('roles', 'id'), // where roles.id in(1,4) 234 | ]); 235 | ``` 236 | 237 | #### Relative filter 238 | 239 | ```php 240 | use Baro\PipelineQueryCollection\RelativeFilter; 241 | use Baro\PipelineQueryCollection\Enums\WildcardPositionEnum; 242 | 243 | // users?name=Baro 244 | User::query()->filter([ 245 | RelativeFilter::make('name'), // where('name', 'like', "%Baro%") 246 | RelativeFilter::make('name', WildcardPositionEnum::LEFT), // where('name', 'like', "%Baro") 247 | RelativeFilter::make('name', WildcardPositionEnum::RIGHT), // where('name', 'like', "Baro%") 248 | ]); 249 | ``` 250 | 251 | You can also filter multiple columns at once 252 | ```php 253 | use Baro\PipelineQueryCollection\FieldsRelativeFilter; 254 | use Baro\PipelineQueryCollection\Enums\WildcardPositionEnum; 255 | 256 | // users?search=Baro 257 | User::query()->filter([ 258 | FieldsRelativeFilter::make('search', ['name', 'title']), // where ("name" like '%Baro%' or "title" like '%Baro%') 259 | ]); 260 | ``` 261 | 262 | #### Scope filter 263 | 264 | ```php 265 | // users?search=Baro 266 | 267 | // User.php 268 | public function scopeSearch(Builder $query, string $keyword) 269 | { 270 | return $query->where(function (Builder $query) use ($keyword) { 271 | $query->where('id', $keyword) 272 | ->orWhere('name', 'like', "%{$keyword}%"); 273 | }); 274 | } 275 | 276 | // Query 277 | use Baro\PipelineQueryCollection\ScopeFilter; 278 | 279 | User::query()->filter([ 280 | ScopeFilter::make('search'), // where (`id` = 'Baro' or `name` like '%Baro%') 281 | ]); 282 | ``` 283 | 284 | #### Trash filter 285 | 286 | When using Laravel's [soft delete](https://laravel.com/docs/master/eloquent#querying-soft-deleted-models), you can use the pipe `TrashFilter` 287 | to query these models. The default query name is `trashed`, and filters responds to particular values: 288 | 289 | - `with`: the query should be `?trashed=with` to include soft deleted records to the result set 290 | - `only`: the query should be `?trashed=only` to return only soft deleted records to the result set 291 | - any other value, or completely remove `trashed` from request query will return only records that are not soft deleted in the result set 292 | 293 | You can change query name `trashed` by passing your custom name to the `TrashFilter` constructor 294 | 295 | ```php 296 | use Baro\PipelineQueryCollection\TrashFilter; 297 | 298 | 299 | // ?removed=only 300 | User::query()->filter([ 301 | TrashFilter::make('removed'), // where `deleted_at` is not null 302 | ]); 303 | ``` 304 | 305 | #### Sort 306 | 307 | ```php 308 | use Baro\PipelineQueryCollection\ScopeFilter; 309 | 310 | // users?sort[name]=asc&sort[permission]=desc 311 | User::query()->filter([ 312 | Sort::make(), // order by `name` asc, `permission` desc 313 | ]); 314 | ``` 315 | 316 | ### Detector 317 | 318 | Sometimes, you want to setup your request with a prefix like `filter.`. You can config every pipe that have it 319 | 320 | ```php 321 | use Baro\PipelineQueryCollection\ExactFilter; 322 | 323 | // users?filter[id]=4&filter[permission][0]=1&filter[permission][1]=4 324 | User::query()->filter([ 325 | ExactFilter::make('id')->detectBy('filter.'), // where id = 4 326 | BitwiseFilter::make('permission')->detectBy('filter.'), // where permission & 5 = 5 327 | ]); 328 | ``` 329 | 330 | Or, you can define it globally 331 | 332 | ```php 333 | // users?filter[id]=4&filter[permission][0]=1&filter[permission][1]=4 334 | 335 | // .env 336 | PIPELINE_QUERY_COLLECTION_DETECT_KEY="filter." 337 | 338 | // Query 339 | User::query()->filter([ 340 | ExactFilter::make('id'), // where id = 4 341 | BitwiseFilter::make('permission'), // where permission & 5 = 5 342 | ]); 343 | ``` 344 | 345 | ### Custom search column 346 | 347 | Sometimes, your request field is not the same with column name. For example, in your database you have column `respond` and want to perform some query against it, but for some reasons, your request query is `reply` instead of `respond`. 348 | 349 | ```php 350 | // users?reply=baro 351 | 352 | User::query()->filter([ 353 | RelativeFilter::make('reply')->filterOn('respond'), // where respond like '%baro%' 354 | ]); 355 | ``` 356 | 357 | ### Custom search value 358 | 359 | Your value that need to be searched isn't from your request? No problems. You can use `value()` function to hard set the search value! 360 | 361 | ```php 362 | User::query()->filter([ 363 | RelativeFilter::make('name')->value('Baro'), // where('name', 'like', "%Baro%") 364 | ]); 365 | ``` 366 | 367 | ### Extend filter 368 | 369 | Yeah, you are free to use your own pipe. Take a look at some of my filters. All of them extends `BaseFilter` to have some useful properties and functions. 370 | 371 | ## Testing 372 | 373 | ```bash 374 | composer test 375 | ``` 376 | 377 | ## Contributing 378 | 379 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 380 | 381 | ## Security Vulnerabilities 382 | 383 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 384 | 385 | ## Credits 386 | 387 | - [l3aro](https://github.com/l3aro) 388 | - [All Contributors](../../contributors) 389 | 390 | ## License 391 | 392 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 393 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "l3aro/pipeline-query-collection", 3 | "description": "A query database collection for use with Laravel Pipeline", 4 | "keywords": [ 5 | "l3aro", 6 | "laravel", 7 | "pipeline-query-collection" 8 | ], 9 | "homepage": "https://github.com/l3aro/pipeline-query-collection", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "l3aro", 14 | "email": "dgbao1340@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "spatie/laravel-package-tools": "^1.9.2", 21 | "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0" 22 | }, 23 | "require-dev": { 24 | "friendsofphp/php-cs-fixer": "^3.8", 25 | "laravel/pint": "^1.13", 26 | "nunomaduro/collision": "^7.0|^8.0", 27 | "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", 28 | "pestphp/pest": "^2.0|^3.7", 29 | "pestphp/pest-plugin-laravel": "^2.0|^3.1", 30 | "spatie/laravel-ray": "^1.26" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Baro\\PipelineQueryCollection\\": "src", 35 | "Baro\\PipelineQueryCollection\\Database\\Factories\\": "database/factories" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Baro\\PipelineQueryCollection\\Tests\\": "tests" 41 | } 42 | }, 43 | "scripts": { 44 | "analyse": "vendor/bin/phpstan analyse", 45 | "test": "vendor/bin/pest", 46 | "test-coverage": "vendor/bin/pest --coverage", 47 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 48 | }, 49 | "config": { 50 | "sort-packages": true, 51 | "allow-plugins": { 52 | "pestphp/pest-plugin": true, 53 | "phpstan/extension-installer": true 54 | } 55 | }, 56 | "extra": { 57 | "laravel": { 58 | "providers": [ 59 | "Baro\\PipelineQueryCollection\\PipelineQueryCollectionServiceProvider" 60 | ], 61 | "aliases": { 62 | "PipelineQueryCollection": "Baro\\PipelineQueryCollection\\Facades\\PipelineQueryCollection" 63 | } 64 | } 65 | }, 66 | "minimum-stability": "dev", 67 | "prefer-stable": true 68 | } 69 | -------------------------------------------------------------------------------- /config/pipeline-query-collection.php: -------------------------------------------------------------------------------- 1 | env('PIPELINE_QUERY_COLLECTION_DETECT_KEY', ''), 9 | 10 | // Allow the default wildcard position for relative filters to be controlled via .env. 11 | 'relative_wildcard_position' => WildcardPositionEnum::tryFrom( 12 | env('PIPELINE_QUERY_COLLECTION_WILDCARD_POSITION', 'both'), 13 | ), 14 | 15 | // type of postfix for date filters 16 | 'date_from_postfix' => env('PIPELINE_QUERY_COLLECTION_DATE_FROM_POSTFIX', 'from'), 17 | 'date_to_postfix' => env('PIPELINE_QUERY_COLLECTION_DATE_TO_POSTFIX', 'to'), 18 | 19 | // default motion for date filters, can be 'find' or 'till' 20 | 'date_motion' => env('PIPELINE_QUERY_COLLECTION_DATE_MOTION', 'find'), 21 | 22 | // type of postfix for range filters 23 | 'range_from_postfix' => env('PIPELINE_QUERY_COLLECTION_RANGE_FROM_POSTFIX', 'from'), 24 | 'range_to_postfix' => env('PIPELINE_QUERY_COLLECTION_RANGE_TO_POSTFIX', 'to'), 25 | ]; 26 | -------------------------------------------------------------------------------- /database/factories/RelatedModelFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name, 16 | 'type_flag' => $this->faker->randomElement([1, 2, 4]), 17 | 'is_visible' => $this->faker->boolean, 18 | 'created_at' => $this->faker->dateTime, 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist.bak: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | tests 24 | 25 | 26 | 27 | 28 | ./src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "per" 3 | } 4 | -------------------------------------------------------------------------------- /src/BaseFilter.php: -------------------------------------------------------------------------------- 1 | ignore(null); 23 | $this->detector = config('pipeline-query-collection.detect_key'); 24 | } 25 | 26 | public function handle($query, \Closure $next) 27 | { 28 | $this->query = $query; 29 | if (!$this->shouldFilter($this->getFilterName())) { 30 | return $next($query); 31 | } 32 | $this->apply(); 33 | 34 | return $next($this->query); 35 | } 36 | 37 | public function value(mixed $searchValue): static 38 | { 39 | $this->searchValue = $searchValue; 40 | 41 | return $this; 42 | } 43 | 44 | protected function getFilterName(): string 45 | { 46 | return "{$this->detector}{$this->field}"; 47 | } 48 | 49 | protected function getSearchValue(): array 50 | { 51 | if (!is_null($this->searchValue)) { 52 | $searchValue = $this->searchValue; 53 | } else { 54 | $searchValue = $this->request->input($this->getFilterName()); 55 | } 56 | if (!is_array($searchValue)) { 57 | $searchValue = [$searchValue]; 58 | } 59 | 60 | return $searchValue; 61 | } 62 | 63 | public function filterOn(string $searchColumn) 64 | { 65 | $this->searchColumn = $searchColumn; 66 | 67 | return $this; 68 | } 69 | 70 | protected function getSearchColumn() 71 | { 72 | return $this->searchColumn ?? $this->field; 73 | } 74 | 75 | public function filterOnColumns(mixed $searchColumns) 76 | { 77 | $this->searchColumns = $searchColumns; 78 | 79 | return $this; 80 | } 81 | 82 | public function ignore(mixed $ignore = '') 83 | { 84 | $this->ignore = $ignore; 85 | 86 | return $this; 87 | } 88 | 89 | public function field(mixed $field = '') 90 | { 91 | $this->field = $field; 92 | 93 | return $this; 94 | } 95 | 96 | public function detectBy(string $detector) 97 | { 98 | $this->detector = $detector; 99 | 100 | return $this; 101 | } 102 | 103 | protected function shouldFilter(string $key) 104 | { 105 | if (isset($this->searchValue)) { 106 | return true; 107 | } 108 | 109 | if (!$this->request->has($key)) { 110 | return false; 111 | } 112 | 113 | if ($this->ignore === $this->request->input($key)) { 114 | return false; 115 | } 116 | 117 | return true; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/BasePipe.php: -------------------------------------------------------------------------------- 1 | request = app(Request::class); 17 | } 18 | 19 | abstract protected function apply(): static; 20 | 21 | abstract public function handle($query, \Closure $next); 22 | 23 | protected function getDriverName(): string 24 | { 25 | /** @var \Illuminate\Database\Connection */ 26 | $connection = $this->query->getConnection(); 27 | 28 | return $connection->getDriverName(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/BaseRangeFilter.php: -------------------------------------------------------------------------------- 1 | field = $field; 16 | } 17 | 18 | abstract protected function getDefaultPostfix(): string; 19 | 20 | protected function apply(): static 21 | { 22 | $searchValue = $this->getSearchValue(); 23 | 24 | $this->query->where($this->field, $this->operator, $searchValue); 25 | 26 | return $this; 27 | } 28 | 29 | protected function getFilterName(): string 30 | { 31 | $postfix = $this->getPostfix() ?? $this->getDefaultPostfix(); 32 | 33 | return "{$this->detector}{$this->field}_{$postfix}"; 34 | } 35 | 36 | public function setPostfix(string $postfix): self 37 | { 38 | $this->postfix = $postfix; 39 | 40 | return $this; 41 | } 42 | 43 | private function getPostfix(): ?string 44 | { 45 | return $this->postfix; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/BaseSort.php: -------------------------------------------------------------------------------- 1 | sortValue = $sortValue; 14 | 15 | return $this; 16 | } 17 | 18 | public function handle($query, \Closure $next) 19 | { 20 | $this->query = $query; 21 | if (!is_null($this->sortValue)) { 22 | $sort = $this->sortValue; 23 | } else { 24 | $sort = $this->request->input('sort', []); 25 | } 26 | $this->sort = !is_array($sort) ? [$sort] : $sort; 27 | $this->apply(); 28 | 29 | return $next($this->query); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/BitwiseFilter.php: -------------------------------------------------------------------------------- 1 | field = $field; 11 | } 12 | 13 | public static function make($field) 14 | { 15 | return new self($field); 16 | } 17 | 18 | protected function apply(): static 19 | { 20 | $flag = null; 21 | foreach ($this->getSearchValue() as $value) { 22 | $flag ??= intval($value); 23 | $flag = intval($flag) | intval($value); 24 | } 25 | if ($flag === null) { 26 | return $this; 27 | } 28 | $this->query->whereRaw("{$this->getSearchColumn()} & ? = ?", [$flag, $flag]); 29 | 30 | return $this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/BooleanFilter.php: -------------------------------------------------------------------------------- 1 | field = $field; 11 | } 12 | 13 | public static function make($field) 14 | { 15 | return new self($field); 16 | } 17 | 18 | protected function apply(): static 19 | { 20 | foreach ($this->getSearchValue() as $value) { 21 | $this->query->where($this->getSearchColumn(), $value ? true : false); 22 | } 23 | 24 | return $this; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Concerns/Filterable.php: -------------------------------------------------------------------------------- 1 | filterCriteria() : $criteria; 13 | 14 | return app(Pipeline::class) 15 | ->send($query) 16 | ->through($criteria) 17 | ->thenReturn(); 18 | } 19 | 20 | public function filterCriteria(): array 21 | { 22 | if (method_exists($this, 'getFilters')) { 23 | return $this->getFilters(); 24 | } 25 | 26 | return []; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Concerns/Sortable.php: -------------------------------------------------------------------------------- 1 | sortCriteria() : $criteria; 13 | 14 | return app(Pipeline::class) 15 | ->send($query) 16 | ->through($criteria) 17 | ->thenReturn(); 18 | } 19 | 20 | public function sortCriteria(): array 21 | { 22 | if (method_exists($this, 'getSorts')) { 23 | return $this->getSorts(); 24 | } 25 | 26 | return []; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Contracts/CanFilterContract.php: -------------------------------------------------------------------------------- 1 | field = $field; 17 | if (is_null($motion)) { 18 | $motion = config('pipeline-query-collection.date_motion'); 19 | } 20 | if (!$motion instanceof MotionEnum) { 21 | $motion = MotionEnum::from($motion); 22 | } 23 | $this->motion = $motion; 24 | } 25 | 26 | public static function make($field = 'created_at', MotionEnum|string|null $motion = null) 27 | { 28 | return new self($field, $motion); 29 | } 30 | 31 | protected function apply(): static 32 | { 33 | $operator = $this->motion === MotionEnum::FIND ? '>=' : '>'; 34 | $action = $this->getAction(); 35 | foreach ($this->getSearchValue() as $value) { 36 | $this->query->$action($this->getSearchColumn(), $operator, $value); 37 | } 38 | 39 | return $this; 40 | } 41 | 42 | private function getAction(): string 43 | { 44 | return match ($this->getDriverName()) { 45 | 'sqlite' => 'whereDate', 46 | default => 'where', 47 | }; 48 | } 49 | 50 | protected function getFilterName(): string 51 | { 52 | $postfix = $this->getPostFix() ?? config('pipeline-query-collection.date_from_postfix'); 53 | 54 | return "{$this->detector}{$this->field}_{$postfix}"; 55 | } 56 | 57 | public function setPostFix(string $postfix): self 58 | { 59 | $this->postfix = $postfix; 60 | 61 | return $this; 62 | } 63 | 64 | private function getPostFix(): ?string 65 | { 66 | return $this->postfix; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/DateToFilter.php: -------------------------------------------------------------------------------- 1 | field = $field; 17 | if (is_null($motion)) { 18 | $motion = config('pipeline-query-collection.date_motion'); 19 | } 20 | if (!$motion instanceof MotionEnum) { 21 | $motion = MotionEnum::from($motion); 22 | } 23 | $this->motion = $motion; 24 | } 25 | 26 | public static function make($field = 'created_at', MotionEnum|string|null $motion = null) 27 | { 28 | return new self($field, $motion); 29 | } 30 | 31 | protected function apply(): static 32 | { 33 | $operator = $this->motion === MotionEnum::FIND ? '<=' : '<'; 34 | $action = $this->getAction(); 35 | foreach ($this->getSearchValue() as $value) { 36 | $this->query->$action($this->getSearchColumn(), $operator, $value); 37 | } 38 | 39 | return $this; 40 | } 41 | 42 | private function getAction(): string 43 | { 44 | return match ($this->getDriverName()) { 45 | 'sqlite' => 'whereDate', 46 | default => 'where', 47 | }; 48 | } 49 | 50 | protected function getFilterName(): string 51 | { 52 | $postfix = $this->getPostFix() ?? config('pipeline-query-collection.date_to_postfix'); 53 | 54 | return "{$this->detector}{$this->field}_{$postfix}"; 55 | } 56 | 57 | public function setPostFix(string $postfix): self 58 | { 59 | $this->postfix = $postfix; 60 | 61 | return $this; 62 | } 63 | 64 | private function getPostFix(): ?string 65 | { 66 | return $this->postfix; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Enums/MotionEnum.php: -------------------------------------------------------------------------------- 1 | field = $field; 11 | } 12 | 13 | public static function make($field) 14 | { 15 | return new self($field); 16 | } 17 | 18 | protected function apply(): static 19 | { 20 | $this->query->whereIn($this->getSearchColumn(), $this->getSearchValue()); 21 | 22 | return $this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/FieldsRelativeFilter.php: -------------------------------------------------------------------------------- 1 | field = $field; 15 | $this->searchColumns = $columns; 16 | if (is_null($wildcardPosition)) { 17 | $wildcardPosition = config('pipeline-query-collection.relative_wildcard_position', WildcardPositionEnum::BOTH); 18 | } 19 | if (!$wildcardPosition instanceof WildcardPositionEnum) { 20 | $wildcardPosition = WildcardPositionEnum::from($wildcardPosition); 21 | } 22 | $this->wildcardPosition = $wildcardPosition; 23 | } 24 | 25 | public static function make($field, $columns, WildcardPositionEnum|string|null $wildcardPosition = null) 26 | { 27 | return new self($field, $columns, $wildcardPosition); 28 | } 29 | 30 | protected function getSearchColumns() 31 | { 32 | return $this->searchColumns ?? $this->field; 33 | } 34 | 35 | protected function apply(): static 36 | { 37 | foreach ($this->getSearchValue() as $value) { 38 | $this->query->whereNested(function ($query) use ($value) { 39 | foreach ($this->getSearchColumns() as $column) { 40 | $query->orWhere($column, 'like', $this->computeSearchValue($value)); 41 | } 42 | }); 43 | } 44 | 45 | return $this; 46 | } 47 | 48 | private function computeSearchValue($value) 49 | { 50 | return match ($this->wildcardPosition) { 51 | WildcardPositionEnum::RIGHT => "$value%", 52 | WildcardPositionEnum::LEFT => "%$value", 53 | default => "%$value%", 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/PipelineQueryCollectionServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('pipeline-query-collection') 19 | ->hasConfigFile(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/RangeFromFilter.php: -------------------------------------------------------------------------------- 1 | ='; 8 | 9 | protected function getDefaultPostfix(): string 10 | { 11 | return config('pipeline-query-collection.range_from_postfix'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/RangeToFilter.php: -------------------------------------------------------------------------------- 1 | relation = $relation; 15 | $this->field = $field; 16 | } 17 | 18 | public static function make($relation, $field) 19 | { 20 | return new self($relation, $field); 21 | } 22 | 23 | protected function getFilterName(): string 24 | { 25 | return "{$this->detector}{$this->relation}_{$this->field}"; 26 | } 27 | 28 | protected function apply(): static 29 | { 30 | $searchValue = $this->getSearchValue(); 31 | $this->relation = Str::camel($this->relation); 32 | $this->query->whereHas($this->relation, function ($query) use ($searchValue) { 33 | $query->whereIn($this->getSearchColumn(), $searchValue); 34 | }); 35 | 36 | return $this; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/RelativeFilter.php: -------------------------------------------------------------------------------- 1 | field = $field; 15 | if (is_null($wildcardPosition)) { 16 | $wildcardPosition = config('pipeline-query-collection.relative_wildcard_position', WildcardPositionEnum::BOTH); 17 | } 18 | if (!$wildcardPosition instanceof WildcardPositionEnum) { 19 | $wildcardPosition = WildcardPositionEnum::from($wildcardPosition); 20 | } 21 | $this->wildcardPosition = $wildcardPosition; 22 | } 23 | 24 | public static function make($field, WildcardPositionEnum|string|null $wildcardPosition = null) 25 | { 26 | return new self($field, $wildcardPosition); 27 | } 28 | 29 | protected function apply(): static 30 | { 31 | foreach ($this->getSearchValue() as $value) { 32 | $this->query->where($this->getSearchColumn(), 'like', $this->computeSearchValue($value)); 33 | } 34 | 35 | return $this; 36 | } 37 | 38 | private function computeSearchValue($value) 39 | { 40 | return match ($this->wildcardPosition) { 41 | WildcardPositionEnum::RIGHT => "$value%", 42 | WildcardPositionEnum::LEFT => "%$value", 43 | default => "%$value%", 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ScopeFilter.php: -------------------------------------------------------------------------------- 1 | field = $scopeName; 11 | } 12 | 13 | public static function make($scopeName) 14 | { 15 | return new self($scopeName); 16 | } 17 | 18 | protected function apply(): static 19 | { 20 | $scopeName = str($this->getSearchColumn())->camel()->toString(); 21 | foreach ($this->getSearchValue() as $value) { 22 | $this->query->{$scopeName}($value); 23 | } 24 | 25 | return $this; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Sort.php: -------------------------------------------------------------------------------- 1 | sort as $field => $direction) { 15 | $this->query->orderBy($field, $direction); 16 | } 17 | 18 | return $this; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/SortAscending.php: -------------------------------------------------------------------------------- 1 | sort as $field) { 15 | $this->query->orderBy($field, 'asc'); 16 | } 17 | 18 | return $this; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/SortDescending.php: -------------------------------------------------------------------------------- 1 | sort as $field) { 15 | $this->query->orderBy($field, 'desc'); 16 | } 17 | 18 | return $this; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/TrashFilter.php: -------------------------------------------------------------------------------- 1 | field = $field; 13 | } 14 | 15 | public static function make($field = 'trashed') 16 | { 17 | return new self($field); 18 | } 19 | 20 | protected function apply(): static 21 | { 22 | $option = TrashOptionEnum::tryFrom($this->getSearchValue()[0]); 23 | match ($option) { 24 | TrashOptionEnum::ONLY => $this->query->onlyTrashed(), // @phpstan-ignore-line 25 | TrashOptionEnum::WITH => $this->query->withTrashed(), // @phpstan-ignore-line 26 | default => $this->query, 27 | }; 28 | 29 | return $this; 30 | } 31 | } 32 | --------------------------------------------------------------------------------