Skip to content

Commit d25b062

Browse files
freekmurzeclaude
andcommitted
Prepare v7 with modernized API and new features
Drop Laravel 10/11, require Laravel 12+, Pest 4, PHPUnit 12. Convert SortDirection to PHP enum, use variadic parameters across all allowed* methods, extract HandlesRelationConstraints trait, move delimiter config from global static state to config file, add aggregate includes (withMin/Max/Sum/Avg), add wildcard allow-all support with environment guard, rename filter classes, remove ordering constraint for allowedFields, raise PHPStan to level 6, and update all docs and tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 27b8ce9 commit d25b062

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1513
-704
lines changed

.github/workflows/run-tests.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,14 @@ jobs:
1313
matrix:
1414
os: [ubuntu-latest]
1515
php: [8.5, 8.4, 8.3, 8.2]
16-
laravel: ['10.*', '11.*', '12.*', '13.*']
16+
laravel: ['12.*', '13.*']
1717
stability: [prefer-stable]
1818
include:
19-
- laravel: 11.*
20-
testbench: 9.*
21-
- laravel: 10.*
22-
testbench: 8.*
2319
- laravel: 12.*
2420
testbench: 10.*
2521
- laravel: 13.*
2622
testbench: 11.*
2723
exclude:
28-
- laravel: 10.*
29-
php: 8.5
3024
- laravel: 13.*
3125
php: 8.2
3226

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ $userQuery = QueryBuilder::for($query) // start from an existing Builder instanc
7070

