├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json └── src ├── Heroicpixels └── Filterable │ ├── Filterable.php │ ├── FilterableServiceProvider.php │ ├── FilterableTrait.php │ └── FilterableWrapper.php └── config ├── .gitkeep └── config.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | before_script: 9 | - curl -s http://getcomposer.org/installer | php 10 | - php composer.phar install --dev 11 | 12 | script: phpunit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Dave Hodgins, Heroicpixels 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Filterable 2 | ======= 3 | This package gives you a convenient way to automatically filter Eloquent results based on query string parameters in the URL. Filterable parses the query string, compares it with columns that you'd like to automatically filter, then creates a dynamic scope that is used by Eloquent to construct the SQL. 4 | 5 | * [Installation](#installation) 6 | * [Copyright & License](#license) 7 | * [Usage](#usage) 8 | * [Single Value](#single-value) 9 | * [Multiple Values](#multiple-values) 10 | * [Multiple Parameters](#multiple-parameters) 11 | * [Boolean Operators](#boolean-operators) 12 | * [Comparison Operators](#comparison-operators) 13 | 14 | 15 | Installation 16 | ============ 17 | Add the package to 'require' in your composer.json file: 18 | 19 | "require": { 20 | "heroicpixels/filterable": "dev-master" 21 | }, 22 | 23 | Run 'composer dump-autoload' from the command line: 24 | 25 | #composer dump-autoload 26 | 27 | Register the service provider in 'app/config/app.php'. Service provider: 28 | 29 | 'providers' => array( 30 | \\... 31 | 'Heroicpixels\Filterable\FilterableServiceProvider', 32 | \\... 33 | ); 34 | 35 | License 36 | ======= 37 | Copyright 2014 Dave Hodgins 38 | Released under MIT license (http://opensource.org/licenses/MIT). See LICENSE file for details. 39 | 40 | Usage 41 | ===== 42 | 43 | NOTE: this package also includes a version (**FilterableWrapper.php**) that can be used to wrap a DB or Eloquent object, and a version (**FilterableTrait.php**) that can be used as a trait with an Eloquent model. 44 | 45 | **Filterable.php** 46 | 47 | Edit your Eloquent model to extend 'Heroicpixels\Filterable\Filterable'. 48 | 49 | class Object extends Heroicpixels\Filterable\Filterable { 50 | // ... 51 | } 52 | 53 | **FilterableWrapper.php** 54 | 55 | Give FilterableWrapper a DB or Eloquent object. 56 | 57 | $object = DB::table('objects'); 58 | $objects = FilterableWrapper($object); 59 | 60 | **FilterableTrait.php** 61 | 62 | class Object extends Eloquent { 63 | 64 | use Heroicpixels\Filterable\FilterableTrait; 65 | 66 | } 67 | 68 | The examples below use the Filterable class! 69 | 70 | In the above example, class Object corresponds to table 'objects': 71 | 72 | | id | color | shape | total | 73 | |:-----|:--------|:-----------|:--------| 74 | | 1 | red | square | 150 | 75 | | 2 | blue | square | 2000 | 76 | | 3 | green | circle | 575 | 77 | | 4 | yellow | triangle | 15 | 78 | | 5 | red | triangle | 900 | 79 | | 6 | red | triangle | 600 | 80 | 81 | Filterable Columns 82 | ------------------ 83 | Specify the column you want to automatically filter. 84 | 85 | $columns = [ 'color', 'shape', 'total' ]; 86 | 87 | For example: 88 | 89 | http://www.your-site/?color=blue&shape=round&total=500 90 | 91 | You can also alias the columns if you prefer not to reveal them: 92 | 93 | $columns = [ 'col' => 'color', 'sha' => 'shape', 'tot' => 'total' ]; 94 | 95 | For example: 96 | 97 | http://www.your-site/?col=blue&sha=round&tot=500 98 | 99 | To filter results, simply pass the columns to Eloquent using filterColumns(): 100 | 101 | $objects = Object::filterColumns($columns)->get()->toArray(); 102 | 103 | You can also filter joins: 104 | 105 | $columns = array('color' => 'objects.color', 106 | 'name' => 'objects.name', 107 | 'shape' => 'objects.shape', 108 | 'category' => 'cat_object.cat_id'); 109 | $objects = Object::join('cat_object', 'objects.id', '=', 'cat_object.object_id') 110 | ->filterColumns($columns) 111 | ->get()->toArray(); 112 | 113 | And you can filter eager loads: 114 | 115 | /** 116 | * Columns available in main query 117 | */ 118 | $columns = array('color' => 'objects.color', 119 | 'name' => 'objects.name', 120 | 'shape' => 'objects.shape'); 121 | $objects = Object::with(array('categories' => function($q) { 122 | /** 123 | * Columns available to sub-query 124 | */ 125 | $columns = array('category' => 'cat_object.cat_id'); 126 | $q->filterColumns($columns); 127 | }))->filterColumns($columns) 128 | ->get() 129 | ->toArray(); 130 | 131 | The following examples demonstrate how query string parameters can be used. 132 | 133 | Single Value 134 | ------------ 135 | 136 | ?color=red 137 | 138 | SELECT ... WHERE ... color = 'red' 139 | 140 | | id | color | shape | total | 141 | |:-----|:--------|:-----------|:--------| 142 | | 1 | red | square | 150 | 143 | | 5 | red | triangle | 900 | 144 | | 6 | red | triangle | 600 | 145 | 146 | Multiple Values 147 | --------------- 148 | 149 | ?color[]=red&color[]=blue 150 | 151 | SELECT ... WHERE ... color = 'red' OR color = 'blue' 152 | 153 | | id | color | shape | total | 154 | |:-----|:--------|:-----------|:--------| 155 | | 1 | red | square | 150 | 156 | | 2 | blue | square | 2000 | 157 | | 5 | red | triangle | 900 | 158 | | 6 | red | triangle | 600 | 159 | 160 | Multiple Parameters 161 | ------------------- 162 | 163 | ?color[]=red&shape[]=triangle 164 | 165 | SELECT ... WHERE ... color = 'red' AND shape = 'triangle' 166 | 167 | | id | color | shape | total | 168 | |:-----|:--------|:-----------|:--------| 169 | | 5 | red | triangle | 900 | 170 | | 6 | red | triangle | 600 | 171 | 172 | Boolean Operators 173 | ----------------- 174 | 175 | ?color[]=red&shape[]=triangle&bool[shape]=or 176 | 177 | SELECT ... WHERE ... color = 'red' OR shape = 'triangle' 178 | 179 | | id | color | shape | total | 180 | |:-----|:--------|:-----------|:--------| 181 | | 4 | yellow | triangle | 15 | 182 | | 5 | red | triangle | 900 | 183 | | 6 | red | triangle | 600 | 184 | 185 | Comparison Operators 186 | -------------------- 187 | **Greater Than** 188 | 189 | ?total=599&operator[total]=> 190 | 191 | SELECT ... WHERE ... total > '599' 192 | 193 | | id | color | shape | total | 194 | |:-----|:--------|:-----------|:--------| 195 | | 2 | blue | square | 2000 | 196 | | 5 | red | triangle | 900 | 197 | | 6 | red | triangle | 600 | 198 | 199 | **Less Than** 200 | 201 | ?total=600&operator[total]=< 202 | 203 | SELECT ... WHERE ... total < '600' 204 | 205 | | id | color | shape | total | 206 | |:-----|:--------|:-----------|:--------| 207 | | 1 | red | square | 150 | 208 | | 3 | green | circle | 575 | 209 | | 4 | yellow | triangle | 15 | 210 | 211 | **Not Equal** 212 | 213 | ?shape=triangle&operator[shape]=!= 214 | 215 | SELECT ... WHERE ... shape != 'triangle' 216 | 217 | | id | color | shape | total | 218 | |:-----|:--------|:-----------|:--------| 219 | | 4 | yellow | triangle | 15 | 220 | | 5 | red | triangle | 900 | 221 | | 6 | red | triangle | 600 | 222 | 223 | **Between** 224 | 225 | ?total[start]=900&total[end]=5000 226 | 227 | SELECT ... WHERE ... total BETWEEN '900' AND '5000' 228 | 229 | | id | color | shape | total | 230 | |:-----|:--------|:-----------|:--------| 231 | | 2 | blue | square | 2000 | 232 | | 5 | red | triangle | 900 | 233 | 234 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heroicpixels/filterable", 3 | "type": "library", 4 | "description": "Laravel package for dynamically filtering Eloquent results using URL query string.", 5 | "license": "MIT", 6 | "keywords": ["laravel", "laravel 4", "eloquent", "database"], 7 | "authors": [ 8 | { 9 | "name": "Dave Hodgins", 10 | "email": "dave@heroicpixels.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3.0", 15 | "illuminate/support": ">=4.1" 16 | }, 17 | "autoload": { 18 | "psr-0": { 19 | "Heroicpixels\\Filterable\\": "src/" 20 | } 21 | }, 22 | "minimum-stability": "stable", 23 | "extra": { 24 | "branch-alias": { 25 | "dev-master": "1.0.x-dev" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Heroicpixels/Filterable/Filterable.php: -------------------------------------------------------------------------------- 1 | filterable = array( 29 | 'bools' => array('and' => 'where', 'or' => 'orWhere'), 30 | 'callbacks' => array(), 31 | 'columns' => array(), 32 | 'defaultOperator' => '=', 33 | 'defaultWhere' => 'where', 34 | 'filters' => array(), 35 | 'orderby' => 'orderby', 36 | 'order' => 'order', 37 | 'operators' => array('=', '<', '<=', '>', '>=', '!=', 'like'), 38 | 'qstring' => array() 39 | ); 40 | return $this; 41 | } 42 | /** 43 | * Specify the columns that can be dynamically filtered from the query string. 44 | * Columns can be referenced directly - array('column1', 'column2') - or they 45 | * can be aliased - array('co1' => 'column1', 'co2' => 'column2'). 46 | * 47 | * @param array $columns The columns to use 48 | * @param bool $append Append or overwrite exisiting values 49 | * 50 | * @return $this 51 | */ 52 | public function setColumns($columns, $append = true) 53 | { 54 | if ( is_null($this->filterable) ) { 55 | $this->resetFilterableOptions(); 56 | } 57 | if ( count(array_filter(array_keys($columns), 'is_string')) == 0 ) { 58 | // Numeric indexes, so build new associative index array 59 | $columns = array_combine($columns, $columns); 60 | } 61 | if ( !$append ) { 62 | // Overwrite data 63 | $this->filterable['columns'] = array(); 64 | } 65 | 66 | foreach ( $columns as $k => $v ) { 67 | // Strip off callbacks 68 | if ( is_callable($v) ) { 69 | $this->filterable['callbacks'][] = $v; 70 | unset($columns[$k]); 71 | } 72 | } 73 | $this->filterable['columns'] = array_merge($this->filterable['columns'], $columns); 74 | return $this; 75 | } 76 | /** 77 | * Parse the query string 78 | * 79 | * @param $str array The query string 80 | * @param $append Append or overwrite existing query string data 81 | * @param $default Default to $_SERVER['QUERY_STRING'] if $str isn't given 82 | * 83 | * @return $this 84 | */ 85 | public function setQuerystring(array $str = array(), $append = true, $default = true) 86 | { 87 | if ( is_null($this->filterable) ) { 88 | $this->resetFilterableOptions(); 89 | } 90 | if ( sizeof($str) == 0 && $default ) { 91 | // Default to PHP query string 92 | parse_str($_SERVER['QUERY_STRING'], $this->filterable['qstring']); 93 | } else { 94 | $this->filterable['qstring'] = $str; 95 | } 96 | if ( sizeof($this->filterable['qstring']) > 0 ) { 97 | if ( !$append ) { 98 | // Overwrite data 99 | $this->filterable['filters'] = array(); 100 | } 101 | foreach ( $this->filterable['qstring'] as $k => $v ) { 102 | if ( $v == '' ) { 103 | continue; 104 | } 105 | $thisColumn = isset($this->filterable['columns'][$k]) ? $this->filterable['columns'][$k] : false; 106 | if ( $thisColumn ) { 107 | // Query string part matches column (or alias) 108 | $this->filterable['filters'][$thisColumn]['val'] = $v; 109 | // Evaluate boolean parameter in query string 110 | $thisBoolData = isset($this->filterable['qstring']['bool'][$k]) ? $this->filterable['qstring']['bool'][$k] : false; 111 | $thisBoolAvailable = $thisBoolData && isset($this->filterable['bools'][$thisBoolData]) ? $this->filterable['bools'][$thisBoolData] : false; 112 | if ( $thisBoolData && $thisBoolAvailable ) { 113 | $this->filterable['filters'][$thisColumn]['boolean'] = $thisBoolAvailable; 114 | } else { 115 | $this->filterable['filters'][$thisColumn]['boolean'] = $this->filterable['defaultWhere']; 116 | } 117 | // Evaluate operator parameters in the query string 118 | if ( isset($this->filterable['qstring']['operator'][$k]) && in_array($this->filterable['qstring']['operator'][$k], $this->filterable['operators']) ) { 119 | $this->filterable['filters'][$thisColumn]['operator'] = $this->filterable['qstring']['operator'][$k]; 120 | } else { 121 | // Default operator 122 | $this->filterable['filters'][$thisColumn]['operator'] = $this->filterable['defaultOperator']; 123 | } 124 | } 125 | } 126 | } 127 | return $this; 128 | } 129 | /** 130 | * Laravel Eloquent query scope. 131 | * 132 | * @param $query Eloquent query object 133 | * @return Eloquent query object 134 | */ 135 | public function scopeFilterColumns($query, $columns = array(), $validate = false) 136 | { 137 | if ( sizeof($columns) > 0 ) { 138 | // Set columns that can be filtered 139 | $this->setColumns($columns); 140 | } 141 | // Validate columns 142 | if ( $validate ) { 143 | $this->validateColumns(); 144 | } 145 | // Ensure that query string is parsed at least once 146 | if ( sizeof($this->filterable['filters']) == 0 ) { 147 | $this->setQuerystring(); 148 | } 149 | // Apply conditions to Eloquent query object 150 | if ( sizeof($this->filterable['filters']) > 0 ) { 151 | foreach ( $this->filterable['filters'] as $k => $v ) { 152 | $where = $v['boolean']; 153 | if ( is_array($v['val']) ) { 154 | if ( isset($v['val']['start']) && isset($v['val']['end']) ) { 155 | // BETWEEN a AND b 156 | $query->whereBetween($k, array($v['val']['start'], $v['val']['end'])); 157 | } else { 158 | // a = b OR c = d OR... 159 | $query->{$where}(function($q) use ($k, $v, $query) 160 | { 161 | foreach ( $v['val'] as $key => $val ) { 162 | $q->orWhere($k, $v['operator'], $val); 163 | } 164 | }); 165 | } 166 | } else { 167 | // a = b 168 | $query->{$where}($k, $v['operator'], $v['val']); 169 | } 170 | } 171 | } 172 | // Apply callbacks 173 | if ( sizeof($this->filterable['callbacks']) > 0 ) { 174 | foreach ( $this->filterable['callbacks'] as $v ) { 175 | $v($query); 176 | } 177 | } 178 | // Sorting 179 | if ( isset($this->filterable['qstring'][$this->filterable['orderby']]) && isset($this->filterable['columns'][$this->filterable['qstring'][$this->filterable['orderby']]]) ) { 180 | $order = isset($this->filterable['qstring'][$this->filterable['order']]) ? $this->filterable['qstring'][$this->filterable['order']] : 'asc'; 181 | $query->orderBy($this->filterable['columns'][$this->filterable['qstring'][$this->filterable['orderby']]], $order); 182 | } 183 | return $query; 184 | } 185 | /** 186 | * 187 | * Validate the specified columns against the table's actual columns. 188 | * 189 | * @return $this 190 | */ 191 | public function validateColumns() 192 | { 193 | if ( class_exists('\\Doctrine\\DBAL\\Driver\\PDOMySql\\Driver') ) { 194 | $columns = DB::connection()->getDoctrineSchemaManager()->listTableColumns($this->getTable()); 195 | foreach ( $columns as $column ) { 196 | $name = $column->getName(); 197 | $columns[$name] = $name; 198 | } 199 | $this->filterable['columns'] = array_intersect($this->filterable['columns'], $columns); 200 | return $this; 201 | } 202 | die('You must have Doctrine installed in order to validate columns'); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/Heroicpixels/Filterable/FilterableServiceProvider.php: -------------------------------------------------------------------------------- 1 | filterable = array( 24 | 'bools' => array('and' => 'where', 'or' => 'orWhere'), 25 | 'callbacks' => array(), 26 | 'columns' => array(), 27 | 'defaultOperator' => '=', 28 | 'defaultWhere' => 'where', 29 | 'filters' => array(), 30 | 'orderby' => 'orderby', 31 | 'order' => 'order', 32 | 'operators' => array('=', '<', '<=', '>', '>=', '!=', 'like'), 33 | 'qstring' => array() 34 | ); 35 | return $this; 36 | } 37 | /** 38 | * Specify the columns that can be dynamically filtered from the query string. 39 | * Columns can be referenced directly - array('column1', 'column2') - or they 40 | * can be aliased - array('co1' => 'column1', 'co2' => 'column2'). 41 | * 42 | * @param array $columns The columns to use 43 | * @param bool $append Append or overwrite exisiting values 44 | * 45 | * @return $this 46 | */ 47 | public function setColumns($columns, $append = true) 48 | { 49 | if ( is_null($this->filterable) ) { 50 | $this->resetFilterableOptions(); 51 | } 52 | if ( count(array_filter(array_keys($columns), 'is_string')) == 0 ) { 53 | // Numeric indexes, so build new associative index array 54 | $columns = array_combine($columns, $columns); 55 | } 56 | if ( !$append ) { 57 | // Overwrite data 58 | $this->filterable['columns'] = array(); 59 | } 60 | 61 | foreach ( $columns as $k => $v ) { 62 | // Strip off callbacks 63 | if ( is_callable($v) ) { 64 | $this->filterable['callbacks'][] = $v; 65 | unset($columns[$k]); 66 | } 67 | } 68 | $this->filterable['columns'] = array_merge($this->filterable['columns'], $columns); 69 | return $this; 70 | } 71 | /** 72 | * Parse the query string 73 | * 74 | * @param $str array The query string 75 | * @param $append Append or overwrite existing query string data 76 | * @param $default Default to $_SERVER['QUERY_STRING'] if $str isn't given 77 | * 78 | * @return $this 79 | */ 80 | public function setQuerystring(array $str = array(), $append = true, $default = true) 81 | { 82 | if ( is_null($this->filterable) ) { 83 | $this->resetFilterableOptions(); 84 | } 85 | if ( sizeof($str) == 0 && $default ) { 86 | // Default to PHP query string 87 | parse_str($_SERVER['QUERY_STRING'], $this->filterable['qstring']); 88 | } else { 89 | $this->filterable['qstring'] = $str; 90 | } 91 | if ( sizeof($this->filterable['qstring']) > 0 ) { 92 | if ( !$append ) { 93 | // Overwrite data 94 | $this->filterable['filters'] = array(); 95 | } 96 | foreach ( $this->filterable['qstring'] as $k => $v ) { 97 | if ( $v == '' ) { 98 | continue; 99 | } 100 | $thisColumn = isset($this->filterable['columns'][$k]) ? $this->filterable['columns'][$k] : false; 101 | if ( $thisColumn ) { 102 | // Query string part matches column (or alias) 103 | $this->filterable['filters'][$thisColumn]['val'] = $v; 104 | // Evaluate boolean parameter in query string 105 | $thisBoolData = isset($this->filterable['qstring']['bool'][$k]) ? $this->filterable['qstring']['bool'][$k] : false; 106 | $thisBoolAvailable = $thisBoolData && isset($this->filterable['bools'][$thisBoolData]) ? $this->filterable['bools'][$thisBoolData] : false; 107 | if ( $thisBoolData && $thisBoolAvailable ) { 108 | $this->filterable['filters'][$thisColumn]['boolean'] = $thisBoolAvailable; 109 | } else { 110 | $this->filterable['filters'][$thisColumn]['boolean'] = $this->filterable['defaultWhere']; 111 | } 112 | // Evaluate operator parameters in the query string 113 | if ( isset($this->filterable['qstring']['operator'][$k]) && in_array($this->filterable['qstring']['operator'][$k], $this->filterable['operators']) ) { 114 | $this->filterable['filters'][$thisColumn]['operator'] = $this->filterable['qstring']['operator'][$k]; 115 | } else { 116 | // Default operator 117 | $this->filterable['filters'][$thisColumn]['operator'] = $this->filterable['defaultOperator']; 118 | } 119 | } 120 | } 121 | } 122 | return $this; 123 | } 124 | /** 125 | * Laravel Eloquent query scope. 126 | * 127 | * @param $query Eloquent query object 128 | * @return Eloquent query object 129 | */ 130 | public function scopeFilterColumns($query, $columns = array(), $validate = false) 131 | { 132 | if ( sizeof($columns) > 0 ) { 133 | // Set columns that can be filtered 134 | $this->setColumns($columns); 135 | } 136 | // Validate columns 137 | if ( $validate ) { 138 | $this->validateColumns(); 139 | } 140 | // Ensure that query string is parsed at least once 141 | if ( sizeof($this->filterable['filters']) == 0 ) { 142 | $this->setQuerystring(); 143 | } 144 | // Apply conditions to Eloquent query object 145 | if ( sizeof($this->filterable['filters']) > 0 ) { 146 | foreach ( $this->filterable['filters'] as $k => $v ) { 147 | $where = $v['boolean']; 148 | if ( is_array($v['val']) ) { 149 | if ( isset($v['val']['start']) && isset($v['val']['end']) ) { 150 | // BETWEEN a AND b 151 | $query->whereBetween($k, array($v['val']['start'], $v['val']['end'])); 152 | } else { 153 | // a = b OR c = d OR... 154 | $query->{$where}(function($q) use ($k, $v, $query) 155 | { 156 | foreach ( $v['val'] as $key => $val ) { 157 | $q->orWhere($k, $v['operator'], $val); 158 | } 159 | }); 160 | } 161 | } else { 162 | // a = b 163 | $query->{$where}($k, $v['operator'], $v['val']); 164 | } 165 | } 166 | } 167 | // Apply callbacks 168 | if ( sizeof($this->filterable['callbacks']) > 0 ) { 169 | foreach ( $this->filterable['callbacks'] as $v ) { 170 | $v($query); 171 | } 172 | } 173 | // Sorting 174 | if ( isset($this->filterable['qstring'][$this->filterable['orderby']]) && isset($this->filterable['columns'][$this->filterable['qstring'][$this->filterable['orderby']]]) ) { 175 | $order = isset($this->filterable['qstring'][$this->filterable['order']]) ? $this->filterable['qstring'][$this->filterable['order']] : 'asc'; 176 | $query->orderBy($this->filterable['columns'][$this->filterable['qstring'][$this->filterable['orderby']]], $order); 177 | } 178 | return $query; 179 | } 180 | /** 181 | * 182 | * Validate the specified columns against the table's actual columns. 183 | * 184 | * @return $this 185 | */ 186 | public function validateColumns() 187 | { 188 | if ( class_exists('\\Doctrine\\DBAL\\Driver\\PDOMySql\\Driver') ) { 189 | $columns = DB::connection()->getDoctrineSchemaManager()->listTableColumns($this->getTable()); 190 | foreach ( $columns as $column ) { 191 | $name = $column->getName(); 192 | $columns[$name] = $name; 193 | } 194 | $this->filterable['columns'] = array_intersect($this->filterable['columns'], $columns); 195 | return $this; 196 | } 197 | die('You must have Doctrine installed in order to validate columns'); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Heroicpixels/Filterable/FilterableWrapper.php: -------------------------------------------------------------------------------- 1 | model = $model; 25 | } 26 | 27 | /** 28 | * Setup data structures and default values 29 | */ 30 | public function resetFilterableOptions() { 31 | $this->filterable = array( 32 | 'bools' => array('and' => 'where', 'or' => 'orWhere'), 33 | 'callbacks' => array(), 34 | 'columns' => array(), 35 | 'defaultOperator' => '=', 36 | 'defaultWhere' => 'where', 37 | 'filters' => array(), 38 | 'orderby' => 'orderby', 39 | 'order' => 'order', 40 | 'operators' => array('=', '<', '<=', '>', '>=', '!=', 'like'), 41 | 'qstring' => array() 42 | ); 43 | return $this; 44 | } 45 | /** 46 | * Specify the columns that can be dynamically filtered from the query string. 47 | * Columns can be referenced directly - array('column1', 'column2') - or they 48 | * can be aliased - array('co1' => 'column1', 'co2' => 'column2'). 49 | * 50 | * @param array $columns The columns to use 51 | * @param bool $append Append or overwrite exisiting values 52 | * 53 | * @return $this 54 | */ 55 | public function setColumns($columns, $append = true) { 56 | if ( is_null($this->filterable) ) { 57 | $this->resetFilterableOptions(); 58 | } 59 | if ( count(array_filter(array_keys($columns), 'is_string')) == 0 ) { 60 | // Numeric indexes, so build new associative index array 61 | $columns = array_combine($columns, $columns); 62 | } 63 | if ( !$append ) { 64 | // Overwrite data 65 | $this->filterable['columns'] = array(); 66 | } 67 | foreach ( $columns as $k => $v ) { 68 | // Strip off callbacks 69 | if ( is_callable($v) ) { 70 | $this->filterable['callbacks'][] = $v; 71 | unset($columns[$k]); 72 | } 73 | } 74 | $this->filterable['columns'] = array_merge($this->filterable['columns'], $columns); 75 | return $this; 76 | } 77 | /** 78 | * Parse the query string 79 | * 80 | * @param $str array The query string 81 | * @param $append Append or overwrite existing query string data 82 | * @param $default Default to $_SERVER['QUERY_STRING'] if $str isn't given 83 | * 84 | * @return $this 85 | */ 86 | public function setQuerystring(array $str = array(), $append = true, $default = true) { 87 | if ( is_null($this->filterable) ) { 88 | $this->resetFilterableOptions(); 89 | } 90 | if ( sizeof($str) == 0 && $default ) { 91 | // Default to PHP query string 92 | parse_str($_SERVER['QUERY_STRING'], $this->filterable['qstring']); 93 | } else { 94 | $this->filterable['qstring'] = $str; 95 | } 96 | if ( sizeof($this->filterable['qstring']) > 0 ) { 97 | if ( !$append ) { 98 | // Overwrite data 99 | $this->filterable['filters'] = array(); 100 | } 101 | foreach ( $this->filterable['qstring'] as $k => $v ) { 102 | if ( $v == '' ) { 103 | continue; 104 | } 105 | $thisColumn = isset($this->filterable['columns'][$k]) ? $this->filterable['columns'][$k] : false; 106 | if ( $thisColumn ) { 107 | // Query string part matches column (or alias) 108 | $this->filterable['filters'][$thisColumn]['val'] = $v; 109 | // Evaluate boolean parameter in query string 110 | $thisBoolData = isset($this->filterable['qstring']['bool'][$k]) ? $this->filterable['qstring']['bool'][$k] : false; 111 | $thisBoolAvailable = $thisBoolData && isset($this->filterable['bools'][$thisBoolData]) ? $this->filterable['bools'][$thisBoolData] : false; 112 | if ( $thisBoolData && $thisBoolAvailable ) { 113 | $this->filterable['filters'][$thisColumn]['boolean'] = $thisBoolAvailable; 114 | } else { 115 | $this->filterable['filters'][$thisColumn]['boolean'] = $this->filterable['defaultWhere']; 116 | } 117 | // Evaluate operator parameters in the query string 118 | if ( isset($this->filterable['qstring']['operator'][$k]) && in_array($this->filterable['qstring']['operator'][$k], $this->filterable['operators']) ) { 119 | $this->filterable['filters'][$thisColumn]['operator'] = $this->filterable['qstring']['operator'][$k]; 120 | } else { 121 | // Default operator 122 | $this->filterable['filters'][$thisColumn]['operator'] = $this->filterable['defaultOperator']; 123 | } 124 | } 125 | } 126 | } 127 | return $this; 128 | } 129 | /** 130 | * Apply filters to DB/Eloquent object 131 | * 132 | * @param $query Eloquent query object 133 | * @return Eloquent query object 134 | */ 135 | public function filterColumns($columns = array(), $validate = false) { 136 | if ( sizeof($columns) > 0 ) { 137 | // Set columns that can be filtered 138 | $this->setColumns($columns); 139 | } 140 | // Validate columns 141 | if ( $validate ) { 142 | $this->validateColumns(); 143 | } 144 | // Ensure that query string is parsed at least once 145 | if ( sizeof($this->filterable['filters']) == 0 ) { 146 | $this->setQuerystring(); 147 | } 148 | // Apply conditions to Eloquent query object 149 | if ( sizeof($this->filterable['filters']) > 0 ) { 150 | foreach ( $this->filterable['filters'] as $k => $v ) { 151 | $where = $v['boolean']; 152 | if ( is_array($v['val']) ) { 153 | if ( isset($v['val']['start']) && isset($v['val']['end']) ) { 154 | // BETWEEN a AND b 155 | $this->model->whereBetween($k, array($v['val']['start'], $v['val']['end'])); 156 | } else { 157 | // a = b OR c = d OR... 158 | $this->model->{$where}(function($q) use ($k, $v) 159 | { 160 | foreach ( $v['val'] as $key => $val ) { 161 | $q->orWhere($k, $v['operator'], $val); 162 | } 163 | }); 164 | } 165 | } else { 166 | // a = b 167 | $this->model->{$where}($k, $v['operator'], $v['val']); 168 | } 169 | } 170 | } 171 | // Apply callbacks 172 | if ( sizeof($this->filterable['callbacks']) > 0 ) { 173 | foreach ( $this->filterable['callbacks'] as $v ) { 174 | $v($this->model); 175 | } 176 | } 177 | // Sorting 178 | if ( isset($this->filterable['qstring'][$this->filterable['orderby']]) && isset($this->filterable['columns'][$this->filterable['qstring'][$this->filterable['orderby']]]) ) { 179 | $order = isset($this->filterable['qstring'][$this->filterable['order']]) ? $this->filterable['qstring'][$this->filterable['order']] : 'asc'; 180 | $this->model->orderBy($this->filterable['columns'][$this->filterable['qstring'][$this->filterable['orderby']]], $order); 181 | } 182 | return $this->model; 183 | } 184 | /** 185 | * 186 | * Validate the specified columns against the table's actual columns. 187 | * 188 | * @return $this 189 | */ 190 | public function validateColumns() { 191 | if ( class_exists('\\Doctrine\\DBAL\\Driver\\PDOMySql\\Driver') ) { 192 | $columns = DB::connection()->getDoctrineSchemaManager()->listTableColumns($this->getTable()); 193 | foreach ( $columns as $column ) { 194 | $name = $column->getName(); 195 | $columns[$name] = $name; 196 | } 197 | $this->filterable['columns'] = array_intersect($this->filterable['columns'], $columns); 198 | return $this; 199 | } 200 | die('You must have Doctrine installed in order to validate columns'); 201 | } 202 | /** 203 | * Pass methods to the DB/Eloquent object 204 | */ 205 | public function __call($name, $arguments) { 206 | $retrieval = in_array($name, array('all', 'find', 'findOrFail', 'first', 'get', 'lists', 'paginate')); 207 | $this->model = call_user_func_array(array($this->model, $name), $arguments); 208 | if ( $retrieval ) { 209 | return $this->model; 210 | } else { 211 | return $this; 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heroicpixels/filterable/04d6bdeaf5fbc77e19d666cb06739a4af85d3d4f/src/config/.gitkeep -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | '=', 3 | 'defaultWhere' => 'where'); --------------------------------------------------------------------------------