├── LICENSE.txt ├── README.md ├── composer.json ├── config └── eloquentfilter.php └── src ├── Commands └── MakeEloquentFilter.php ├── Filterable.php ├── LumenServiceProvider.php ├── ModelFilter.php ├── ServiceProvider.php └── stubs └── modelfilter.stub /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 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 | # Eloquent Filter 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/tucker-eric/eloquentfilter/v/stable)](https://packagist.org/packages/tucker-eric/eloquentfilter) 4 | [![Total Downloads](https://poser.pugx.org/tucker-eric/eloquentfilter/downloads)](https://packagist.org/packages/tucker-eric/eloquentfilter) 5 | [![Daily Downloads](https://poser.pugx.org/tucker-eric/eloquentfilter/d/daily)](https://packagist.org/packages/tucker-eric/eloquentfilter) 6 | [![License](https://poser.pugx.org/tucker-eric/eloquentfilter/license)](https://packagist.org/packages/tucker-eric/eloquentfilter) 7 | [![StyleCI](https://styleci.io/repos/53163405/shield)](https://styleci.io/repos/53163405/) 8 | [![PHPUnit Status](https://github.com/Tucker-Eric/EloquentFilter/workflows/PHPUnit/badge.svg?branch=master)](https://github.com/Tucker-Eric/EloquentFilter/actions?query=branch%3Amaster) 9 | 10 | An Eloquent way to filter Eloquent Models and their relationships. 11 | 12 | ## Introduction 13 | Lets say we want to return a list of users filtered by multiple parameters. When we navigate to: 14 | 15 | `/users?name=er&last_name=&company_id=2&roles[]=1&roles[]=4&roles[]=7&industry=5` 16 | 17 | `$request->all()` will return: 18 | 19 | ```php 20 | [ 21 | 'name' => 'er', 22 | 'last_name' => '', 23 | 'company_id' => '2', 24 | 'roles' => ['1','4','7'], 25 | 'industry' => '5' 26 | ] 27 | ``` 28 | 29 | To filter by all those parameters we would need to do something like: 30 | 31 | ```php 32 | input('company_id')); 46 | 47 | if ($request->has('last_name')) 48 | { 49 | $query->where('last_name', 'LIKE', '%' . $request->input('last_name') . '%'); 50 | } 51 | 52 | if ($request->has('name')) 53 | { 54 | $query->where(function ($q) use ($request) 55 | { 56 | return $q->where('first_name', 'LIKE', $request->input('name') . '%') 57 | ->orWhere('last_name', 'LIKE', '%' . $request->input('name') . '%'); 58 | }); 59 | } 60 | 61 | $query->whereHas('roles', function ($q) use ($request) 62 | { 63 | return $q->whereIn('id', $request->input('roles')); 64 | }) 65 | ->whereHas('clients', function ($q) use ($request) 66 | { 67 | return $q->whereHas('industry_id', $request->input('industry')); 68 | }); 69 | 70 | return $query->get(); 71 | } 72 | 73 | } 74 | ``` 75 | 76 | To filter that same input With Eloquent Filters: 77 | 78 | ```php 79 | all())->get(); 93 | } 94 | 95 | } 96 | ``` 97 | 98 | ## Configuration 99 | ### Install Through Composer 100 | ``` 101 | composer require tucker-eric/eloquentfilter 102 | ``` 103 | 104 | There are a few ways to define the filter a model will use: 105 | 106 | - [Use EloquentFilter's Default Settings](#default-settings) 107 | - [Use A Custom Namespace For All Filters](#with-configuration-file-optional) 108 | - [Define A Model's Default Filter](#define-the-default-model-filter-optional) 109 | - [Dynamically Select A Model's Filter](#dynamic-filters) 110 | 111 | 112 | #### Default Settings 113 | The default namespace for all filters is `App\ModelFilters\` and each Model expects the filter classname to follow the `{$ModelName}Filter` naming convention regardless of the namespace the model is in. Here is an example of Models and their respective filters based on the default naming convention. 114 | 115 | |Model|ModelFilter| 116 | |-----|-----------| 117 | |`App\User`|`App\ModelFilters\UserFilter`| 118 | |`App\FrontEnd\PrivatePost`|`App\ModelFilters\PrivatePostFilter`| 119 | |`App\FrontEnd\Public\GuestPost`|`App\ModelFilters\GuestPostFilter`| 120 | 121 | #### Laravel 122 | 123 | ##### With Configuration File (Optional) 124 | 125 | > Registering the service provider will give you access to the `php artisan model:filter {model}` command as well as allow you to publish the configuration file. Registering the service provider is not required and only needed if you want to change the default namespace or use the artisan command 126 | 127 | After installing the Eloquent Filter library, register the `EloquentFilter\ServiceProvider::class` in your `config/app.php` configuration file: 128 | 129 | ```php 130 | 'providers' => [ 131 | // Other service providers... 132 | 133 | EloquentFilter\ServiceProvider::class, 134 | ], 135 | ``` 136 | 137 | Copy the package config to your local config with the publish command: 138 | 139 | ```bash 140 | php artisan vendor:publish --provider="EloquentFilter\ServiceProvider" 141 | ``` 142 | 143 | In the `config/eloquentfilter.php` config file. Set the namespace your model filters will reside in: 144 | 145 | ```php 146 | 'namespace' => "App\\ModelFilters\\", 147 | ``` 148 | 149 | #### Lumen 150 | 151 | ##### Register The Service Provider (Optional) 152 | 153 | >This is only required if you want to use the `php artisan model:filter` command. 154 | 155 | In `bootstrap/app.php`: 156 | 157 | ```php 158 | $app->register(EloquentFilter\LumenServiceProvider::class); 159 | ``` 160 | 161 | ##### Change The Default Namespace 162 | 163 | In `bootstrap/app.php`: 164 | 165 | ```php 166 | config(['eloquentfilter.namespace' => "App\\Models\\ModelFilters\\"]); 167 | ``` 168 | 169 | #### Define The Default Model Filter (optional) 170 | 171 | > The following is optional. If no `modelFilter` method is found on the model the model's filter class will be resolved by the [default naming conventions](#default-settings) 172 | 173 | Create a public method `modelFilter()` that returns `$this->provideFilter(Your\Model\Filter::class);` in your model. 174 | 175 | ```php 176 | provideFilter(\App\ModelFilters\CustomFilters\CustomUserFilter::class); 190 | } 191 | 192 | //User Class 193 | } 194 | ``` 195 | #### Dynamic Filters 196 | 197 | You can define the filter dynamically by passing the filter to use as the second parameter of the `filter()` method. Defining a filter dynamically will take precedent over any other filters defined for the model. 198 | 199 | ```php 200 | isAdmin() ? AdminFilter::class : BasicUserFilter::class; 216 | 217 | return User::filter($request->all(), $userFilter)->get(); 218 | } 219 | } 220 | 221 | ``` 222 | 223 | ### Generating The Filter 224 | > Only available if you have registered `EloquentFilter\ServiceProvider::class` in the providers array in your `config/app.php' 225 | 226 | You can create a model filter with the following artisan command: 227 | 228 | ```bash 229 | php artisan model:filter User 230 | ``` 231 | 232 | Where `User` is the Eloquent Model you are creating the filter for. This will create `app/ModelFilters/UserFilter.php` 233 | 234 | The command also supports psr-4 namespacing for creating filters. You just need to make sure you escape the backslashes in the class name. For example: 235 | 236 | ```bash 237 | php artisan model:filter AdminFilters\\User 238 | ``` 239 | 240 | This would create `app/ModelFilters/AdminFilters/UserFilter.php` 241 | 242 | ## Usage 243 | 244 | ### Defining The Filter Logic 245 | Define the filter logic based on the camel cased input key passed to the `filter()` method. 246 | 247 | - Empty strings and null values are ignored by default. 248 | - Empty strings and values can be configured not to be ignored by setting `protected $allowedEmptyFilters = false;` on a filter. 249 | - If a `setup()` method is defined it will be called once before any filter methods regardless of input 250 | - `_id` is dropped from the end of the input key to define the method so filtering `user_id` would use the `user()` method 251 | - Can be changed with by definining `protected $drop_id = false;` on a filter 252 | - Input without a corresponding filter method are ignored 253 | - The value of the key is injected into the method 254 | - All values are accessible through the `$this->input()` method or a single value by key `$this->input($key)` 255 | - All Eloquent Builder methods are accessible in `$this` context in the model filter class. 256 | 257 | To define methods for the following input: 258 | 259 | ```php 260 | [ 261 | 'company_id' => 5, 262 | 'name' => 'Tuck', 263 | 'mobile_phone' => '888555' 264 | ] 265 | ``` 266 | 267 | You would use the following methods: 268 | 269 | ```php 270 | 271 | use EloquentFilter\ModelFilter; 272 | 273 | class UserFilter extends ModelFilter 274 | { 275 | protected $blacklist = ['secretMethod']; 276 | 277 | // This will filter 'company_id' OR 'company' 278 | public function company($id) 279 | { 280 | return $this->where('company_id', $id); 281 | } 282 | 283 | public function name($name) 284 | { 285 | return $this->where(function($q) use ($name) 286 | { 287 | return $q->where('first_name', 'LIKE', "%$name%") 288 | ->orWhere('last_name', 'LIKE', "%$name%"); 289 | }); 290 | } 291 | 292 | public function mobilePhone($phone) 293 | { 294 | return $this->where('mobile_phone', 'LIKE', "$phone%"); 295 | } 296 | 297 | public function setup() 298 | { 299 | $this->onlyShowDeletedForAdmins(); 300 | } 301 | 302 | public function onlyShowDeletedForAdmins() 303 | { 304 | if(Auth::user()->isAdmin()) 305 | { 306 | $this->withTrashed(); 307 | } 308 | } 309 | 310 | public function secretMethod($secretParameter) 311 | { 312 | return $this->where('some_column', true); 313 | } 314 | } 315 | ``` 316 | 317 | > **Note:** In the above example if you do not want `_id` dropped from the end of the input you can set `protected $drop_id = false` on your filter class. Doing this would allow you to have a `company()` filter method as well as a `companyId()` filter method. 318 | 319 | 320 | > **Note:** In the above example if you do not want `mobile_phone` to be mapped to `mobilePhone()` you can set `protected $camel_cased_methods = false` on your filter class. Doing this would allow you to have a `mobile_phone()` filter method instead of `mobilePhone()`. By default, `mobilePhone()` filter method can be called thanks to one of the following input key: `mobile_phone`, `mobilePhone`, `mobile_phone_id` 321 | 322 | > **Note:** In the example above all methods inside `setup()` will be called every time `filter()` is called on the model 323 | 324 | #### Blacklist 325 | 326 | Any methods defined in the `blackist` array will not be called by the filter. Those methods are normally used for internal filter logic. 327 | 328 | The `blacklistMethod()` and `whitelistMethod()` methods can be used to dynamically blacklist and whitelist methods. 329 | 330 | In the example above `secretMethod()` will not be called, even if there is a `secret_method` key in the input array. In order to call this method it would need to be whitelisted dynamically: 331 | 332 | Example: 333 | ```php 334 | public function setup() 335 | { 336 | if(Auth::user()->isAdmin()) { 337 | $this->whitelistMethod('secretMethod'); 338 | } 339 | } 340 | ``` 341 | 342 | 343 | #### Additional Filter Methods 344 | 345 | The `Filterable` trait also comes with the below query builder helper methods: 346 | 347 | |EloquentFilter Method|QueryBuilder Equivalent| 348 | |---|---| 349 | |`$this->whereLike($column, $string)`|`$query->where($column, 'LIKE', '%'.$string.'%')`| 350 | |`$this->whereLike($column, $string, 'or')`|`$query->orWhere($column, 'LIKE', '%'.$string.'%')`| 351 | |`$this->whereBeginsWith($column, $string)`|`$query->where($column, 'LIKE', $string.'%')`| 352 | |`$this->whereBeginsWith($column, $string, 'or')`|`$query->orWhere($column, 'LIKE', $string.'%')`| 353 | |`$this->whereEndsWith($column, $string)`|`$query->where($column, 'LIKE', '%'.$string)`| 354 | |`$this->whereEndsWith($column, $string, 'or')`|`$query->orWhere($column, 'LIKE', '%'.$string)`| 355 | 356 | Since these methods are part of the `Filterable` trait they are accessible from any model that implements the trait without the need to call in the Model's EloquentFilter. 357 | 358 | 359 | ### Applying The Filter To A Model 360 | 361 | Implement the `EloquentFilter\Filterable` trait on any Eloquent model: 362 | 363 | ```php 364 | all())->get(); 387 | } 388 | } 389 | ``` 390 | 391 | ## Filtering By Relationships 392 | >There are two ways to filter by related models. Using the `$relations` array to define the input to be injected into the related Model's filter. If the related model doesn't have a model filter of it's own or you just want to define how to filter that relationship locally instead of adding the logic to that Model's filter then use the `related()` method to filter by a related model that doesn't have a ModelFilter. You can even combine the 2 and define which input fields in the `$relations` array you want to use that Model's filter for as well as use the `related()` method to define local methods on that same relation. Both methods nest the filter constraints into the same `whereHas()` query on that relation. 393 | 394 | For both examples we will use the following models: 395 | 396 | A `App\User` that `hasMany` `App\Client::class`: 397 | 398 | ```php 399 | class User extends Model 400 | { 401 | use Filterable; 402 | 403 | public function clients() 404 | { 405 | return $this->hasMany(Client::class); 406 | } 407 | } 408 | ``` 409 | 410 | And each `App\Client` belongs to `App\Industry::class`: 411 | 412 | ```php 413 | class Client extends Model 414 | { 415 | use Filterable; 416 | 417 | public function industry() 418 | { 419 | return $this->belongsTo(Industry::class); 420 | } 421 | 422 | public function scopeHasRevenue($query) 423 | { 424 | return $query->where('total_revenue', '>', 0); 425 | } 426 | } 427 | ``` 428 | 429 | 430 | We want to query our users and filter them by the industry and volume potential of their clients that have done revenue in the past. 431 | 432 | Input used to filter: 433 | 434 | ```php 435 | $input = [ 436 | 'industry' => '5', 437 | 'potential_volume' => '10000' 438 | ]; 439 | ``` 440 | 441 | ### Setup 442 | 443 | Both methods will invoke a setup query on the relationship that will be called EVERY time this relationship is queried. The setup methods signature is `{$related}Setup()` and is injected with an instance of that relations query builder. For this example let's say when querying users by their clients I only ever want to show agents that have clients with revenue. Without choosing wich method to put it in (because sometimes we may not have all the input and miss the scope all together if we choose the wrong one) and to avoid query duplication by placing that constraint on ALL methods for that relation we call the related setup method in the `UserFilter` like: 444 | 445 | ```php 446 | class UserFilter extends ModelFilter 447 | { 448 | public function clientsSetup($query) 449 | { 450 | return $query->hasRevenue(); 451 | } 452 | } 453 | ``` 454 | This will prepend the query to the `clients()` relation with `hasRevenue()` whenever the `UserFilter` runs any constriants on the `clients()` relationship. If there are no queries to the `clients()` relationship then this method will not be invoked. 455 | 456 | > You can learn more about scopes [here](https://laravel.com/docs/master/eloquent#local-scopes) 457 | 458 | 459 | ### Ways To Filter Related Models 460 | 461 | - [With The `related()` Method](#filter-related-models-with-the-related-method) 462 | - [Using The `$relations` Array](#filter-related-models-using-the-relations-array) 463 | - [With Both Methods](#filter-related-models-with-both-methods) 464 | 465 | #### Filter Related Models With The `related()` Method: 466 | 467 | The `related()` method is a little easier to setup and is great if you aren't going to be using the related Model's filter to ever filter that Model explicitly. The `related()` method takes the same parameters as the `Eloquent\Builder`'s `where()` method except for the first parameter being the relationship name. 468 | 469 | ##### Example: 470 | 471 | 472 | `UserFilter` with an `industry()` method that uses the `ModelFilter`'s `related()` method 473 | 474 | ```php 475 | class UserFilter extends ModelFilter 476 | { 477 | public function industry($id) 478 | { 479 | return $this->related('clients', 'industry_id', '=', $id); 480 | 481 | // This would also be shorthand for the same query 482 | // return $this->related('clients', 'industry_id', $id); 483 | } 484 | 485 | public function potentialVolume($volume) 486 | { 487 | return $this->related('clients', 'potential_volume', '>=', $volume); 488 | } 489 | } 490 | ``` 491 | 492 | Or you can even pass a closure as the second argument which will inject an instance of the related model's query builder like: 493 | 494 | ```php 495 | $this->related('clients', function($query) use ($id) { 496 | return $query->where('industry_id', $id); 497 | }); 498 | ``` 499 | 500 | #### Filter Related Models Using The `$relations` Array: 501 | 502 | Add the relation in the `$relations` array with the name of the relation as referred to on the model as the key and an array of input keys that was passed to the `filter()` method. 503 | 504 | The related model **MUST** have a ModelFilter associated with it. We instantiate the related model's filter and use the input values from the `$relations` array to call the associated methods. 505 | 506 | This is helpful when querying multiple columns on a relation's table while avoiding multiple `whereHas()` calls for the same relationship. For a single column using a `$this->whereHas()` method in the model filter works just fine. In fact, under ther hood the model filter applies all constraints in the `whereHas()` method. 507 | 508 | ##### Example: 509 | 510 | `UserFilter` with the relation defined so it's able to be queried. 511 | 512 | ```php 513 | class UserFilter extends ModelFilter 514 | { 515 | public $relations = [ 516 | 'clients' => ['industry', 'potential_volume'], 517 | ]; 518 | } 519 | ``` 520 | 521 | `ClientFilter` with the `industry` method that's used to filter: 522 | > **Note:** The `$relations` array should identify the relation and the input key to filter by that relation. Just as the `ModelFilter` works, this will access the camelCased method on that relation's filter. If the above example was using the key `industry_type` for the input the relations array would be `$relations = ['clients' => ['industry_type']]` and the `ClientFilter` would have the method `industryType()`. 523 | 524 | ```php 525 | class ClientFilter extends ModelFilter 526 | { 527 | public $relations = []; 528 | 529 | public function industry($id) 530 | { 531 | return $this->where('industry_id', $id); 532 | } 533 | 534 | public function potentialVolume($volume) 535 | { 536 | return $this->where('potential_volume', '>=', $volume); 537 | } 538 | } 539 | ``` 540 | ##### `$relations` array alias support 541 | The `$relations` array supports aliases. This is used when the input doesn't match the related model's filter method. 542 | This will transform the input keys being passed to the related model filter's input. 543 | 544 | ##### Example: 545 | ```php 546 | class UserFilter extends ModelFilter 547 | { 548 | public $relations = [ 549 | 'clients' => [ 550 | 'client_industry' => 'industry', 551 | 'client_potential' => 'potential_volume' 552 | ] 553 | ]; 554 | } 555 | ``` 556 | 557 | The above will receive an array like: 558 | ```php 559 | [ 560 | 'client_industry' => 1, 561 | 'client_potential' => 100000 562 | ] 563 | ``` 564 | And the `ClientFilter` will receive it as: 565 | ```php 566 | [ 567 | 'industry' => 1, 568 | 'potential_volume' => 100000 569 | ] 570 | ``` 571 | Allowing for more descriptive input names without filters needing to match. Allowing for more reuse of the same filters. 572 | 573 | #### Filter Related Models With Both Methods 574 | You can even use both together and it will produce the same result and only query the related model once. An example would be: 575 | 576 | If the following array is passed to the `filter()` method: 577 | 578 | ```php 579 | [ 580 | 'name' => 'er', 581 | 'last_name' => '', 582 | 'company_id' => 2, 583 | 'roles' => [1,4,7], 584 | 'industry' => 5, 585 | 'potential_volume' => '10000' 586 | ] 587 | ``` 588 | 589 | In `app/ModelFilters/UserFilter.php`: 590 | 591 | ```php 592 | ['industry'], 600 | ]; 601 | 602 | public function clientsSetup($query) 603 | { 604 | return $query->hasRevenue(); 605 | } 606 | 607 | public function name($name) 608 | { 609 | return $this->where(function($q) 610 | { 611 | return $q->where('first_name', 'LIKE', $name . '%')->orWhere('last_name', 'LIKE', '%' . $name.'%'); 612 | }); 613 | } 614 | 615 | public function potentialVolume($volume) 616 | { 617 | return $this->related('clients', 'potential_volume', '>=', $volume); 618 | } 619 | 620 | public function lastName($lastName) 621 | { 622 | return $this->where('last_name', 'LIKE', '%' . $lastName); 623 | } 624 | 625 | public function company($id) 626 | { 627 | return $this->where('company_id',$id); 628 | } 629 | 630 | public function roles($ids) 631 | { 632 | return $this->whereHas('roles', function($query) use ($ids) 633 | { 634 | return $query->whereIn('id', $ids); 635 | }); 636 | } 637 | } 638 | ``` 639 | 640 | ##### Adding Relation Values To Filter 641 | 642 | Sometimes, based on the value of a parameter you may need to push data to a relation filter. The `push()` method does just this. 643 | It accepts one argument as an array of key value pairs or two arguments as a key value pair `push($key, $value)`. 644 | Related models are filtered AFTER all local values have been executed you can use this method in any filter method. 645 | This avoids having to query a related table more than once. For Example: 646 | 647 | ```php 648 | public $relations = [ 649 | 'clients' => ['industry', 'status'], 650 | ]; 651 | 652 | public function statusType($type) 653 | { 654 | if($type === 'all') { 655 | $this->push('status', 'all'); 656 | } 657 | } 658 | ``` 659 | 660 | The above example will pass `'all'` to the `status()` method on the `clients` relation of the model. 661 | > Calling the `push()` method in the `setup()` method will allow you to push values to the input for filter it's called on 662 | 663 | #### Pagination 664 | 665 | If you want to paginate your query and keep the url query string without having to use: 666 | 667 | ```php 668 | {!! $pages->appends(Input::except('page'))->render() !!} 669 | ``` 670 | 671 | The `paginateFilter()` and `simplePaginateFilter()` methods accept the same input as [Laravel's paginator](https://laravel.com/docs/master/pagination#basic-usage) and returns the respective paginator. 672 | 673 | ```php 674 | class UserController extends Controller 675 | { 676 | public function index(Request $request) 677 | { 678 | $users = User::filter($request->all())->paginateFilter(); 679 | 680 | return view('users.index', compact('users')); 681 | } 682 | ``` 683 | 684 | OR: 685 | 686 | ```php 687 | public function simpleIndex(Request $request) 688 | { 689 | $users = User::filter($request->all())->simplePaginateFilter(); 690 | 691 | return view('users.index', compact('users')); 692 | } 693 | } 694 | ``` 695 | 696 | In your view `$users->render()` will return pagination links as it normally would but with the original query string with empty input ignored if `protected $allowedEmptyFilters` is not set to `false` on the filter. 697 | 698 | 699 | # Contributing 700 | Any contributions are welcome! 701 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tucker-eric/eloquentfilter", 3 | "description": "An Eloquent way to filter Eloquent Models", 4 | "keywords": [ 5 | "laravel", 6 | "eloquent", 7 | "model", 8 | "search", 9 | "query", 10 | "filter" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Eric Tucker", 16 | "email": "tucker.ericm@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.2", 21 | "illuminate/console": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0", 22 | "illuminate/filesystem": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0", 23 | "illuminate/database": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0", 24 | "illuminate/support": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0", 25 | "illuminate/config": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0", 26 | "illuminate/pagination": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^8", 30 | "mockery/mockery": "^1.3" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "EloquentFilter\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "EloquentFilter\\TestClass\\": "tests/classes" 40 | } 41 | }, 42 | "config": { 43 | "preferred-install": "dist" 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true, 47 | "extra": { 48 | "laravel": { 49 | "providers": [ 50 | "EloquentFilter\\ServiceProvider" 51 | ] 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /config/eloquentfilter.php: -------------------------------------------------------------------------------- 1 | 'App\\ModelFilters\\', 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Custom generator stub 19 | |-------------------------------------------------------------------------- 20 | | 21 | | If you want to override the default stub this package provides 22 | | you can enter the path to your own at this point 23 | | 24 | */ 25 | // 'generator' => [ 26 | // 'stub' => app_path('stubs/modelfilter.stub') 27 | // ] 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Default Paginator Limit For `paginateFilter` and `simplePaginateFilter` 32 | |-------------------------------------------------------------------------- 33 | | 34 | | Set paginate limit 35 | | 36 | */ 37 | 'paginate_limit' => env('PAGINATION_LIMIT_DEFAULT',15) 38 | 39 | ]; 40 | -------------------------------------------------------------------------------- /src/Commands/MakeEloquentFilter.php: -------------------------------------------------------------------------------- 1 | files = $files; 49 | } 50 | 51 | /** 52 | * Execute the console command. 53 | * 54 | * @return mixed 55 | */ 56 | public function handle() 57 | { 58 | $this->makeClassName()->compileStub(); 59 | $this->info(class_basename($this->getClassName()).' Created Successfully!'); 60 | } 61 | 62 | public function compileStub() 63 | { 64 | if ($this->files->exists($path = $this->getPath())) { 65 | $this->error("\n\n\t".$path.' Already Exists!'."\n"); 66 | exit; 67 | } 68 | $this->makeDirectory($path); 69 | 70 | $stubPath = config('eloquentfilter.generator.stub', __DIR__.'/../stubs/modelfilter.stub'); 71 | 72 | if (! $this->files->exists($stubPath) || ! is_readable($stubPath)) { 73 | $this->error(sprintf('File "%s" does not exist or is unreadable.', $stubPath)); 74 | exit; 75 | } 76 | 77 | $tmp = $this->applyValuesToStub($this->files->get($stubPath)); 78 | $this->files->put($path, $tmp); 79 | } 80 | 81 | public function applyValuesToStub($stub) 82 | { 83 | $className = $this->getClassBasename($this->getClassName()); 84 | $search = ['{{class}}', '{{namespace}}']; 85 | $replace = [$className, str_replace('\\'.$className, '', $this->getClassName())]; 86 | 87 | return str_replace($search, $replace, $stub); 88 | } 89 | 90 | private function getClassBasename($class) 91 | { 92 | $class = is_object($class) ? get_class($class) : $class; 93 | 94 | return basename(str_replace('\\', '/', $class)); 95 | } 96 | 97 | public function getPath() 98 | { 99 | return $this->laravel->path.DIRECTORY_SEPARATOR.$this->getFileName(); 100 | } 101 | 102 | public function getFileName() 103 | { 104 | return str_replace([$this->getAppNamespace(), '\\'], ['', DIRECTORY_SEPARATOR], $this->getClassName().'.php'); 105 | } 106 | 107 | public function getAppNamespace() 108 | { 109 | return $this->laravel->getNamespace(); 110 | } 111 | 112 | /** 113 | * Build the directory for the class if necessary. 114 | * 115 | * @param string $path 116 | * @return string 117 | */ 118 | protected function makeDirectory($path) 119 | { 120 | if (! $this->files->isDirectory(dirname($path))) { 121 | $this->files->makeDirectory(dirname($path), 0777, true, true); 122 | } 123 | } 124 | 125 | /** 126 | * Create Filter Class Name. 127 | * 128 | * @return $this 129 | */ 130 | public function makeClassName() 131 | { 132 | $parts = array_map([Str::class, 'studly'], explode('\\', $this->argument('name'))); 133 | $className = array_pop($parts); 134 | $ns = count($parts) > 0 ? implode('\\', $parts).'\\' : ''; 135 | 136 | $fqClass = config('eloquentfilter.namespace', 'App\\ModelFilters\\').$ns.$className; 137 | 138 | if (substr($fqClass, -6, 6) !== 'Filter') { 139 | $fqClass .= 'Filter'; 140 | } 141 | 142 | if (class_exists($fqClass)) { 143 | $this->error("\n\n\t$fqClass Already Exists!\n"); 144 | exit; 145 | } 146 | 147 | $this->setClassName($fqClass); 148 | 149 | return $this; 150 | } 151 | 152 | public function setClassName($name) 153 | { 154 | $this->class = $name; 155 | 156 | return $this; 157 | } 158 | 159 | public function getClassName() 160 | { 161 | return $this->class; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Filterable.php: -------------------------------------------------------------------------------- 1 | getModelFilterClass(); 27 | } 28 | 29 | // Create the model filter instance 30 | $modelFilter = new $filter($query, $input); 31 | 32 | // Set the input that was used in the filter (this will exclude empty strings) 33 | $this->filtered = $modelFilter->input(); 34 | 35 | // Return the filter query 36 | return $modelFilter->handle(); 37 | } 38 | 39 | /** 40 | * Paginate the given query with url query params appended. 41 | * 42 | * @param int $perPage 43 | * @param array $columns 44 | * @param string $pageName 45 | * @param int|null $page 46 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 47 | * 48 | * @throws \InvalidArgumentException 49 | */ 50 | public function scopePaginateFilter($query, $perPage = null, $columns = ['*'], $pageName = 'page', $page = null) 51 | { 52 | $perPage = $perPage ?: config('eloquentfilter.paginate_limit'); 53 | $paginator = $query->paginate($perPage, $columns, $pageName, $page); 54 | $paginator->appends($this->filtered); 55 | 56 | return $paginator; 57 | } 58 | 59 | /** 60 | * Paginate the given query with url query params appended. 61 | * 62 | * @param int $perPage 63 | * @param array $columns 64 | * @param string $pageName 65 | * @param int|null $page 66 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 67 | * 68 | * @throws \InvalidArgumentException 69 | */ 70 | public function scopeSimplePaginateFilter($query, $perPage = null, $columns = ['*'], $pageName = 'page', $page = null) 71 | { 72 | $perPage = $perPage ?: config('eloquentfilter.paginate_limit'); 73 | $paginator = $query->simplePaginate($perPage, $columns, $pageName, $page); 74 | $paginator->appends($this->filtered); 75 | 76 | return $paginator; 77 | } 78 | 79 | /** 80 | * Returns ModelFilter class to be instantiated. 81 | * 82 | * @param null|string $filter 83 | * @return string 84 | */ 85 | public function provideFilter($filter = null) 86 | { 87 | if ($filter === null) { 88 | $filter = config('eloquentfilter.namespace', 'App\\ModelFilters\\').class_basename($this).'Filter'; 89 | } 90 | 91 | return $filter; 92 | } 93 | 94 | /** 95 | * Returns the ModelFilter for the current model. 96 | * 97 | * @return string 98 | */ 99 | public function getModelFilterClass() 100 | { 101 | return method_exists($this, 'modelFilter') ? $this->modelFilter() : $this->provideFilter(); 102 | } 103 | 104 | /** 105 | * WHERE $column LIKE %$value% query. 106 | * 107 | * @param $query 108 | * @param $column 109 | * @param $value 110 | * @param string $boolean 111 | * @return mixed 112 | */ 113 | public function scopeWhereLike($query, $column, $value, $boolean = 'and') 114 | { 115 | return $query->where($column, 'LIKE', "%$value%", $boolean); 116 | } 117 | 118 | /** 119 | * WHERE $column LIKE $value% query. 120 | * 121 | * @param $query 122 | * @param $column 123 | * @param $value 124 | * @param string $boolean 125 | * @return mixed 126 | */ 127 | public function scopeWhereBeginsWith($query, $column, $value, $boolean = 'and') 128 | { 129 | return $query->where($column, 'LIKE', "$value%", $boolean); 130 | } 131 | 132 | /** 133 | * WHERE $column LIKE %$value query. 134 | * 135 | * @param $query 136 | * @param $column 137 | * @param $value 138 | * @param string $boolean 139 | * @return mixed 140 | */ 141 | public function scopeWhereEndsWith($query, $column, $value, $boolean = 'and') 142 | { 143 | return $query->where($column, 'LIKE', "%$value", $boolean); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/LumenServiceProvider.php: -------------------------------------------------------------------------------- 1 | commands(MakeEloquentFilter::class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ModelFilter.php: -------------------------------------------------------------------------------- 1 | [input_key1, input_key2]]. 17 | * 18 | * @var array 19 | */ 20 | public $relations = []; 21 | 22 | /** 23 | * Container to hold all relation queries defined as closures as ['relation' => [\Closure, \Closure]]. 24 | * (This allows us to not be required to define a filter for the related models). 25 | * 26 | * @var array 27 | */ 28 | protected $localRelatedFilters = []; 29 | 30 | /** 31 | * Container for all relations (local and related ModelFilters). 32 | * 33 | * @var array 34 | */ 35 | protected $allRelations = []; 36 | 37 | /** 38 | * Array of method names that should not be called. 39 | * 40 | * @var array 41 | */ 42 | protected $blacklist = []; 43 | 44 | /** 45 | * Filter out empty input so filter methods won't be called with empty values (strings, arrays, null). 46 | * 47 | * @var bool 48 | */ 49 | protected $allowedEmptyFilters = false; 50 | 51 | /** 52 | * Array of input to filter. 53 | * 54 | * @var array 55 | */ 56 | protected $input; 57 | 58 | /** 59 | * @var QueryBuilder 60 | */ 61 | protected $query; 62 | 63 | /** 64 | * Drop `_id` from the end of input keys when referencing methods. 65 | * 66 | * @var bool 67 | */ 68 | protected $drop_id = true; 69 | 70 | /** 71 | * Convert input keys to camelCase 72 | * Ex: my_awesome_key will be converted to myAwesomeKey($value). 73 | * 74 | * @var bool 75 | */ 76 | protected $camel_cased_methods = true; 77 | 78 | /** 79 | * This is to be able to bypass relations if we are filtering a joined table. 80 | * 81 | * @var bool 82 | */ 83 | protected $relationsEnabled; 84 | 85 | /** 86 | * Tables already joined in the query to filter by the joined column instead of using 87 | * ->whereHas to save a little bit of resources. 88 | * 89 | * @var null 90 | */ 91 | private $_joinedTables; 92 | 93 | /** 94 | * ModelFilter constructor. 95 | * 96 | * @param $query 97 | * @param array $input 98 | * @param bool $relationsEnabled 99 | */ 100 | public function __construct($query, array $input = [], $relationsEnabled = true) 101 | { 102 | $this->query = $query; 103 | $this->input = $this->allowedEmptyFilters ? $input : $this->removeEmptyInput($input); 104 | $this->relationsEnabled = $relationsEnabled; 105 | 106 | $this->registerMacros(); 107 | } 108 | 109 | /** 110 | * @param $method 111 | * @param $args 112 | * @return mixed 113 | */ 114 | public function __call($method, $args) 115 | { 116 | $resp = call_user_func_array([$this->query, $method], $args); 117 | 118 | // Only return $this if query builder is returned 119 | // We don't want to make actions to the builder unreachable 120 | return $resp instanceof QueryBuilder ? $this : $resp; 121 | } 122 | 123 | /** 124 | * Remove empty strings from the input array. 125 | * 126 | * @param array $input 127 | * @return array 128 | */ 129 | public function removeEmptyInput($input) 130 | { 131 | $filterableInput = []; 132 | 133 | foreach ($input as $key => $val) { 134 | if ($this->includeFilterInput($key, $val)) { 135 | $filterableInput[$key] = $val; 136 | } 137 | } 138 | 139 | return $filterableInput; 140 | } 141 | 142 | /** 143 | * Handle all filters. 144 | * 145 | * @return QueryBuilder 146 | */ 147 | public function handle() 148 | { 149 | // Filter global methods 150 | if (method_exists($this, 'setup')) { 151 | $this->setup(); 152 | } 153 | 154 | // Run input filters 155 | $this->filterInput(); 156 | // Set up all the whereHas and joins constraints 157 | $this->filterRelations(); 158 | 159 | return $this->query; 160 | } 161 | 162 | /** 163 | * Locally defines a relation filter method that will be called in the context of the related model. 164 | * 165 | * @param $relation 166 | * @param \Closure $closure 167 | * @return $this 168 | */ 169 | public function addRelated($relation, \Closure $closure) 170 | { 171 | $this->localRelatedFilters[$relation][] = $closure; 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Add a where constraint to a relationship. 178 | * 179 | * @param $relation 180 | * @param $column 181 | * @param string|null $operator 182 | * @param string|null $value 183 | * @param string $boolean 184 | * @return $this 185 | */ 186 | public function related($relation, $column, $operator = null, $value = null, $boolean = 'and') 187 | { 188 | if ($column instanceof \Closure) { 189 | return $this->addRelated($relation, $column); 190 | } 191 | 192 | // If there is no value it is a where = ? query and we set the appropriate params 193 | if ($value === null) { 194 | $value = $operator; 195 | $operator = '='; 196 | } 197 | 198 | return $this->addRelated($relation, function ($query) use ($column, $operator, $value, $boolean) { 199 | return $query->where($column, $operator, $value, $boolean); 200 | }); 201 | } 202 | 203 | /** 204 | * @param $key 205 | * @return string 206 | */ 207 | public function getFilterMethod($key) 208 | { 209 | // Remove '.' chars in methodName 210 | $methodName = str_replace('.', '', $this->drop_id ? preg_replace('/^(.*)_id$/', '$1', $key) : $key); 211 | 212 | // Convert key to camelCase? 213 | return $this->camel_cased_methods ? Str::camel($methodName) : $methodName; 214 | } 215 | 216 | /** 217 | * Filter with input array. 218 | */ 219 | public function filterInput() 220 | { 221 | foreach ($this->input as $key => $val) { 222 | // Call all local methods on filter 223 | $method = $this->getFilterMethod($key); 224 | 225 | if ($this->methodIsCallable($method)) { 226 | $this->{$method}($val); 227 | } 228 | } 229 | } 230 | 231 | /** 232 | * Filter relationships defined in $this->relations array. 233 | * 234 | * @return $this 235 | */ 236 | public function filterRelations() 237 | { 238 | // Verify we can filter by relations and there are relations to filter by 239 | if ($this->relationsEnabled()) { 240 | foreach ($this->getAllRelations() as $related => $filterable) { 241 | // Make sure we have filterable input 242 | if (count($filterable) > 0) { 243 | if ($this->relationIsJoined($related)) { 244 | $this->filterJoinedRelation($related); 245 | } else { 246 | $this->filterUnjoinedRelation($related); 247 | } 248 | } 249 | } 250 | } 251 | 252 | return $this; 253 | } 254 | 255 | /** 256 | * Returns all local relations and relations requiring other Model's Filter's. 257 | * 258 | * @return array 259 | */ 260 | public function getAllRelations() 261 | { 262 | if (count($this->allRelations) === 0) { 263 | $allRelations = array_merge(array_keys($this->relations), array_keys($this->localRelatedFilters)); 264 | 265 | foreach ($allRelations as $related) { 266 | $this->allRelations[$related] = array_merge($this->getLocalRelation($related), $this->getRelatedFilterInput($related)); 267 | } 268 | } 269 | 270 | return $this->allRelations; 271 | } 272 | 273 | /** 274 | * Get all input to pass through related filters and local closures as an array. 275 | * 276 | * @param string $relation 277 | * @return array 278 | */ 279 | public function getRelationConstraints($relation) 280 | { 281 | return array_key_exists($relation, $this->allRelations) ? $this->allRelations[$relation] : []; 282 | } 283 | 284 | /** 285 | * Call setup method for relation before filtering on it. 286 | * 287 | * @param $related 288 | * @param $query 289 | */ 290 | public function callRelatedLocalSetup($related, $query) 291 | { 292 | if (method_exists($this, $method = Str::camel($related).'Setup')) { 293 | $this->{$method}($query); 294 | } 295 | } 296 | 297 | /** 298 | * Run the filter on models that already have their tables joined. 299 | * 300 | * @param $related 301 | */ 302 | public function filterJoinedRelation($related) 303 | { 304 | // Apply any relation based scope to avoid method duplication 305 | $this->callRelatedLocalSetup($related, $this->query); 306 | 307 | foreach ($this->getLocalRelation($related) as $closure) { 308 | // If a relation is defined locally in a method AND is joined 309 | // Then we call those defined relation closures on this query 310 | $closure($this->query); 311 | } 312 | 313 | // Check if we have input we need to pass through a related Model's filter 314 | // Then filter by that related model's filter 315 | if (count($relatedFilterInput = $this->getRelatedFilterInput($related)) > 0) { 316 | $filterClass = $this->getRelatedFilter($related); 317 | 318 | // Disable querying joined relations on filters of joined tables. 319 | (new $filterClass($this->query, $relatedFilterInput, false))->handle(); 320 | } 321 | } 322 | 323 | /** 324 | * Gets all the joined tables. 325 | * 326 | * @return array 327 | */ 328 | public function getJoinedTables() 329 | { 330 | $joins = []; 331 | 332 | if (is_array($queryJoins = $this->query->getQuery()->joins)) { 333 | $joins = array_map(function ($join) { 334 | return $join->table; 335 | }, $queryJoins); 336 | } 337 | 338 | return $joins; 339 | } 340 | 341 | /** 342 | * Checks if the relation to filter's table is already joined. 343 | * 344 | * @param $relation 345 | * @return bool 346 | */ 347 | public function relationIsJoined($relation) 348 | { 349 | if ($this->_joinedTables === null) { 350 | $this->_joinedTables = $this->getJoinedTables(); 351 | } 352 | 353 | return in_array($this->getRelatedTable($relation), $this->_joinedTables, true); 354 | } 355 | 356 | /** 357 | * Get an empty instance of a related model. 358 | * 359 | * @param $relation 360 | * @return \Illuminate\Database\Eloquent\Model 361 | */ 362 | public function getRelatedModel($relation) 363 | { 364 | if (strpos($relation, '.') !== false) { 365 | return $this->getNestedRelatedModel($relation); 366 | } 367 | 368 | return $this->query->getModel()->{$relation}()->getRelated(); 369 | } 370 | 371 | /** 372 | * @param $relationString 373 | * @return QueryBuilder|\Illuminate\Database\Eloquent\Model 374 | */ 375 | protected function getNestedRelatedModel($relationString) 376 | { 377 | $parts = explode('.', $relationString); 378 | $related = $this->query->getModel(); 379 | 380 | do { 381 | $relation = array_shift($parts); 382 | $related = $related->{$relation}()->getRelated(); 383 | } while (! empty($parts)); 384 | 385 | return $related; 386 | } 387 | 388 | /** 389 | * Get the table name from a relationship. 390 | * 391 | * @param $relation 392 | * @return string 393 | */ 394 | public function getRelatedTable($relation) 395 | { 396 | return $this->getRelatedModel($relation)->getTable(); 397 | } 398 | 399 | /** 400 | * Get the model filter of a related model. 401 | * 402 | * @param $relation 403 | * @return mixed 404 | */ 405 | public function getRelatedFilter($relation) 406 | { 407 | return $this->getRelatedModel($relation)->getModelFilterClass(); 408 | } 409 | 410 | /** 411 | * Filters by a relationship that isn't joined by using that relation's ModelFilter. 412 | * 413 | * @param $related 414 | */ 415 | public function filterUnjoinedRelation($related) 416 | { 417 | $this->query->whereHas($related, function ($q) use ($related) { 418 | $this->callRelatedLocalSetup($related, $q); 419 | 420 | // If we defined it locally then we're running the closure on the related model here right. 421 | foreach ($this->getLocalRelation($related) as $closure) { 422 | // Run in context of the related model locally 423 | $closure($q); 424 | } 425 | 426 | if (count($filterableRelated = $this->getRelatedFilterInput($related)) > 0) { 427 | $q->filter($filterableRelated); 428 | } 429 | 430 | return $q; 431 | }); 432 | } 433 | 434 | /** 435 | * Get input to pass to a related Model's Filter. 436 | * 437 | * @param $related 438 | * @return array 439 | */ 440 | public function getRelatedFilterInput($related) 441 | { 442 | $output = []; 443 | 444 | if (array_key_exists($related, $this->relations)) { 445 | foreach ((array) $this->relations[$related] as $alias => $name) { 446 | // If the alias is a string that is what we grab from the input 447 | // Then use the name for the output so we can alias relations 448 | $keyName = is_string($alias) ? $alias : $name; 449 | 450 | if (array_key_exists($keyName, $this->input)) { 451 | $keyValue = $this->input[$keyName]; 452 | 453 | if ($this->includeFilterInput($keyName, $keyValue)) { 454 | $output[$name] = $keyValue; 455 | } 456 | } 457 | } 458 | } 459 | 460 | return $output; 461 | } 462 | 463 | /** 464 | * Check to see if there is input or locally defined methods for the given relation. 465 | * 466 | * @param $relation 467 | * @return bool 468 | */ 469 | public function relationIsFilterable($relation) 470 | { 471 | return $this->relationUsesFilter($relation) || $this->relationIsLocal($relation); 472 | } 473 | 474 | /** 475 | * Checks if there is input that should be passed to a related Model Filter. 476 | * 477 | * @param $related 478 | * @return bool 479 | */ 480 | public function relationUsesFilter($related) 481 | { 482 | return count($this->getRelatedFilterInput($related)) > 0; 483 | } 484 | 485 | /** 486 | * Checks to see if there are locally defined relations to filter. 487 | * 488 | * @param $related 489 | * @return bool 490 | */ 491 | public function relationIsLocal($related) 492 | { 493 | return count($this->getLocalRelation($related)) > 0; 494 | } 495 | 496 | /** 497 | * @param string $related 498 | * @return array 499 | */ 500 | public function getLocalRelation($related) 501 | { 502 | return array_key_exists($related, $this->localRelatedFilters) ? $this->localRelatedFilters[$related] : []; 503 | } 504 | 505 | /** 506 | * Retrieve input by key or all input as array. 507 | * 508 | * @param string|null $key 509 | * @param mixed|null $default 510 | * @return array|mixed|null 511 | */ 512 | public function input($key = null, $default = null) 513 | { 514 | if ($key === null) { 515 | return $this->input; 516 | } 517 | 518 | return array_key_exists($key, $this->input) ? $this->input[$key] : $default; 519 | } 520 | 521 | /** 522 | * Disable querying relations (Mainly for joined tables as the related model isn't queried). 523 | * 524 | * @return $this 525 | */ 526 | public function disableRelations() 527 | { 528 | $this->relationsEnabled = false; 529 | 530 | return $this; 531 | } 532 | 533 | /** 534 | * Enable querying relations. 535 | * 536 | * @return $this 537 | */ 538 | public function enableRelations() 539 | { 540 | $this->relationsEnabled = true; 541 | 542 | return $this; 543 | } 544 | 545 | /** 546 | * Checks if filtering by relations is enabled. 547 | * 548 | * @return bool 549 | */ 550 | public function relationsEnabled() 551 | { 552 | return $this->relationsEnabled; 553 | } 554 | 555 | /** 556 | * Add values to filter by if called in setup(). 557 | * Will ONLY filter relations if called on additional method. 558 | * 559 | * @param $key 560 | * @param null $value 561 | */ 562 | public function push($key, $value = null) 563 | { 564 | if (is_array($key)) { 565 | $this->input = array_merge($this->input, $key); 566 | } else { 567 | $this->input[$key] = $value; 568 | } 569 | } 570 | 571 | /** 572 | * Set to drop `_id` from input. Mainly for testing. 573 | * 574 | * @param null $bool 575 | * @return bool 576 | */ 577 | public function dropIdSuffix($bool = null) 578 | { 579 | if ($bool === null) { 580 | return $this->drop_id; 581 | } 582 | 583 | return $this->drop_id = $bool; 584 | } 585 | 586 | /** 587 | * Convert input to camel_case. Mainly for testing. 588 | * 589 | * @param null $bool 590 | * @return bool 591 | */ 592 | public function convertToCamelCasedMethods($bool = null) 593 | { 594 | if ($bool === null) { 595 | return $this->camel_cased_methods; 596 | } 597 | 598 | return $this->camel_cased_methods = $bool; 599 | } 600 | 601 | /** 602 | * Add method to the blacklist so disable calling it. 603 | * 604 | * @param string $method 605 | * @return $this 606 | */ 607 | public function blacklistMethod($method) 608 | { 609 | $this->blacklist[] = $method; 610 | 611 | return $this; 612 | } 613 | 614 | /** 615 | * Remove a method from the blacklist. 616 | * 617 | * @param string $method 618 | * @return $this 619 | */ 620 | public function whitelistMethod($method) 621 | { 622 | $this->blacklist = array_filter($this->blacklist, function ($name) use ($method) { 623 | return $name !== $method; 624 | }); 625 | 626 | return $this; 627 | } 628 | 629 | /** 630 | * @param $method 631 | * @return bool 632 | */ 633 | public function methodIsBlacklisted($method) 634 | { 635 | return in_array($method, $this->blacklist, true); 636 | } 637 | 638 | /** 639 | * Check if the method is not blacklisted and callable on the extended class. 640 | * 641 | * @param $method 642 | * @return bool 643 | */ 644 | public function methodIsCallable($method) 645 | { 646 | return ! $this->methodIsBlacklisted($method) && 647 | method_exists($this, $method) && 648 | ! method_exists(ModelFilter::class, $method); 649 | } 650 | 651 | /** 652 | * Method to determine if input should be passed to the filter 653 | * Returning false will exclude the input from being used in filter logic. 654 | * 655 | * @param mixed $value 656 | * @param string $key 657 | * @return bool 658 | */ 659 | protected function includeFilterInput($key, $value) 660 | { 661 | return $value !== '' && $value !== null && ! (is_array($value) && empty($value)); 662 | } 663 | 664 | /** 665 | * Register paginate and simplePaginate macros on relations 666 | * BelongsToMany overrides the QueryBuilder's paginate to append the pivot. 667 | */ 668 | private function registerMacros() 669 | { 670 | if ( 671 | method_exists(Relation::class, 'hasMacro') && 672 | method_exists(Relation::class, 'macro') && 673 | ! Relation::hasMacro('paginateFilter') && 674 | ! Relation::hasMacro('simplePaginateFilter') 675 | ) { 676 | Relation::macro('paginateFilter', function () { 677 | $paginator = call_user_func_array([$this, 'paginate'], func_get_args()); 678 | $paginator->appends($this->getRelated()->filtered); 679 | 680 | return $paginator; 681 | }); 682 | Relation::macro('simplePaginateFilter', function () { 683 | $paginator = call_user_func_array([$this, 'simplePaginate'], func_get_args()); 684 | $paginator->appends($this->getRelated()->filtered); 685 | 686 | return $paginator; 687 | }); 688 | } 689 | } 690 | } 691 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 18 | __DIR__.'/../config/eloquentfilter.php' => config_path('eloquentfilter.php'), 19 | ]); 20 | } 21 | 22 | /** 23 | * Register any application services. 24 | * 25 | * @return void 26 | */ 27 | public function register() 28 | { 29 | $this->commands(MakeEloquentFilter::class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/stubs/modelfilter.stub: -------------------------------------------------------------------------------- 1 | [input_key1, input_key2]]. 12 | * 13 | * @var array 14 | */ 15 | public $relations = []; 16 | } 17 | --------------------------------------------------------------------------------