├── .gitignore ├── src ├── Contract │ └── QueryFilterInterface.php ├── Facade │ └── QueryFilter.php ├── QueryFilter.php ├── QueryFilterServiceProvider.php ├── Mixins │ └── Mixins.php └── Filter.php ├── .github └── workflows │ └── main.yml ├── composer.json ├── CHANGELOG.md ├── banner.svg └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | composer.lock 4 | vendor 5 | -------------------------------------------------------------------------------- /src/Contract/QueryFilterInterface.php: -------------------------------------------------------------------------------- 1 | pipeline 33 | ->send($builder) 34 | ->through($filters) 35 | ->thenReturn(); 36 | } 37 | } -------------------------------------------------------------------------------- /src/QueryFilterServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(QueryFilterInterface::class, function($app) { 35 | return new QueryFilter($app->make(Pipeline::class)); 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: AI Code Review 2 | 3 | on: 4 | pull_request: # fire on every PR update 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | ai-review: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | # 1. Checkout the PR code 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | # 2. (Optional) Build dist/ if you generate code in the workflow 17 | # – skip if not needed 18 | #- run: bun install --frozen-lockfile && bun run build 19 | 20 | # 3. Run the AI review action 21 | - name: AI Code Review 22 | uses: samushi/code-review-action@v1 23 | with: 24 | provider: openai 25 | github-token: ${{ secrets.GH_PAT }} 26 | openai-api-key: ${{ secrets.OPENAI_API_KEY }} 27 | 28 | # === Optional overrides === 29 | stack: laravel 30 | file-patterns: '**/*.php,**/*.blade.php,**/*.yml' 31 | # ai-model: gpt-4o-mini 32 | # exclude-patterns: '**/*.test.*' 33 | # min-score-threshold: 7 34 | # fail-on-low-score: false 35 | # post-comment: true 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samushi/queryfilter", 3 | "description": "This package allows you to filter, sort and include eloquent relations based on a request. The QueryFilter used in this package extends Laravel's default Eloquent builder.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Sami Maxhuni", 8 | "email": "samimaxhuni510@gmail.com" 9 | } 10 | ], 11 | "suggest": { 12 | "samushi/vuex-global-settings": "Vue plugin that helps to set and get data from vuex store" 13 | }, 14 | "minimum-stability": "dev", 15 | "require": { 16 | "php": "^8.2", 17 | "illuminate/database": "^10.0|^11.0|^12.0", 18 | "illuminate/http": "^10.0|^11.0|^12.0", 19 | "illuminate/pipeline": "^10.0|^11.0|^12.0", 20 | "illuminate/support": "^10.0|^11.0|^12.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Samushi\\QueryFilter\\": "src/" 25 | } 26 | }, 27 | "extra": { 28 | "laravel": { 29 | "providers": [ 30 | "Samushi\\QueryFilter\\QueryFilterServiceProvider" 31 | ], 32 | "aliases": { 33 | "QueryFilter": "Samushi\\QueryFilter\\Facade\\QueryFilter" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Mixins/Mixins.php: -------------------------------------------------------------------------------- 1 | where(function (Builder $query) use ($attributes, $searchTerm) { 25 | foreach (Arr::wrap($attributes) as $attribute) { 26 | $query->when( 27 | Str::contains($attribute, '.'), 28 | function (Builder $query) use ($attribute, $searchTerm) { 29 | [$relationName, $relationAttribute] = explode('.', $attribute); 30 | 31 | $query->orWhereHas($relationName, function (Builder $query) use ($relationAttribute, $searchTerm) { 32 | $query->where($relationAttribute, 'LIKE', "%{$searchTerm}%"); 33 | }); 34 | }, 35 | function (Builder $query) use ($attribute, $searchTerm) { 36 | $query->orWhere($attribute, 'LIKE', "%{$searchTerm}%"); 37 | } 38 | ); 39 | } 40 | }); 41 | 42 | return $this; 43 | }; 44 | } 45 | 46 | /** 47 | * Search by date between 48 | * 49 | * @return Closure 50 | */ 51 | public function whereDateBetween(): Closure 52 | { 53 | return function(string $attribute, string $fromDate, string $toDate, string $fromFormat = 'd/m/Y', string $toFormat = 'Y-m-d'): Builder { 54 | $startDate = Carbon::createFromFormat($fromFormat, $fromDate)->format($toFormat); 55 | $endDate = Carbon::createFromFormat($fromFormat, $toDate)->format($toFormat); 56 | /** @var Builder $this */ 57 | return $this->whereDate($attribute, '>=', $startDate) 58 | ->whereDate($attribute, '<=', $endDate); 59 | }; 60 | } 61 | 62 | /** 63 | * Filter by query filter 64 | * @return Closure 65 | */ 66 | public function queryFilter(): Closure 67 | { 68 | return function(array $pipes = []): Builder { 69 | /** @var Builder $this */ 70 | return QueryFilterFacade::query($this, $pipes); 71 | }; 72 | } 73 | } -------------------------------------------------------------------------------- /src/Filter.php: -------------------------------------------------------------------------------- 1 | data = $data; 30 | } 31 | 32 | /** 33 | * Get the requests 34 | * @return Request 35 | */ 36 | private function getRequests(): Request 37 | { 38 | return app(Request::class); 39 | } 40 | 41 | /** 42 | * Get the raw value from data or request 43 | * 44 | * @return mixed 45 | */ 46 | private function getValueSource(): mixed 47 | { 48 | return $this->data[$this->filterName()] 49 | ?? $this->getRequests()->get($this->filterName()); 50 | } 51 | 52 | /** 53 | * Middleware handle method 54 | * 55 | * @param Builder $builder 56 | * @param Closure $next 57 | * @return Builder 58 | */ 59 | public function handle(Builder $builder, Closure $next): Builder 60 | { 61 | $value = $this->getValueSource(); 62 | 63 | if ($value === null || $value === "") { 64 | return $next($builder); 65 | } 66 | 67 | $builder = $this->applyFilter($builder); 68 | return $next($builder); 69 | } 70 | 71 | /** 72 | * Apply the filter to the builder 73 | * 74 | * @param Builder $builder 75 | * @return Builder 76 | */ 77 | protected abstract function applyFilter(Builder $builder): Builder; 78 | 79 | /** 80 | * Get the filter value from the request or data 81 | * 82 | * @param bool $asArray Whether to return the value as an array 83 | * @return string|array 84 | */ 85 | protected function getValue(bool $asArray = false): string|array 86 | { 87 | $value = $this->getValueSource(); 88 | 89 | // If value is already an array (e.g., ?cases[]=sent&cases[]=delivered) 90 | if (is_array($value)) { 91 | return $asArray ? $value : implode(',', $value); 92 | } 93 | 94 | // If asArray is true and value is a comma-separated string 95 | if ($asArray && is_string($value) && str_contains($value, ',')) { 96 | return array_map('trim', explode(',', $value)); 97 | } 98 | 99 | // If asArray is true but no comma, return as single-element array 100 | if ($asArray) { 101 | return [$value]; 102 | } 103 | 104 | return (string) $value; 105 | } 106 | 107 | /** 108 | * Get the filter name 109 | * 110 | * @return string 111 | */ 112 | protected function filterName(): string 113 | { 114 | if ($this->name) { 115 | return $this->name; 116 | } 117 | 118 | return Str::snake(class_basename($this)); 119 | } 120 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `QueryFilter` will be documented in this file. 4 | 5 | ## [v2.2.0] - 2024-12-15 6 | 7 | ### Added 8 | - **Manual Data Injection**: Filters can now be used outside HTTP request contexts (Jobs, Commands, Tests) 9 | - Added optional constructor parameter to inject data manually: `new StatusFilter(['status' => 'active'])` 10 | - Filters automatically detect data source (manual data or HTTP request) 11 | - Full backward compatibility - existing code works without changes 12 | - **Comprehensive Documentation**: Added "Using Filters Outside HTTP Requests" section with examples: 13 | - Queue Jobs examples 14 | - Console Commands examples 15 | - Unit Tests examples 16 | - Scheduled Tasks examples 17 | - Mixed usage (HTTP + Manual data) 18 | 19 | ### Changed 20 | - Enhanced `Filter` class with optional constructor for data injection 21 | - Added `getValueSource()` helper method for clean value retrieval 22 | - Updated `getValue()` and `handle()` methods to support both HTTP and manual data sources 23 | - Improved documentation with real-world use cases 24 | 25 | ### Technical Details 26 | - Constructor accepts optional `?array $data = null` parameter 27 | - Manual data takes precedence over HTTP request parameters 28 | - Filter name must match array key when using manual data injection 29 | - Zero breaking changes - 100% backward compatible 30 | 31 | ## [v2.1.0] - 2024-12-15 32 | 33 | ### Added 34 | - **Array Support in `getValue()` Method**: The `getValue()` method now accepts an optional boolean parameter `$asArray` (default: `false`) to automatically convert values to arrays 35 | - Supports comma-separated values: `?status=active,pending,completed` 36 | - Supports array query parameters: `?status[]=active&status[]=pending` 37 | - Automatic whitespace trimming for comma-separated values 38 | - Backward compatible - default behavior remains unchanged 39 | - **Comprehensive Documentation**: Complete rewrite of README.md with detailed examples and best practices 40 | - Added "Working with Arrays" section with real-world examples 41 | - Added "Advanced Usage" section with practical filter implementations 42 | - Added "Best Practices" section for developers 43 | - Added Table of Contents for easier navigation 44 | - Improved code examples throughout 45 | 46 | ### Changed 47 | - Enhanced `getValue()` return type from `string` to `string|array` when using array mode 48 | - Improved documentation structure with better organization and clearer examples 49 | 50 | ### Technical Details 51 | - `getValue()` now intelligently detects array inputs from query parameters 52 | - Automatic conversion between comma-separated strings and arrays 53 | - Native array parameter support (e.g., `?param[]=value1¶m[]=value2`) 54 | 55 | ## [v2.0] - Previous Release 56 | 57 | ### Added 58 | - Initial stable release with core filtering functionality 59 | - `whereLike` macro for searching across multiple columns 60 | - `whereDateBetween` macro for date range filtering 61 | - Pipeline-based filter system 62 | - Model trait for direct filtering on Eloquent models 63 | 64 | ## [v1.0.6] - Previous Release 65 | 66 | ### Changed 67 | - Minor improvements and bug fixes 68 | 69 | ## [v1.0.5] - Previous Release 70 | 71 | ### Changed 72 | - Minor improvements and bug fixes 73 | 74 | ## [v1.0.4] - Previous Release 75 | 76 | ### Changed 77 | - Minor improvements and bug fixes 78 | 79 | ## [v1.0.3] - Previous Release 80 | 81 | ### Added 82 | - Initial public release 83 | -------------------------------------------------------------------------------- /banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | QueryFilter 13 | for Laravel 14 | 15 | 16 | 17 | v2.0 18 | 19 | 20 | 21 | Laravel 12 22 | 23 | 24 | 25 | PHP 8.2+ 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Elegant Eloquent filtering based on request parameters 43 | 44 | 45 | 46 | Filter, sort and include relations with minimal code 47 | 48 | 49 | Create reusable, modular filter components 50 | 51 | 52 | Powerful macros: whereLike, whereDateBetween, and more 53 | 54 | 55 | by Sami Maxhuni 56 | 57 | 58 | Star on GitHub ★ 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![QueryFilter Banner](banner.svg) 2 | # QueryFilter for Laravel 3 | 4 | A powerful and flexible package for filtering, sorting, and managing Eloquent queries based on request parameters. QueryFilter seamlessly extends Laravel's Eloquent builder, preserving all your favorite methods and macros while adding robust filtering capabilities. 5 | 6 | ## Requirements 7 | 8 | - PHP 8.2+ 9 | - Laravel 10/11/12 10 | 11 | ## Installation 12 | 13 | Install the package via Composer: 14 | 15 | ```bash 16 | composer require samushi/queryfilter 17 | ``` 18 | 19 | ## Table of Contents 20 | 21 | - [Basic Usage](#basic-usage) 22 | - [Creating Filters](#creating-filters) 23 | - [Working with Arrays](#working-with-arrays) 24 | - [Available Macros](#available-macros) 25 | - [Advanced Usage](#advanced-usage) 26 | - [Custom Filter Names](#custom-filter-names) 27 | - [Best Practices](#best-practices) 28 | 29 | ## Basic Usage 30 | 31 | ### Quick Start 32 | 33 | This package helps you filter Eloquent queries effortlessly based on request parameters. 34 | 35 | **Step 1:** Create a `Filters` directory inside your `app` folder. 36 | 37 | **Step 2:** Create filter classes that extend the base `Filter` class: 38 | 39 | ```php 40 | namespace App\Filters; 41 | 42 | use Samushi\QueryFilter\Filter; 43 | use Illuminate\Database\Eloquent\Builder; 44 | 45 | class Search extends Filter 46 | { 47 | /** 48 | * Search results using whereLike 49 | * 50 | * @param Builder $builder 51 | * @return Builder 52 | */ 53 | protected function applyFilter(Builder $builder): Builder 54 | { 55 | return $builder->whereLike(['name', 'email'], $this->getValue()); 56 | } 57 | } 58 | ``` 59 | 60 | **Step 3:** Apply filters in your controller: 61 | 62 | ```php 63 | namespace App\Http\Controllers; 64 | 65 | use App\Models\User; 66 | use App\Filters\Search; 67 | use Samushi\QueryFilter\Facade\QueryFilter; 68 | 69 | class UserController extends Controller 70 | { 71 | public function index() 72 | { 73 | $filters = [ 74 | Search::class, 75 | // Add more filters here 76 | ]; 77 | 78 | return QueryFilter::query(User::query(), $filters)->paginate(10); 79 | } 80 | } 81 | ``` 82 | 83 | ### Using the Model Method 84 | 85 | You can use the `queryFilter` method directly on models for cleaner code: 86 | 87 | ```php 88 | use App\Filters\Search; 89 | use App\Filters\Status; 90 | use App\Models\User; 91 | 92 | // Usage in controller 93 | $users = User::queryFilter([ 94 | Search::class, 95 | Status::class, 96 | ])->paginate(10); 97 | ``` 98 | 99 | **Example Request:** 100 | ``` 101 | GET /users?search=john&status=active 102 | ``` 103 | 104 | ## Creating Filters 105 | 106 | ### Filter Naming Convention 107 | 108 | **Important:** By default, filter class names are automatically converted to snake_case to match request parameters. 109 | 110 | | Class Name | Request Parameter | 111 | |------------|-------------------| 112 | | `Search` | `search` | 113 | | `Status` | `status` | 114 | | `PriceRange` | `price_range` | 115 | | `CreatedDate` | `created_date` | 116 | 117 | ### Basic Filter Example 118 | 119 | ```php 120 | namespace App\Filters; 121 | 122 | use Samushi\QueryFilter\Filter; 123 | use Illuminate\Database\Eloquent\Builder; 124 | 125 | class Status extends Filter 126 | { 127 | protected function applyFilter(Builder $builder): Builder 128 | { 129 | return $builder->where('status', $this->getValue()); 130 | } 131 | } 132 | ``` 133 | 134 | **Usage:** 135 | ``` 136 | GET /users?status=active 137 | ``` 138 | 139 | ## Working with Arrays 140 | 141 | ### Overview 142 | 143 | The `getValue()` method supports automatic array detection and conversion, making it easy to handle multiple values in your filters. 144 | 145 | ### Array Support Features 146 | 147 | ✅ **Comma-separated values**: `?status=active,pending,completed` 148 | ✅ **Array query parameters**: `?status[]=active&status[]=pending` 149 | ✅ **Automatic detection**: Detects arrays and converts them appropriately 150 | ✅ **Backward compatible**: Default behavior returns strings 151 | 152 | ### Using getValue() with Arrays 153 | 154 | #### Default Behavior (String) 155 | 156 | ```php 157 | class Search extends Filter 158 | { 159 | protected function applyFilter(Builder $builder): Builder 160 | { 161 | // Returns string: "john doe" 162 | $value = $this->getValue(); 163 | 164 | return $builder->where('name', 'like', "%{$value}%"); 165 | } 166 | } 167 | ``` 168 | 169 | **Request:** `GET /users?search=john doe` 170 | 171 | #### Array Mode (Multiple Values) 172 | 173 | ```php 174 | class Status extends Filter 175 | { 176 | protected function applyFilter(Builder $builder): Builder 177 | { 178 | // Returns array: ["active", "pending", "completed"] 179 | $statuses = $this->getValue(true); 180 | 181 | return $builder->whereIn('status', $statuses); 182 | } 183 | } 184 | ``` 185 | 186 | **Works with both formats:** 187 | ``` 188 | GET /users?status=active,pending,completed 189 | GET /users?status[]=active&status[]=pending&status[]=completed 190 | ``` 191 | 192 | ### Real-World Array Examples 193 | 194 | #### Multiple Categories Filter 195 | 196 | ```php 197 | namespace App\Filters; 198 | 199 | use Samushi\QueryFilter\Filter; 200 | use Illuminate\Database\Eloquent\Builder; 201 | 202 | class Categories extends Filter 203 | { 204 | protected function applyFilter(Builder $builder): Builder 205 | { 206 | $categories = $this->getValue(true); // Get as array 207 | 208 | return $builder->whereIn('category_id', $categories); 209 | } 210 | } 211 | ``` 212 | 213 | **Usage:** 214 | ``` 215 | GET /products?categories=1,2,3,4 216 | GET /products?categories[]=1&categories[]=2&categories[]=3 217 | ``` 218 | 219 | #### Multiple Tags Filter 220 | 221 | ```php 222 | namespace App\Filters; 223 | 224 | use Samushi\QueryFilter\Filter; 225 | use Illuminate\Database\Eloquent\Builder; 226 | 227 | class Tags extends Filter 228 | { 229 | protected function applyFilter(Builder $builder): Builder 230 | { 231 | $tags = $this->getValue(true); // ["laravel", "php", "vue"] 232 | 233 | return $builder->whereHas('tags', function ($query) use ($tags) { 234 | $query->whereIn('name', $tags); 235 | }); 236 | } 237 | } 238 | ``` 239 | 240 | **Usage:** 241 | ``` 242 | GET /posts?tags=laravel,php,vue 243 | GET /posts?tags[]=laravel&tags[]=php&tags[]=vue 244 | ``` 245 | 246 | #### Case Status Filter 247 | 248 | ```php 249 | namespace App\Filters; 250 | 251 | use Samushi\QueryFilter\Filter; 252 | use Illuminate\Database\Eloquent\Builder; 253 | 254 | class Cases extends Filter 255 | { 256 | protected function applyFilter(Builder $builder): Builder 257 | { 258 | $cases = $this->getValue(true); // ["sent", "delivered", "failed"] 259 | 260 | return $builder->whereIn('case_status', $cases); 261 | } 262 | } 263 | ``` 264 | 265 | **Usage:** 266 | ``` 267 | GET /orders?cases=sent,delivered,failed 268 | GET /orders?cases[]=sent&cases[]=delivered&cases[]=failed 269 | ``` 270 | 271 | ### How getValue() Works 272 | 273 | | Input Type | `getValue()` | `getValue(true)` | 274 | |------------|--------------|------------------| 275 | | `?status=active` | `"active"` | `["active"]` | 276 | | `?status=active,pending` | `"active,pending"` | `["active", "pending"]` | 277 | | `?status[]=active&status[]=pending` | `"active,pending"` | `["active", "pending"]` | 278 | 279 | ### Array Detection Logic 280 | 281 | The `getValue()` method intelligently handles arrays: 282 | 283 | 1. **Detects native arrays**: Automatically recognizes `?param[]=value` format 284 | 2. **Splits comma-separated values**: Converts `?param=val1,val2` to array when requested 285 | 3. **Trims whitespace**: Automatically cleans `?param=val1, val2, val3` 286 | 4. **Maintains compatibility**: Returns string by default, array only when `$asArray = true` 287 | 288 | ## Available Macros 289 | 290 | ### whereLike 291 | 292 | Search across multiple columns or relationships with ease: 293 | 294 | ```php 295 | // Search in a single column 296 | $users = User::whereLike(['name'], $searchTerm)->get(); 297 | 298 | // Search across multiple columns 299 | $users = User::whereLike(['name', 'email'], $searchTerm)->get(); 300 | 301 | // Search in relationship columns 302 | $users = User::whereLike(['name', 'posts.title', 'comments.body'], $searchTerm)->get(); 303 | ``` 304 | 305 | **Example Filter:** 306 | ```php 307 | class Search extends Filter 308 | { 309 | protected function applyFilter(Builder $builder): Builder 310 | { 311 | return $builder->whereLike(['name', 'email', 'phone'], $this->getValue()); 312 | } 313 | } 314 | ``` 315 | 316 | **Request:** `GET /users?search=john` 317 | 318 | ### whereDateBetween 319 | 320 | Filter records between two dates with flexible formatting: 321 | 322 | ```php 323 | // Default format: d/m/Y 324 | $users = User::whereDateBetween('created_at', '01/01/2023', '31/12/2023')->get(); 325 | 326 | // Custom date formats 327 | $users = User::whereDateBetween('created_at', '01-01-2023', '31-12-2023', 'd-m-Y', 'Y-m-d')->get(); 328 | 329 | // Different formats for start and end dates 330 | $users = User::whereDateBetween('created_at', '2023/01/01', '31-12-2023', 'Y/m/d', 'd-m-Y')->get(); 331 | ``` 332 | 333 | **Example Filter:** 334 | ```php 335 | class DateRange extends Filter 336 | { 337 | protected function applyFilter(Builder $builder): Builder 338 | { 339 | $dates = explode(',', $this->getValue()); 340 | 341 | if (count($dates) === 2) { 342 | return $builder->whereDateBetween('created_at', $dates[0], $dates[1]); 343 | } 344 | 345 | return $builder; 346 | } 347 | } 348 | ``` 349 | 350 | **Request:** `GET /users?date_range=01/01/2024,31/12/2024` 351 | 352 | ## Advanced Usage 353 | 354 | ### Price Range Filter 355 | 356 | ```php 357 | namespace App\Filters; 358 | 359 | use Samushi\QueryFilter\Filter; 360 | use Illuminate\Database\Eloquent\Builder; 361 | 362 | class PriceRange extends Filter 363 | { 364 | protected function applyFilter(Builder $builder): Builder 365 | { 366 | $range = $this->getValue(true); // Get as array 367 | 368 | if (count($range) === 2) { 369 | return $builder->whereBetween('price', [$range[0], $range[1]]); 370 | } 371 | 372 | return $builder; 373 | } 374 | } 375 | ``` 376 | 377 | **Usage:** 378 | ``` 379 | GET /products?price_range=10,100 380 | GET /products?price_range[]=10&price_range[]=100 381 | ``` 382 | 383 | ### Sort Filter 384 | 385 | ```php 386 | namespace App\Filters; 387 | 388 | use Samushi\QueryFilter\Filter; 389 | use Illuminate\Database\Eloquent\Builder; 390 | 391 | class Sort extends Filter 392 | { 393 | protected function applyFilter(Builder $builder): Builder 394 | { 395 | $sortBy = $this->getValue(); // e.g., "price:desc" or "name:asc" 396 | 397 | [$column, $direction] = array_pad(explode(':', $sortBy), 2, 'asc'); 398 | 399 | return $builder->orderBy($column, $direction); 400 | } 401 | } 402 | ``` 403 | 404 | **Usage:** 405 | ``` 406 | GET /products?sort=price:desc 407 | GET /products?sort=name:asc 408 | ``` 409 | 410 | ### Active Records Filter 411 | 412 | ```php 413 | namespace App\Filters; 414 | 415 | use Samushi\QueryFilter\Filter; 416 | use Illuminate\Database\Eloquent\Builder; 417 | 418 | class Active extends Filter 419 | { 420 | protected function applyFilter(Builder $builder): Builder 421 | { 422 | $isActive = filter_var($this->getValue(), FILTER_VALIDATE_BOOLEAN); 423 | 424 | return $builder->where('is_active', $isActive); 425 | } 426 | } 427 | ``` 428 | 429 | **Usage:** 430 | ``` 431 | GET /users?active=true 432 | GET /users?active=1 433 | ``` 434 | 435 | ### Relationship Filter 436 | 437 | ```php 438 | namespace App\Filters; 439 | 440 | use Samushi\QueryFilter\Filter; 441 | use Illuminate\Database\Eloquent\Builder; 442 | 443 | class HasPosts extends Filter 444 | { 445 | protected function applyFilter(Builder $builder): Builder 446 | { 447 | $hasPosts = filter_var($this->getValue(), FILTER_VALIDATE_BOOLEAN); 448 | 449 | return $hasPosts 450 | ? $builder->has('posts') 451 | : $builder->doesntHave('posts'); 452 | } 453 | } 454 | ``` 455 | 456 | **Usage:** 457 | ``` 458 | GET /users?has_posts=true 459 | ``` 460 | 461 | ## Using Filters Outside HTTP Requests 462 | 463 | ### Overview 464 | 465 | Filters can be used in **Jobs**, **Commands**, **Tests**, and other non-HTTP contexts by injecting data manually through the constructor. 466 | 467 | ### Manual Data Injection 468 | 469 | Instead of relying on HTTP request parameters, you can pass data directly to filters: 470 | 471 | ```php 472 | namespace App\Jobs; 473 | 474 | use App\Models\Order; 475 | use App\Filters\Status; 476 | use App\Filters\DateRange; 477 | 478 | class ProcessOrdersJob 479 | { 480 | public function handle() 481 | { 482 | // Manual data injection 483 | $orders = Order::queryFilter([ 484 | new Status(['status' => 'pending,processing']), 485 | new DateRange(['date_range' => '01/01/2024,31/12/2024']), 486 | ])->get(); 487 | 488 | // Process orders... 489 | } 490 | } 491 | ``` 492 | 493 | ### Console Commands 494 | 495 | ```php 496 | namespace App\Console\Commands; 497 | 498 | use App\Models\User; 499 | use App\Filters\Status; 500 | use App\Filters\Role; 501 | use Illuminate\Console\Command; 502 | 503 | class ExportUsersCommand extends Command 504 | { 505 | protected $signature = 'users:export {status} {role}'; 506 | 507 | public function handle() 508 | { 509 | $users = User::queryFilter([ 510 | new Status(['status' => $this->argument('status')]), 511 | new Role(['role' => $this->argument('role')]), 512 | ])->get(); 513 | 514 | // Export users... 515 | } 516 | } 517 | ``` 518 | 519 | **Usage:** 520 | ```bash 521 | php artisan users:export active admin 522 | ``` 523 | 524 | ### Unit Tests 525 | 526 | ```php 527 | namespace Tests\Unit; 528 | 529 | use App\Models\Product; 530 | use App\Filters\PriceRange; 531 | use App\Filters\Categories; 532 | use Tests\TestCase; 533 | 534 | class ProductFilterTest extends TestCase 535 | { 536 | public function test_filters_products_by_price_and_category() 537 | { 538 | $products = Product::queryFilter([ 539 | new PriceRange(['price_range' => '100,500']), 540 | new Categories(['categories' => '1,2,3']), 541 | ])->get(); 542 | 543 | $this->assertCount(5, $products); 544 | } 545 | } 546 | ``` 547 | 548 | ### Mixed Usage (HTTP + Manual) 549 | 550 | You can combine HTTP request parameters with manual data injection: 551 | 552 | ```php 553 | // In Controller 554 | // GET /products?search=laptop 555 | 556 | public function index() 557 | { 558 | $products = Product::queryFilter([ 559 | SearchFilter::class, // Takes 'search' from HTTP request 560 | new PriceRange(['price_range' => '100,1000']), // Manual data 561 | new Stock(['stock' => 'in_stock']), // Manual data 562 | ])->paginate(10); 563 | } 564 | ``` 565 | 566 | ### Queue Jobs Example 567 | 568 | ```php 569 | namespace App\Jobs; 570 | 571 | use App\Models\Notification; 572 | use App\Filters\Status; 573 | use App\Filters\Priority; 574 | 575 | class SendNotificationsJob implements ShouldQueue 576 | { 577 | public function handle() 578 | { 579 | $notifications = Notification::queryFilter([ 580 | new Status(['status' => 'pending']), 581 | new Priority(['priority' => 'high,urgent']), 582 | ])->get(); 583 | 584 | foreach ($notifications as $notification) { 585 | // Send notification... 586 | } 587 | } 588 | } 589 | ``` 590 | 591 | ### Scheduled Tasks 592 | 593 | ```php 594 | namespace App\Console\Kernel; 595 | 596 | use App\Models\Order; 597 | use App\Filters\Status; 598 | use App\Filters\DateRange; 599 | use Carbon\Carbon; 600 | 601 | protected function schedule(Schedule $schedule) 602 | { 603 | $schedule->call(function () { 604 | $yesterday = Carbon::yesterday()->format('d/m/Y'); 605 | $today = Carbon::today()->format('d/m/Y'); 606 | 607 | $orders = Order::queryFilter([ 608 | new Status(['status' => 'completed']), 609 | new DateRange(['date_range' => "$yesterday,$today"]), 610 | ])->get(); 611 | 612 | // Process completed orders... 613 | })->daily(); 614 | } 615 | ``` 616 | 617 | ### How It Works 618 | 619 | The filter automatically detects the data source: 620 | 621 | 1. **HTTP Request Context**: If no data is provided, filters read from HTTP request parameters 622 | 2. **Manual Data Context**: If data is provided via constructor, filters use that data 623 | 3. **Priority**: Manual data takes precedence over HTTP request parameters 624 | 625 | ```php 626 | // HTTP Request (automatic) 627 | StatusFilter::class → reads from request()->get('status') 628 | 629 | // Manual Data (explicit) 630 | new StatusFilter(['status' => 'active']) → uses provided data 631 | 632 | // The filter name must match the array key 633 | new Status(['status' => 'active']) → ✅ Correct 634 | new Status(['state' => 'active']) → ❌ Won't work (key mismatch) 635 | ``` 636 | 637 | ## Custom Filter Names 638 | 639 | Override the default snake_case naming convention by setting a custom `$name` property: 640 | 641 | ```php 642 | namespace App\Filters; 643 | 644 | use Samushi\QueryFilter\Filter; 645 | use Illuminate\Database\Eloquent\Builder; 646 | 647 | class Search extends Filter 648 | { 649 | protected ?string $name = 'q'; // Use 'q' instead of 'search' 650 | 651 | protected function applyFilter(Builder $builder): Builder 652 | { 653 | return $builder->whereLike(['name', 'email'], $this->getValue()); 654 | } 655 | } 656 | ``` 657 | 658 | **Usage:** 659 | ``` 660 | GET /users?q=john // Instead of ?search=john 661 | 662 | // Or with manual data: 663 | new Search(['q' => 'john']) // Must use 'q', not 'search' 664 | ``` 665 | 666 | ## Best Practices 667 | 668 | ### 1. **Organize Filters by Feature** 669 | 670 | ``` 671 | app/ 672 | ├── Filters/ 673 | │ ├── User/ 674 | │ │ ├── UserSearch.php 675 | │ │ ├── UserStatus.php 676 | │ │ └── UserRole.php 677 | │ ├── Product/ 678 | │ │ ├── ProductCategory.php 679 | │ │ ├── ProductPrice.php 680 | │ │ └── ProductStock.php 681 | ``` 682 | 683 | ### 2. **Use Type Hints and Return Types** 684 | 685 | ```php 686 | protected function applyFilter(Builder $builder): Builder 687 | { 688 | return $builder->where('status', $this->getValue()); 689 | } 690 | ``` 691 | 692 | ### 3. **Validate Input in Filters** 693 | 694 | ```php 695 | protected function applyFilter(Builder $builder): Builder 696 | { 697 | $statuses = $this->getValue(true); 698 | $allowed = ['active', 'pending', 'completed']; 699 | 700 | $validated = array_intersect($statuses, $allowed); 701 | 702 | return $builder->whereIn('status', $validated); 703 | } 704 | ``` 705 | 706 | ### 4. **Combine Multiple Filters** 707 | 708 | ```php 709 | $users = User::queryFilter([ 710 | Search::class, 711 | Status::class, 712 | Role::class, 713 | DateRange::class, 714 | ])->paginate(10); 715 | ``` 716 | 717 | **Request:** 718 | ``` 719 | GET /users?search=john&status=active,pending&role=admin&date_range=01/01/2024,31/12/2024 720 | ``` 721 | 722 | ### 5. **Use Arrays for Multiple Values** 723 | 724 | Always use `getValue(true)` when filtering by multiple values: 725 | 726 | ```php 727 | // ✅ Good 728 | $categories = $this->getValue(true); 729 | return $builder->whereIn('category_id', $categories); 730 | 731 | // ❌ Bad 732 | $categories = explode(',', $this->getValue()); 733 | return $builder->whereIn('category_id', $categories); 734 | ``` 735 | 736 | ### 6. **Handle Empty Values Gracefully** 737 | 738 | The filter automatically skips when the parameter is missing or empty, but you can add custom logic: 739 | 740 | ```php 741 | protected function applyFilter(Builder $builder): Builder 742 | { 743 | $value = $this->getValue(); 744 | 745 | if (empty($value)) { 746 | return $builder; // Skip filter 747 | } 748 | 749 | return $builder->where('status', $value); 750 | } 751 | ``` 752 | 753 | ## License 754 | 755 | The MIT License (MIT). Please see the [License File](LICENSE) for more information. --------------------------------------------------------------------------------