7171
```php
7272
$users = QueryBuilder::for(User::class)
73-
->allowedFields(['id', 'email'])
73+
->allowedFields('id', 'email')
7474
->get();
7575

7676
// the fetched `User`s will only have their id & email set

UPGRADING.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,125 @@
11
# Upgrading
22

3+
## From v6 to v7
4+
5+
### Requirements
6+
7+
- PHP 8.2+
8+
- Laravel 12 or 13
9+
10+
Support for Laravel 10 and 11 has been dropped.
11+
12+
### Variadic parameters
13+
14+
The `allowedFilters()`, `allowedSorts()`, `allowedIncludes()`, `allowedFields()`, `defaultSort()`, and `defaultSorts()` methods now accept variadic arguments instead of arrays.
15+
16+
```php
17+
// Before
18+
QueryBuilder::for(User::class)
19+
->allowedFilters(['name', 'email'])
20+
->allowedSorts(['name'])
21+
->allowedIncludes(['posts'])
22+
->allowedFields(['id', 'name']);
23+
24+
// After
25+
QueryBuilder::for(User::class)
26+
->allowedFilters('name', 'email')
27+
->allowedSorts('name')
28+
->allowedIncludes('posts')
29+
->allowedFields('id', 'name');
30+
```
31+
32+
If you have dynamic arrays, use the spread operator:
33+
34+
```php
35+
$filters = ['name', 'email'];
36+
QueryBuilder::for(User::class)->allowedFilters(...$filters);
37+
```
38+
39+
### `SortDirection` is now an enum
40+
41+
The `SortDirection` class with string constants has been replaced with a proper PHP enum.
42+
43+
```php
44+
// Before
45+
use Spatie\QueryBuilder\Enums\SortDirection;
46+
47+
AllowedSort::field('name')->defaultDirection(SortDirection::DESCENDING);
48+
49+
// After
50+
use Spatie\QueryBuilder\Enums\SortDirection;
51+
52+
AllowedSort::field('name')->defaultDirection(SortDirection::Descending);
53+
```
54+
55+
The `AllowedSort::defaultDirection()` method now requires a `SortDirection` enum value instead of a string.
56+
57+
### Filter renames
58+
59+
`AllowedFilter::beginsWithStrict()` has been renamed to `AllowedFilter::beginsWith()`.
60+
`AllowedFilter::endsWithStrict()` has been renamed to `AllowedFilter::endsWith()`.
61+
62+
```php
63+
// Before
64+
AllowedFilter::beginsWithStrict('name');
65+
AllowedFilter::endsWithStrict('name');
66+
67+
// After
68+
AllowedFilter::beginsWith('name');
69+
AllowedFilter::endsWith('name');
70+
```
71+
72+
### `AllowedInclude` factory methods now return `self` instead of `Collection`
73+
74+
`AllowedInclude::relationship()`, `AllowedInclude::count()`, `AllowedInclude::exists()`, `AllowedInclude::callback()`, and `AllowedInclude::custom()` now return a single `AllowedInclude` instance instead of a `Collection`. This should not affect most usage since you typically pass them to `allowedIncludes()`.
75+
76+
### Static delimiter methods removed
77+
78+
The static delimiter methods on `QueryBuilderRequest` have been removed (`setArrayValueDelimiter`, `setFilterArrayValueDelimiter`, `setSortsArrayValueDelimiter`, `setIncludesArrayValueDelimiter`, `setFieldsArrayValueDelimiter`, `setAppendsArrayValueDelimiter`, `resetDelimiters`).
79+
80+
Delimiters are now configured via the `delimiter` key in the config file:
81+
82+
```php
83+
// config/query-builder.php
84+
'delimiter' => ',',
85+
```
86+
87+
The `$arrayValueDelimiter` parameter has also been removed from all `AllowedFilter` factory methods.
88+
89+
### `allowedFields()` no longer needs to be called before `allowedIncludes()`
90+
91+
The `AllowedFieldsMustBeCalledBeforeAllowedIncludes` exception has been removed. You can now call `allowedFields()` and `allowedIncludes()` in any order.
92+
93+
### Config changes
94+
95+
The `disable_invalid_includes_query_exception` config key has been renamed to `disable_invalid_include_query_exception` (singular "include").
96+
97+
The `convert_relation_table_name_strategy` config now uses `null` instead of `false` as its default/disabled value.
98+
99+
A new `delimiter` config key has been added (default: `','`).
100+
101+
### Filter interface return type
102+
103+
The `Filter` interface's `__invoke` method now has an explicit `void` return type. If you have custom filter classes, update them:
104+
105+
```php
106+
// Before
107+
public function __invoke(Builder $query, $value, string $property)
108+
109+
// After
110+
public function __invoke(Builder $query, mixed $value, string $property): void
111+
```
112+
113+
The same applies to the `Sort` interface and `IncludeInterface`.
114+
115+
### Filter class hierarchy refactored
116+
117+
`FiltersPartial` no longer extends `FiltersExact`, and `FiltersOperator` no longer extends `FiltersExact`. If you were extending these classes and relying on the inheritance chain, note that relation constraint handling is now provided via the `HandlesRelationConstraints` trait in `Spatie\QueryBuilder\Filters\Concerns`.
118+
119+
### PHPStan level raised to 6
120+
121+
The PHPStan analysis level has been bumped from 5 to 6.
122+
3123
## From v5 to v6
4124

5125
A lot of the query builder classes now have typed properties and method parameters. If you have any custom sorts, includes, or filters, you will need to specify the property and parameter types used.

composer.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@
2121
],
2222
"require": {
2323
"php": "^8.2",
24-
"illuminate/database": "^10.0|^11.0|^12.0|^13.0",
25-
"illuminate/http": "^10.0|^11.0|^12.0|^13.0",
26-
"illuminate/support": "^10.0|^11.0|^12.0|^13.0",
24+
"illuminate/database": "^12.0|^13.0",
25+
"illuminate/http": "^12.0|^13.0",
26+
"illuminate/support": "^12.0|^13.0",
2727
"spatie/laravel-package-tools": "^1.11"
2828
},
2929
"require-dev": {
3030
"ext-json": "*",
31-
"larastan/larastan": "^2.7 || ^3.3",
31+
"larastan/larastan": "^3.3",
3232
"mockery/mockery": "^1.4",
33-
"orchestra/testbench": "^7.0|^8.0|^10.0|^11.0",
34-
"pestphp/pest": "^2.0|^3.7|^4.0",
35-
"phpunit/phpunit": "^10.0|^11.5.3|^12.0",
33+
"orchestra/testbench": "^10.0|^11.0",
34+
"pestphp/pest": "^4.0",
35+
"phpunit/phpunit": "^12.0",
3636
"spatie/invade": "^2.0"
3737
},
3838
"autoload": {

config/query-builder.php

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@
1010
*/
1111
'parameters' => [
1212
'include' => 'include',
13-
1413
'filter' => 'filter',
15-
1614
'sort' => 'sort',
17-
1815
'fields' => 'fields',
19-
2016
'append' => 'append',
2117
],
2218

19+
/*
20+
* The delimiter used to split array values in query parameters.
21+
* For example: ?filter[name]=John,Jane uses ',' as delimiter.
22+
*/
23+
'delimiter' => ',',
24+
2325
/*
2426
* Related model counts are included using the relationship name suffixed with this string.
2527
* For example: GET /users?include=postsCount
@@ -32,6 +34,30 @@
3234
*/
3335
'exists_suffix' => 'Exists',
3436

37+
/*
38+
* Related model min aggregate is included using the relationship name suffixed with this string.
39+
* For example: GET /users?include=postsViewsMin
40+
*/
41+
'min_suffix' => 'Min',
42+
43+
/*
44+
* Related model max aggregate is included using the relationship name suffixed with this string.
45+
* For example: GET /users?include=postsViewsMax
46+
*/
47+
'max_suffix' => 'Max',
48+
49+
/*
50+
* Related model sum aggregate is included using the relationship name suffixed with this string.
51+
* For example: GET /users?include=postsViewsSum
52+
*/
53+
'sum_suffix' => 'Sum',
54+
55+
/*
56+
* Related model avg aggregate is included using the relationship name suffixed with this string.
57+
* For example: GET /users?include=postsViewsAvg
58+
*/
59+
'avg_suffix' => 'Avg',
60+
3561
/*
3662
* By default the package will throw an `InvalidFilterQuery` exception when a filter in the
3763
* URL is not allowed in the `allowedFilters()` method.
@@ -48,7 +74,7 @@
4874
* By default the package will throw an `InvalidIncludeQuery` exception when an include in the
4975
* URL is not allowed in the `allowedIncludes()` method.
5076
*/
51-
'disable_invalid_includes_query_exception' => false,
77+
'disable_invalid_include_query_exception' => false,
5278

5379
/*
5480
* By default, the package expects relationship names to be snake case plural when using fields[relationship].
@@ -71,7 +97,7 @@
7197
* `camelCase` => Matches table names like 'top_orders' to 'fields[topOrders]'
7298
* `none` => Uses the exact table name
7399
*/
74-
'convert_relation_table_name_strategy' => false,
100+
'convert_relation_table_name_strategy' => null,
75101

76102
/*
77103
* By default, the package expects the field names to match the database names

docs/advanced-usage/extending-query-builder.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ As the `QueryBuilder` extends Laravel's default Eloquent query builder you can u
77

88
```php
99
QueryBuilder::for(User::where('id', 42)) // base query instead of model
10-
->allowedIncludes(['posts'])
10+
->allowedIncludes('posts')
1111
->where('activated', true) // chain on any of Laravel's query methods
1212
->first(); // we only need one specific user
1313
```

docs/advanced-usage/multi-value-delimiter.md

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,20 @@ title: Multi value delimiter
33
weight: 4
44
---
55

6-
Sometimes values to filter for could include commas. This is why you can specify the delimiter symbol using the `QueryBuilderRequest` to overwrite the default behaviour.
6+
Sometimes values to filter for could include commas. You can change the delimiter used to split array values by setting the `delimiter` key in the `query-builder` config file.
77

88
```php
9-
// GET /api/endpoint?filter=12,4V|4,7V|2,1V
9+
// config/query-builder.php
1010

