├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── ArrayBuilder.php └── Traits │ └── ArrayQueryable.php └── tests ├── AbstractTestCase.php ├── ArrayBuilderTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | 7 | sudo: false 8 | 9 | install: travis_retry composer install --no-interaction --prefer-dist 10 | 11 | script: vendor/bin/phpunit --verbose -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis CI Build Status](https://travis-ci.org/williamoliveira/eloquent-array-query-builder.svg?branch=master)](https://travis-ci.org/williamoliveira/eloquent-array-query-builder) 2 | [![Latest Stable Version](https://poser.pugx.org/williamoliveira/eloquent-array-query-builder/v/stable)](https://packagist.org/packages/williamoliveira/eloquent-array-query-builder) 3 | [![Total Downloads](https://poser.pugx.org/williamoliveira/eloquent-array-query-builder/downloads)](https://packagist.org/packages/williamoliveira/eloquent-array-query-builder) 4 | [![License](https://poser.pugx.org/williamoliveira/eloquent-array-query-builder/license)](https://packagist.org/packages/williamoliveira/eloquent-array-query-builder) 5 | 6 | ## Why this nonsense? 7 | 8 | So you can have easy to use query "language" to query your data, without the need to write long conditional queries by hand, very useful for REST APIs. 9 | 10 | ## How to install 11 | 12 | `composer require williamoliveira/eloquent-array-query-builder` 13 | ## How to use 14 | 15 | We let the wiring of the request to the model to you, so you can use it wherever you want. 16 | 17 | Example in a controller: 18 | ```php 19 | public function index(Request $request, \Williamoliveira\ArrayQueryBuilder\ArrayBuilder $arrayBuilder) 20 | { 21 | $query = User::query(); 22 | $query = $arrayBuilder->apply($query, $request->all()); 23 | 24 | return $query->paginate($request->get('per_page')); // Note it does not do pagination or call get(), 25 | // you need to do it yourself 26 | } 27 | ``` 28 | 29 | You can also use the ArrayQueryable trait in your model: 30 | ```php 31 | // Model 32 | class User extends Model{ 33 | use \Williamoliveira\ArrayQueryBuilder\Traits\ArrayQueryable; 34 | // ... 35 | 36 | // Usage 37 | return User::arrayQuery($request->all())->get(); //static 38 | return (new User())->newArrayQuery($request->all())->get(); //instance 39 | ``` 40 | 41 | #### Query format 42 | 43 | Here is a example of what a query can look like: 44 | ```php 45 | $exampleArrayQuery = [ 46 | 'where' => [ 47 | 'name' => ['like' => '%joao%'], 48 | 'created_at' => [ 49 | 'between' => [ 50 | '2014-10-10', 51 | '2015-10-10', 52 | ], 53 | ], 54 | 'or' => [ // nested boolean where clauses 55 | 'foo' => 'bar', 56 | 'baz' => 'qux', 57 | ], 58 | ], 59 | 'fields' => ['id', 'name', 'created_at'], 60 | 'order' => 'name', 61 | 'include' => [ // relations, can have where, order and fields 62 | 'permissions' => true, 63 | 'roles' => [ 64 | 'where' => [ 65 | 'name' => 'admin', 66 | ], 67 | 'fields' => ['id', 'name'], 68 | 'order' => 'name DESC', 69 | ], 70 | ], 71 | 'groupBy' => ['foo', 'bar', 'baz'], 72 | 'having' => [ 73 | 'foo' => 'x', 74 | 'bar' => ['in' => ['1', '2']], 75 | 'baz' => ['neq' => '3'], 76 | ], 77 | 'offset' => 5, 78 | 'limit' => 15, 79 | ]; 80 | ``` 81 | 82 | Just as a reference to people building REST APIs, the same query as a query string: 83 | ``` 84 | ?where[name][like]=%joao% 85 | &where[created_at][between][]=2014-10-10 86 | &where[created_at][between][]=2015-10-10 87 | &where[or][foo]=bar 88 | &where[or][baz]=qux 89 | &fields[]=id 90 | &fields[]=name 91 | &fields[]=created_at 92 | &order=name 93 | &include[permissions]=true 94 | &include[roles][where][name]=admin 95 | &include[roles][fields][]=id 96 | &include[roles][fields][]=name 97 | &include[roles][order]=name DESC 98 | &groupBy[]=foo 99 | &groupBy[]=bar 100 | &groupBy[]=baz 101 | &having[foo]=x 102 | &having[bar][in][]=1 103 | &having[bar][in][]=2 104 | &having[baz][neq]=3 105 | &offset=5 106 | &limit=15 107 | ``` 108 | Tip: for Javascript you can create a query string using [Qs](https://github.com/ljharb/qs). 109 | 110 | #### Where/Having operators aliases 111 | 112 | ``` 113 | 'eq' => '=', 114 | 'neq' => '<>', 115 | 'gt' => '>', 116 | 'gte' => '>=', 117 | 'lt' => '<', 118 | 'lte' => '<=', 119 | 'nlike' => 'not like', 120 | 'nin' => 'not in', 121 | 'notnull' => 'not null', 122 | 'nn' => 'not null', 123 | 'inq' => 'in' 124 | ``` 125 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "williamoliveira/eloquent-array-query-builder", 3 | "description": "Query Laravel Eloquent with and array", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "William Oliveira", 8 | "email": "william.oliveir4@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "illuminate/database": "*" 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "^7.0", 16 | "mockery/mockery": "^1.0", 17 | "symfony/var-dumper": "^3.3.0", 18 | "squizlabs/php_codesniffer": "^3.2" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Williamoliveira\\ArrayQueryBuilder\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Tests\\": "tests/" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | ./tests 18 | 19 | 20 | 21 | 22 | ./src 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/ArrayBuilder.php: -------------------------------------------------------------------------------- 1 | '=', 15 | 'neq' => '<>', 16 | 'gt' => '>', 17 | 'gte' => '>=', 18 | 'lt' => '<', 19 | 'lte' => '<=', 20 | 'nlike' => 'not like', 21 | 'nin' => 'not in', 22 | 'notnull' => 'not null', 23 | 'nn' => 'not null', 24 | 'inq' => 'in' 25 | ]; 26 | 27 | /** 28 | * @param Builder|QueryBuilder $query 29 | * @param array $arrayQuery 30 | * @return Builder|QueryBuilder 31 | * @throws \InvalidArgumentException 32 | */ 33 | public function apply($query, array $arrayQuery) 34 | { 35 | if (isset($arrayQuery['include'])) { 36 | if ($query instanceof QueryBuilder) { 37 | throw new \InvalidArgumentException( 38 | QueryBuilder::class . " does not support relations, you need " . Builder::class . " for that." 39 | ); 40 | } 41 | 42 | $this->buildIncludes($query, $arrayQuery['include']); 43 | } 44 | 45 | if (isset($arrayQuery['where'])) { 46 | $this->buildWheres($query, $arrayQuery['where']); 47 | } 48 | 49 | if (isset($arrayQuery['fields'])) { 50 | $this->buildFields($query, $arrayQuery['fields']); 51 | } 52 | 53 | if (isset($arrayQuery['order'])) { 54 | $this->buildOrderBy($query, $arrayQuery['order']); 55 | } 56 | 57 | if (isset($arrayQuery['limit'])) { 58 | $this->buildLimit($query, $arrayQuery['limit']); 59 | } 60 | 61 | if (isset($arrayQuery['offset'])) { 62 | $this->buildOffset($query, $arrayQuery['offset']); 63 | } elseif (isset($arrayQuery['skip'])) { 64 | // "skip" is an alias for "offset" 65 | $this->buildOffset($query, $arrayQuery['skip']); 66 | } 67 | 68 | if (isset($arrayQuery['groupBy'])) { 69 | $this->buildGroupBy($query, $arrayQuery['groupBy']); 70 | } elseif (isset($arrayQuery['group_by'])) { 71 | // "group_by" is an alias for "groupBy" 72 | $this->buildGroupBy($query, $arrayQuery['group_by']); 73 | } 74 | 75 | if (isset($arrayQuery['having'])) { 76 | $this->buildHavings($query, $arrayQuery['having']); 77 | } 78 | 79 | return $query; 80 | } 81 | 82 | /** 83 | * @param Builder|QueryBuilder $queryBuilder 84 | * @param array $wheres 85 | * @param string $boolean 86 | */ 87 | protected function buildWheres($queryBuilder, array $wheres, $boolean = 'and') 88 | { 89 | foreach ($wheres as $whereField => $where) { 90 | if (!isset($whereField) || !isset($where)) { 91 | continue; 92 | } 93 | 94 | $whereField = strtolower($whereField); 95 | 96 | // Nested OR where 97 | // Example: 'or' => ['foo' => 'bar', 'x => 'y'] 98 | if ($whereField === 'or') { 99 | $queryBuilder->whereNested(function ($queryBuilder) use ($where) { 100 | $this->buildWheres($queryBuilder, $where, 'or'); 101 | }, $boolean); 102 | 103 | continue; 104 | } 105 | 106 | // Nested AND where 107 | // Example: 'and' => ['foo' => 'bar', 'x => 'y'] 108 | if ($whereField === 'and') { 109 | $queryBuilder->whereNested(function ($queryBuilder) use ($where) { 110 | $this->buildWheres($queryBuilder, $where, 'and'); 111 | }, $boolean); 112 | 113 | continue; 114 | } 115 | 116 | // Operator is present on query 117 | // Example: 'foo' => ['like' => '%bar%'] 118 | if (is_array($where)) { 119 | foreach ($where as $whereOperator => $whereValue) { 120 | $whereOperator = $this->parseOperator($whereOperator); 121 | $this->buildWhere($queryBuilder, $whereField, $whereOperator, $whereValue, $boolean); 122 | } 123 | 124 | continue; 125 | } 126 | 127 | // Operator is omitted on query, assumes '=' 128 | // Example: 'foo' => 'bar' 129 | $whereOperator = is_array($where) ? array_keys($where)[0] : '='; 130 | $whereValue = is_array($where) ? $where[$whereOperator] : $where; 131 | 132 | $whereOperator = $this->parseOperator($whereOperator); 133 | 134 | $this->buildWhere($queryBuilder, $whereField, $whereOperator, $whereValue, $boolean); 135 | } 136 | } 137 | 138 | /** 139 | * @param Builder|QueryBuilder $queryBuilder 140 | * @param string $field 141 | * @param string|null $operator 142 | * @param array|string $value 143 | * @param string $boolean 144 | */ 145 | protected function buildWhere($queryBuilder, $field, $operator, $value, $boolean = 'and') 146 | { 147 | if (strpos($field, '.') > -1) { 148 | $this->buildWhereHas($queryBuilder, $field, $operator, $value, $boolean); 149 | return; 150 | } 151 | 152 | switch ($operator) { 153 | case 'between': 154 | $queryBuilder->whereBetween($field, [$value[0], $value[1]], $boolean); 155 | return; 156 | case 'not null': 157 | $queryBuilder->whereNotNull($field, $boolean); 158 | return; 159 | case 'in': 160 | $queryBuilder->whereIn($field, (!is_array($value) ? [$value] : $value), $boolean); 161 | return; 162 | case 'not in': 163 | $queryBuilder->whereNotIn($field, (!is_array($value) ? [$value] : $value), $boolean); 164 | return; 165 | case 'search': 166 | $this->buildTextSearchWhere($queryBuilder, $field, $value, $boolean); 167 | return; 168 | default: 169 | $queryBuilder->where($field, $operator, $value, $boolean); 170 | } 171 | } 172 | 173 | /** 174 | * @param Builder|QueryBuilder $queryBuilder 175 | * @param array $columns 176 | */ 177 | protected function buildFields($queryBuilder, $columns = ['*']) 178 | { 179 | $queryBuilder->select($columns); 180 | } 181 | 182 | /** 183 | * @param Builder|QueryBuilder $queryBuilder 184 | * @param array|string $order 185 | */ 186 | protected function buildOrderBy($queryBuilder, $order) 187 | { 188 | if (is_array($order)) { 189 | foreach ($order as $orderItem) { 190 | $this->buildOrderBySingle($queryBuilder, $orderItem); 191 | } 192 | 193 | return; 194 | } 195 | 196 | $this->buildOrderBySingle($queryBuilder, $order); 197 | } 198 | 199 | /** 200 | * @param Builder|QueryBuilder $queryBuilder 201 | * @param string $order 202 | */ 203 | protected function buildOrderBySingle($queryBuilder, $order) 204 | { 205 | $order = strtolower($order); 206 | 207 | $orderBy = str_replace([' asc', ' desc'], '', $order); 208 | $orderDirection = $this->ends_with($order, ' desc') ? 'desc' : 'asc'; 209 | 210 | $queryBuilder->orderBy($orderBy, $orderDirection); 211 | } 212 | 213 | protected function ends_with($haystack, $needles) 214 | { 215 | foreach ((array) $needles as $needle) { 216 | if (substr($haystack, -strlen($needle)) === (string) $needle) { 217 | return true; 218 | } 219 | } 220 | 221 | return false; 222 | } 223 | 224 | /** 225 | * @param Builder|QueryBuilder $queryBuilder 226 | * @param int $limit 227 | */ 228 | protected function buildLimit($queryBuilder, $limit) 229 | { 230 | $queryBuilder->limit($limit); 231 | } 232 | 233 | /** 234 | * @param Builder|QueryBuilder $queryBuilder 235 | * @param int $offset 236 | */ 237 | protected function buildOffset($queryBuilder, $offset) 238 | { 239 | $queryBuilder->offset($offset); 240 | } 241 | 242 | /** 243 | * @param Builder $queryBuilder 244 | * @param array $includes 245 | */ 246 | protected function buildIncludes($queryBuilder, array $includes) 247 | { 248 | $builtIncludes = []; 249 | 250 | foreach ($includes as $includeName => $include) { 251 | // Support for array includes, example: ['user', 'post'] 252 | // If it's a single dimension array the key will be numeric 253 | $includeName = is_numeric($includeName) ? $include : $includeName; 254 | 255 | if (empty($include['where']) && empty($include['fields']) && empty($include['order'])) { 256 | $builtIncludes[] = $includeName; 257 | continue; 258 | } 259 | 260 | $builtIncludes[$includeName] = function ($query) use ($include) { 261 | 262 | if (isset($include['where'])) { 263 | $this->buildWheres($query, $include['where']); 264 | } 265 | 266 | if (isset($include['fields'])) { 267 | $this->buildFields($query, $include['fields']); 268 | } 269 | 270 | if (isset($include['order'])) { 271 | $this->buildOrderBy($query, $include['order']); 272 | } 273 | }; 274 | } 275 | 276 | $queryBuilder->with($builtIncludes); 277 | } 278 | 279 | /** 280 | * @param Builder|QueryBuilder $queryBuilder 281 | * @param $field 282 | * @param $value 283 | * @param string $boolean 284 | */ 285 | protected function buildTextSearchWhere($queryBuilder, $field, $value, $boolean = 'and') 286 | { 287 | $value = preg_replace('/\s\s+/', ' ', trim($value)); 288 | $value = '%' . strtolower(str_replace(' ', '%', $value)) . '%'; 289 | 290 | $theQueryBuilder = $queryBuilder instanceof Builder 291 | ? $queryBuilder->getQuery() 292 | : $queryBuilder; 293 | $grammar = $theQueryBuilder->getGrammar(); 294 | 295 | // Postgres 296 | if ($grammar instanceof PostgresGrammar) { 297 | $queryBuilder->where($field, 'ilike', $value, $boolean); 298 | return; 299 | } 300 | 301 | $column = $grammar->wrap($field); 302 | 303 | // MySql 304 | if ($grammar instanceof MySqlGrammar) { 305 | $queryBuilder->whereRaw($column . ' COLLATE utf8_general_ci like ?', [$value], $boolean); 306 | return; 307 | } 308 | 309 | // Others 310 | $queryBuilder->whereRaw('lower(' . $column . ') like ?', [$value], $boolean); 311 | } 312 | 313 | /** 314 | * @param Builder|QueryBuilder $queryBuilder 315 | * @param $hasField 316 | * @param $operator 317 | * @param $value 318 | * @param string $boolean 319 | */ 320 | protected function buildWhereHas($queryBuilder, $hasField, $operator, $value, $boolean = 'and') 321 | { 322 | $reversedParts = explode('.', strrev($hasField), 2); 323 | 324 | $hasField = strrev($reversedParts[1]); 325 | $field = strrev($reversedParts[0]); 326 | 327 | $queryBuilder->whereHas($hasField, function ($query) use ($queryBuilder, $field, $operator, $value, $boolean) { 328 | $this->buildWhere($query, $field, $operator, $value, $boolean); 329 | }); 330 | } 331 | 332 | /** 333 | * @param Builder|QueryBuilder $queryBuilder 334 | * @param string|array $groupBy 335 | */ 336 | protected function buildGroupBy($queryBuilder, $groupBy) 337 | { 338 | $queryBuilder->groupBy($groupBy); 339 | } 340 | 341 | /** 342 | * @param Builder|QueryBuilder $queryBuilder 343 | * @param array $havings 344 | * @param string $boolean 345 | */ 346 | protected function buildHavings($queryBuilder, array $havings, $boolean = 'and') 347 | { 348 | foreach ($havings as $havingField => $having) { 349 | if (!isset($havingField) || !isset($having)) { 350 | continue; 351 | } 352 | 353 | $havingField = strtolower($havingField); 354 | 355 | // Operator is omitted on query, assumes '=' 356 | // Example: 'foo' => 'bar' 357 | $havingOperator = is_array($having) ? array_keys($having)[0] : '='; 358 | $havingValue = is_array($having) ? $having[$havingOperator] : $having; 359 | 360 | $havingOperator = $this->parseOperator($havingOperator); 361 | 362 | $this->buildHaving($queryBuilder, $havingField, $havingOperator, $havingValue, $boolean); 363 | } 364 | } 365 | 366 | /** 367 | * @param Builder|QueryBuilder $queryBuilder 368 | * @param string $havingField 369 | * @param string $havingOperator 370 | * @param string $havingValue 371 | * @param string $boolean 372 | * 373 | * @internal param array|string $having 374 | */ 375 | protected function buildHaving($queryBuilder, $havingField, $havingOperator, $havingValue, $boolean) 376 | { 377 | $queryBuilder->having($havingField, $havingOperator, $havingValue, $boolean); 378 | } 379 | 380 | /** 381 | * @param string $operator 382 | * @return string 383 | */ 384 | protected function parseOperator($operator) 385 | { 386 | $operator = array_key_exists($operator, $this->aliases) 387 | ? $this->aliases[$operator] 388 | : $operator; 389 | 390 | return strtolower($operator); 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/Traits/ArrayQueryable.php: -------------------------------------------------------------------------------- 1 | apply($query, $arrayQuery); 19 | } 20 | 21 | /** 22 | * @param array $arrayQuery 23 | * @return \Illuminate\Database\Eloquent\Builder 24 | */ 25 | public function newArrayQuery(array $arrayQuery) 26 | { 27 | $query = $this->newQuery(); 28 | 29 | return (new ArrayBuilder())->apply($query, $arrayQuery); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/AbstractTestCase.php: -------------------------------------------------------------------------------- 1 | buildArrayQuery([]); 22 | 23 | $fluentQueryBuilder = $this->getQueryBuilder(); 24 | 25 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 26 | } 27 | 28 | public function testBasicWhere() 29 | { 30 | $arrayQueryBuilder = $this->buildArrayQuery([ 31 | 'where' => [ 32 | 'foo' => 'bar' 33 | ] 34 | ]); 35 | 36 | $fluentQueryBuilder = $this->getQueryBuilder() 37 | ->where('foo', 'bar'); 38 | 39 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 40 | } 41 | 42 | public function testWhereLike() 43 | { 44 | $arrayQueryBuilder = $this->buildArrayQuery([ 45 | 'where' => [ 46 | 'name' => ['like' => '%joao%'] 47 | ], 48 | ]); 49 | 50 | $fluentQueryBuilder = $this->getQueryBuilder() 51 | ->where('name', 'like', '%joao%'); 52 | 53 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 54 | } 55 | 56 | public function testWhereILike() 57 | { 58 | $arrayQueryBuilder = $this->buildArrayQuery([ 59 | 'where' => [ 60 | 'name' => ['ilike' => '%joao%'] 61 | ], 62 | ]); 63 | 64 | $fluentQueryBuilder = $this->getQueryBuilder() 65 | ->where('name', 'ilike', '%joao%'); 66 | 67 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 68 | } 69 | 70 | public function testWhereSearchForPostgres() 71 | { 72 | $arrayQueryBuilder = $this->buildArrayQuery([ 73 | 'where' => [ 74 | 'name' => ['search' => 'joao'] 75 | ], 76 | ], new PostgresGrammar()); 77 | 78 | $fluentQueryBuilder = $this->getQueryBuilder(new PostgresGrammar()) 79 | ->where('name', 'ilike', '%joao%'); 80 | 81 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 82 | } 83 | 84 | public function testWhereSearchForMysql() 85 | { 86 | $arrayQueryBuilder = $this->buildArrayQuery([ 87 | 'where' => [ 88 | 'name' => ['search' => 'joao'] 89 | ], 90 | ], new MySqlGrammar()); 91 | 92 | $fluentQueryBuilder = $this->getQueryBuilder(new MySqlGrammar()) 93 | ->whereRaw('`name` COLLATE utf8_general_ci like ?', ['%joao%']); 94 | 95 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 96 | } 97 | 98 | public function testWhereSearchDefault() 99 | { 100 | $arrayQueryBuilder = $this->buildArrayQuery([ 101 | 'where' => [ 102 | 'name' => ['search' => 'joao'] 103 | ], 104 | ]); 105 | 106 | $fluentQueryBuilder = $this->getQueryBuilder() 107 | ->whereRaw('lower("name") like ?', ['%joao%']); 108 | 109 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 110 | } 111 | 112 | public function testWhereBetween() 113 | { 114 | $arrayQueryBuilder = $this->buildArrayQuery([ 115 | 'where' => [ 116 | 'created_at' => [ 117 | 'between' => [ 118 | '2014-10-10', 119 | '2015-10-10' 120 | ] 121 | ], 122 | ] 123 | ]); 124 | 125 | $fluentQueryBuilder = $this->getQueryBuilder() 126 | ->whereBetween('created_at', ['2014-10-10', '2015-10-10']); 127 | 128 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 129 | } 130 | 131 | public function testWhereOr() 132 | { 133 | $arrayQueryBuilder = $this->buildArrayQuery([ 134 | 'where' => [ 135 | 'or' => [ 136 | 'foo' => 'bar', 137 | 'baz' => 'qux' 138 | ] 139 | ] 140 | ]); 141 | 142 | $fluentQueryBuilder = $this->getQueryBuilder() 143 | ->whereNested(function (QueryBuilder $query) { 144 | $query->orWhere('foo', 'bar'); 145 | $query->orWhere('baz', 'qux'); 146 | }); 147 | 148 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 149 | } 150 | 151 | public function testWhereAnd() 152 | { 153 | $arrayQueryBuilder = $this->buildArrayQuery([ 154 | 'where' => [ 155 | 'and' => [ 156 | 'foo' => 'bar', 157 | 'baz' => 'qux' 158 | ] 159 | ] 160 | ]); 161 | 162 | $fluentQueryBuilder = $this->getQueryBuilder() 163 | ->whereNested(function (QueryBuilder $query) { 164 | $query->where('foo', 'bar'); 165 | $query->where('baz', 'qux'); 166 | }); 167 | 168 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 169 | } 170 | 171 | public function testSelectFields() 172 | { 173 | $arrayQueryBuilder = $this->buildArrayQuery([ 174 | 'fields' => ['id', 'name', 'created_at'] 175 | ]); 176 | 177 | $fluentQueryBuilder = $this->getQueryBuilder() 178 | ->select(['id', 'name', 'created_at']); 179 | 180 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 181 | } 182 | 183 | public function testOrderBySingleField() 184 | { 185 | $arrayQueryBuilder = $this->buildArrayQuery([ 186 | 'order' => 'name', 187 | ]); 188 | 189 | $fluentQueryBuilder = $this->getQueryBuilder() 190 | ->orderBy('name'); 191 | 192 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 193 | } 194 | 195 | public function testOrderByManyFields() 196 | { 197 | $arrayQueryBuilder = $this->buildArrayQuery([ 198 | 'order' => ['id', 'name'], 199 | ]); 200 | 201 | $fluentQueryBuilder = $this->getQueryBuilder() 202 | ->orderBy('id') 203 | ->orderBy('name'); 204 | 205 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 206 | } 207 | 208 | 209 | public function testOrderBySingleFieldWithDirection() 210 | { 211 | $arrayQueryBuilder = $this->buildArrayQuery([ 212 | 'order' => 'name ASC', 213 | ]); 214 | 215 | $fluentQueryBuilder = $this->getQueryBuilder() 216 | ->orderBy('name', 'ASC'); 217 | 218 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 219 | } 220 | 221 | public function testOrderByManyFieldsWithDirection() 222 | { 223 | $arrayQueryBuilder = $this->buildArrayQuery([ 224 | 'order' => ['id ASC', 'name DESC'], 225 | ]); 226 | 227 | $fluentQueryBuilder = $this->getQueryBuilder() 228 | ->orderBy('id', 'ASC') 229 | ->orderBy('name', 'DESC'); 230 | 231 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 232 | } 233 | 234 | public function testLimit() 235 | { 236 | $arrayQueryBuilder = $this->buildArrayQuery([ 237 | 'limit' => 15, 238 | ]); 239 | 240 | $fluentQueryBuilder = $this->getQueryBuilder() 241 | ->limit(15); 242 | 243 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 244 | } 245 | 246 | public function testOffset() 247 | { 248 | $arrayQueryBuilder = $this->buildArrayQuery([ 249 | 'offset' => 5, 250 | ]); 251 | 252 | $fluentQueryBuilder = $this->getQueryBuilder() 253 | ->offset(5); 254 | 255 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 256 | } 257 | 258 | public function testSkip() 259 | { 260 | $arrayQueryBuilder = $this->buildArrayQuery([ 261 | 'skip' => 5, 262 | ]); 263 | 264 | $fluentQueryBuilder = $this->getQueryBuilder() 265 | ->offset(5); 266 | 267 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 268 | } 269 | 270 | public function testBasicInclude() 271 | { 272 | $arrayQueryBuilder = $this->buildArrayQueryFromEloquent([ 273 | 'include' => ['foo', 'bar'] 274 | ]); 275 | 276 | $fluentQueryBuilder = $this->getEloquentBuilder() 277 | ->with(['foo', 'bar']); 278 | 279 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 280 | } 281 | 282 | public function testIncludeWithWhere() 283 | { 284 | $arrayQueryBuilder = $this->buildArrayQueryFromEloquent([ 285 | 'include' => [ 286 | 'roles' => [ 287 | 'where' => [ 288 | 'name' => 'admin' 289 | ] 290 | ] 291 | ] 292 | ]); 293 | 294 | $fluentQueryBuilder = $this->getEloquentBuilder() 295 | ->with([ 296 | 'roles' => function (QueryBuilder $query) { 297 | $query->where('name', 'admin'); 298 | } 299 | ]); 300 | 301 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 302 | } 303 | 304 | public function testComplexIncludes() 305 | { 306 | $arrayQueryBuilder = $this->buildArrayQueryFromEloquent([ 307 | 'include' => [ 308 | 'permissions' => true, 309 | 'roles' => [ 310 | 'where' => [ 311 | 'name' => 'admin' 312 | ], 313 | 'fields' => ['id', 'name'], 314 | 'order' => 'name DESC' 315 | ] 316 | ] 317 | ]); 318 | 319 | $fluentQueryBuilder = $this->getEloquentBuilder() 320 | ->with([ 321 | 'permission' => true, 322 | 'roles' => function (QueryBuilder $query) { 323 | $query->select('id', 'name') 324 | ->where('name', 'admin') 325 | ->orderBy('name', 'DESC'); 326 | } 327 | ]); 328 | 329 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 330 | } 331 | 332 | public function testGroupBySingle() 333 | { 334 | $arrayQueryBuilder = $this->buildArrayQuery([ 335 | 'groupBy' => 'foo', 336 | ]); 337 | 338 | $fluentQueryBuilder = $this->getQueryBuilder() 339 | ->groupBy('foo'); 340 | 341 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 342 | } 343 | 344 | public function testGroupByArray() 345 | { 346 | $arrayQueryBuilder = $this->buildArrayQuery([ 347 | 'groupBy' => ['foo', 'bar', 'baz'], 348 | ]); 349 | 350 | $fluentQueryBuilder = $this->getQueryBuilder() 351 | ->groupBy(['foo', 'bar', 'baz']); 352 | 353 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 354 | } 355 | 356 | public function testHavingBasic() 357 | { 358 | $arrayQueryBuilder = $this->buildArrayQuery([ 359 | 'having' => [ 360 | 'foo' => 'bar', 361 | ], 362 | ]); 363 | 364 | $fluentQueryBuilder = $this->getQueryBuilder() 365 | ->having('foo', '=', 'bar', 'and'); 366 | 367 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 368 | } 369 | 370 | public function testHavingMany() 371 | { 372 | $arrayQueryBuilder = $this->buildArrayQuery([ 373 | 'having' => [ 374 | 'foo' => 'x', 375 | 'bar' => ['nin' => ['1', '2']], 376 | 'baz' => ['neq' => '3'], 377 | ], 378 | ]); 379 | 380 | $fluentQueryBuilder = $this->getQueryBuilder() 381 | ->having('foo', '=', 'x', 'and') 382 | ->having('bar', 'not in', ['1', '2'], 'and') 383 | ->having('baz', '<>', '3', 'and'); 384 | 385 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 386 | } 387 | 388 | public function testComplexQueryFromReadmeExample() 389 | { 390 | $arrayQueryBuilder = $this->buildArrayQueryFromEloquent([ 391 | 'where' => [ 392 | 'name' => ['like' => '%joao%'], 393 | 'created_at' => [ 394 | 'between' => [ 395 | '2014-10-10', 396 | '2015-10-10' 397 | ] 398 | ], 399 | 'or' => [ 400 | 'foo' => 'bar', 401 | 'baz' => 'qux' 402 | ] 403 | ], 404 | 'fields' => ['id', 'name', 'created_at'], 405 | 'order' => 'name', 406 | 'include' => [ 407 | 'permissions' => true, 408 | 'roles' => [ 409 | 'where' => [ 410 | 'name' => 'admin' 411 | ], 412 | 'fields' => ['id', 'name'], 413 | 'order' => 'name DESC' 414 | ] 415 | ], 416 | ]); 417 | 418 | $fluentQueryBuilder = $this->getEloquentBuilder() 419 | ->select(['id', 'name', 'created_at']) 420 | ->orderBy('name') 421 | ->where('name', 'like', '%joao%') 422 | ->whereBetween('created_at', ['2014-10-10', '2015-10-10']) 423 | ->whereNested(function (QueryBuilder $query) { 424 | $query->orWhere('foo', 'bar'); 425 | $query->orWhere('baz', 'qux'); 426 | }) 427 | ->with([ 428 | 'permission' => true, 429 | 'roles' => function (QueryBuilder $query) { 430 | $query->select('id', 'name') 431 | ->where('name', 'admin') 432 | ->orderBy('name', 'DESC'); 433 | } 434 | ]); 435 | 436 | $this->assertQueryEquals($fluentQueryBuilder, $arrayQueryBuilder); 437 | } 438 | 439 | public function testArrayBuilderDoesNotAcceptsQueryBuilderWithIncludes() 440 | { 441 | $this->expectException(InvalidArgumentException::class); 442 | 443 | $this->buildArrayQuery([ 444 | 'include' => ['foo', 'bar'] 445 | ]); 446 | } 447 | 448 | /** 449 | * @param EloquentBuilder|QueryBuilder $query1 450 | * @param EloquentBuilder|QueryBuilder $query2 451 | */ 452 | protected function assertQueryEquals($query1, $query2) 453 | { 454 | $queryComponents1 = $this->toQueryComponentsArray($query1); 455 | $queryComponents2 = $this->toQueryComponentsArray($query2); 456 | 457 | $this->assertEquals($queryComponents1, $queryComponents2); 458 | } 459 | 460 | /** 461 | * @param array $arrayQuery 462 | * @param Grammar|null $grammar 463 | * @return QueryBuilder 464 | */ 465 | protected function buildArrayQuery($arrayQuery, $grammar = null) 466 | { 467 | return (new ArrayBuilder())->apply($this->getQueryBuilder($grammar), $arrayQuery); 468 | } 469 | 470 | /** 471 | * @param array $arrayQuery 472 | * @return EloquentBuilder|QueryBuilder 473 | */ 474 | protected function buildArrayQueryFromEloquent($arrayQuery) 475 | { 476 | return (new ArrayBuilder())->apply($this->getEloquentBuilder(), $arrayQuery); 477 | } 478 | 479 | /** 480 | * @param Grammar|null $grammar 481 | * @return QueryBuilder 482 | */ 483 | protected function getQueryBuilder($grammar = null) 484 | { 485 | $grammar = $grammar ?: new Grammar; 486 | $processor = Mockery::mock(Processor::class); 487 | 488 | /** @var ConnectionInterface $connection */ 489 | $connection = Mockery::mock(ConnectionInterface::class); 490 | 491 | return new QueryBuilder($connection, $grammar, $processor); 492 | } 493 | 494 | /** 495 | * @return EloquentBuilder|QueryBuilder 496 | */ 497 | protected function getEloquentBuilder() 498 | { 499 | return new EloquentBuilder($this->getQueryBuilder()); 500 | } 501 | 502 | /** 503 | * @param EloquentBuilder|QueryBuilder $query 504 | * @return array 505 | */ 506 | protected function toQueryComponentsArray($query) 507 | { 508 | if ($query instanceof EloquentBuilder) { 509 | $query = $query->getQuery(); 510 | } 511 | 512 | $selectComponents = [ 513 | 'aggregate', 514 | 'columns', 515 | 'from', 516 | 'joins', 517 | 'wheres', 518 | 'groups', 519 | 'havings', 520 | 'orders', 521 | 'limit', 522 | 'offset', 523 | 'unions', 524 | 'lock', 525 | ]; 526 | 527 | $rawQueryComponents = []; 528 | 529 | foreach ($selectComponents as $component) { 530 | if (! is_null($query->$component)) { 531 | $rawQueryComponents[$component] = $query->$component; 532 | } 533 | } 534 | 535 | return json_decode(json_encode($rawQueryComponents), true); 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |