Skip to content

Commit

Permalink
Merge branch 'release/1.5.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cerbero90 committed Jul 14, 2020
2 parents aa8a20a + 08e503b commit a28f749
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 11 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -7,6 +7,7 @@ php:
- 7.1
- 7.2
- 7.3
- 7.4

# This triggers builds to run on the new TravisCI infrastructure.
# See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,12 @@ All Notable changes to `query-filters` will be documented in this file.

Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles.

## 1.5.0 - 2020-07-14

### Added
- Values of filters can be validated: only filters that pass the validation are applied.


## 1.4.1 - 2020-03-04

### Added
Expand Down
17 changes: 17 additions & 0 deletions README.md
Expand Up @@ -165,6 +165,23 @@ public function index(Request $request)
}
```

Sometimes you may want to silently skip filters if their value is not valid. Instead of setting validation rules in HTTP requests, you may define them in the query filters class.

This avoids a failed validation to stop the filtering process and applies only valid filters while ignoring filters with values that do not pass the validation:

```php
class ActorFilters extends QueryFilters
{
protected function getRules()
{
return [
'acting' => 'bool',
'acted-in' => 'int|digits:4',
];
}
}
```

## Change log

Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
Expand Down
2 changes: 1 addition & 1 deletion src/Console/Commands/MakeQueryFiltersCommand.php
Expand Up @@ -114,7 +114,7 @@ protected function getArguments()
{
return [
['name', InputArgument::REQUIRED, 'The name of the class'],
['filters', InputArgument::OPTIONAL, 'The name of the filters e.g. won_oscar&acting=bool&acted-in=year'],
['filters', InputArgument::OPTIONAL, "The name of the filters e.g. 'won_oscar&acting=bool&acted-in=year'"],
];
}
}
30 changes: 23 additions & 7 deletions src/QueryFilters.php
Expand Up @@ -4,6 +4,8 @@

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use ReflectionMethod;

Expand Down Expand Up @@ -71,10 +73,8 @@ public function applyToQuery(Builder $query)
$this->query = $query;

foreach ($this->request->all() as $filter => $value) {
$method = Str::camel($filter);

if ($this->filterCanBeApplied($method, $value)) {
call_user_func([$this, $method], $value);
if ($this->filterCanBeApplied($filter, $value)) {
call_user_func([$this, Str::camel($filter)], $value);
}
}

Expand All @@ -90,17 +90,33 @@ public function applyToQuery(Builder $query)
*/
protected function filterCanBeApplied($filter, $value)
{
$method = Str::camel($filter);

// do not apply query filters that haven't been implemented
if (!method_exists($this, $filter)) {
if (!method_exists($this, $method)) {
return false;
}

// apply query filters with valid values
if ($value !== '' && $value !== null) {
return true;
$data = $this->request->only($filter);
$rules = Arr::only($this->getRules(), $filter);

return !Validator::make($data, $rules)->fails();
}

// apply query filters that don't need values (implicit filters)
return (new ReflectionMethod($this, $filter))->getNumberOfParameters() === 0;
return (new ReflectionMethod($this, $method))->getNumberOfParameters() === 0;
}

/**
* Retrieve the rules to validate filters value.
* If a filter validation fails, the filter is not applied.
*
* @return array
*/
protected function getRules()
{
return [];
}
}
18 changes: 18 additions & 0 deletions tests/DummyModelTest.php
Expand Up @@ -2,7 +2,9 @@

namespace Cerbero\QueryFilters;

use Illuminate\Contracts\Validation\Validator as ValidatorContract;
use Illuminate\Database\MySqlConnection;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Mockery;
use PHPUnit\Framework\TestCase;
Expand All @@ -24,6 +26,20 @@ public function filtersRecordsBasedOnQueryFilters()
'acted-in' => '2000',
]);

$validator = Mockery::mock(ValidatorContract::class)
->expects()
->make(Mockery::type('array'), Mockery::type('array'))
->twice()
->andReturnSelf()
->getMock()
->expects()
->fails()
->twice()
->andReturns(false)
->getMock();

Validator::swap($validator);

$pdo = Mockery::mock('PDO');
$connection = new MySqlConnection($pdo);

Expand All @@ -39,5 +55,7 @@ public function filtersRecordsBasedOnQueryFilters()
$actual = Str::replaceArray('?', $queryBuilder->getBindings(), $queryBuilder->toSql());

$this->assertSame($expected, $actual);

Mockery::close();
}
}
25 changes: 22 additions & 3 deletions tests/DummyQueryFiltersTest.php
Expand Up @@ -8,7 +8,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Mockery;
use PHPUnit\Framework\TestCase;
use Orchestra\Testbench\TestCase;

/**
* The dummy query filters test.
Expand Down Expand Up @@ -53,15 +53,17 @@ public function appliesFilters()
* Retrieve the SQL statement after query filters are applied based on the given request
*
* @param \Illuminate\Http\Request $request
* @param string $filtersClass
* @return string
*/
private function getSqlAfterApplyingFiltersFromRequest(Request $request)
private function getSqlAfterApplyingFiltersFromRequest(Request $request, $filtersClass = null)
{
$pdo = Mockery::mock('PDO');
$queryBuilder = new QueryBuilder(new MySqlConnection($pdo));
$eloquentBuilder = new EloquentBuilder($queryBuilder);
$filtersClass = $filtersClass ?: DummyQueryFilters::class;

(new DummyQueryFilters($request))->applyToQuery($eloquentBuilder);
(new $filtersClass($request))->applyToQuery($eloquentBuilder);

return Str::replaceArray('?', $queryBuilder->getBindings(), $queryBuilder->toSql());
}
Expand Down Expand Up @@ -100,4 +102,21 @@ public function doesNotApplyExplicitFiltersWithNoValue()

$this->assertSame($expected, $actual);
}

/**
* @test
*/
public function doesNotApplyFiltersWithInvalidValue()
{
$request = new Request([
'won_oscar' => null, // implicit filter (does not need a value), will be applied
'acting' => 'abc', // should be a boolean, won't be applied
'acted-in' => 2000, // the year is valid, will be applied
]);

$expected = 'select * where `oscars` > 0 and year(`started_acting_at`) <= 2000 and year(`finished_acting_at`) >= 2000';
$actual = $this->getSqlAfterApplyingFiltersFromRequest($request, ValidatedDummyQueryFilters::class);

$this->assertSame($expected, $actual);
}
}
24 changes: 24 additions & 0 deletions tests/ValidatedDummyQueryFilters.php
@@ -0,0 +1,24 @@
<?php

namespace Cerbero\QueryFilters;

/**
* Filter records based on query parameters.
*
*/
class ValidatedDummyQueryFilters extends DummyQueryFilters
{
/**
* Retrieve the rules to validate filters value.
* If a filter validation fails, the filter is not applied.
*
* @return array
*/
protected function getRules()
{
return [
'acting' => 'bool',
'acted-in' => 'int|digits:4',
];
}
}

0 comments on commit a28f749

Please sign in to comment.