├── .coveralls.yml ├── .features-plan ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── laravel.yml ├── .gitignore ├── .gitkeep ├── .scrutinizer.yml ├── .stickler.yml ├── Aban-21-1402 14-27-04.gif ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── _ide_helper.php ├── composer.json ├── eloquent-filter.png ├── makefile ├── phpunit.xml ├── phpunit.xml.dist ├── src ├── Command │ ├── MakeEloquentFilter.php │ └── modelfilter.stub ├── Facade │ └── EloquentFilter.php ├── QueryFilter │ ├── Core │ │ ├── DbBuilder │ │ │ ├── DbBuilderWrapper.php │ │ │ └── DbBuilderWrapperInterface.php │ │ ├── EloquentBuilder │ │ │ ├── EloquentModelBuilderWrapper.php │ │ │ └── QueryBuilderWrapperInterface.php │ │ ├── FilterBuilder │ │ │ ├── Core │ │ │ │ ├── QueryFilterCore.php │ │ │ │ └── QueryFilterCoreBuilder.php │ │ │ ├── IO │ │ │ │ ├── Encoder.php │ │ │ │ ├── RequestFilter.php │ │ │ │ └── ResponseFilter.php │ │ │ ├── MainQueryFilterBuilder.php │ │ │ └── QueryBuilder │ │ │ │ ├── DBQueryFilterBuilder.php │ │ │ │ ├── EloquentQueryFilterBuilder.php │ │ │ │ └── QueryFilterBuilder.php │ │ ├── HelperEloquentFilter.php │ │ ├── HelperFilter.php │ │ ├── RateLimiting.php │ │ └── ResolverDetection │ │ │ ├── ResolverDetectionDb.php │ │ │ ├── ResolverDetectionEloquent.php │ │ │ └── ResolverDetections.php │ ├── Detection │ │ ├── ConditionsDetect │ │ │ ├── DB │ │ │ │ └── DBBuilderQueryByCondition.php │ │ │ ├── Eloquent │ │ │ │ └── MainBuilderQueryByCondition.php │ │ │ └── TypeQueryConditions │ │ │ │ ├── SpecialCondition.php │ │ │ │ ├── WhereBetweenCondition.php │ │ │ │ ├── WhereByOptCondition.php │ │ │ │ ├── WhereCondition.php │ │ │ │ ├── WhereDateCondition.php │ │ │ │ ├── WhereDayCondition.php │ │ │ │ ├── WhereDoesntHaveCondition.php │ │ │ │ ├── WhereHasCondition.php │ │ │ │ ├── WhereInCondition.php │ │ │ │ ├── WhereLikeCondition.php │ │ │ │ ├── WhereMonthCondition.php │ │ │ │ ├── WhereNullCondition.php │ │ │ │ ├── WhereOrCondition.php │ │ │ │ └── WhereYearCondition.php │ │ ├── Contract │ │ │ ├── ConditionsContract.php │ │ │ ├── DefaultConditionsContract.php │ │ │ ├── DetectorConditionContract.php │ │ │ ├── DetectorDbFactoryContract.php │ │ │ ├── DetectorFactoryContract.php │ │ │ └── MainBuilderConditionsContract.php │ │ ├── DetectionFactory │ │ │ ├── DetectionDbFactory.php │ │ │ └── DetectionEloquentFactory.php │ │ └── Detector │ │ │ ├── DetectorConditionCondition.php │ │ │ └── DetectorConditionDbCondition.php │ ├── Exceptions │ │ └── EloquentFilterException.php │ ├── Factory │ │ ├── QueryBuilderWrapperFactory.php │ │ └── QueryFilterCoreFactory.php │ ├── ModelFilters │ │ └── Filterable.php │ └── Queries │ │ ├── BaseClause.php │ │ ├── DB │ │ ├── Special.php │ │ ├── Where.php │ │ ├── WhereBetween.php │ │ ├── WhereByOpt.php │ │ ├── WhereDate.php │ │ ├── WhereDayQuery.php │ │ ├── WhereDoesntHave.php │ │ ├── WhereHas.php │ │ ├── WhereIn.php │ │ ├── WhereLike.php │ │ ├── WhereMonthQuery.php │ │ ├── WhereNotNull.php │ │ ├── WhereNull.php │ │ ├── WhereOr.php │ │ └── WhereYearQuery.php │ │ └── Eloquent │ │ ├── Special.php │ │ ├── Where.php │ │ ├── WhereBetween.php │ │ ├── WhereByOpt.php │ │ ├── WhereCustom.php │ │ ├── WhereDate.php │ │ ├── WhereDayQuery.php │ │ ├── WhereDoesntHave.php │ │ ├── WhereHas.php │ │ ├── WhereIn.php │ │ ├── WhereLike.php │ │ ├── WhereMonthQuery.php │ │ ├── WhereNotNull.php │ │ ├── WhereNull.php │ │ ├── WhereOr.php │ │ └── WhereYearQuery.php ├── ServiceProvider.php └── config │ ├── .gitkeep │ └── config.php └── tests ├── Models ├── Car.php ├── Category.php ├── CategoryPosts.php ├── CustomDetect │ ├── WhereLikeRelation.php │ └── WhereRelationLikeCondition.php ├── Filters │ └── usersFilter.php ├── Order.php ├── Post.php ├── Stat.php ├── Tag.php └── User.php ├── TestCase.php └── Tests ├── Db └── DbFilterMockTest.php ├── Eloquent ├── MakeEloquentFilterCommandTest.php └── ModelFilterMockTest.php └── RateLimiting └── RateLimitTest.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: Nh0K6VukSDiNmaVyO93Z7AWK4Hs1WOzj3 2 | -------------------------------------------------------------------------------- /.features-plan: -------------------------------------------------------------------------------- 1 | 1- make blacklist array for disable some method for custom query 2 | 2- adding support callback 3 | 3- support algolia search and 3rd party api tools 4 | 4- add macro for get some information 5 | 5- add limit number for some column to prevent bad performance queries by users 6 | 6- set max_limit on the method 7 | 7- Limit some default operators on the method or Model for prevent manipulation 8 | 8- Support change driver ORM and query builder 9 | 9- Consider a meaningfully method name for custom methods 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/laravel.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | php-tests: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | php: [ 8.0, 8.1, 8.2, 8.3 , 8.4 ] 15 | laravel: [ 8.*, 9.*, 10.*, 11.*, 12.* ] 16 | dependency-version: [ prefer-stable ] 17 | os: [ ubuntu-latest ] 18 | include: 19 | - laravel: 12.* 20 | testbench: v10.0.0 21 | database: 10.0.x-dev 22 | - laravel: 11.* 23 | testbench: v9.0.0 24 | database: 9.0.x-dev 25 | - laravel: 10.* 26 | testbench: 8.* 27 | database: 8.0.x-dev 28 | - laravel: 9.* 29 | testbench: 7.* 30 | database: 7.0.x-dev 31 | - laravel: 8.* 32 | testbench: 6.* 33 | database: 6.x-dev 34 | exclude: 35 | - laravel: 10.* 36 | php: 8.0 37 | - laravel: 11.* 38 | php: 8.1 39 | - laravel: 11.* 40 | php: 8.0 41 | - laravel: 12.* 42 | php: 8.1 43 | - laravel: 12.* 44 | php: 8.0 45 | - laravel: 8.* 46 | dependency-version: prefer-lowest 47 | 48 | 49 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 50 | 51 | steps: 52 | - name: Checkout code 53 | uses: actions/checkout@v1 54 | 55 | - name: Setup PHP 56 | uses: shivammathur/setup-php@v2 57 | with: 58 | php-version: ${{ matrix.php }} 59 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 60 | coverage: xdebug 61 | 62 | - name: Install dependencies 63 | run: | 64 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 65 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 66 | 67 | - name: Execute tests 68 | run: make test 69 | - name: Upload coverage reports to Codecov 70 | uses: codecov/codecov-action@v3 71 | with: 72 | token: ${{ secrets.CODECOV_TOKEN }} 73 | file: ./tests/build/logs/clover.xml 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | composer.lock 4 | tests/build 5 | .phpunit.result.cache 6 | .DS_Store 7 | tests/.DS_Store 8 | php-cs-fixer 9 | -------------------------------------------------------------------------------- /.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdi-fathi/eloquent-filter/65a9af3ad7913977c89e57c2fbbf5276092e2e66/.gitkeep -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | tests: 3 | override: 4 | - 5 | coverage: 6 | file: 'tests/coverage.xml' 7 | format: 'clover' 8 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | #linters: 2 | # phpcs: 3 | # standard: PSR2 4 | # fixer: true 5 | #files: 6 | # ignore: 7 | # - 'vendor/*' 8 | #fixers: 9 | # enable: true 10 | -------------------------------------------------------------------------------- /Aban-21-1402 14-27-04.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdi-fathi/eloquent-filter/65a9af3ad7913977c89e57c2fbbf5276092e2e66/Aban-21-1402 14-27-04.gif -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Eloquent-Filter 2 | 3 | First off, thank you for considering contributing to Eloquent-Filter! It's people like you that make the open source community such a great place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 4 | 5 | ## Code of Conduct 6 | 7 | This project and everyone participating in it is governed by the [Eloquent-Filter Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 8 | 9 | ## How Can I Contribute? 10 | 11 | There are many ways you can contribute to Eloquent-Filter, and not all of them involve writing code. Here's a few ideas to get started: 12 | 13 | - Reporting Bugs 14 | - Suggesting Enhancements 15 | - Writing Code 16 | - Reviewing Pull Requests 17 | - Improving Documentation 18 | 19 | ### Reporting Bugs 20 | 21 | This section guides you through submitting a bug report for Eloquent-Filter. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports. 22 | 23 | **Before Submitting A Bug Report:** 24 | 25 | - Check the documentation for ways to configure or use the tool that might solve your issue. 26 | - Perform a [cursory search](https://github.com/mehdi-fathi/eloquent-filter/issues) to see if the issue has already been reported. If it has, add a comment to the existing issue instead of opening a new one. 27 | 28 | **How Do I Submit A (Good) Bug Report?** 29 | 30 | Bugs are tracked as [GitHub issues](https://github.com/mehdi-fathi/eloquent-filter/issues). Create an issue and provide the following information: 31 | 32 | - Use a clear and descriptive title for the issue. 33 | - Describe the exact steps to reproduce the problem with as much detail as possible. 34 | - Provide specific examples to demonstrate the steps. 35 | 36 | ### Suggesting Enhancements 37 | 38 | This section guides you through submitting an enhancement suggestion for Eloquent-Filter, including completely new features and minor improvements to existing functionality. 39 | 40 | **How Do I Submit A (Good) Enhancement Suggestion?** 41 | 42 | Enhancement suggestions are tracked as GitHub issues: 43 | 44 | - Use a clear and descriptive title for the issue. 45 | - Provide a step-by-step description of the suggested enhancement with as many details as possible. 46 | - Provide specific examples to demonstrate the steps. 47 | 48 | ### Your First Code Contribution 49 | 50 | Unsure where to begin contributing to Eloquent-Filter? You can start by looking through 'beginner' and 'help-wanted' issues: 51 | 52 | - [Beginner issues](https://github.com/mehdi-fathi/eloquent-filter/labels/beginner) 53 | - [Help wanted issues](https://github.com/mehdi-fathi/eloquent-filter/labels/help%20wanted) 54 | 55 | ### Pull Requests 56 | 57 | The process described here has several goals: 58 | 59 | - Maintain Eloquent-Filter's quality 60 | - Fix problems that are important to users 61 | - Engage the community in working toward the best possible Eloquent-Filter 62 | 63 | Please follow these steps to have your contribution considered by the maintainers: 64 | 65 | 1. Follow all instructions in [the template](PULL_REQUEST_TEMPLATE.md) 66 | 2. Follow the [styleguides](#styleguides) 67 | 3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing 68 | 69 | ## Styleguides 70 | 71 | ### Git Commit Messages 72 | 73 | - Use the present tense ("Add feature" not "Added feature") 74 | - Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 75 | - Limit the first line to 72 characters or less 76 | - Reference issues and pull requests liberally after the first line 77 | 78 | ### PHP Styleguide 79 | 80 | Use the [PSR-12: Extended Coding Style](https://www.php-fig.org/psr/psr-12/). 81 | 82 | ## Additional Notes 83 | 84 | ### Issue and Pull Request Labels 85 | 86 | This section lists the labels we use to help us track and manage issues and pull requests. 87 | 88 | #### Type of Issue and Issue State 89 | 90 | - `bug`: Indicates an issue with the project 91 | - `enhancement`: Indicates a new feature request 92 | - `help wanted`: Indicates that a maintainer wants help on an issue or pull request 93 | - `question`: Indicates that an issue or pull request needs more information 94 | - `good first issue`: Indicates a good issue for first-time contributors 95 | 96 | ## License 97 | 98 | By contributing to Eloquent-Filter, you agree that your contributions will be licensed under its [MIT License](LICENSE). 99 | 100 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mehdi Fathi 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /_ide_helper.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ./tests/ 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ./src 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Command/MakeEloquentFilter.php: -------------------------------------------------------------------------------- 1 | files = $files; 60 | } 61 | 62 | /** 63 | * Execute the console command. 64 | * 65 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 66 | * 67 | * @return mixed 68 | */ 69 | public function handle() 70 | { 71 | $this->makeClassName()->compileStub(); 72 | $this->info(class_basename($this->getClassName()).' Created Successfully!'); 73 | } 74 | 75 | /** 76 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 77 | */ 78 | public function compileStub() 79 | { 80 | if ($this->files->exists($path = $this->getPath())) { 81 | $this->error("\n\n\t".$path.' Already Exists!'."\n"); 82 | exit; 83 | } 84 | $this->makeDirectory($path); 85 | 86 | $stubPath = __DIR__.'/modelfilter.stub'; 87 | 88 | if (!$this->files->exists($stubPath) || !is_readable($stubPath)) { 89 | $this->error(sprintf('File "%s" does not exist or is unreadable.', $stubPath)); 90 | exit; 91 | } 92 | 93 | $tmp = $this->applyValuesToStub($this->files->get($stubPath)); 94 | $this->files->put($path, $tmp); 95 | } 96 | 97 | /** 98 | * @param $stub 99 | * 100 | * @return string|string[] 101 | */ 102 | public function applyValuesToStub($stub) 103 | { 104 | $className = $this->getClassBasename($this->getClassName()); 105 | $search = ['{{class}}', '{{namespace}}']; 106 | $replace = [$className, str_replace('\\'.$className, '', $this->getClassName())]; 107 | 108 | return str_replace($search, $replace, $stub); 109 | } 110 | 111 | /** 112 | * @param $class 113 | * 114 | * @return string 115 | */ 116 | private function getClassBasename($class) 117 | { 118 | $class = is_object($class) ? get_class($class) : $class; 119 | 120 | return basename(str_replace('\\', '/', $class)); 121 | } 122 | 123 | /** 124 | * @return string 125 | */ 126 | public function getPath() 127 | { 128 | return $this->laravel->path.DIRECTORY_SEPARATOR.$this->getFileName(); 129 | } 130 | 131 | /** 132 | * @return string|string[] 133 | */ 134 | public function getFileName() 135 | { 136 | return str_replace([$this->getAppNamespace(), '\\'], ['', DIRECTORY_SEPARATOR], $this->getClassName().'.php'); 137 | } 138 | 139 | /** 140 | * @return string 141 | */ 142 | public function getAppNamespace() 143 | { 144 | return $this->laravel->getNamespace(); 145 | } 146 | 147 | /** 148 | * Build the directory for the class if necessary. 149 | * 150 | * @param string $path 151 | * 152 | */ 153 | protected function makeDirectory(string $path) 154 | { 155 | if (!$this->files->isDirectory(dirname($path))) { 156 | $this->files->makeDirectory(dirname($path), 0777, true, true); 157 | } 158 | } 159 | 160 | /** 161 | * Create Filter Class Name. 162 | * 163 | * @return $this 164 | */ 165 | public function makeClassName() 166 | { 167 | $parts = array_map([Str::class, 'studly'], explode('\\', $this->argument('name'))); 168 | $className = array_pop($parts); 169 | $ns = count($parts) > 0 ? implode('\\', $parts).'\\' : ''; 170 | 171 | $fqClass = config('eloquentFilter.namespace', 'App\\ModelFilters\\').$ns.$className; 172 | 173 | if (substr($fqClass, -6, 6) !== 'Filter') { 174 | $fqClass .= 'Filter'; 175 | } 176 | 177 | if (class_exists($fqClass)) { 178 | $this->error("\n\n\t$fqClass Already Exists!\n"); 179 | exit; 180 | } 181 | 182 | $this->setClassName($fqClass); 183 | 184 | return $this; 185 | } 186 | 187 | /** 188 | * @param $name 189 | * 190 | * @return $this 191 | */ 192 | public function setClassName($name) 193 | { 194 | $this->class = $name; 195 | 196 | return $this; 197 | } 198 | 199 | /** 200 | * @return array|string 201 | */ 202 | public function getClassName() 203 | { 204 | return $this->class; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Command/modelfilter.stub: -------------------------------------------------------------------------------- 1 | where('your_field', 'like', '%'.$value.'%'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Facade/EloquentFilter.php: -------------------------------------------------------------------------------- 1 | builder; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/DbBuilder/DbBuilderWrapperInterface.php: -------------------------------------------------------------------------------- 1 | builder; 23 | } 24 | 25 | /** 26 | * @return mixed 27 | */ 28 | public function getModel(): mixed 29 | { 30 | return $this->getBuilder()->getModel(); 31 | } 32 | 33 | /** 34 | * @return mixed 35 | */ 36 | public function getAliasListFilter(): mixed 37 | { 38 | return $this->getModel()->getAliasListFilter(); 39 | } 40 | 41 | /** 42 | * @param $request 43 | * @return mixed 44 | */ 45 | public function serializeRequestFilter($request): mixed 46 | { 47 | return $this->getBuilder()->getModel()->serializeRequestFilter($request); 48 | 49 | } 50 | 51 | /** 52 | * @param $response 53 | * @return mixed 54 | */ 55 | public function getResponseFilter($response): mixed 56 | { 57 | return $this->getBuilder()->getModel()->getResponseFilter($response); 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/EloquentBuilder/QueryBuilderWrapperInterface.php: -------------------------------------------------------------------------------- 1 | setInjectedDetections($injectedDetections); 56 | } 57 | 58 | $this->setDefaultDetect($defaultDetections); 59 | 60 | 61 | if ($mainBuilderConditions->getName() == DBBuilderQueryByCondition::NAME) { 62 | 63 | $factories = $this->getDbDetectorFactory($this->getDefaultDetect(), $this->getInjectedDetections()); 64 | 65 | $this->setDetectDbFactory($factories); 66 | 67 | } else { 68 | 69 | $factories = $this->getDetectorFactory($this->getDefaultDetect(), $this->getInjectedDetections()); 70 | 71 | $this->setDetectFactory($factories); 72 | 73 | } 74 | 75 | $this->mainBuilderConditions = $mainBuilderConditions; 76 | } 77 | 78 | /** 79 | * @return \eloquentFilter\QueryFilter\Detection\Contract\MainBuilderConditionsContract 80 | */ 81 | public function getMainBuilderConditions(): MainBuilderConditionsContract 82 | { 83 | return $this->mainBuilderConditions; 84 | } 85 | 86 | /** 87 | * @param mixed $default_detect 88 | */ 89 | public function setDefaultDetect($default_detect): void 90 | { 91 | $this->default_detect = $default_detect; 92 | } 93 | 94 | /** 95 | * @return array 96 | */ 97 | public function getDefaultDetect(): array 98 | { 99 | return $this->default_detect; 100 | } 101 | 102 | /** 103 | * @param array $detections 104 | */ 105 | public function setDetections(array $detections): void 106 | { 107 | $this->detections = $detections; 108 | } 109 | 110 | /** 111 | * @param array|null $detections 112 | * @throws \ReflectionException 113 | */ 114 | public function unsetDetection(?array $detections): void 115 | { 116 | if (is_array($detections)) { 117 | $detections = array_map(function ($item) { 118 | $class = new \ReflectionClass(WhereCondition::class); 119 | return $class->getNamespaceName() . '\\' . $item; 120 | }, $detections); 121 | 122 | $array = array_diff($this->getDefaultDetect(), $detections); 123 | 124 | $this->setDefaultDetect($array); 125 | } 126 | } 127 | 128 | /** 129 | * @param DetectionEloquentFactory $detect_factory 130 | */ 131 | public function setDetectFactory(DetectorFactoryContract $detect_factory): void 132 | { 133 | $this->detect_factory = $detect_factory; 134 | } 135 | 136 | /** 137 | * @return DetectionEloquentFactory 138 | */ 139 | public function getDetectFactory(): DetectorFactoryContract 140 | { 141 | return $this->detect_factory; 142 | } 143 | 144 | /** 145 | * @param \eloquentFilter\QueryFilter\Detection\Contract\DetectorDbFactoryContract $detect_factory 146 | */ 147 | public function setDetectDbFactory(DetectorDBFactoryContract $detect_factory): void 148 | { 149 | $this->detect_db_factory = $detect_factory; 150 | } 151 | 152 | /** 153 | * @return \eloquentFilter\QueryFilter\Detection\Contract\DetectorDbFactoryContract 154 | */ 155 | public function getDetectDbFactory(): DetectorDBFactoryContract 156 | { 157 | return $this->detect_db_factory; 158 | } 159 | 160 | /** 161 | * @return array 162 | */ 163 | public function getDetections(): array 164 | { 165 | return $this->detections; 166 | } 167 | 168 | /** 169 | * @param mixed $detectInjected 170 | */ 171 | public function setInjectedDetections($detectInjected): void 172 | { 173 | if (!config('eloquentFilter.enabled_custom_detection')) { 174 | return; 175 | } 176 | $this->injected_detections = $detectInjected; 177 | } 178 | 179 | /** 180 | * @return mixed 181 | */ 182 | public function getInjectedDetections(): mixed 183 | { 184 | return $this->injected_detections; 185 | } 186 | 187 | /** 188 | * @param array|null $default_detect 189 | * @param array|null $detectInjected 190 | * 191 | * @return DetectionEloquentFactory 192 | */ 193 | public function getDetectorFactory(array $default_detect = null, array $detectInjected = null): DetectorFactoryContract 194 | { 195 | $this->mergeTypesDetections($default_detect, $detectInjected); 196 | 197 | return app(DetectionEloquentFactory::class, ['detections' => $this->getDetections()]); 198 | } 199 | 200 | /** 201 | * @param array|null $default_detect 202 | * @param array|null $detectInjected 203 | * 204 | * @return \eloquentFilter\QueryFilter\Detection\DetectionFactory\DetectionDbFactory 205 | */ 206 | public function getDbDetectorFactory(array $default_detect = null, array $detectInjected = null): DetectorDbFactoryContract 207 | { 208 | $this->mergeTypesDetections($default_detect, $detectInjected); 209 | 210 | return app(DetectionDbFactory::class, ['detections' => $this->getDetections()]); 211 | } 212 | 213 | /** 214 | * @param array|null $injected_detections 215 | * @return void 216 | */ 217 | public function setDetectionsInjected(?array $injected_detections): void 218 | { 219 | if (!empty($injected_detections)) { 220 | $this->setInjectedDetections($injected_detections); 221 | $this->setDetectFactory($this->getDetectorFactory($this->getDefaultDetect(), $this->getInjectedDetections())); 222 | } 223 | } 224 | 225 | /** 226 | * @return void 227 | */ 228 | public function reloadDetectionInjected(): void 229 | { 230 | $this->setDetectFactory($this->getDetectorFactory($this->getDefaultDetect(), $this->getInjectedDetections())); 231 | } 232 | 233 | /** 234 | * @param array|null $injected_detections 235 | * @return void 236 | */ 237 | public function setDetectionsDbInjected(?array $injected_detections): void 238 | { 239 | if (!empty($injected_detections)) { 240 | $this->setInjectedDetections($injected_detections); 241 | $this->setDetectFactory($this->getDbDetectorFactory($this->getDefaultDetect(), $this->getInjectedDetections())); 242 | } 243 | } 244 | 245 | /** 246 | * @param array|null $default_detect 247 | * @param array|null $detectInjected 248 | * @return void 249 | */ 250 | private function mergeTypesDetections(?array $default_detect, ?array $detectInjected): void 251 | { 252 | $detections = $default_detect; 253 | 254 | if (!empty($detectInjected)) { 255 | $detections = array_merge($detectInjected, $default_detect); 256 | } 257 | 258 | $this->setDetections($detections); 259 | } 260 | 261 | } 262 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/FilterBuilder/IO/Encoder.php: -------------------------------------------------------------------------------- 1 | request = $request; 42 | } 43 | 44 | /** 45 | * @param $request 46 | * @return void 47 | */ 48 | public function setPureRequest($request) 49 | { 50 | $this->request = $request; 51 | $this->handleRequestEncoded($request); 52 | } 53 | 54 | /** 55 | * @param array|null $request 56 | * @param $salt 57 | */ 58 | public function setRequestEncoded(?array $request, $salt): void 59 | { 60 | $this->requestEncoded = $this->encodeWithSalt(json_encode($request), $salt); 61 | } 62 | 63 | /** 64 | * @return void 65 | */ 66 | public function handelSerializeRequestFilter($request) 67 | { 68 | $this->setRequest($request); 69 | } 70 | 71 | /** 72 | * @param array|null $request 73 | */ 74 | public function setRequest(?array $request): void 75 | { 76 | if (!empty($request['page'])) { 77 | unset($request['page']); 78 | } 79 | 80 | $request_key_filter = config('eloquentFilter.request_filter_key'); 81 | 82 | if (!empty($request_key_filter)) { 83 | $request = (!empty($request[$request_key_filter])) ? $request[$request_key_filter] : []; 84 | } 85 | 86 | $request = array_filter($request, function ($value) { 87 | return !is_null($value) && $value !== ''; 88 | }); 89 | 90 | foreach ($request as $key => $item) { 91 | if (is_array($item)) { 92 | if (array_key_exists('start', $item) && array_key_exists('end', $item)) { 93 | if (!isset($item['start']) && !isset($item['end'])) { 94 | unset($request[$key]); 95 | } 96 | } 97 | } 98 | } 99 | 100 | $this->request = $request; 101 | } 102 | 103 | /** 104 | * @return array|null 105 | */ 106 | public function getRequest(): ?array 107 | { 108 | return $this->request; 109 | } 110 | 111 | /** 112 | * @param array|null $ignore_request 113 | * @param array|null $accept_request 114 | * @param $builder_model 115 | * @return array|null 116 | */ 117 | public function setFilterRequests(array $ignore_request = null, array $accept_request = null, $builder_model): ?array 118 | { 119 | if (!empty($this->getRequest())) { 120 | if (!empty(config('eloquentFilter.ignore_request'))) { 121 | $ignore_request = array_merge(config('eloquentFilter.ignore_request'), (array)$ignore_request); 122 | } 123 | if (!empty($ignore_request)) { 124 | $this->updateRequestByIgnoreRequest($ignore_request); 125 | } 126 | if (!empty($accept_request)) { 127 | $this->setAcceptRequest($accept_request); 128 | $this->updateRequestByAcceptRequest($this->getAcceptRequest()); 129 | } 130 | 131 | foreach ($this->getRequest() as $name => $value) { 132 | 133 | $value = $this->getCastedMethodValue($name, $builder_model, $value); 134 | 135 | if (is_array($value) && !empty($builder_model) && method_exists($builder_model, $name)) { 136 | if (HelperFilter::isAssoc($value)) { 137 | unset($this->request[$name]); 138 | $out = HelperFilter::convertRelationArrayRequestToStr($name, $value); 139 | $this->setRequest(array_merge($out, $this->request)); 140 | } 141 | } 142 | } 143 | } 144 | 145 | return $this->getRequest(); 146 | } 147 | 148 | /** 149 | * @param $alias_list_filter 150 | * @return void 151 | */ 152 | public function makeAliasRequestFilter($alias_list_filter) 153 | { 154 | if (empty($this->getRequest())) { 155 | return; 156 | } 157 | $req = $this->getRequest(); 158 | 159 | $req = collect($req)->mapWithKeys(function ($item, $key) use ($alias_list_filter) { 160 | $key1 = array_search($key, $alias_list_filter); 161 | 162 | if (!empty($alias_list_filter[$key1])) { 163 | $req[$key1] = $this->getRequest()[$key]; 164 | } else { 165 | $req[$key] = $item; 166 | } 167 | 168 | return $req; 169 | })->toArray(); 170 | 171 | if (!empty($req)) { 172 | $this->setRequest($req); 173 | } 174 | } 175 | 176 | /** 177 | * @param $ignore_request 178 | */ 179 | private function updateRequestByIgnoreRequest($ignore_request) 180 | { 181 | $this->setIgnoreRequest($ignore_request); 182 | $data = Arr::except($this->getRequest(), $ignore_request); 183 | $this->setRequest($data); 184 | } 185 | 186 | /** 187 | * @param $accept_request 188 | */ 189 | private function updateRequestByAcceptRequest($accept_request) 190 | { 191 | $accept_request_new = HelperFilter::array_slice_keys($this->getRequest(), $accept_request); 192 | if (!empty($accept_request_new)) { 193 | $this->setAcceptRequest(HelperFilter::array_slice_keys($this->getRequest(), $accept_request)); 194 | $this->setRequest($this->getAcceptRequest()); 195 | } else { 196 | $this->setRequest([]); 197 | } 198 | } 199 | 200 | /** 201 | * @param array|null $ignore_request 202 | * @param array|null $accept_request 203 | * @param array|null $serialize_request_filter 204 | * @param $alias_list_filter 205 | * @param $model 206 | * @return void 207 | */ 208 | public function requestAlter(?array $ignore_request, ?array $accept_request, ?array $serialize_request_filter, $alias_list_filter, $model): void 209 | { 210 | $this->handelSerializeRequestFilter($serialize_request_filter); 211 | 212 | if ($alias_list_filter) { 213 | $this->makeAliasRequestFilter($alias_list_filter); 214 | } 215 | 216 | $this->setFilterRequests($ignore_request, $accept_request, $model); 217 | } 218 | 219 | /** 220 | * @param array $ignore_request 221 | */ 222 | private function setIgnoreRequest(array $ignore_request): void 223 | { 224 | $this->ignore_request = $ignore_request; 225 | } 226 | 227 | /** 228 | * @param array $accept_request 229 | */ 230 | private function setAcceptRequest(array $accept_request): void 231 | { 232 | if (!empty($accept_request)) { 233 | $this->accept_request = $accept_request; 234 | } 235 | } 236 | 237 | /** 238 | * @return mixed 239 | */ 240 | public function getAcceptRequest() 241 | { 242 | return $this->accept_request; 243 | } 244 | 245 | /** 246 | * @return mixed 247 | */ 248 | public function getIgnoreRequest() 249 | { 250 | return $this->ignore_request; 251 | } 252 | 253 | /** 254 | * @param int|string $name 255 | * @param $builder_model 256 | * @param mixed $value 257 | * @return mixed 258 | */ 259 | private function getCastedMethodValue(int|string $name, $builder_model, mixed $value): mixed 260 | { 261 | $castMethod = config('eloquentFilter.cast_method_sign') . $name; 262 | 263 | if (!empty($builder_model) && method_exists($builder_model, $castMethod)) { 264 | $value = $builder_model->{$castMethod}($value); 265 | $this->request[$name] = $value; 266 | } 267 | return $value; 268 | } 269 | 270 | /** 271 | * @param array|null $ignore_request 272 | * @param array|null $accept_request 273 | * @return void 274 | */ 275 | public function handleRequestDb(?array $ignore_request, ?array $accept_request): void 276 | { 277 | 278 | $serialize_request_filter = $this->getRequest(); 279 | 280 | $this->requestAlter( 281 | ignore_request: $ignore_request, 282 | accept_request: $accept_request, 283 | serialize_request_filter: $serialize_request_filter, 284 | alias_list_filter: $alias_list_filter ?? [], 285 | model: null, 286 | ); 287 | } 288 | 289 | /** 290 | * @param $builder 291 | * @param array|null $ignore_request 292 | * @param array|null $accept_request 293 | * @return void 294 | */ 295 | public function handleRequest($builder, ?array $ignore_request, ?array $accept_request): void 296 | { 297 | 298 | $serialize_request_filter = $builder->getModel()->serializeRequestFilter($this->getRequest()); 299 | 300 | $alias_list_filter = $builder->getModel()->getAliasListFilter(); 301 | 302 | $this->requestAlter( 303 | ignore_request: $ignore_request, 304 | accept_request: $accept_request, 305 | serialize_request_filter: $serialize_request_filter, 306 | alias_list_filter: $alias_list_filter ?? [], 307 | model: $builder->getModel(), 308 | ); 309 | } 310 | 311 | /** 312 | * @param $request 313 | * @return void 314 | */ 315 | private function handleRequestEncoded($request): void 316 | { 317 | if (isset($request['hashed_filters'])) { 318 | $this->request = json_decode($this->decodeWithSalt($request['hashed_filters'], config('eloquentFilter.request_salt')), true); 319 | } else { 320 | $this->setRequestEncoded($request, config('eloquentFilter.request_salt')); 321 | } 322 | } 323 | 324 | } 325 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/FilterBuilder/IO/ResponseFilter.php: -------------------------------------------------------------------------------- 1 | response; 21 | } 22 | 23 | /** 24 | * @param mixed $response 25 | */ 26 | public function setResponse(mixed $response): void 27 | { 28 | $this->response = $response; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/FilterBuilder/MainQueryFilterBuilder.php: -------------------------------------------------------------------------------- 1 | checkRateLimit(); 46 | 47 | if (!empty($request)) { 48 | $this->requestFilter->setPureRequest($request); 49 | } 50 | 51 | if ($this->checkEnableEloquentFilter()) { 52 | return $builder; 53 | } 54 | 55 | return $this->buildQuery( 56 | builder: $builder, 57 | ignore_request: $ignore_request, 58 | accept_request: $accept_request, 59 | detections_injected: $detections_injected, 60 | black_list_detections: $black_list_detections 61 | ); 62 | 63 | } 64 | 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getNameBuilder(): string 70 | { 71 | $MainBuilderConditions = $this->queryFilterCore->getMainBuilderConditions(); 72 | 73 | return $MainBuilderConditions->getName(); 74 | } 75 | 76 | /** 77 | * @return bool 78 | */ 79 | private function checkEnableEloquentFilter(): bool 80 | { 81 | return !config('eloquentFilter.enabled') || empty($this->requestFilter->getRequest()); 82 | } 83 | 84 | /** 85 | * @param $builder 86 | * @param array|null $ignore_request 87 | * @param array|null $accept_request 88 | * @param array|null $detections_injected 89 | * @param array|null $black_list_detections 90 | * @return mixed|null 91 | * @throws \ReflectionException 92 | */ 93 | private function buildQuery($builder, ?array $ignore_request, ?array $accept_request, ?array $detections_injected, ?array $black_list_detections): mixed 94 | { 95 | if ($this->isDbBuilder()) { 96 | 97 | return $this->buildDbQuery($builder, $ignore_request, $accept_request, $detections_injected, $black_list_detections); 98 | } 99 | 100 | return $this->buildEloquentQuery($builder, $ignore_request, $accept_request, $detections_injected, $black_list_detections); 101 | } 102 | 103 | /** 104 | * @param $builder 105 | * @param array|null $ignore_request 106 | * @param array|null $accept_request 107 | * @param array|null $detections_injected 108 | * @param array|null $black_list_detections 109 | * @return null 110 | * @throws \ReflectionException 111 | */ 112 | private function buildDbQuery($builder, ?array $ignore_request, ?array $accept_request, ?array $detections_injected, ?array $black_list_detections): mixed 113 | { 114 | 115 | $this->requestFilter->handleRequestDb( 116 | ignore_request: $ignore_request, 117 | accept_request: $accept_request 118 | ); 119 | 120 | $DBQueryFilterBuilder = new DBQueryFilterBuilder($this->queryFilterCore, $this->requestFilter, $this->responseFilter); 121 | 122 | return $DBQueryFilterBuilder->apply( 123 | builder: $builder, 124 | detections_injected: $detections_injected, 125 | black_list_detections: $black_list_detections 126 | ); 127 | } 128 | 129 | /** 130 | * @param $builder 131 | * @param array|null $ignore_request 132 | * @param array|null $accept_request 133 | * @param array|null $detections_injected 134 | * @param array|null $black_list_detections 135 | * @return mixed 136 | * @throws \ReflectionException 137 | */ 138 | private function buildEloquentQuery($builder, ?array $ignore_request, ?array $accept_request, ?array $detections_injected, ?array $black_list_detections): mixed 139 | { 140 | $this->requestFilter->handleRequest( 141 | builder: $builder, 142 | ignore_request: $ignore_request, 143 | accept_request: $accept_request 144 | ); 145 | 146 | $eloquentQueryFilterBuilder = new EloquentQueryFilterBuilder($this->queryFilterCore, $this->requestFilter, $this->responseFilter); 147 | 148 | return $eloquentQueryFilterBuilder->apply( 149 | builder: $builder, 150 | detections_injected: $detections_injected, 151 | black_list_detections: $black_list_detections 152 | ); 153 | } 154 | 155 | /** 156 | * @return bool 157 | */ 158 | private function isDbBuilder(): bool 159 | { 160 | return $this->getNameBuilder() == DBBuilderQueryByCondition::NAME; 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/FilterBuilder/QueryBuilder/DBQueryFilterBuilder.php: -------------------------------------------------------------------------------- 1 | queryBuilderWrapper = $queryBuilderWrapper; 23 | } 24 | 25 | /** 26 | * @return \eloquentFilter\QueryFilter\Core\DbBuilder\DbBuilderWrapperInterface 27 | */ 28 | public function getQueryBuilderWrapper(): DbBuilderWrapperInterface 29 | { 30 | return $this->queryBuilderWrapper; 31 | } 32 | 33 | /** 34 | * @param $builder 35 | * @param array|null $detections_injected 36 | * @param array|null $black_list_detections 37 | * 38 | * @return mixed 39 | * @throws \ReflectionException 40 | */ 41 | public function apply($builder, array $detections_injected = null, array $black_list_detections = null): mixed 42 | { 43 | $this->setMacroIsUsedPackage(); 44 | 45 | $this->setQueryBuilderWrapper(QueryBuilderWrapperFactory::createDbQueryBuilder($builder)); 46 | 47 | $this->resolveDetections($detections_injected, $black_list_detections); 48 | 49 | return $this->responseFilter->getResponse(); 50 | } 51 | 52 | /** 53 | * @return void 54 | * @throws \ReflectionException 55 | */ 56 | private function resolveDetections($detections_injected, $black_list_detections) 57 | { 58 | $this->queryFilterCore->unsetDetection($black_list_detections); 59 | $this->queryFilterCore->setDetectionsDbInjected($detections_injected); 60 | 61 | /** @see \eloquentFilter\QueryFilter\Core\ResolverDetection\ResolverDetectionDb */ 62 | app()->bind('ResolverDetectionsDb', function () { 63 | return new ResolverDetectionDb( 64 | builder: $this->getQueryBuilderWrapper()->getBuilder(), 65 | request: $this->requestFilter->getRequest(), 66 | detector_db_factory: $this->queryFilterCore->getDetectDbFactory(), 67 | main_builder_conditions_contract: $this->queryFilterCore->getMainBuilderConditions() 68 | ); 69 | }); 70 | 71 | /** @see ResolverDetectionDb::getResolverOut() */ 72 | $responseResolver = app('ResolverDetectionsDb')->getResolverOut(); 73 | 74 | $this->responseFilter->setResponse($responseResolver); 75 | } 76 | 77 | 78 | /** 79 | * @return void 80 | */ 81 | private function setMacroIsUsedPackage(): void 82 | { 83 | \Illuminate\Database\Query\Builder::macro('isUsedEloquentFilter', function () { 84 | return config('eloquentFilter.enabled'); 85 | }); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/FilterBuilder/QueryBuilder/EloquentQueryFilterBuilder.php: -------------------------------------------------------------------------------- 1 | queryBuilderWrapper = $queryBuilderWrapper; 23 | } 24 | 25 | /** 26 | * @return \eloquentFilter\QueryFilter\Core\EloquentBuilder\EloquentModelBuilderWrapper 27 | */ 28 | public function getQueryBuilderWrapper(): EloquentModelBuilderWrapper 29 | { 30 | return $this->queryBuilderWrapper; 31 | } 32 | 33 | /** 34 | * @param $builder 35 | * @param array|null $detections_injected 36 | * @param array|null $black_list_detections 37 | * 38 | * @return mixed 39 | * @throws \ReflectionException 40 | */ 41 | public function apply($builder, array $detections_injected = null, array $black_list_detections = null): mixed 42 | { 43 | 44 | $this->buildExclusiveMacros($detections_injected); 45 | 46 | $this->setQueryBuilderWrapper(QueryBuilderWrapperFactory::createEloquentQueryBuilder($builder)); 47 | 48 | $this->resolveDetections($detections_injected, $black_list_detections); 49 | 50 | return $this->getQueryBuilderWrapper()->getResponseFilter($this->responseFilter->getResponse()); 51 | } 52 | 53 | /** 54 | * @return void 55 | * @throws \ReflectionException 56 | */ 57 | private function resolveDetections($detections_injected, $black_list_detections) 58 | { 59 | $this->queryFilterCore->unsetDetection($black_list_detections); 60 | $this->queryFilterCore->reloadDetectionInjected(); 61 | 62 | $this->queryFilterCore->setDetectionsInjected($detections_injected); 63 | 64 | /** @see ResolverDetectionEloquent */ 65 | app()->bind('ResolverDetectionEloquent', function () { 66 | return new ResolverDetectionEloquent( 67 | builder: $this->getQueryBuilderWrapper()->getBuilder(), 68 | request: $this->requestFilter->getRequest(), 69 | detector_factory: $this->queryFilterCore->getDetectFactory(), 70 | main_builder_conditions_contract: $this->queryFilterCore->getMainBuilderConditions() 71 | ); 72 | }); 73 | 74 | /** @see ResolverDetectionEloquent::getResolverOut() */ 75 | $responseResolver = app('ResolverDetectionEloquent')->getResolverOut(); 76 | 77 | 78 | $this->responseFilter->setResponse($responseResolver); 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getNameDriver() 85 | { 86 | $MainBuilderConditions = $this->queryFilterCore->getMainBuilderConditions(); 87 | 88 | return $MainBuilderConditions->getName(); 89 | } 90 | 91 | /** 92 | * @param array|null $detections_injected 93 | * @return void 94 | */ 95 | private function buildExclusiveMacros(?array $detections_injected): void 96 | { 97 | $this->setMacroIsUsedPackage(); 98 | 99 | $this->setMacroDetectionInjectedList($detections_injected); 100 | 101 | } 102 | 103 | /** 104 | * @return void 105 | */ 106 | private function setMacroIsUsedPackage(): void 107 | { 108 | \Illuminate\Database\Eloquent\Builder::macro('isUsedEloquentFilter', function () { 109 | return config('eloquentFilter.enabled'); 110 | }); 111 | } 112 | 113 | /** 114 | * @param array|null $detections_injected 115 | * @return void 116 | */ 117 | private function setMacroDetectionInjectedList(?array $detections_injected): void 118 | { 119 | \Illuminate\Database\Eloquent\Builder::macro('getDetectionsInjected', function () use ($detections_injected) { 120 | return $detections_injected; 121 | }); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/FilterBuilder/QueryBuilder/QueryFilterBuilder.php: -------------------------------------------------------------------------------- 1 | requestFilter->getRequest()[$index]; 19 | } 20 | 21 | return $this->requestFilter->getRequest(); 22 | } 23 | 24 | /** 25 | * @return mixed 26 | */ 27 | public function getAcceptedRequest() 28 | { 29 | return $this->requestFilter->getAcceptRequest(); 30 | } 31 | 32 | /** 33 | * @return mixed 34 | */ 35 | public function getIgnoredRequest() 36 | { 37 | return $this->requestFilter->getIgnoreRequest(); 38 | } 39 | 40 | 41 | /** 42 | * @return mixed 43 | */ 44 | public function getRequestEncoded() 45 | { 46 | return $this->requestFilter->requestEncoded; 47 | } 48 | 49 | /** 50 | * @return mixed 51 | */ 52 | public function setRequestEncoded($request, $salt) 53 | { 54 | return $this->requestFilter->setRequestEncoded($request, $salt); 55 | } 56 | 57 | /** 58 | * @return mixed 59 | */ 60 | public function getInjectedDetections() 61 | { 62 | return $this->queryFilterCore->getInjectedDetections(); 63 | } 64 | 65 | /** 66 | * @return mixed 67 | */ 68 | public function getResponse() 69 | { 70 | return $this->responseFilter->getResponse(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/HelperFilter.php: -------------------------------------------------------------------------------- 1 | $item) { 27 | $index = $key; 28 | for ($i = 0; $i <= 9; $i++) { 29 | $index = rtrim($index, '.'.$i); 30 | } 31 | $new[$index][] = $out[$key]; 32 | } 33 | $out = $new; 34 | } else { 35 | $key_search_start = $field.'.'.key($args).'.start'; 36 | $key_search_end = $field.'.'.key($args).'.end'; 37 | 38 | if (Arr::exists($out, $key_search_start) && Arr::exists($out, $key_search_end)) { 39 | foreach ($args as $key => $item) { 40 | $new[$field.'.'.$key] = $args[$key]; 41 | } 42 | $out = $new; 43 | } 44 | } 45 | } else { 46 | $out = Arr::dot($args, $field.'.'); 47 | } 48 | 49 | return $out; 50 | } 51 | 52 | /** 53 | * @param array $arr 54 | * 55 | * @return bool 56 | */ 57 | public static function isAssoc(array $arr): bool 58 | { 59 | if ([] === $arr) { 60 | return false; 61 | } 62 | 63 | return array_keys($arr) !== range(0, count($arr) - 1); 64 | } 65 | 66 | /** 67 | * @param $request 68 | * @param null $keys 69 | * 70 | * @return array 71 | */ 72 | public static function array_slice_keys($request, $keys = null): array 73 | { 74 | $request = (array) $request; 75 | 76 | return array_intersect_key($request, array_fill_keys($keys, '1')); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/RateLimiting.php: -------------------------------------------------------------------------------- 1 | resolveRateLimitKey(); 28 | $maxAttempts = config('eloquentFilter.rate_limit.max_attempts', 60); 29 | $decayMinutes = config('eloquentFilter.rate_limit.decay_minutes', 1); 30 | 31 | if ($limiter->tooManyAttempts($key, $maxAttempts)) { 32 | $seconds = $limiter->availableIn($key); 33 | 34 | $headers = [ 35 | 'X-RateLimit-Limit' => $maxAttempts, 36 | 'X-RateLimit-Remaining' => 0, 37 | 'X-RateLimit-Reset' => $seconds, 38 | 'Retry-After' => $seconds, 39 | ]; 40 | 41 | throw new ThrottleRequestsException( 42 | config('eloquentFilter.rate_limit.error_message', 'Too many filter requests. Please try again later.'), 43 | null, 44 | $headers 45 | ); 46 | } 47 | 48 | $limiter->hit($key, $decayMinutes * 60); 49 | 50 | // Store rate limit info in request for later use 51 | if (config('eloquentFilter.rate_limit.include_headers', true)) { 52 | $remaining = $limiter->remaining($key, $maxAttempts); 53 | $request = request(); 54 | 55 | // Ensure attributes is initialized 56 | if (!isset($request->attributes)) { 57 | $request->attributes = new ParameterBag(); 58 | } 59 | 60 | 61 | $request->attributes->set('rate_limit', [ 62 | 'limit' => $maxAttempts, 63 | 'remaining' => $remaining 64 | ]); 65 | } 66 | } 67 | 68 | /** 69 | * Get the rate limiting key 70 | */ 71 | protected function resolveRateLimitKey(): string 72 | { 73 | $request = request(); 74 | 75 | return sha1(sprintf( 76 | '%s|%s|%s', 77 | $request->user() ? $request->user()->getAuthIdentifier() : $request->ip(), 78 | $request->path(), 79 | get_class($this) 80 | )); 81 | } 82 | } -------------------------------------------------------------------------------- /src/QueryFilter/Core/ResolverDetection/ResolverDetectionDb.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 24 | $this->request = $request; 25 | $this->detector_db_factory = $detector_db_factory; 26 | $this->main_builder_conditions = $main_builder_conditions_contract; 27 | } 28 | 29 | /** 30 | * @return array 31 | */ 32 | public function getFiltersDetection(): array 33 | { 34 | $filter_detections = collect($this->request)->map(function ($values, $filter){ 35 | return $this->resolve($filter, $values); 36 | })->reverse()->filter(function ($item) { 37 | return $item instanceof BaseClause; 38 | })->toArray(); 39 | 40 | $out = Arr::isAssoc($filter_detections) ? $filter_detections : []; 41 | 42 | return $out; 43 | } 44 | 45 | /** 46 | * @param $filterName 47 | * @param $values 48 | * 49 | * @return Application|mixed 50 | * @throws ReflectionException 51 | * 52 | */ 53 | protected function resolve($filterName, $values) 54 | { 55 | $detectedConditions = $this->detector_db_factory->buildDetections($filterName, $values); 56 | 57 | $builderDriver = $this->main_builder_conditions->build($detectedConditions); 58 | 59 | return app($builderDriver, ['filter' => $filterName, 'values' => $values]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/ResolverDetection/ResolverDetectionEloquent.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 24 | $this->request = $request; 25 | $this->detector_factory = $detector_factory; 26 | 27 | $this->main_builder_conditions = $main_builder_conditions_contract; 28 | } 29 | /** 30 | * @return array 31 | */ 32 | public function getFiltersDetection(): array 33 | { 34 | $model = $this->builder->getModel(); 35 | 36 | $filter_detections = collect($this->request)->map(function ($values, $filter) use ($model) { 37 | return $this->resolve($filter, $values, $model); 38 | })->reverse()->filter(function ($item) { 39 | return $item instanceof BaseClause; 40 | })->toArray(); 41 | 42 | $out = Arr::isAssoc($filter_detections) ? $filter_detections : []; 43 | 44 | return $out; 45 | } 46 | 47 | /** 48 | * @param $filterName 49 | * @param $values 50 | * @param $model 51 | * 52 | * @return Application|mixed 53 | * @throws ReflectionException 54 | * 55 | */ 56 | protected function resolve($filterName, $values, $model) 57 | { 58 | $detectedConditions = $this->detector_factory->buildDetections($filterName, $values, $model); 59 | 60 | $builderDriver = $this->main_builder_conditions->build($detectedConditions); 61 | 62 | return app($builderDriver, ['filter' => $filterName, 'values' => $values]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/QueryFilter/Core/ResolverDetection/ResolverDetections.php: -------------------------------------------------------------------------------- 1 | getFiltersDetection(); 42 | 43 | $out = app(Pipeline::class) 44 | ->send($this->builder) 45 | ->through($filter_detections) 46 | ->thenReturn(); 47 | 48 | return $out; 49 | } 50 | 51 | /** 52 | * @return array 53 | */ 54 | abstract public function getFiltersDetection(): array; 55 | } 56 | -------------------------------------------------------------------------------- /src/QueryFilter/Detection/ConditionsDetect/DB/DBBuilderQueryByCondition.php: -------------------------------------------------------------------------------- 1 | Where::class, 43 | 'WhereBetween' => WhereBetween::class, 44 | 'WhereByOpt' => WhereByOpt::class, 45 | 'WhereDate' => WhereDate::class, 46 | 'WhereDoesntHave' => WhereDoesntHave::class, 47 | 'WhereHas' => WhereHas::class, 48 | 'WhereIn' => WhereIn::class, 49 | 'WhereLike' => WhereLike::class, 50 | 'WhereOr' => WhereOr::class, 51 | 'WhereNull' => WhereNull::class, 52 | 'WhereNotNull' => WhereNotNull::class, 53 | 'Special' => Special::class, 54 | // 'WhereCustom' => WhereCustom::class, 55 | default => null, 56 | }; 57 | 58 | if (empty($builder) && !empty($condition)) { 59 | return $condition; 60 | } 61 | 62 | return $builder; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/QueryFilter/Detection/ConditionsDetect/Eloquent/MainBuilderQueryByCondition.php: -------------------------------------------------------------------------------- 1 | Where::class, 43 | 'WhereBetween' => WhereBetween::class, 44 | 'WhereByOpt' => WhereByOpt::class, 45 | 'WhereDate' => WhereDate::class, 46 | 'WhereHas' => WhereHas::class, 47 | 'WhereIn' => WhereIn::class, 48 | 'WhereLike' => WhereLike::class, 49 | 'WhereOr' => WhereOr::class, 50 | 'WhereDoesntHave' => WhereDoesntHave::class, 51 | 'WhereNull' => WhereNull::class, 52 | 'WhereNotNull' => WhereNotNull::class, 53 | 'Special' => Special::class, 54 | 'WhereCustom' => WhereCustom::class, 55 | default => null, 56 | }; 57 | 58 | if (empty($builder) && !empty($condition)) { 59 | return $condition; 60 | } 61 | 62 | return $builder; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/QueryFilter/Detection/ConditionsDetect/TypeQueryConditions/SpecialCondition.php: -------------------------------------------------------------------------------- 1 | $this->detections]); 32 | 33 | $method = $detect->detect($field, $params); 34 | 35 | return $method; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/QueryFilter/Detection/DetectionFactory/DetectionEloquentFactory.php: -------------------------------------------------------------------------------- 1 | $this->detections]); 34 | 35 | $class_name = null; 36 | if (!empty($model)) { 37 | $class_name = class_basename($model); 38 | } 39 | 40 | $method = $detect->detect($field, $params, $model->getWhiteListFilter(), $model->checkModelHasOverrideMethod($field), $class_name); 41 | 42 | return $method; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/QueryFilter/Detection/Detector/DetectorConditionCondition.php: -------------------------------------------------------------------------------- 1 | map(function ($detector_obj) { 36 | if (!empty($detector_obj)) { 37 | $reflect = new \ReflectionClass($detector_obj); 38 | if ($reflect->implementsInterface(DefaultConditionsContract::class)) { 39 | return $detector_obj; 40 | } 41 | } 42 | })->toArray(); 43 | 44 | $this->setDetector($detector_collect); 45 | } 46 | 47 | /** 48 | * @param \Illuminate\Support\Collection $detector 49 | */ 50 | public function setDetector(\Illuminate\Support\Collection $detector): void 51 | { 52 | $this->detector = $detector; 53 | } 54 | 55 | /** 56 | * @return \Illuminate\Support\Collection 57 | */ 58 | public function getDetector(): \Illuminate\Support\Collection 59 | { 60 | return $this->detector; 61 | } 62 | 63 | /** 64 | * @param string $field 65 | * @param $params 66 | * @param null $getWhiteListFilter 67 | * @param bool $hasOverrideMethod 68 | * @param $className 69 | * @return string|null 70 | */ 71 | public function detect(string $field, $params, $getWhiteListFilter = null, bool $hasOverrideMethod = false, $className = null): ?string 72 | { 73 | $out = $this->getDetector()->map(function ($item) use ($field, $params, $getWhiteListFilter, $hasOverrideMethod, $className) { 74 | if ($this->handelListFields($field, $getWhiteListFilter, $hasOverrideMethod, $className)) { 75 | if ($hasOverrideMethod) { 76 | $query = WhereCustom::class; 77 | } else { 78 | /** @see DefaultConditionsContract::detect() */ 79 | $query = $item::detect($field, $params); 80 | } 81 | 82 | if (!empty($query)) { 83 | return $query; 84 | } 85 | } 86 | })->filter(); 87 | 88 | return $out->first(); 89 | } 90 | 91 | /** 92 | * @param string $field 93 | * @param array|null $list_white_filter_model 94 | * @param bool $has_override_method 95 | * @param $model_class 96 | * 97 | * @return bool 98 | * @throws Exception 99 | * 100 | */ 101 | private function handelListFields(string $field, ?array $list_white_filter_model, bool $has_override_method, $model_class): bool 102 | { 103 | if ($this->checkSetWhiteListFields($field, $list_white_filter_model) || $this->checkReservedParam($field) || $has_override_method) { 104 | return true; 105 | } 106 | 107 | throw new EloquentFilterException(sprintf($this->errorExceptionWhileList, $field, $model_class, $field, $field), 1); 108 | } 109 | 110 | /** 111 | * @param string $field 112 | * @param array|null $query 113 | * 114 | * @return bool 115 | */ 116 | private function checkSetWhiteListFields(string $field, ?array $query): bool 117 | { 118 | if (in_array($field, $query) || (!empty($query[0]) && $query[0] == '*')) { 119 | return true; 120 | } 121 | 122 | return false; 123 | } 124 | 125 | /** 126 | * @param string $field 127 | * @return bool 128 | */ 129 | private function checkReservedParam(string $field): bool 130 | { 131 | return ($field == SpecialCondition::PARAM_NAME || $field == WhereOrCondition::PARAM_NAME || $field == WhereDoesntHaveCondition::PARAM_NAME); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/QueryFilter/Detection/Detector/DetectorConditionDbCondition.php: -------------------------------------------------------------------------------- 1 | map(function ($detector_obj) { 29 | if (!empty($detector_obj)) { 30 | $reflect = new \ReflectionClass($detector_obj); 31 | if ($reflect->implementsInterface(DefaultConditionsContract::class)) { 32 | return $detector_obj; 33 | } 34 | } 35 | })->toArray(); 36 | 37 | $this->setDetector($detector_collect); 38 | } 39 | 40 | /** 41 | * @param \Illuminate\Support\Collection $detector 42 | */ 43 | public function setDetector(\Illuminate\Support\Collection $detector): void 44 | { 45 | $this->detector = $detector; 46 | } 47 | 48 | /** 49 | * @return \Illuminate\Support\Collection 50 | */ 51 | public function getDetector(): \Illuminate\Support\Collection 52 | { 53 | return $this->detector; 54 | } 55 | 56 | /** 57 | * @param string $field 58 | * @param $params 59 | * @param null $getWhiteListFilter 60 | * @param bool $hasOverrideMethod 61 | * @param $className 62 | * @return string|null 63 | */ 64 | public function detect(string $field, $params, $getWhiteListFilter = null, bool $hasOverrideMethod = false, $className = null): ?string 65 | { 66 | $out = $this->getDetector()->map(function ($item) use ($field, $params) { 67 | /** @see DefaultConditionsContract::detect() */ 68 | $query = $item::detect($field, $params); 69 | 70 | if (!empty($query)) { 71 | return $query; 72 | } 73 | })->filter(); 74 | 75 | return $out->first(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/QueryFilter/Exceptions/EloquentFilterException.php: -------------------------------------------------------------------------------- 1 | $this->getDefaultDetectorsEloquent(), 38 | 'injectedDetections' => null, 39 | 'mainBuilderConditions' => $mainBuilderConditions 40 | ] 41 | ); 42 | } 43 | 44 | /** 45 | * @return \eloquentFilter\QueryFilter\Core\FilterBuilder\Core\QueryFilterCore 46 | */ 47 | public function createQueryFilterCoreDBQueryBuilder(): QueryFilterCore 48 | { 49 | $mainBuilderConditions = new DBBuilderQueryByCondition(); 50 | return app(QueryFilterCoreBuilder::class, 51 | [ 52 | 'defaultDetections' => $this->getDefaultDetectorsEloquent(), 53 | 'injectedDetections' => null, 54 | 'mainBuilderConditions' => $mainBuilderConditions 55 | ] 56 | ); 57 | } 58 | 59 | /** 60 | * @return array 61 | * @note DON'T CHANGE ORDER THESE BASED ON FLIMSY REASON. 62 | */ 63 | private function getDefaultDetectorsEloquent(): array 64 | { 65 | return [ 66 | SpecialCondition::class, 67 | WhereBetweenCondition::class, 68 | WhereByOptCondition::class, 69 | WhereLikeCondition::class, 70 | WhereInCondition::class, 71 | WhereOrCondition::class, 72 | WhereHasCondition::class, 73 | WhereDoesntHaveCondition::class, 74 | WhereDateCondition::class, 75 | WhereNullCondition::class, 76 | WhereMonthCondition::class, 77 | WhereYearCondition::class, 78 | WhereDayCondition::class, 79 | WhereCondition::class, 80 | ]; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/QueryFilter/ModelFilters/Filterable.php: -------------------------------------------------------------------------------- 1 | ignore_request, 56 | accept_request: $this->accept_request, 57 | detections_injected: $this->getObjectCustomDetect(), 58 | black_list_detections: $this->black_list_detections 59 | ); 60 | } 61 | 62 | /** 63 | * @param $builder 64 | * @param array|null $request 65 | */ 66 | public function scopeIgnoreRequest($builder, ?array $request = null) 67 | { 68 | $this->ignoreRequestFilter($request); 69 | } 70 | 71 | public function ignoreRequestFilter(?array $request = null) 72 | { 73 | $this->ignore_request = $request; 74 | return $this; 75 | } 76 | 77 | /** 78 | * @param $builder 79 | * @param array|null $request 80 | */ 81 | public function scopeAcceptRequest($builder, ?array $request = null) 82 | { 83 | $this->acceptRequestFilter($request); 84 | } 85 | 86 | /** 87 | * @param array|null $request 88 | * @return \eloquentFilter\QueryFilter\ModelFilters\Filterable 89 | */ 90 | public function acceptRequestFilter(?array $request = null) 91 | { 92 | $this->accept_request = $request; 93 | return $this; 94 | 95 | } 96 | 97 | /** 98 | * @param $builder 99 | * @param array|null $black_list_detections 100 | */ 101 | public function scopeSetBlackListDetection($builder, ?array $black_list_detections = null) 102 | { 103 | $this->black_list_detections = $black_list_detections; 104 | } 105 | 106 | /** 107 | * @param $builder 108 | * @param array|null $object_custom_detect 109 | */ 110 | public function scopeSetCustomDetection($builder, ?array $object_custom_detect = null) 111 | { 112 | $this->setObjectCustomDetect($object_custom_detect); 113 | } 114 | 115 | /** 116 | * @return mixed 117 | */ 118 | private function getObjectCustomDetect() 119 | { 120 | if (method_exists($this, 'EloquentFilterCustomDetection') && empty($this->object_custom_detect) && $this->getLoadInjectedDetections()) { 121 | $this->setObjectCustomDetect($this->EloquentFilterCustomDetection()); 122 | } 123 | 124 | return $this->object_custom_detect; 125 | } 126 | 127 | /** 128 | * @param mixed $object_custom_detect 129 | */ 130 | private function setObjectCustomDetect($object_custom_detect): void 131 | { 132 | $this->object_custom_detect = $object_custom_detect; 133 | } 134 | 135 | /** 136 | * @return mixed 137 | */ 138 | public static function getWhiteListFilter(): array 139 | { 140 | return (self::$whiteListFilter ?? []); 141 | } 142 | 143 | /** 144 | * @return array|null 145 | */ 146 | public function getAliasListFilter(): ?array 147 | { 148 | return ($this->aliasListFilter ?? null); 149 | } 150 | 151 | /** 152 | * @param $value 153 | * 154 | * @return mixed 155 | */ 156 | public static function addWhiteListFilter($value) 157 | { 158 | if (isset(self::$whiteListFilter)) { 159 | self::$whiteListFilter[] = $value; 160 | } 161 | } 162 | 163 | /** 164 | * @param array $array 165 | */ 166 | public static function setWhiteListFilter(array $array) 167 | { 168 | if (isset(self::$whiteListFilter)) { 169 | self::$whiteListFilter = $array; 170 | } 171 | } 172 | 173 | /** 174 | * @param string $method 175 | * 176 | * @return bool 177 | */ 178 | public function checkModelHasOverrideMethod(string $method): bool 179 | { 180 | $method = WhereCustom::getMethod($method); 181 | 182 | return method_exists($this, $method); 183 | } 184 | 185 | /** 186 | * @param $builder 187 | * @param $load_default_detection 188 | */ 189 | public function scopeSetLoadInjectedDetection($builder, $load_default_detection) 190 | { 191 | $this->load_injected_detections = $load_default_detection; 192 | } 193 | 194 | /** 195 | * @return bool 196 | */ 197 | public function getLoadInjectedDetections(): bool 198 | { 199 | return $this->load_injected_detections; 200 | } 201 | 202 | /** 203 | * @return null 204 | */ 205 | public function getResponseFilter($response) 206 | { 207 | return $response; 208 | } 209 | 210 | /** 211 | * @return null 212 | */ 213 | public function serializeRequestFilter($request) 214 | { 215 | return $request; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/BaseClause.php: -------------------------------------------------------------------------------- 1 | apply($query); 35 | 36 | $this->recordLog($out, $startTime); 37 | 38 | return $out; 39 | } 40 | 41 | /** 42 | * @param $query 43 | * 44 | * @return Builder 45 | */ 46 | abstract protected function apply($query); 47 | 48 | /** 49 | * @param $query 50 | * @param $startTime 51 | * @return void 52 | */ 53 | public function recordLog($query, $startTime): void 54 | { 55 | if (config('eloquentFilter.log.has_keeping_query')) { 56 | 57 | $endTime = microtime(true); 58 | $executionTime = $endTime - $startTime; 59 | 60 | if (!empty(config('eloquentFilter.log.max_time_query'))) { 61 | 62 | if ($executionTime >= config('eloquentFilter.log.max_time_query')) { 63 | 64 | $this->createLog($query, $executionTime); 65 | } 66 | 67 | } else { 68 | 69 | $this->createLog($query, $executionTime); 70 | } 71 | 72 | } 73 | 74 | } 75 | 76 | /** 77 | * @param $query 78 | * @param $executionTime 79 | * @return void 80 | */ 81 | private function createLog($query, $executionTime): void 82 | { 83 | Log::info('eloquentFilter query', [ 84 | 'query' => $query->toSql(), 85 | 'binding' => $query->getBindings(), 86 | 'time' => $executionTime, 87 | 'type' => config('eloquentFilter.log.type'), 88 | ]); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/Special.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'limit', 22 | 'orderBy', 23 | ], 24 | ]; 25 | 26 | /** 27 | * @param $query 28 | * 29 | * @return Builder 30 | * @throws \Exception 31 | * 32 | */ 33 | public function apply($query) 34 | { 35 | foreach ($this->values as $key => $param_value) { 36 | if (!in_array($key, self::$reserve_param[SpecialCondition::PARAM_NAME])) { 37 | throw new EloquentFilterException("$key is not in f_params array.", 2); 38 | } 39 | if (is_array($param_value)) { 40 | $this->values['orderBy']['field'] = explode(',', $this->values['orderBy']['field']); 41 | foreach ($this->values['orderBy']['field'] as $order_by) { 42 | $query->orderBy($order_by, $this->values['orderBy']['type']); 43 | } 44 | } else { 45 | if (config('eloquentFilter.max_limit') > 0) { 46 | $param_value = min(config('eloquentFilter.max_limit'), $param_value); 47 | } 48 | $query->limit($param_value); 49 | } 50 | } 51 | 52 | return $query; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/Where.php: -------------------------------------------------------------------------------- 1 | where($this->filter, $this->values); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereBetween.php: -------------------------------------------------------------------------------- 1 | values['start']; 21 | $end = $this->values['end']; 22 | 23 | return $query->whereBetween($this->filter, [$start, $end]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereByOpt.php: -------------------------------------------------------------------------------- 1 | values['operator']; 20 | $value = $this->values['value']; 21 | 22 | return $query->where("$this->filter", "$opt", $value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereDate.php: -------------------------------------------------------------------------------- 1 | whereDate($this->filter, $this->values); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereDayQuery.php: -------------------------------------------------------------------------------- 1 | whereDay($this->filter, $this->values['day']); 21 | } 22 | } -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereDoesntHave.php: -------------------------------------------------------------------------------- 1 | from; 22 | $tableJoin = Str::singular($this->values); 23 | $foreignKey = sprintf('"%s"."%s_id"', $from, $tableJoin); 24 | 25 | $key = $this->values . '.id'; 26 | 27 | return $query->from($from)->whereNotExists(function ($q) use ($foreignKey, $from, $key) { 28 | $q->select(DB::raw(1))->from($this->values)->where($key, '=', new Raw($foreignKey)); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereHas.php: -------------------------------------------------------------------------------- 1 | filter); 25 | $field_row = end($field_row); 26 | 27 | $relation = str_replace('.'.$field_row, '', $this->filter); 28 | $relationTable = Str::plural($relation); 29 | 30 | $value = $this->values; 31 | $from = $query->from; 32 | 33 | return $query->whereExists(function ($q) use ($relationTable, $field_row, $value, $from) { 34 | $foreignKey = sprintf('%s.%s_id', $from, Str::singular($relationTable)); 35 | 36 | $q->select(DB::raw(1)) 37 | ->from($relationTable) 38 | ->whereRaw("$relationTable.id = $foreignKey"); 39 | 40 | if (is_array($value)) { 41 | $q->whereIn($field_row, $value); 42 | } else { 43 | $q->where($field_row, $value); 44 | } 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereIn.php: -------------------------------------------------------------------------------- 1 | whereIn($this->filter, $this->values); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereLike.php: -------------------------------------------------------------------------------- 1 | where("$this->filter", 'like', $this->values['like']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereMonthQuery.php: -------------------------------------------------------------------------------- 1 | whereMonth($this->filter, $this->values['month']); 21 | } 22 | } -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereNotNull.php: -------------------------------------------------------------------------------- 1 | whereNotNull($this->filter); 22 | } 23 | } -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereNull.php: -------------------------------------------------------------------------------- 1 | whereNull($this->filter); 22 | } 23 | } -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereOr.php: -------------------------------------------------------------------------------- 1 | values); 21 | $value = reset($this->values); 22 | 23 | return $query->orWhere($field, $value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/DB/WhereYearQuery.php: -------------------------------------------------------------------------------- 1 | whereYear($this->filter, $this->values['year']); 21 | } 22 | } -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/Special.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'limit', 21 | 'orderBy', 22 | ], 23 | ]; 24 | 25 | /** 26 | * @param $query 27 | * 28 | * @return Builder 29 | * @throws \Exception 30 | * 31 | */ 32 | public function apply($query) 33 | { 34 | foreach ($this->values as $key => $param_value) { 35 | if (!in_array($key, self::$reserve_param[SpecialCondition::PARAM_NAME])) { 36 | throw new EloquentFilterException("$key is not in f_params array.", 2); 37 | } 38 | if (is_array($param_value)) { 39 | $this->values['orderBy']['field'] = explode(',', $this->values['orderBy']['field']); 40 | foreach ($this->values['orderBy']['field'] as $order_by) { 41 | $query->orderBy($order_by, $this->values['orderBy']['type']); 42 | } 43 | } else { 44 | if (config('eloquentFilter.max_limit') > 0) { 45 | $param_value = min(config('eloquentFilter.max_limit'), $param_value); 46 | } 47 | $query->limit($param_value); 48 | } 49 | } 50 | 51 | return $query; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/Where.php: -------------------------------------------------------------------------------- 1 | where($this->filter, $this->values); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereBetween.php: -------------------------------------------------------------------------------- 1 | values['start']; 21 | $end = $this->values['end']; 22 | 23 | return $query->whereBetween($this->filter, [$start, $end]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereByOpt.php: -------------------------------------------------------------------------------- 1 | values['operator']; 21 | $value = $this->values['value']; 22 | 23 | return $query->where("$this->filter", "$opt", $value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereCustom.php: -------------------------------------------------------------------------------- 1 | getMethod($this->filter); 21 | return $query->getModel()->$method($query, $this->values); 22 | } 23 | 24 | /** 25 | * @param $filter 26 | * @return string 27 | */ 28 | public static function getMethod($filter): string 29 | { 30 | $custom_method_sign = config('eloquentFilter.custom_method_sign'); 31 | 32 | $filter = ucfirst($filter); 33 | $method = $custom_method_sign . $filter; 34 | return $method; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereDate.php: -------------------------------------------------------------------------------- 1 | whereDate($this->filter, $this->values); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereDayQuery.php: -------------------------------------------------------------------------------- 1 | whereDay($this->filter, $this->values['day']); 22 | } 23 | } -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereDoesntHave.php: -------------------------------------------------------------------------------- 1 | doesntHave($this->values); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereHas.php: -------------------------------------------------------------------------------- 1 | filter); 21 | $field_row = end($field_row); 22 | 23 | $conditions = str_replace('.'.$field_row, '', $this->filter); 24 | 25 | $value = $this->values; 26 | 27 | return $query->whereHas( 28 | $conditions, 29 | function ($q) use ($value, $field_row) { 30 | $condition = 'where'; 31 | if (is_array($value)) { 32 | $condition = 'whereIn'; 33 | } 34 | $q->$condition($field_row, $value); 35 | } 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereIn.php: -------------------------------------------------------------------------------- 1 | whereIn($this->filter, $this->values); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereLike.php: -------------------------------------------------------------------------------- 1 | where("$this->filter", 'like', $this->values['like']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereMonthQuery.php: -------------------------------------------------------------------------------- 1 | whereMonth($this->filter, $this->values['month']); 22 | } 23 | } -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereNotNull.php: -------------------------------------------------------------------------------- 1 | whereNotNull($this->filter); 22 | } 23 | } -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereNull.php: -------------------------------------------------------------------------------- 1 | whereNull($this->filter); 22 | } 23 | } -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereOr.php: -------------------------------------------------------------------------------- 1 | values); 21 | $value = reset($this->values); 22 | 23 | return $query->orWhere($field, $value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QueryFilter/Queries/Eloquent/WhereYearQuery.php: -------------------------------------------------------------------------------- 1 | whereYear($this->filter, $this->values['year']); 22 | } 23 | } -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | configurePaths(); 26 | 27 | $this->mergeConfig(); 28 | 29 | $this->registerBindings(); 30 | 31 | // Register RateLimiter singleton 32 | $this->app->singleton('eloquent.filter.limiter', function ($app) { 33 | return new RateLimiter($app['cache.store']); 34 | }); 35 | } 36 | 37 | /** 38 | * Bootstrap any application services. 39 | */ 40 | public function boot(): void 41 | { 42 | if ($this->app['config']->get('eloquentFilter.rate_limit.enabled', true)) { 43 | $this->app['router']->aliasMiddleware('filter.throttle', FilterRateLimiter::class); 44 | } 45 | } 46 | 47 | /** 48 | * Configure package paths. 49 | */ 50 | private function configurePaths(): void 51 | { 52 | $this->publishes([ 53 | __DIR__ . '/config/config.php' => config_path('eloquentFilter.php'), 54 | ]); 55 | } 56 | 57 | /** 58 | * Merge configuration. 59 | */ 60 | private function mergeConfig() 61 | { 62 | $this->mergeConfigFrom( 63 | __DIR__ . '/config/config.php', 64 | 'eloquentFilter' 65 | ); 66 | } 67 | 68 | /** 69 | * 70 | */ 71 | private function registerBindings(): void 72 | { 73 | $this->registerAllDependencies(); 74 | 75 | $this->commands([MakeEloquentFilter::class]); 76 | } 77 | 78 | /** 79 | * @return void 80 | */ 81 | private function registerAllDependencies(): void 82 | { 83 | /* @var $queryFilterCoreFactory QueryFilterCoreFactory */ 84 | $queryFilterCoreFactory = app(QueryFilterCoreFactory::class); 85 | 86 | $mainQueryFilterBuilder = $this->setMainQueryFilterBuilder(); 87 | 88 | $this->attachFilterToQueryBuilder($mainQueryFilterBuilder, $queryFilterCoreFactory); 89 | 90 | $this->attachResponseFilterToQueryBuilder($mainQueryFilterBuilder, $queryFilterCoreFactory); 91 | 92 | $this->setEloquentFilter($mainQueryFilterBuilder, $queryFilterCoreFactory); 93 | } 94 | 95 | /** 96 | * @param \Closure $mainQueryFilterBuilder 97 | * @param \eloquentFilter\QueryFilter\Factory\QueryFilterCoreFactory $queryFilterCoreFactory 98 | * @return void 99 | */ 100 | private function attachFilterToQueryBuilder(\Closure $mainQueryFilterBuilder, QueryFilterCoreFactory $queryFilterCoreFactory): void 101 | { 102 | \Illuminate\Database\Query\Builder::macro('filter', function ($request = null) use ($mainQueryFilterBuilder, $queryFilterCoreFactory) { 103 | 104 | if (empty($request)) { 105 | $request = request()->query(); 106 | } 107 | 108 | app()->singleton( 109 | 'eloquentFilter', 110 | function () use ($mainQueryFilterBuilder, $queryFilterCoreFactory, $request) { 111 | return $mainQueryFilterBuilder($request, $queryFilterCoreFactory->createQueryFilterCoreDBQueryBuilder()); 112 | } 113 | ); 114 | 115 | /** @see MainQueryFilterBuilder::apply() */ 116 | return app('eloquentFilter')->apply(builder: $this, request: $request); 117 | }); 118 | } 119 | 120 | /** 121 | * @param \Closure $mainQueryFilterBuilder 122 | * @param \eloquentFilter\QueryFilter\Factory\QueryFilterCoreFactory $queryFilterCoreFactory 123 | * @return void 124 | */ 125 | private function attachResponseFilterToQueryBuilder(\Closure $mainQueryFilterBuilder, QueryFilterCoreFactory $queryFilterCoreFactory): void 126 | { 127 | \Illuminate\Database\Query\Builder::macro('getResponseFilter', function ($callback = null) use ($mainQueryFilterBuilder, $queryFilterCoreFactory) { 128 | 129 | if (!empty($callback)) { 130 | return call_user_func($callback, EloquentFilter::getResponse()); 131 | } 132 | 133 | }); 134 | } 135 | 136 | /** 137 | * @param \Closure $mainQueryFilterBuilder 138 | * @param \eloquentFilter\QueryFilter\Factory\QueryFilterCoreFactory $queryFilterCoreFactory 139 | * @return void 140 | */ 141 | private function setEloquentFilter(\Closure $mainQueryFilterBuilder, QueryFilterCoreFactory $queryFilterCoreFactory): void 142 | { 143 | $this->app->singleton( 144 | 'eloquentFilter', 145 | function () use ($mainQueryFilterBuilder, $queryFilterCoreFactory) { 146 | 147 | /* @see MainQueryFilterBuilder */ 148 | return $mainQueryFilterBuilder($this->app->get('request')->query(), $queryFilterCoreFactory->createQueryFilterCoreEloquentBuilder()); 149 | } 150 | ); 151 | } 152 | 153 | /** 154 | * @return \Closure 155 | */ 156 | private function setMainQueryFilterBuilder(): \Closure 157 | { 158 | $mainQueryFilterBuilder = function ($requestData, QueryFilterCore $queryFilterCore) { 159 | $requestFilter = app(RequestFilter::class, ['request' => $requestData]); 160 | $responseFilter = app(ResponseFilter::class); 161 | 162 | return app(MainQueryFilterBuilder::class, [ 163 | 'queryFilterCore' => $queryFilterCore, 164 | 'requestFilter' => $requestFilter, 165 | 'responseFilter' => $responseFilter, 166 | ]); 167 | }; 168 | return $mainQueryFilterBuilder; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdi-fathi/eloquent-filter/65a9af3ad7913977c89e57c2fbbf5276092e2e66/src/config/.gitkeep -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | env('EloquentFilter_ENABLED', true), 9 | 10 | /* 11 | * Enable / disable Custom Detection EloquentFilter. 12 | */ 13 | 'enabled_custom_detection' => env('EloquentFilter_Custom_Detection_ENABLED', true), 14 | 15 | /* 16 | * Set key for declare request for just eloquent filter 17 | */ 18 | 'request_filter_key' => '', // filter 19 | 20 | /* 21 | * Set index array for ignore request by default example : [ 'show_query','new_trend' ] 22 | */ 23 | 'ignore_request' => [], 24 | 25 | 'max_limit' => 20, /* It's a limitation for preventing making awful queries mistakenly by the developer or intentionally by a villain user. you can disable it just with comment it. */ 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Eloquent Filter Settings 30 | |-------------------------------------------------------------------------- 31 | | 32 | | This is the namespace custom eloquent filter 33 | | 34 | */ 35 | 36 | 'namespace' => 'App\\ModelFilters\\', 37 | 38 | 'log' => [ 39 | 'has_keeping_query' => false, 40 | 'max_time_query' => null, 41 | 'type' => 'eloquentFilter.query' 42 | ], 43 | 'filtering_keys' => [ 44 | ], 45 | 46 | /* 47 | * Set salt for encode request. 48 | */ 49 | 'request_salt' => 1234, 50 | 51 | /* 52 | * Cast sign method is prefix name method for change data before filtering. 53 | */ 54 | 'cast_method_sign' => 'filterSet', 55 | 56 | /* 57 | * custom sign method is prefix name method for custom methods in models. 58 | */ 59 | 'custom_method_sign' => 'filterCustom', 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Rate Limiting 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Configure the rate limiting for filter requests. This helps prevent 67 | | abuse and ensures optimal performance of your application. 68 | | 69 | */ 70 | 'rate_limit' => [ 71 | // Whether to enable rate limiting 72 | 'enabled' => env('EloquentFilter_RATE_LIMIT_ENABLED', false), 73 | 74 | // Maximum number of attempts within the decay minutes 75 | 'max_attempts' => env('EloquentFilter_RATE_LIMIT', 60), 76 | 77 | // Number of minutes until the rate limit resets 78 | 'decay_minutes' => env('EloquentFilter_RATE_DECAY', 1), 79 | 80 | // Whether to include rate limit headers in the response 81 | 'include_headers' => env('EloquentFilter_RATE_LIMIT_HEADERS', true), 82 | 83 | // Custom response message when rate limit is exceeded 84 | 'error_message' => env('EloquentFilter_RATE_LIMIT_MESSAGE', 'Too many filter requests. Please try again later.'), 85 | ], 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Cache Configuration 90 | |-------------------------------------------------------------------------- 91 | | 92 | | Configure caching settings for rate limiting. 93 | | 94 | */ 95 | 'cache' => [ 96 | // Cache prefix for rate limiting keys 97 | 'prefix' => 'eloquent_filter_rate_limit:', 98 | 99 | // Cache store to use for rate limiting 100 | 'store' => env('EloquentFilter_CACHE_DRIVER', null), 101 | ], 102 | ]; 103 | -------------------------------------------------------------------------------- /tests/Models/Car.php: -------------------------------------------------------------------------------- 1 | belongsTo(Category::class); 29 | } 30 | 31 | public function address() 32 | { 33 | return $this->belongsTo(Category::class, 'foo_id'); 34 | } 35 | 36 | public function activeFoo() 37 | { 38 | return $this->belongsTo(Category::class, 'foo_id')->where('active', true); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Models/Category.php: -------------------------------------------------------------------------------- 1 | hasMany(Stat::class); 35 | } 36 | 37 | /** 38 | * @param array|null $request 39 | * @return mixed 40 | */ 41 | public function serializeRequestFilter(?array $request) 42 | { 43 | if (!empty($request['new_title'])) { 44 | foreach ($request['new_title'] as &$item) { 45 | $item = trim($item, '__'); 46 | } 47 | $request['title'] = $request['new_title']; 48 | unset($request['new_title']); 49 | } 50 | 51 | return $request; 52 | } 53 | 54 | /** 55 | * This is a sample custom query. 56 | * 57 | * @param \Illuminate\Database\Eloquent\Builder $builder 58 | * @param $value 59 | * 60 | * @return \Illuminate\Database\Eloquent\Builder 61 | */ 62 | public function filterCustomSample_like(Builder $builder, $value) 63 | { 64 | return $builder->where('title', 'like', '%'.$value.'%'); 65 | } 66 | 67 | public function filterSetDesc($value) 68 | { 69 | return trim($value); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Models/CategoryPosts.php: -------------------------------------------------------------------------------- 1 | whereHas('foo', function ($q) { 22 | $q->where('bam', 'like', '%'.$this->values['like_relation_value'].'%'); 23 | }) 24 | ->where("$this->filter", '<>', $this->values['value']) 25 | ->where('email', 'like', '%'.$this->values['email'].'%') 26 | ->limit($this->values['limit']); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Models/CustomDetect/WhereRelationLikeCondition.php: -------------------------------------------------------------------------------- 1 | where('username', 'like', '%'.$value.'%'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Models/Order.php: -------------------------------------------------------------------------------- 1 | 'code', 25 | ]; 26 | 27 | public function getResponseFilter($out) 28 | { 29 | $data['data'] = $out; 30 | 31 | return $data; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /tests/Models/Tag.php: -------------------------------------------------------------------------------- 1 | belongsTo(Category::class); 28 | } 29 | 30 | public function address() 31 | { 32 | return $this->belongsTo(Category::class, 'foo_id'); 33 | } 34 | 35 | public function activeFoo() 36 | { 37 | return $this->belongsTo(Category::class, 'foo_id')->where('active', true); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Models/User.php: -------------------------------------------------------------------------------- 1 | belongsTo(Category::class); 36 | } 37 | 38 | public function address() 39 | { 40 | return $this->belongsTo(Category::class, 'foo_id'); 41 | } 42 | 43 | public function activeFoo() 44 | { 45 | return $this->belongsTo(Category::class, 'foo_id')->where('active', true); 46 | } 47 | 48 | public function EloquentFilterCustomDetection(): array 49 | { 50 | return [ 51 | WhereRelationLikeCondition::class, 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | request = m::mock(Request::class); 29 | 30 | // Initialize ParameterBag for attributes 31 | $this->requestAttributes = new ParameterBag(); 32 | 33 | // Mock User implementing Authenticatable 34 | $this->user = new class implements Authenticatable { 35 | public function getAuthIdentifier() { 36 | return 1; 37 | } 38 | 39 | public function getAuthIdentifierName() { 40 | return 'id'; 41 | } 42 | 43 | public function getAuthPassword() { 44 | return 'hashed-password'; 45 | } 46 | 47 | public function getRememberToken() { 48 | return 'remember-token'; 49 | } 50 | 51 | public function setRememberToken($value) { 52 | // Not needed for our tests 53 | } 54 | 55 | public function getRememberTokenName() { 56 | return 'remember_token'; 57 | } 58 | 59 | public function getAuthPasswordName() 60 | { 61 | return 'password'; 62 | } 63 | }; 64 | 65 | // Setup request methods 66 | $this->request->shouldReceive('user')->byDefault()->andReturn(null); 67 | $this->request->shouldReceive('path')->andReturn('/test'); 68 | $this->request->shouldReceive('ip')->andReturn('127.0.0.1'); 69 | 70 | // Setup attributes methods 71 | $this->request->shouldReceive('getAttribute') 72 | ->andReturnUsing(function ($key, $default = null) { 73 | return $this->requestAttributes->get($key, $default); 74 | }); 75 | 76 | $this->request->shouldReceive('attributes') 77 | ->andReturn($this->requestAttributes); 78 | 79 | app()->bind( 80 | 'request', 81 | function () { 82 | return $this->request; 83 | } 84 | ); 85 | } 86 | 87 | /** 88 | * @param Application $app 89 | * 90 | * @return array 91 | */ 92 | protected function getPackageProviders($app) 93 | { 94 | return [eloquentFilter\ServiceProvider::class]; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Tests/Db/DbFilterMockTest.php: -------------------------------------------------------------------------------- 1 | builder = m::mock(Builder::class); 28 | } 29 | 30 | public function testWhere() 31 | { 32 | $this->request->shouldReceive('query')->andReturn( 33 | [ 34 | 'title' => 'sport', 35 | ] 36 | ); 37 | 38 | $categories = DB::table('categories')->filter(); 39 | 40 | $builder = DB::table('categories')->where('title', 'sport'); 41 | 42 | $this->assertSame($categories->toSql(), $builder->toSql()); 43 | 44 | $this->assertEquals(['sport'], $categories->getBindings()); 45 | } 46 | 47 | public function testWhereWithEloquentFilter() 48 | { 49 | $this->request->shouldReceive('query')->andReturn( 50 | [ 51 | 'title' => 'sport', 52 | ] 53 | ); 54 | 55 | $categories = DB::table('categories')->filter(); 56 | 57 | $builder = DB::table('categories')->where('title', 'sport'); 58 | 59 | $this->assertSame($categories->toSql(), $builder->toSql()); 60 | 61 | $this->assertEquals(['sport'], $categories->getBindings()); 62 | 63 | $builder = new Category(); 64 | 65 | $builder = $builder->query()->where('title', 'sport'); 66 | 67 | $this->request->shouldReceive('query')->andReturn( 68 | [ 69 | 'title' => 'sport', 70 | ] 71 | ); 72 | 73 | $categories = Category::filter($this->request->query()); 74 | 75 | $this->assertSame($categories->toSql(), $builder->toSql()); 76 | 77 | $this->assertEquals(['sport'], $categories->getBindings()); 78 | } 79 | 80 | public function testWhereEloquentWithDB() 81 | { 82 | 83 | $builder = new Category(); 84 | 85 | $builder = $builder->query()->where('title', 'sport'); 86 | 87 | $this->request->shouldReceive('query')->andReturn( 88 | [ 89 | 'title' => 'sport', 90 | ] 91 | ); 92 | 93 | $categories = Category::filter(); 94 | 95 | $this->assertSame($categories->toSql(), $builder->toSql()); 96 | 97 | $this->assertEquals(['sport'], $categories->getBindings()); 98 | 99 | $this->request->shouldReceive('query')->andReturn( 100 | [ 101 | 'title' => 'sport', 102 | ] 103 | ); 104 | 105 | $categories = DB::table('categories')->filter(); 106 | 107 | $builder = DB::table('categories')->where('title', 'sport'); 108 | 109 | $this->assertSame($categories->toSql(), $builder->toSql()); 110 | 111 | $this->assertEquals(['sport'], $categories->getBindings()); 112 | } 113 | 114 | 115 | public function testWhereIn() 116 | { 117 | $this->request->shouldReceive('query')->andReturn( 118 | [ 119 | 'username' => ['mehdi', 'ali'], 120 | 'family' => null, 121 | ] 122 | ); 123 | 124 | $users = DB::table('users')->filter(); 125 | 126 | $builder = DB::table('users') 127 | ->wherein('username', ['mehdi', 'ali']); 128 | 129 | $this->assertSame($users->toSql(), $builder->toSql()); 130 | 131 | $this->assertEquals(['mehdi', 'ali'], $users->getBindings()); 132 | } 133 | 134 | public function testWhereInWithArray() 135 | { 136 | $this->request->shouldReceive('query')->andReturn( 137 | [ 138 | ] 139 | ); 140 | 141 | $users = DB::table('users')->filter([ 142 | 'username' => ['mehdi', 'ali'], 143 | 'family' => null, 144 | ]); 145 | 146 | $builder = DB::table('users') 147 | ->wherein('username', ['mehdi', 'ali']); 148 | 149 | $this->assertSame($users->toSql(), $builder->toSql()); 150 | 151 | $this->assertEquals(['mehdi', 'ali'], $users->getBindings()); 152 | } 153 | 154 | public function testWhereLike() 155 | { 156 | $builder = DB::table('users')->where('email', 'like', '%meh%'); 157 | $this->request->shouldReceive('query')->andReturn( 158 | [ 159 | 'email' => [ 160 | 'like' => '%meh%', 161 | ], 162 | ] 163 | ); 164 | 165 | $users = DB::table('users')->filter(); 166 | 167 | $this->assertSame($users->toSql(), $builder->toSql()); 168 | $this->assertSame(['%meh%'], $builder->getBindings()); 169 | } 170 | 171 | public function testWhereOr1() 172 | { 173 | $builder = DB::table('users') 174 | ->where('baz', 'boo') 175 | ->where('count_posts', 22) 176 | ->orWhere('baz', 'joo'); 177 | 178 | $this->request->shouldReceive('query')->andReturn( 179 | [ 180 | 'baz' => 'boo', 181 | 'count_posts' => 22, 182 | 'or' => [ 183 | 'baz' => 'joo', 184 | ], 185 | ] 186 | ); 187 | 188 | $users = DB::table('users')->filter(); 189 | 190 | $users_to_sql = str_replace('(', '', $users->toSql()); 191 | $users_to_sql = str_replace(')', '', $users_to_sql); 192 | $this->assertSame($users_to_sql, $builder->toSql()); 193 | $this->assertEquals(['boo', 22, 'joo'], $users->getBindings()); 194 | } 195 | 196 | public function testWhereByOpt() 197 | { 198 | $builder = DB::table('categories')->where('count_posts', '>', 35); 199 | 200 | $this->request->shouldReceive('query')->andReturn( 201 | [ 202 | 'count_posts' => [ 203 | 'operator' => '>', 204 | 'value' => 35, 205 | ], 206 | ] 207 | ); 208 | 209 | $categories = DB::table('categories')->filter(); 210 | 211 | $this->assertSame($categories->toSql(), $builder->toSql()); 212 | 213 | $this->assertEquals([35], $categories->getBindings()); 214 | } 215 | 216 | public function testMaxLimit() 217 | { 218 | 219 | $builder = DB::table('users')->limit(20); 220 | 221 | $this->request->shouldReceive('query')->andReturn( 222 | [ 223 | 'f_params' => [ 224 | 'limit' => 25, 225 | ], 226 | ] 227 | ); 228 | 229 | $users = DB::table('users')->filter(); 230 | 231 | $this->assertSame($users->toSql(), $builder->toSql()); 232 | } 233 | 234 | 235 | public function testFParamException() 236 | { 237 | try { 238 | $this->request->shouldReceive('query')->andReturn( 239 | [ 240 | 'f_params' => [ 241 | 'orderBys' => [ 242 | 'field' => 'id', 243 | 'type' => 'ASC', 244 | ], 245 | ], 246 | ] 247 | ); 248 | 249 | DB::table('users')->filter(); 250 | } catch (EloquentFilterException $e) { 251 | $this->assertEquals(2, $e->getCode()); 252 | } 253 | } 254 | 255 | public function testExclusiveException() 256 | { 257 | $this->expectException(EloquentFilterException::class); 258 | 259 | $this->request->shouldReceive('query')->andReturn( 260 | [ 261 | 'f_params' => [ 262 | 'orderBys11' => [ 263 | 'field' => 'id', 264 | 'type' => 'ASC', 265 | ], 266 | ], 267 | ] 268 | ); 269 | 270 | DB::table('users')->filter(); 271 | } 272 | 273 | 274 | public function testFParamOrder() 275 | { 276 | $builder = DB::table('users') 277 | ->orderBy('id') 278 | ->orderBy('count_posts'); 279 | 280 | $this->request->shouldReceive('query')->andReturn( 281 | [ 282 | 'f_params' => [ 283 | 'orderBy' => [ 284 | 'field' => 'id,count_posts', 285 | 'type' => 'ASC', 286 | ], 287 | ], 288 | ] 289 | ); 290 | 291 | $users = DB::table('users')->filter(); 292 | 293 | $this->assertSame($users->toSql(), $builder->toSql()); 294 | } 295 | 296 | 297 | // 298 | // public function testWhereByOptWithTrashed() 299 | // { 300 | // $builder = new Category(); 301 | // 302 | // $builder = $builder->newQuery()->withTrashed() 303 | // ->where('count_posts', '>', 35); 304 | // 305 | // $this->request->shouldReceive('query')->andReturn( 306 | // [ 307 | // 'count_posts' => [ 308 | // 'operator' => '>', 309 | // 'value' => 35, 310 | // ], 311 | // ] 312 | // ); 313 | // 314 | // $users = Category::withTrashed()->filter(); 315 | // 316 | // $this->assertSame($users->toSql(), $builder->toSql()); 317 | // 318 | // $this->assertEquals([35], $users->getBindings()); 319 | // } 320 | // 321 | // public function testMessRequest() 322 | // { 323 | // $builder = new User(); 324 | // 325 | // $builder = $builder->newQuery() 326 | // ->where('username', 'mehdi'); 327 | // 328 | // $this->request->shouldReceive('query')->andReturn( 329 | // [ 330 | // ] 331 | // ); 332 | // 333 | // $data = ['username' => 'mehdi']; 334 | // 335 | // $users = User::filter($data); 336 | // 337 | // $this->assertSame($users->toSql(), $builder->toSql()); 338 | // } 339 | 340 | public function testWhereByOptZero() 341 | { 342 | $builder = new Category(); 343 | 344 | $builder = DB::table('categories') 345 | ->where('count_posts', '>', 0); 346 | 347 | $this->request->shouldReceive('query')->andReturn( 348 | [ 349 | 'count_posts' => [ 350 | 'operator' => '>', 351 | 'value' => 0, 352 | ], 353 | ] 354 | ); 355 | 356 | $categories = DB::table('categories')->filter(); 357 | 358 | $this->assertSame($categories->toSql(), $builder->toSql()); 359 | 360 | $this->assertEquals([0], $categories->getBindings()); 361 | } 362 | 363 | public function testWhereBetween() 364 | { 365 | 366 | $builder = DB::table('tags')->whereBetween( 367 | 'created_at', 368 | [ 369 | '2019-01-01 17:11:46', 370 | '2019-02-06 10:11:46', 371 | ] 372 | ); 373 | 374 | $this->request->shouldReceive('query')->andReturn( 375 | [ 376 | 'created_at' => [ 377 | 'start' => '2019-01-01 17:11:46', 378 | 'end' => '2019-02-06 10:11:46', 379 | ], 380 | ] 381 | ); 382 | 383 | $users = DB::table('tags')->filter(); 384 | 385 | $this->assertSame($users->toSql(), $builder->toSql()); 386 | $this->assertEquals(['2019-01-01 17:11:46', '2019-02-06 10:11:46'], $builder->getBindings()); 387 | $this->assertEquals(['2019-01-01 17:11:46', '2019-02-06 10:11:46'], $users->getBindings()); 388 | } 389 | 390 | // 391 | public function testWhereDate() 392 | { 393 | $builder = new Tag(); 394 | 395 | $builder = DB::table('tags')->whereDate( 396 | 'created_at', 397 | '2022-07-14', 398 | ); 399 | 400 | $this->request->shouldReceive('query')->andReturn( 401 | [ 402 | 'created_at' => 403 | '2022-07-14' 404 | 405 | ] 406 | ); 407 | 408 | $users = DB::table('tags')->filter(); 409 | 410 | $this->assertSame($users->toSql(), $builder->toSql()); 411 | $this->assertEquals(['2022-07-14'], $builder->getBindings()); 412 | $this->assertEquals(['2022-07-14'], $users->getBindings()); 413 | } 414 | 415 | public function testResponseCallback() 416 | { 417 | $this->request->shouldReceive('query')->andReturn( 418 | [ 419 | 'title' => 'sport', 420 | ] 421 | ); 422 | 423 | $categories = DB::table('categories')->filter()->getResponseFilter(function ($out) { 424 | 425 | $data['data'] = $out; 426 | 427 | return $data; 428 | }); 429 | 430 | $builder = DB::table('categories')->where('title', 'sport'); 431 | 432 | $this->assertSame($categories['data']->toSql(), $builder->toSql()); 433 | 434 | $this->assertEquals(['sport'], $categories['data']->getBindings()); 435 | } 436 | 437 | 438 | public function testMacros() 439 | { 440 | 441 | $this->request->shouldReceive('query')->andReturn( 442 | [ 443 | 'title' => 'sport', 444 | ] 445 | ); 446 | 447 | $categories = DB::table('categories')->filter(); 448 | 449 | $builder = DB::table('categories')->where('title', 'sport'); 450 | 451 | $this->assertSame($categories->toSql(), $builder->toSql()); 452 | 453 | $this->assertEquals(['sport'], $categories->getBindings()); 454 | 455 | $this->assertTrue($categories->isUsedEloquentFilter()); 456 | 457 | } 458 | 459 | public function testWhereDoesntHave() 460 | { 461 | $tags_db = DB::table('tags') 462 | ->whereNotExists(function ($query) { 463 | $query->select(DB::raw(1)) 464 | ->from('categories') 465 | ->whereColumn('categories.id', 'tags.category_id'); 466 | })->where('baz', 'joo'); 467 | 468 | $this->request->shouldReceive('query')->andReturn( 469 | [ 470 | 'doesnt_have' => 'categories', 471 | 'baz' => 'joo', 472 | ] 473 | ); 474 | 475 | $tags_filters = DB::table('tags')->filter(); 476 | 477 | $this->assertSame($tags_filters->toSql(), $tags_db->toSql()); 478 | $this->assertEquals(['joo'], $tags_db->getBindings()); 479 | $this->assertEquals(['joo'], $tags_filters->getBindings()); 480 | } 481 | 482 | public function testSimpleWhereHas() 483 | { 484 | // Create the expected query 485 | $expected = DB::table('posts') 486 | ->whereExists(function ($query) { 487 | $query->select(DB::raw(1)) 488 | ->from('categories') 489 | ->whereRaw('categories.id = posts.category_id') 490 | ->where('name', 'Technology'); 491 | }); 492 | 493 | // Create the filter query 494 | $this->request->shouldReceive('query')->andReturn([ 495 | 'category.name' => 'Technology' 496 | ]); 497 | 498 | $filtered = DB::table('posts')->filter(); 499 | 500 | // Assert the queries match 501 | $this->assertSame($filtered->toSql(), $expected->toSql()); 502 | $this->assertEquals(['Technology'], $filtered->getBindings()); 503 | $this->assertEquals(['Technology'], $expected->getBindings()); 504 | } 505 | 506 | public function testWhereHasWithArray() 507 | { 508 | // Create the expected query 509 | $expected = DB::table('posts') 510 | ->whereExists(function ($query) { 511 | $query->select(DB::raw(1)) 512 | ->from('categories') 513 | ->whereRaw('categories.id = posts.category_id') 514 | ->whereIn('name', ['Technology', 'Science']); 515 | }); 516 | 517 | // Create the filter query 518 | $this->request->shouldReceive('query')->andReturn([ 519 | 'category.name' => ['Technology', 'Science'] 520 | ]); 521 | 522 | $filtered = DB::table('posts')->filter(); 523 | 524 | // Assert the queries match 525 | $this->assertSame($filtered->toSql(), $expected->toSql()); 526 | $this->assertEquals(['Technology', 'Science'], $filtered->getBindings()); 527 | $this->assertEquals(['Technology', 'Science'], $expected->getBindings()); 528 | } 529 | 530 | public function testWhereHasWithMultipleConditions() 531 | { 532 | // Create the expected query 533 | $expected = DB::table('posts') 534 | ->whereExists(function ($query) { 535 | $query->select(DB::raw(1)) 536 | ->from('categories') 537 | ->whereRaw('categories.id = posts.category_id') 538 | ->where('name', 'Technology'); 539 | }) 540 | ->where('status', 'published'); 541 | 542 | // Create the filter query 543 | $this->request->shouldReceive('query')->andReturn([ 544 | 'category.name' => 'Technology', 545 | 'status' => 'published' 546 | ]); 547 | 548 | $filtered = DB::table('posts')->filter(); 549 | 550 | // Assert the queries match 551 | $this->assertSame($filtered->toSql(), $expected->toSql()); 552 | $this->assertEquals(['Technology', 'published'], $filtered->getBindings()); 553 | $this->assertEquals(['Technology', 'published'], $expected->getBindings()); 554 | } 555 | 556 | public function testWhereHasWithNestedRelation() 557 | { 558 | // Create the expected query 559 | $expected = DB::table('posts') 560 | ->whereExists(function ($query) { 561 | $query->select(DB::raw(1)) 562 | ->from('categories') 563 | ->whereRaw('categories.id = posts.category_id') 564 | ->where('type', 'featured'); 565 | }); 566 | 567 | // Create the filter query 568 | $this->request->shouldReceive('query')->andReturn([ 569 | 'category.type' => 'featured' 570 | ]); 571 | 572 | $filtered = DB::table('posts')->filter(); 573 | 574 | // Assert the queries match 575 | $this->assertSame($filtered->toSql(), $expected->toSql()); 576 | $this->assertEquals(['featured'], $filtered->getBindings()); 577 | $this->assertEquals(['featured'], $expected->getBindings()); 578 | } 579 | 580 | public function testWhereYear() 581 | { 582 | $builder = DB::table('users'); 583 | 584 | $builder = $builder->whereYear('created_at', 2024); 585 | 586 | $this->request->shouldReceive('query')->andReturn([ 587 | 'created_at' => [ 588 | 'year' => 2024 589 | ] 590 | ]); 591 | $filtered = DB::table('users')->filter(); 592 | 593 | $this->assertEquals($builder->toSql(), $filtered->toSql()); 594 | $this->assertEquals([2024], $builder->getBindings()); 595 | } 596 | 597 | public function testWhereMonth() 598 | { 599 | $builder = DB::table('users'); 600 | $builder = $builder->whereMonth('created_at', 3); 601 | 602 | $this->request->shouldReceive('query')->andReturn([ 603 | 'created_at' => [ 604 | 'month' => 3 605 | ] 606 | ]); 607 | 608 | $filtered = DB::table('users')->filter(); 609 | 610 | $this->assertEquals($builder->toSql(), $filtered->toSql()); 611 | $this->assertEquals([03], $builder->getBindings()); 612 | } 613 | 614 | public function testWhereDay() 615 | { 616 | $builder = DB::table('users'); 617 | $builder = $builder->whereDay('created_at', 15); 618 | 619 | $this->request->shouldReceive('query')->andReturn([ 620 | 'created_at' => [ 621 | 'day' => 15 622 | ] 623 | ]); 624 | 625 | $filtered = DB::table('users')->filter(); 626 | 627 | $this->assertEquals($builder->toSql(), $filtered->toSql()); 628 | $this->assertEquals([15], $builder->getBindings()); 629 | } 630 | 631 | public function tearDown(): void 632 | { 633 | m::close(); 634 | } 635 | } 636 | -------------------------------------------------------------------------------- /tests/Tests/Eloquent/MakeEloquentFilterCommandTest.php: -------------------------------------------------------------------------------- 1 | filesystem = m::mock(Filesystem::class); 27 | $this->command = m::mock('eloquentFilter\Command\MakeEloquentFilter[argument]', [$this->filesystem]); 28 | } 29 | 30 | public function tearDown(): void 31 | { 32 | m::close(); 33 | } 34 | 35 | /** 36 | * 37 | * @param $argument 38 | * @param $class 39 | */ 40 | public function testMakeClassName($argument = 'User', $class = 'UserFilter') 41 | { 42 | $this->command->shouldReceive('argument')->andReturn($argument); 43 | $this->command->makeClassName(); 44 | $this->assertEquals("App\\ModelFilters\\$class", $this->command->getClassName()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Tests/RateLimiting/RateLimitTest.php: -------------------------------------------------------------------------------- 1 | rateLimiter = m::mock(RateLimiter::class); 25 | app()->instance('eloquent.filter.limiter', $this->rateLimiter); 26 | config(['eloquentFilter.rate_limit.enabled' => true]); 27 | } 28 | 29 | protected function mockRateLimiterNotExceeded() 30 | { 31 | $this->rateLimiter->shouldReceive('tooManyAttempts') 32 | ->once() 33 | ->andReturn(false); 34 | 35 | $this->rateLimiter->shouldReceive('hit') 36 | ->once(); 37 | 38 | $this->rateLimiter->shouldReceive('remaining') 39 | ->once() 40 | ->andReturn(59); 41 | } 42 | 43 | protected function mockRateLimiterExceeded() 44 | { 45 | $this->rateLimiter->shouldReceive('tooManyAttempts') 46 | ->once() 47 | ->andReturn(true); 48 | 49 | $this->rateLimiter->shouldReceive('availableIn') 50 | ->once() 51 | ->andReturn(60); 52 | } 53 | 54 | public function testRateLimitNotExceeded() 55 | { 56 | $this->mockRateLimiterNotExceeded(); 57 | 58 | // This should not throw an exception 59 | $this->checkRateLimit(); 60 | 61 | // Verify rate limit info was stored 62 | $rateLimitInfo = request()->attributes->all()['rate_limit']; 63 | 64 | $this->assertEquals(60, $rateLimitInfo['limit']); 65 | $this->assertEquals(59, $rateLimitInfo['remaining']); 66 | } 67 | 68 | public function testRateLimitExceeded() 69 | { 70 | $this->mockRateLimiterExceeded(); 71 | 72 | $this->expectException(ThrottleRequestsException::class); 73 | $this->checkRateLimit(); 74 | } 75 | 76 | public function testRateLimitDisabled() 77 | { 78 | config(['eloquentFilter.rate_limit.enabled' => false]); 79 | 80 | // No methods should be called on the rate limiter 81 | $this->rateLimiter->shouldNotReceive('tooManyAttempts'); 82 | $this->rateLimiter->shouldNotReceive('hit'); 83 | $this->rateLimiter->shouldNotReceive('remaining'); 84 | 85 | // This should not throw an exception 86 | $this->checkRateLimit(); 87 | } 88 | 89 | public function testRateLimitWithAuthenticatedUser() 90 | { 91 | $this->actingAs($this->user); 92 | $this->mockRateLimiterNotExceeded(); 93 | 94 | $this->checkRateLimit(); 95 | 96 | // Verify rate limit info was stored 97 | $rateLimitInfo = request()->attributes->all()['rate_limit']; 98 | 99 | 100 | $this->assertEquals(60, $rateLimitInfo['limit']); 101 | $this->assertEquals(59, $rateLimitInfo['remaining']); 102 | } 103 | 104 | public function testRateLimitWithCustomUser() 105 | { 106 | $customUser = new class implements Authenticatable { 107 | public function getAuthIdentifier() { 108 | return 999; 109 | } 110 | 111 | public function getAuthIdentifierName() { 112 | return 'id'; 113 | } 114 | 115 | public function getAuthPassword() { 116 | return 'hashed-password'; 117 | } 118 | 119 | public function getRememberToken() { 120 | return 'remember-token'; 121 | } 122 | 123 | public function setRememberToken($value) { 124 | // Not needed for our tests 125 | } 126 | 127 | public function getRememberTokenName() { 128 | return 'remember_token'; 129 | } 130 | 131 | public function getAuthPasswordName() 132 | { 133 | return 'password'; 134 | } 135 | }; 136 | 137 | $this->actingAs($customUser); 138 | $this->mockRateLimiterNotExceeded(); 139 | 140 | $this->checkRateLimit(); 141 | 142 | // Verify rate limit info was stored 143 | $rateLimitInfo = request()->attributes->all()['rate_limit']; 144 | $this->assertEquals(60, $rateLimitInfo['limit']); 145 | $this->assertEquals(59, $rateLimitInfo['remaining']); 146 | } 147 | } --------------------------------------------------------------------------------