11-
QueryBuilderRequest::setArrayValueDelimiter('|');
12-
13-
QueryBuilder::for(Model::class)
14-
->allowedFilters(AllowedFilter::exact('voltage'))
15-
->get();
16-
17-
// filters: [ 'voltage' => [ '12,4V', '4,7V', '2,1V' ]]
11+
return [
12+
'delimiter' => '|',
13+
];
1814
```
1915

20-
__Note that this applies to ALL values for filters, includes and sorts__
21-
22-
## Usage
23-
24-
There are multiple opportunities where the delimiter can be set.
25-
26-
You can define it in a `ServiceProvider` to apply it globally, or define a middleware that can be applied only on certain `Controllers`.
27-
```php
28-
// YourServiceProvider.php
29-
public function boot() {
30-
QueryBuilderRequest::setArrayValueDelimiter(';');
31-
}
32-
33-
// ApplySemicolonDelimiterMiddleware.php
34-
public function handle($request, $next) {
35-
QueryBuilderRequest::setArrayValueDelimiter(';');
36-
return $next($request);
37-
}
38-
```
16+
With this configuration, a request like `GET /api/endpoint?filter[voltage]=12,4V|4,7V|2,1V` would be parsed as:
3917

40-
You can also set the delimiter for each feature individually:
4118
```php
42-
QueryBuilderRequest::setIncludesArrayValueDelimiter(';'); // Includes
43-
QueryBuilderRequest::setAppendsArrayValueDelimiter(';'); // Appends
44-
QueryBuilderRequest::setFieldsArrayValueDelimiter(';'); // Fields
45-
QueryBuilderRequest::setSortsArrayValueDelimiter(';'); // Sorts
46-
QueryBuilderRequest::setFilterArrayValueDelimiter(';'); // Filter
19+
// filters: [ 'voltage' => [ '12,4V', '4,7V', '2,1V' ]]
4720
```
4821

49-
You can override the default delimiter for single filters:
50-
```php
51-
// GET /api/endpoint?filter[id]=h4S4MG3(+>azv4z/I<o>,>XZII/Q1On
52-
AllowedFilter::exact('id', 'ref_id', true, ';');
53-
```
22+
__Note that this applies to ALL values for filters, includes and sorts.__

docs/advanced-usage/pagination.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ By default the query parameters wont be added to the pagination json. You can ap
1313

1414
```php
1515
$users = QueryBuilder::for(User::class)
16-
->allowedFilters(['name', 'email'])
16+
->allowedFilters('name', 'email')
1717
->paginate()
1818
->appends(request()->query());
1919
```

0 commit comments

Comments
 (0)