├── .gitattributes ├── .gitignore ├── .phpstorm.meta.php ├── publish └── eloquentfilter.php ├── src ├── Commands │ ├── stubs │ │ └── modelfilter.stub │ └── FilterCommand.php ├── ConfigProvider.php ├── Filterable.php └── ModelFilter.php ├── phpunit.xml ├── .travis.yml ├── composer.json ├── README.md └── .php_cs /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor/ 3 | composer.lock 4 | *.cache 5 | *.log -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | 'App\\ModelFilters\\', 14 | 'paginate_limit' => env('PAGINATION_LIMIT_DEFAULT', 15), 15 | ]; 16 | -------------------------------------------------------------------------------- /src/Commands/stubs/modelfilter.stub: -------------------------------------------------------------------------------- 1 | [input_key1, input_key2]]. 13 | * 14 | * @var array 15 | */ 16 | public $relations = []; 17 | } 18 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | ./tests/ 14 | 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: required 4 | 5 | matrix: 6 | include: 7 | - php: 7.2 8 | env: SW_VERSION="4.4.7" 9 | - php: 7.3 10 | env: SW_VERSION="4.4.7" 11 | - php: master 12 | env: SW_VERSION="4.4.7" 13 | 14 | allow_failures: 15 | - php: master 16 | 17 | services: 18 | - mysql 19 | - redis-server 20 | - docker 21 | 22 | before_install: 23 | - export PHP_MAJOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 1)" 24 | - export PHP_MINOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 2)" 25 | - echo $PHP_MAJOR 26 | - echo $PHP_MINOR 27 | 28 | install: 29 | - cd $TRAVIS_BUILD_DIR 30 | - bash ./tests/swoole.install.sh 31 | - phpenv config-rm xdebug.ini || echo "xdebug not available" 32 | - phpenv config-add ./tests/ci.ini 33 | 34 | before_script: 35 | - cd $TRAVIS_BUILD_DIR 36 | - composer config -g process-timeout 900 && composer update 37 | 38 | script: 39 | - composer analyze 40 | - composer test -------------------------------------------------------------------------------- /src/Commands/FilterCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Create a new model filter class'); 26 | } 27 | 28 | protected function getStub(): string 29 | { 30 | return __DIR__ . '/stubs/modelfilter.stub'; 31 | } 32 | 33 | protected function getDefaultNamespace(): string 34 | { 35 | return config('eloquentfilter.namespace') ?? 'App\\ModelFilters'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 20 | ], 21 | 'commands' => [ 22 | ], 23 | 'annotations' => [ 24 | 'scan' => [ 25 | 'paths' => [ 26 | __DIR__, 27 | ], 28 | ], 29 | ], 30 | 'publish' => [ 31 | [ 32 | 'id' => 'eloquentfilter', 33 | 'description' => 'hyperf eloquent filter config file', 34 | 'source' => __DIR__ . '/../publish/eloquentfilter.php', 35 | 'destination' => BASE_PATH . '/config/autoload/eloquentfilter.php', 36 | ], 37 | ], 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jie-anthony/eloquent-filter-in-hyperf", 3 | "type": "library", 4 | "license": "MIT", 5 | "keywords": [ 6 | "php", 7 | "hyperf", 8 | "Eloquent", 9 | "Models", 10 | "Model Filter" 11 | ], 12 | "description": "An Eloquent way to filter Eloquent Models", 13 | "autoload": { 14 | "psr-4": { 15 | "JieAnthony\\EloquentFilter\\": "src/" 16 | } 17 | }, 18 | "autoload-dev": { 19 | "psr-4": { 20 | "HyperfTest\\": "tests" 21 | } 22 | }, 23 | "require": { 24 | "php": ">=7.2", 25 | "ext-swoole": ">=4.5", 26 | "hyperf/command": "~2.0|~2.1", 27 | "hyperf/devtool": "~2.0|~2.1", 28 | "hyperf/framework": "~2.0|~2.1", 29 | "hyperf/database": "~2.0|~2.1", 30 | "hyperf/utils": "~2.0|~2.1" 31 | }, 32 | "require-dev": { 33 | "friendsofphp/php-cs-fixer": "^2.14", 34 | "phpstan/phpstan": "^0.12.36", 35 | "hyperf/testing": "~2.0|~2.1", 36 | "swoft/swoole-ide-helper": "dev-master" 37 | }, 38 | "config": { 39 | "sort-packages": true 40 | }, 41 | "scripts": { 42 | "test": "co-phpunit -c phpunit.xml --colors=always", 43 | "analyze": "phpstan analyse --memory-limit 300M -l 0 ./src", 44 | "cs-fix": "php-cs-fixer fix $1" 45 | }, 46 | "extra": { 47 | "hyperf": { 48 | "config": "JieAnthony\\EloquentFilter\\ConfigProvider" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jie-anthony/eloquent-filter-in-hyperf 2 | 3 | 基于tucker-eric/eloquentfilter 调整为适配Hyperf的模型查询过滤器 4 | 5 | ### 安装 6 | ``` 7 | composer require jie-anthony/eloquent-filter-in-hyperf -vvv 8 | ``` 9 | ### 发布配置 10 | ``` 11 | php bin/hyperf.php vendor:publish jie-anthony/eloquent-filter-in-hyperf 12 | ``` 13 | 14 | ## 命令行创建文件 15 | 16 | ``` 17 | php bin/hyperf.php gen:eloquent-filter UserFilter 18 | ``` 19 | 20 | 21 | ## Filter文件 22 | ``` 23 | where('name', 'LIKE', "$name%"); 35 | } 36 | 37 | public function age($age) 38 | { 39 | return $this->where('age', $age); 40 | } 41 | } 42 | ``` 43 | 44 | ## 模型 45 | ``` 46 | provideFilter(\App\ModelFilters\UserFilter::class); 68 | } 69 | } 70 | ``` 71 | 72 | ## 查询 73 | ``` 74 | request->all(); 100 | $data = User::filter($params)->get(); 101 | return [ 102 | 'params' => $params, 103 | 'data' => $data 104 | ]; 105 | } 106 | } 107 | 108 | ``` 109 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | '@Symfony' => true, 17 | '@DoctrineAnnotation' => true, 18 | '@PhpCsFixer' => true, 19 | 'header_comment' => [ 20 | 'commentType' => 'PHPDoc', 21 | 'header' => $header, 22 | 'separate' => 'none', 23 | 'location' => 'after_declare_strict', 24 | ], 25 | 'array_syntax' => [ 26 | 'syntax' => 'short' 27 | ], 28 | 'list_syntax' => [ 29 | 'syntax' => 'short' 30 | ], 31 | 'concat_space' => [ 32 | 'spacing' => 'one' 33 | ], 34 | 'blank_line_before_statement' => [ 35 | 'statements' => [ 36 | 'declare', 37 | ], 38 | ], 39 | 'general_phpdoc_annotation_remove' => [ 40 | 'annotations' => [ 41 | 'author' 42 | ], 43 | ], 44 | 'ordered_imports' => [ 45 | 'imports_order' => [ 46 | 'class', 'function', 'const', 47 | ], 48 | 'sort_algorithm' => 'alpha', 49 | ], 50 | 'single_line_comment_style' => [ 51 | 'comment_types' => [ 52 | ], 53 | ], 54 | 'yoda_style' => [ 55 | 'always_move_variable' => false, 56 | 'equal' => false, 57 | 'identical' => false, 58 | ], 59 | 'phpdoc_align' => [ 60 | 'align' => 'left', 61 | ], 62 | 'multiline_whitespace_before_semicolons' => [ 63 | 'strategy' => 'no_multi_line', 64 | ], 65 | 'class_attributes_separation' => true, 66 | 'combine_consecutive_unsets' => true, 67 | 'declare_strict_types' => true, 68 | 'linebreak_after_opening_tag' => true, 69 | 'lowercase_constants' => true, 70 | 'lowercase_static_reference' => true, 71 | 'no_useless_else' => true, 72 | 'no_unused_imports' => true, 73 | 'not_operator_with_successor_space' => true, 74 | 'not_operator_with_space' => false, 75 | 'ordered_class_elements' => true, 76 | 'php_unit_strict' => false, 77 | 'phpdoc_separation' => false, 78 | 'single_quote' => true, 79 | 'standardize_not_equals' => true, 80 | 'multiline_comment_opening_closing' => true, 81 | ]) 82 | ->setFinder( 83 | PhpCsFixer\Finder::create() 84 | ->exclude('vendor') 85 | ->in(__DIR__) 86 | ) 87 | ->setUsingCache(false); 88 | -------------------------------------------------------------------------------- /src/Filterable.php: -------------------------------------------------------------------------------- 1 | getModelFilterClass(); 35 | } 36 | 37 | // Create the model filter instance 38 | $modelFilter = new $filter($query, $input); 39 | 40 | // Set the input that was used in the filter (this will exclude empty strings) 41 | $this->filtered = $modelFilter->input(); 42 | 43 | // Return the filter query 44 | return $modelFilter->handle(); 45 | } 46 | 47 | /** 48 | * Paginate the given query with url query params appended. 49 | * 50 | * @param int $perPage 51 | * @param array $columns 52 | * @param string $pageName 53 | * @param null|int $page 54 | * @param mixed $query 55 | * @throws \InvalidArgumentException 56 | * @return \Hyperf\Contract\LengthAwarePaginatorInterface 57 | */ 58 | public function scopePaginateFilter($query, $perPage = null, $columns = ['*'], $pageName = 'page', $page = null) 59 | { 60 | $perPage = $perPage ?: config('eloquentfilter.paginate_limit'); 61 | $paginator = $query->paginate($perPage, $columns, $pageName, $page); 62 | $paginator->appends($this->filtered); 63 | 64 | return $paginator; 65 | } 66 | 67 | /** 68 | * Paginate the given query with url query params appended. 69 | * 70 | * @param int $perPage 71 | * @param array $columns 72 | * @param string $pageName 73 | * @param null|int $page 74 | * @param mixed $query 75 | * @throws \InvalidArgumentException 76 | * @return \Hyperf\Contract\LengthAwarePaginatorInterface 77 | */ 78 | public function scopeSimplePaginateFilter($query, $perPage = null, $columns = ['*'], $pageName = 'page', $page = null) 79 | { 80 | $perPage = $perPage ?: config('eloquentfilter.paginate_limit'); 81 | $paginator = $query->simplePaginate($perPage, $columns, $pageName, $page); 82 | $paginator->appends($this->filtered); 83 | 84 | return $paginator; 85 | } 86 | 87 | /** 88 | * Returns ModelFilter class to be instantiated. 89 | * 90 | * @param null|string $filter 91 | * @return string 92 | */ 93 | public function provideFilter($filter = null) 94 | { 95 | if ($filter === null) { 96 | $filter = config('eloquentfilter.namespace', 'App\\ModelFilters\\') . class_basename($this) . 'Filter'; 97 | } 98 | 99 | return $filter; 100 | } 101 | 102 | /** 103 | * Returns the ModelFilter for the current model. 104 | * 105 | * @return string 106 | */ 107 | public function getModelFilterClass() 108 | { 109 | return method_exists($this, 'modelFilter') ? $this->modelFilter() : $this->provideFilter(); 110 | } 111 | 112 | /** 113 | * WHERE $column LIKE %$value% query. 114 | * 115 | * @param $query 116 | * @param $column 117 | * @param $value 118 | * @param string $boolean 119 | * @return mixed 120 | */ 121 | public function scopeWhereLike($query, $column, $value, $boolean = 'and') 122 | { 123 | return $query->where($column, 'LIKE', "%{$value}%", $boolean); 124 | } 125 | 126 | /** 127 | * WHERE $column LIKE $value% query. 128 | * 129 | * @param $query 130 | * @param $column 131 | * @param $value 132 | * @param string $boolean 133 | * @return mixed 134 | */ 135 | public function scopeWhereBeginsWith($query, $column, $value, $boolean = 'and') 136 | { 137 | return $query->where($column, 'LIKE', "{$value}%", $boolean); 138 | } 139 | 140 | /** 141 | * WHERE $column LIKE %$value query. 142 | * 143 | * @param $query 144 | * @param $column 145 | * @param $value 146 | * @param string $boolean 147 | * @return mixed 148 | */ 149 | public function scopeWhereEndsWith($query, $column, $value, $boolean = 'and') 150 | { 151 | return $query->where($column, 'LIKE', "%{$value}", $boolean); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/ModelFilter.php: -------------------------------------------------------------------------------- 1 | [input_key1, input_key2]]. 27 | * 28 | * @var array 29 | */ 30 | public $relations = []; 31 | 32 | /** 33 | * Container to hold all relation queries defined as closures as ['relation' => [\Closure, \Closure]]. 34 | * (This allows us to not be required to define a filter for the related models). 35 | * 36 | * @var array 37 | */ 38 | protected $localRelatedFilters = []; 39 | 40 | /** 41 | * Container for all relations (local and related ModelFilters). 42 | * @var array 43 | */ 44 | protected $allRelations = []; 45 | 46 | /** 47 | * Array of method names that should not be called. 48 | * @var array 49 | */ 50 | protected $blacklist = []; 51 | 52 | /** 53 | * Array of input to filter. 54 | * 55 | * @var array 56 | */ 57 | protected $input; 58 | 59 | /** 60 | * @var QueryBuilder 61 | */ 62 | protected $query; 63 | 64 | /** 65 | * Drop `_id` from the end of input keys when referencing methods. 66 | * 67 | * @var bool 68 | */ 69 | protected $drop_id = true; 70 | 71 | /** 72 | * Convert input keys to camelCase 73 | * Ex: my_awesome_key will be converted to myAwesomeKey($value). 74 | * 75 | * @var bool 76 | */ 77 | protected $camel_cased_methods = true; 78 | 79 | /** 80 | * This is to be able to bypass relations if we are filtering a joined table. 81 | * 82 | * @var bool 83 | */ 84 | protected $relationsEnabled; 85 | 86 | /** 87 | * Tables already joined in the query to filter by the joined column instead of using 88 | * ->whereHas to save a little bit of resources. 89 | * 90 | * @var null 91 | */ 92 | private $_joinedTables; 93 | 94 | /** 95 | * ModelFilter constructor. 96 | * 97 | * @param $query 98 | * @param array $input 99 | * @param bool $relationsEnabled 100 | */ 101 | public function __construct($query, array $input = [], $relationsEnabled = true) 102 | { 103 | $this->query = $query; 104 | $this->input = $this->removeEmptyInput($input); 105 | $this->relationsEnabled = $relationsEnabled; 106 | $this->registerMacros(); 107 | } 108 | 109 | /** 110 | * @param $method 111 | * @param $args 112 | * @return mixed 113 | */ 114 | public function __call($method, $args) 115 | { 116 | $resp = call_user_func_array([$this->query, $method], $args); 117 | 118 | // Only return $this if query builder is returned 119 | // We don't want to make actions to the builder unreachable 120 | return $resp instanceof QueryBuilder ? $this : $resp; 121 | } 122 | 123 | /** 124 | * Remove empty strings from the input array. 125 | * 126 | * @param array $input 127 | * @return array 128 | */ 129 | public function removeEmptyInput($input) 130 | { 131 | $filterableInput = []; 132 | 133 | foreach ($input as $key => $val) { 134 | if ($val !== '' && $val !== null) { 135 | $filterableInput[$key] = $val; 136 | } 137 | } 138 | 139 | return $filterableInput; 140 | } 141 | 142 | /** 143 | * Handle all filters. 144 | * 145 | * @return QueryBuilder 146 | */ 147 | public function handle() 148 | { 149 | // Filter global methods 150 | if (method_exists($this, 'setup')) { 151 | $this->setup(); 152 | } 153 | 154 | // Run input filters 155 | $this->filterInput(); 156 | // Set up all the whereHas and joins constraints 157 | $this->filterRelations(); 158 | 159 | return $this->query; 160 | } 161 | 162 | /** 163 | * Locally defines a relation filter method that will be called in the context of the related model. 164 | * 165 | * @param $relation 166 | * @param \Closure $closure 167 | * @return $this 168 | */ 169 | public function addRelated($relation, \Closure $closure) 170 | { 171 | $this->localRelatedFilters[$relation][] = $closure; 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Add a where constraint to a relationship. 178 | * 179 | * @param $relation 180 | * @param $column 181 | * @param null $operator 182 | * @param null $value 183 | * @param string $boolean 184 | * @return $this 185 | */ 186 | public function related($relation, $column, $operator = null, $value = null, $boolean = 'and') 187 | { 188 | if ($column instanceof \Closure) { 189 | return $this->addRelated($relation, $column); 190 | } 191 | 192 | // If there is no value it is a where = ? query and we set the appropriate params 193 | if ($value === null) { 194 | $value = $operator; 195 | $operator = '='; 196 | } 197 | 198 | return $this->addRelated($relation, function ($query) use ($column, $operator, $value, $boolean) { 199 | return $query->where($column, $operator, $value, $boolean); 200 | }); 201 | } 202 | 203 | /** 204 | * @param $key 205 | * @return string 206 | */ 207 | public function getFilterMethod($key) 208 | { 209 | // Remove '.' chars in methodName 210 | $methodName = str_replace('.', '', $this->drop_id ? preg_replace('/^(.*)_id$/', '$1', $key) : $key); 211 | 212 | // Convert key to camelCase? 213 | return $this->camel_cased_methods ? Str::camel($methodName) : $methodName; 214 | } 215 | 216 | /** 217 | * Filter with input array. 218 | */ 219 | public function filterInput() 220 | { 221 | foreach ($this->input as $key => $val) { 222 | // Call all local methods on filter 223 | $method = $this->getFilterMethod($key); 224 | 225 | if ($this->methodIsCallable($method)) { 226 | $this->{$method}($val); 227 | } 228 | } 229 | } 230 | 231 | /** 232 | * Filter relationships defined in $this->relations array. 233 | * 234 | * @return $this 235 | */ 236 | public function filterRelations() 237 | { 238 | // Verify we can filter by relations and there are relations to filter by 239 | if ($this->relationsEnabled()) { 240 | foreach ($this->getAllRelations() as $related => $filterable) { 241 | // Make sure we have filterable input 242 | if (count($filterable) > 0) { 243 | if ($this->relationIsJoined($related)) { 244 | $this->filterJoinedRelation($related); 245 | } else { 246 | $this->filterUnjoinedRelation($related); 247 | } 248 | } 249 | } 250 | } 251 | 252 | return $this; 253 | } 254 | 255 | /** 256 | * Returns all local relations and relations requiring other Model's Filter's. 257 | * @return array 258 | */ 259 | public function getAllRelations() 260 | { 261 | if (count($this->allRelations) === 0) { 262 | $allRelations = array_merge(array_keys($this->relations), array_keys($this->localRelatedFilters)); 263 | 264 | foreach ($allRelations as $related) { 265 | $this->allRelations[$related] = array_merge($this->getLocalRelation($related), $this->getRelatedFilterInput($related)); 266 | } 267 | } 268 | 269 | return $this->allRelations; 270 | } 271 | 272 | /** 273 | * Get all input to pass through related filters and local closures as an array. 274 | * 275 | * @param string $relation 276 | * @return array 277 | */ 278 | public function getRelationConstraints($relation) 279 | { 280 | return array_key_exists($relation, $this->allRelations) ? $this->allRelations[$relation] : []; 281 | } 282 | 283 | /** 284 | * Call setup method for relation before filtering on it. 285 | * 286 | * @param $related 287 | * @param $query 288 | */ 289 | public function callRelatedLocalSetup($related, $query) 290 | { 291 | if (method_exists($this, $method = Str::camel($related).'Setup')) { 292 | $this->{$method}($query); 293 | } 294 | } 295 | 296 | /** 297 | * Run the filter on models that already have their tables joined. 298 | * 299 | * @param $related 300 | */ 301 | public function filterJoinedRelation($related) 302 | { 303 | // Apply any relation based scope to avoid method duplication 304 | $this->callRelatedLocalSetup($related, $this->query); 305 | 306 | foreach ($this->getLocalRelation($related) as $closure) { 307 | // If a relation is defined locally in a method AND is joined 308 | // Then we call those defined relation closures on this query 309 | $closure($this->query); 310 | } 311 | 312 | // Check if we have input we need to pass through a related Model's filter 313 | // Then filter by that related model's filter 314 | if (count($relatedFilterInput = $this->getRelatedFilterInput($related)) > 0) { 315 | $filterClass = $this->getRelatedFilter($related); 316 | 317 | // Disable querying joined relations on filters of joined tables. 318 | (new $filterClass($this->query, $relatedFilterInput, false))->handle(); 319 | } 320 | } 321 | 322 | /** 323 | * Gets all the joined tables. 324 | * 325 | * @return array 326 | */ 327 | public function getJoinedTables() 328 | { 329 | $joins = []; 330 | 331 | if (is_array($queryJoins = $this->query->getQuery()->joins)) { 332 | $joins = array_map(function ($join) { 333 | return $join->table; 334 | }, $queryJoins); 335 | } 336 | 337 | return $joins; 338 | } 339 | 340 | /** 341 | * Checks if the relation to filter's table is already joined. 342 | * 343 | * @param $relation 344 | * @return bool 345 | */ 346 | public function relationIsJoined($relation) 347 | { 348 | if ($this->_joinedTables === null) { 349 | $this->_joinedTables = $this->getJoinedTables(); 350 | } 351 | 352 | return in_array($this->getRelatedTable($relation), $this->_joinedTables, true); 353 | } 354 | 355 | /** 356 | * Get an empty instance of a related model. 357 | * 358 | * @param $relation 359 | * @return \Hyperf\Database\Model\Model 360 | */ 361 | public function getRelatedModel($relation) 362 | { 363 | if (strpos($relation, '.') !== false) { 364 | return $this->getNestedRelatedModel($relation); 365 | } 366 | 367 | return $this->query->getModel()->{$relation}()->getRelated(); 368 | } 369 | 370 | /** 371 | * @param $relationString 372 | * @return QueryBuilder|\Hyperf\Database\Model\Model 373 | */ 374 | protected function getNestedRelatedModel($relationString) 375 | { 376 | $parts = explode('.', $relationString); 377 | $related = $this->query->getModel(); 378 | 379 | do { 380 | $relation = array_shift($parts); 381 | $related = $related->{$relation}()->getRelated(); 382 | } while (! empty($parts)); 383 | 384 | return $related; 385 | } 386 | 387 | /** 388 | * Get the table name from a relationship. 389 | * 390 | * @param $relation 391 | * @return string 392 | */ 393 | public function getRelatedTable($relation) 394 | { 395 | return $this->getRelatedModel($relation)->getTable(); 396 | } 397 | 398 | /** 399 | * Get the model filter of a related model. 400 | * 401 | * @param $relation 402 | * @return mixed 403 | */ 404 | public function getRelatedFilter($relation) 405 | { 406 | return $this->getRelatedModel($relation)->getModelFilterClass(); 407 | } 408 | 409 | /** 410 | * Filters by a relationship that isn't joined by using that relation's ModelFilter. 411 | * 412 | * @param $related 413 | */ 414 | public function filterUnjoinedRelation($related) 415 | { 416 | $this->query->whereHas($related, function ($q) use ($related) { 417 | $this->callRelatedLocalSetup($related, $q); 418 | 419 | // If we defined it locally then we're running the closure on the related model here right. 420 | foreach ($this->getLocalRelation($related) as $closure) { 421 | // Run in context of the related model locally 422 | $closure($q); 423 | } 424 | 425 | if (count($filterableRelated = $this->getRelatedFilterInput($related)) > 0) { 426 | $q->filter($filterableRelated); 427 | } 428 | 429 | return $q; 430 | }); 431 | } 432 | 433 | /** 434 | * Get input to pass to a related Model's Filter. 435 | * 436 | * @param $related 437 | * @return array 438 | */ 439 | public function getRelatedFilterInput($related) 440 | { 441 | $output = []; 442 | 443 | if (array_key_exists($related, $this->relations)) { 444 | foreach ((array) $this->relations[$related] as $alias => $name) { 445 | // If the alias is a string that is what we grab from the input 446 | // Then use the name for the output so we can alias relations 447 | if ($value = Arr::get($this->input, is_string($alias) ? $alias : $name)) { 448 | $output[$name] = $value; 449 | } 450 | } 451 | } 452 | 453 | return $output; 454 | } 455 | 456 | /** 457 | * Check to see if there is input or locally defined methods for the given relation. 458 | * 459 | * @param $relation 460 | * @return bool 461 | */ 462 | public function relationIsFilterable($relation) 463 | { 464 | return $this->relationUsesFilter($relation) || $this->relationIsLocal($relation); 465 | } 466 | 467 | /** 468 | * Checks if there is input that should be passed to a related Model Filter. 469 | * 470 | * @param $related 471 | * @return bool 472 | */ 473 | public function relationUsesFilter($related) 474 | { 475 | return count($this->getRelatedFilterInput($related)) > 0; 476 | } 477 | 478 | /** 479 | * Checks to see if there are locally defined relations to filter. 480 | * 481 | * @param $related 482 | * @return bool 483 | */ 484 | public function relationIsLocal($related) 485 | { 486 | return count($this->getLocalRelation($related)) > 0; 487 | } 488 | 489 | /** 490 | * @param string $related 491 | * @return array 492 | */ 493 | public function getLocalRelation($related) 494 | { 495 | return array_key_exists($related, $this->localRelatedFilters) ? $this->localRelatedFilters[$related] : []; 496 | } 497 | 498 | /** 499 | * Retrieve input by key or all input as array. 500 | * 501 | * @param null $key 502 | * @param null $default 503 | * @return array|mixed|null 504 | */ 505 | public function input($key = null, $default = null) 506 | { 507 | if ($key === null) { 508 | return $this->input; 509 | } 510 | 511 | return array_key_exists($key, $this->input) ? $this->input[$key] : $default; 512 | } 513 | 514 | /** 515 | * Disable querying relations (Mainly for joined tables as the related model isn't queried). 516 | * 517 | * @return $this 518 | */ 519 | public function disableRelations() 520 | { 521 | $this->relationsEnabled = false; 522 | 523 | return $this; 524 | } 525 | 526 | /** 527 | * Enable querying relations. 528 | * 529 | * @return $this 530 | */ 531 | public function enableRelations() 532 | { 533 | $this->relationsEnabled = true; 534 | 535 | return $this; 536 | } 537 | 538 | /** 539 | * Checks if filtering by relations is enabled. 540 | * 541 | * @return bool 542 | */ 543 | public function relationsEnabled() 544 | { 545 | return $this->relationsEnabled; 546 | } 547 | 548 | /** 549 | * Add values to filter by if called in setup(). 550 | * Will ONLY filter relations if called on additional method. 551 | * 552 | * @param $key 553 | * @param null $value 554 | */ 555 | public function push($key, $value = null) 556 | { 557 | if (is_array($key)) { 558 | $this->input = array_merge($this->input, $key); 559 | } else { 560 | $this->input[$key] = $value; 561 | } 562 | } 563 | 564 | /** 565 | * Set to drop `_id` from input. Mainly for testing. 566 | * 567 | * @param null $bool 568 | * 569 | * @return bool 570 | */ 571 | public function dropIdSuffix($bool = null) 572 | { 573 | if ($bool === null) { 574 | return $this->drop_id; 575 | } 576 | 577 | return $this->drop_id = $bool; 578 | } 579 | 580 | /** 581 | * Convert input to camel_case. Mainly for testing. 582 | * 583 | * @param null $bool 584 | * 585 | * @return bool 586 | */ 587 | public function convertToCamelCasedMethods($bool = null) 588 | { 589 | if ($bool === null) { 590 | return $this->camel_cased_methods; 591 | } 592 | 593 | return $this->camel_cased_methods = $bool; 594 | } 595 | 596 | /** 597 | * Add method to the blacklist so disable calling it. 598 | * @param string $method 599 | * @return $this 600 | */ 601 | public function blacklistMethod($method) 602 | { 603 | $this->blacklist[] = $method; 604 | 605 | return $this; 606 | } 607 | 608 | /** 609 | * Remove a method from the blacklist. 610 | * @param string $method 611 | * @return $this 612 | */ 613 | public function whitelistMethod($method) 614 | { 615 | $this->blacklist = array_filter($this->blacklist, function ($name) use ($method) { 616 | return $name !== $method; 617 | }); 618 | 619 | return $this; 620 | } 621 | 622 | /** 623 | * @param $method 624 | * @return bool 625 | */ 626 | public function methodIsBlacklisted($method) 627 | { 628 | return in_array($method, $this->blacklist, true); 629 | } 630 | 631 | /** 632 | * Check if the method is not blacklisted and callable on the extended class. 633 | * @param $method 634 | * @return bool 635 | */ 636 | public function methodIsCallable($method) 637 | { 638 | return ! $this->methodIsBlacklisted($method) && 639 | method_exists($this, $method) && 640 | ! method_exists(ModelFilter::class, $method); 641 | } 642 | 643 | /** 644 | * Register paginate and simplePaginate macros on relations 645 | * BelongsToMany overrides the QueryBuilder's paginate to append the pivot. 646 | */ 647 | private function registerMacros() 648 | { 649 | if ( 650 | method_exists(Relation::class, 'hasMacro') && 651 | method_exists(Relation::class, 'macro') && 652 | ! Relation::hasMacro('paginateFilter') && 653 | ! Relation::hasMacro('simplePaginateFilter') 654 | ) { 655 | Relation::macro('paginateFilter', function () { 656 | $paginator = call_user_func_array([$this, 'paginate'], func_get_args()); 657 | $paginator->appends($this->getRelated()->filtered); 658 | 659 | return $paginator; 660 | }); 661 | Relation::macro('simplePaginateFilter', function () { 662 | $paginator = call_user_func_array([$this, 'simplePaginate'], func_get_args()); 663 | $paginator->appends($this->getRelated()->filtered); 664 | 665 | return $paginator; 666 | }); 667 | } 668 | } 669 | } 670 | --------------------------------------------------------------------------------