Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions config/query-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,18 @@
* Set this to `true` if you want to convert the firstName into first_name for the underlying query
*/
'convert_field_names_to_snake_case' => false,

/*
* Determines how filter property names are converted to scope method names.
*
* Available strategies:
* - 'camel': Converts 'published_in_year' to 'publishedInYear' (default)
* - 'none': Uses the exact name without conversion
* - 'auto': Tries exact match first, falls back to camelCase
*
* Example: GET /posts?filter[published_in_year]=2023
* - 'camel' calls: scopePublishedInYear()
* - 'none' calls: scopePublished_in_year()
*/
'scope_name_converter' => 'camel',
];
33 changes: 32 additions & 1 deletion src/Filters/FiltersScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public function __invoke(Builder $query, mixed $values, string $property): Build
{
$propertyParts = collect(explode('.', $property));

$scope = Str::camel($propertyParts->pop()); // TODO: Make this configurable?
$rawScope = $propertyParts->pop();

$scope = $this->resolveScopeName($query, $rawScope);

$values = array_values(Arr::wrap($values));
$values = $this->resolveParameters($query, $values, $scope);
Expand Down Expand Up @@ -75,6 +77,35 @@ protected function resolveParameters(Builder $query, $values, string $scope): ar
return $values;
}

protected function resolveScopeName(Builder $query, string $rawScope): string
{
$converter = config('query-builder.scope_name_converter', 'camel');

return match ($converter) {
'none' => $rawScope,
'auto' => $this->autoResolveScopeName($query, $rawScope),
default => Str::camel($rawScope),
};
}

protected function autoResolveScopeName(Builder $query, string $rawScope): string
{
$model = $query->getModel();

$exactMethod = 'scope' . ucfirst($rawScope);
if (method_exists($model, $exactMethod)) {
return $rawScope;
}

$camelized = Str::camel($rawScope);
$camelMethod = 'scope' . ucfirst($camelized);
if (method_exists($model, $camelMethod)) {
return $camelized;
}

return $camelized;
}

protected function getClass(ReflectionParameter $parameter): ?ReflectionClass
{
$type = $parameter->getType();
Expand Down
59 changes: 59 additions & 0 deletions tests/FilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -908,3 +908,62 @@ public function __invoke(Builder $query, $value, string $property): Builder

expect($results)->toHaveCount(2);
});

it('converts snake case filter names to camel case scope by default', function () {
Carbon::setTestNow(Carbon::parse('2016-05-05'));
TestModel::create(['name' => 'John Testing Doe']);

// 'created_between' should be converted to 'createdBetween' scope
$modelsResult = createQueryFromFilterRequest(['created_between' => '2016-01-01,2017-01-01'])
->allowedFilters(AllowedFilter::scope('created_between'))
->get();

expect($modelsResult)->toHaveCount(1);
});

it('respects scope name converter config with none option', function () {
$originalConfig = config('query-builder.scope_name_converter');
config(['query-builder.scope_name_converter' => 'none']);

TestModel::create(['name' => 'John Testing Doe']);

$modelsResult = createQueryFromFilterRequest(['named' => 'John Testing Doe'])
->allowedFilters(AllowedFilter::scope('named'))
->get();

expect($modelsResult)->toHaveCount(1);

config(['query-builder.scope_name_converter' => $originalConfig]);
});

it('uses auto converter to detect exact scope method name', function () {
$originalConfig = config('query-builder.scope_name_converter');
config(['query-builder.scope_name_converter' => 'auto']);

TestModel::create(['name' => 'John Testing Doe']);

$modelsResult = createQueryFromFilterRequest(['named' => 'John Testing Doe'])
->allowedFilters(AllowedFilter::scope('named'))
->get();

expect($modelsResult)->toHaveCount(1);

config(['query-builder.scope_name_converter' => $originalConfig]);
});

it('auto converter falls back to camel case when exact method not found', function () {
$originalConfig = config('query-builder.scope_name_converter');
config(['query-builder.scope_name_converter' => 'auto']);

Carbon::setTestNow(Carbon::parse('2016-05-05'));
TestModel::create(['name' => 'John Testing Doe']);

// 'created_between' exact scope doesn't exist, should fallback to 'createdBetween'
$modelsResult = createQueryFromFilterRequest(['created_between' => '2016-01-01,2017-01-01'])
->allowedFilters(AllowedFilter::scope('created_between'))
->get();

expect($modelsResult)->toHaveCount(1);

config(['query-builder.scope_name_converter' => $originalConfig]);
});