From 8f1a8c567ee2c79b5f1f5b647de60476f63cb7d3 Mon Sep 17 00:00:00 2001 From: Martin Kiesel Date: Sun, 30 Sep 2018 16:43:23 +0200 Subject: [PATCH] Implement #10 - add `hasAnyFilter` macro on request - improve test suite - update readme - update changelog - remove Laravel 5.5 from Travis-CI configuration file --- .travis.yml | 4 - CHANGELOG.md | 18 ++++ README.md | 47 +++++++--- src/Filter.php | 56 +++++++++++- src/FilterContract.php | 26 +++++- src/Filterable.php | 6 +- src/FilterableServiceProvider.php | 15 ++++ src/Generic/Filter.php | 51 +++++++---- src/helpers.php | 24 +++++ tests/DefaultFilterTest.php | 97 +++++++++++++++++++++ tests/DefaultGenericFilterTest.php | 65 ++++++++++++++ tests/FilterTest.php | 48 ++++++++++ tests/GenericFilterTest.php | 82 +++++++++++------ tests/{JoinTest.php => JoinSupportTest.php} | 6 +- tests/RequestMacrosTest.php | 78 +++++++++++++++++ tests/Stubs/Filter.php | 29 ++++++ tests/Stubs/GenericFilter.php | 37 ++++++++ tests/Stubs/RoleFilter.php | 12 --- tests/TestCase.php | 13 +-- 19 files changed, 631 insertions(+), 83 deletions(-) create mode 100644 tests/DefaultFilterTest.php create mode 100644 tests/DefaultGenericFilterTest.php create mode 100644 tests/FilterTest.php rename tests/{JoinTest.php => JoinSupportTest.php} (93%) create mode 100644 tests/RequestMacrosTest.php create mode 100644 tests/Stubs/Filter.php create mode 100644 tests/Stubs/GenericFilter.php diff --git a/.travis.yml b/.travis.yml index d8bd7a6..7638713 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ git: depth: 3 env: - - version=L55 - version=L56 - version=L57 @@ -24,7 +23,6 @@ before_install: install: - travis_retry composer install --no-interaction --prefer-dist --no-suggest; - - if [[ $version = 'L55' ]]; then travis_retry composer require --dev --update-with-dependencies --no-suggest --no-interaction orchestra/testbench:"3.5.*" phpunit/phpunit:"^6.0"; fi - if [[ $version = 'L56' ]]; then travis_retry composer require --dev --update-with-dependencies --no-suggest --no-interaction orchestra/testbench:"3.6.*" phpunit/phpunit:"^7.0"; fi - if [[ $version = 'L57' ]]; then travis_retry composer require --dev --update-with-dependencies --no-suggest --no-interaction orchestra/testbench:"3.7.*" phpunit/phpunit:"^7.0"; fi @@ -33,5 +31,3 @@ script: vendor/bin/phpunit branches: only: - master - - L5.5-6 - - L5.5-7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a74eb6..d3896fa 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to `kyslik/laravel-filterable` will be documented in this file +## 2.0.0 - 2018-10-01 + +### Added + +- default filtering see [Additional features](https://github.com/Kyslik/laravel-filterable#additional-features) section + +### Changed + +- trait `JoinSupport` namespace moved up one level +- signature of [`FilterContract`](https://github.com/Kyslik/laravel-filterable/blob/master/src/FilterContract.php) +- dropped support for Laravel 5.5 + - **reason**: while using default filtering; filter needs to `abort(redirect())`, which was introduced in Laravel 5.6 + +### Improved + +- test-suite +- readme + ## 1.1.3 - 2018-09-04 ### Added diff --git a/README.md b/README.md index c22f0b5..02f4cab 100755 --- a/README.md +++ b/README.md @@ -20,14 +20,11 @@ You may continue by publishing [configuration](./config/filterable.php) by issui ## Introduction -Package lets you to create && apply two kinds of filters - -1. **custom** -1. **generic** +Package lets you to create && apply two kinds of filters **custom** and **generic**. ### Custom filters -**Custom** filters are just like in Jeffrey's video. You define a logic on a builder instance and package applies it via [local scope](https://laravel.com/docs/5.6/eloquent#local-scopes). +**Custom** filters are just like in Jeffrey's video. You define a logic on a builder instance and package applies it via [local scope](https://laravel.com/docs/5.7/eloquent#local-scopes). Let's say a product requires displaying recently created records. You create a method `recent($minutes = null)` inside a filter class, which returns Builder instance: @@ -87,7 +84,7 @@ public function recent($minutes = null): \Illuminate\Database\Eloquent\Builder While using both **custom** or **generic** filters you must: -1. have [local scope](https://laravel.com/docs/5.6/eloquent#local-scopes) on model with the signature `scopeFilter(Builder $query, FILTERNAME $filters)` +1. have [local scope](https://laravel.com/docs/5.7/eloquent#local-scopes) on model with the signature `scopeFilter(Builder $query, FILTERNAME $filters)` 2. have particular (`FILTERNAME`) filter class that extends one of: - `Kyslik\LaravelFilterable\Generic\Filter` class - allows usage of both **custom** & **generic** filters - `Kyslik\LaravelFilterable\Filter` class - allows usage of only **custom** filters @@ -116,7 +113,7 @@ return [ ### Example with custom filters -Let's say you want to use filterable on `User` model. You will have to create the filter class `App/Filters/UserFilter.php`, specify `filterMap()` and **filter** method (`recent(...)`) with the custom logic. +Let's say you want to use filterable on `User` model. You will have to create the filter class `App/Filters/UserFilter.php` (`php artisan make:filter UserFilter`), specify `filterMap()` and **filter** method (`recent(...)`) with the custom logic. ```php **Note**: `filterMap()` shall return an associative array where **key** is a method name and **value** is either alias or array of aliases -Now add a [local scope](https://laravel.com/docs/5.6/eloquent#local-scopes) to the `User` model via [Filterable](https://github.com/Kyslik/laravel-filterable/blob/master/src/Filterable.php): +Now add a [local scope](https://laravel.com/docs/5.7/eloquent#local-scopes) to the `User` model via [Filterable](https://github.com/Kyslik/laravel-filterable/blob/master/src/Filterable.php): ```php use Kyslik\LaravelFilterable\Filterable; @@ -170,7 +167,7 @@ Now end-user can visit `users?recent` or `users?recently` or `users?recent=25` a ### Example with generic filters -Let's say you want to use generic filters on `User` model. You will have to create filter class `App/Filters/UserFilter.php` and specify `$filterables` just like below: +Let's say you want to use generic filters on `User` model. You will have to create filter class `App/Filters/UserFilter.php` (`php artisan make:filter UserFilter -g`) and specify `$filterables` just like below: ```php default(['recent' => now()->toDateTimeString(), 'filter-id' => '!=5']); + + return $user->filter($filter)->paginate(); +} +``` + +End-user is going be redirected from `http://filters.test/users` to `http://filters.test/users?recent=2018-10-01 13:52:40&filter-id=!=5`. +In case the filter that you specify as *default* does not exist `Kyslik\LaravelFilterable\Exceptions\InvalidArgumentException` is thrown. + +> **Caution**: be careful of **infinite redirects** + +You can read more about the feature in the [original issue #10](https://github.com/Kyslik/laravel-filterable/issues/10). + +#### JoinSupport for filters + +TBA + +You can read more about the feature in the [original PR #9](https://github.com/Kyslik/laravel-filterable/pull/9). + ## Testing ``` bash diff --git a/src/Filter.php b/src/Filter.php index 9c80352..9cf7df6 100644 --- a/src/Filter.php +++ b/src/Filter.php @@ -3,7 +3,9 @@ namespace Kyslik\LaravelFilterable; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Http\Request; +use Kyslik\LaravelFilterable\Exceptions\InvalidArgumentException; use Kyslik\LaravelFilterable\Exceptions\MissingBuilderInstance; abstract class Filter implements FilterContract @@ -44,6 +46,33 @@ public function apply(Builder $builder): Builder } + /** + * @inheritdoc + */ + public function availableFilters(): array + { + return array_flatten($this->filterMap); + } + + + /** + * @param array $defaults + * @param int $code + * + * @throws \Kyslik\LaravelFilterable\Exceptions\InvalidArgumentException + */ + public function default(array $defaults, int $code = 307) + { + if ($this->request->isMethod('GET') && ! empty($defaults) && ! $this->request->hasAnyFilter()) { + $appends = $this->appendableDefaults($defaults); + + if ( ! empty($appends)) { + throw new HttpResponseException(redirect($this->request->fullUrlWithQuery($appends), $code)); + } + } + } + + /** * @return \Illuminate\Database\Eloquent\Builder */ @@ -66,6 +95,29 @@ public function setBuilder(Builder $builder) } + /** + * @param array $defaults + * + * @return array + * @throws \Kyslik\LaravelFilterable\Exceptions\InvalidArgumentException + */ + protected function appendableDefaults(array $defaults): array + { + $appends = []; + $filters = $this->availableFilters(); + $defaults = force_assoc_array($defaults, ''); + + foreach ($defaults as $filter => $default) { + if ( ! in_array($filter, $filters)) { + throw new InvalidArgumentException('Attempting to use default filter \''.$filter.'\', with no effect.'); + } + $appends[$filter] = $default; + } + + return $appends; + } + + /** * @throws \Kyslik\LaravelFilterable\Exceptions\MissingBuilderInstance */ @@ -91,7 +143,7 @@ protected function applyFilters() $this->builder = (is_null($value)) ? $this->$filter() : $this->$filter($value); continue; } - throw new \Exception('Filter \''.$filter.'\' is declared in \'filterMap\', but it does not exist.'); + throw new InvalidArgumentException('Filter \''.$filter.'\' is declared in \'filterMap\', but it does not exist.'); } return $this; @@ -107,7 +159,7 @@ private function filters(): array foreach ($this->filterMap as $filter => $value) { $method = (is_string($filter)) ? $filter : $value; - // head([]) === false, we check if head returns false and remove that item from array + // head([]) === false, we check if head returns false and remove that item from array, I am sorry if (($filters[$method] = head($this->request->only($value))) === false) { unset($filters[$method]); } diff --git a/src/FilterContract.php b/src/FilterContract.php index 0735c0a..abd234d 100644 --- a/src/FilterContract.php +++ b/src/FilterContract.php @@ -7,7 +7,31 @@ interface FilterContract { - function apply(Builder $builder); + /** + * Applies filters on a $builder instance. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * + * @return \Illuminate\Database\Eloquent\Builder + */ + function apply(Builder $builder): Builder; + + + /** + * @param array $defaults + * @param int $code + * + * @throws \Kyslik\LaravelFilterable\Exceptions\InvalidArgumentException + */ + function default(array $defaults, int $code = 307); + + + /** + * Available filters that we can expect in the query string. + * + * @return array + */ + public function availableFilters(): array; /** diff --git a/src/Filterable.php b/src/Filterable.php index ee586bc..72313bf 100644 --- a/src/Filterable.php +++ b/src/Filterable.php @@ -9,12 +9,12 @@ trait Filterable /** * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Kyslik\LaravelFilterable\FilterContract $filters + * @param \Kyslik\LaravelFilterable\FilterContract $filter * * @return \Illuminate\Database\Eloquent\Builder */ - public function scopeFilter(Builder $query, FilterContract $filters) + public function scopeFilter(Builder $query, FilterContract $filter) { - return $filters->apply($query); + return $filter->apply($query); } } diff --git a/src/FilterableServiceProvider.php b/src/FilterableServiceProvider.php index 4345843..173e421 100755 --- a/src/FilterableServiceProvider.php +++ b/src/FilterableServiceProvider.php @@ -2,7 +2,9 @@ namespace Kyslik\LaravelFilterable; +use Illuminate\Http\Request; use Illuminate\Support\ServiceProvider; +use Kyslik\LaravelFilterable\Exceptions\InvalidArgumentException; class FilterableServiceProvider extends ServiceProvider { @@ -19,6 +21,19 @@ public function boot() if ($this->app->runningInConsole()) { $this->commands(FilterMakeCommand::class); } + + Request::macro('hasAnyFilter', function (?FilterContract $filter = null) { + if (is_null($filter)) { + $filter = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 4)[3]['object'] ?? null; + } + + if ( ! $filter instanceof FilterContract) { + throw new InvalidArgumentException('Macro \'->hasAnyFilter\' requires a parameter of a \Kyslik\LaravelFilterable\FilterContract.'); + } + + /** @var Request $this */ + return $this->hasAny($filter->availableFilters()); + }); } diff --git a/src/Generic/Filter.php b/src/Generic/Filter.php index 7e6e2df..7c05b39 100644 --- a/src/Generic/Filter.php +++ b/src/Generic/Filter.php @@ -15,6 +15,8 @@ abstract class Filter extends BaseFilter protected $filterables = []; + protected $prefixedFilterables = []; + /** * @var string * */ @@ -74,6 +76,25 @@ function __construct(Request $request, Templater $templater) $this->settings(); $this->determineGroupingOperator(); $this->loadFilterTypes(config('filterable.filter_types', [])); + $this->prefixFilterables(); + } + + + /** + * @param string $defaultFilterType + */ + public function setDefaultFilterType($defaultFilterType) + { + $this->defaultFilterType = $defaultFilterType; + } + + + /** + * @param string $prefix + */ + public function setPrefix($prefix) + { + $this->prefix = $prefix; } @@ -94,27 +115,21 @@ public function apply(Builder $builder): Builder } - public function filterMap(): array - { - return []; - } - - /** - * @param string $defaultFilterType + * @inheritdoc */ - public function setDefaultFilterType($defaultFilterType) + public function availableFilters(): array { - $this->defaultFilterType = $defaultFilterType; + return array_merge($this->prefixedFilterables, parent::availableFilters()); } /** - * @param string $prefix + * @inheritdoc */ - public function setPrefix($prefix) + public function filterMap(): array { - $this->prefix = $prefix; + return []; } @@ -193,6 +208,14 @@ protected function applyGenericFilters() } + protected function prefixFilterables() + { + $this->prefixedFilterables = array_map(function ($value) { + return $this->prefix.$value; + }, $this->filterables); + } + + private function loadFilterTypes($configuration) { $types = []; @@ -263,9 +286,7 @@ private function determineGenericFilters() private function filters() { // Grab all data from query strings that start with the prefix $this->prefix. - $data = $this->request->only(array_map(function ($value) { - return $this->prefix.$value; - }, $this->filterables)); + $data = $this->request->only($this->prefixedFilterables); // Get rid of empty values. $data = array_filter($data, 'strlen'); diff --git a/src/helpers.php b/src/helpers.php index 0680d59..5a5e99b 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -15,3 +15,27 @@ function remove_prefix($prefix, $subject, $check = true) return $check ? str_replace_first($prefix, '', $subject) : substr($subject, strlen($prefix)); } } + +if ( ! function_exists('force_assoc_array')) { + /** + * Transforms array to be associative. + * + * @param array $array + * @param null $empty + * + * @return array + */ + function force_assoc_array(array $array, $empty = null) + { + $new = []; + foreach ($array as $key => $value) { + if (is_numeric($key)) { + $new[$value] = $empty; + } else { + $new[$key] = $value; + } + } + + return $new; + } +} diff --git a/tests/DefaultFilterTest.php b/tests/DefaultFilterTest.php new file mode 100644 index 0000000..d088fda --- /dev/null +++ b/tests/DefaultFilterTest.php @@ -0,0 +1,97 @@ +expectException(\Illuminate\Http\Exceptions\HttpResponseException::class); + $filter = $this->buildFilter(DefaultFilter::class, 'page=1'); + + $filter->default(['name' => 'neo']); + } + + + function test_redirect_does_not_happen() + { + $filter = $this->buildFilter(DefaultFilter::class, 'name=neo&scheduled'); + + $filter->default(['scheduled']); + + $this->assertTrue(true, 'We are testing, that \Illuminate\Http\Exceptions\HttpResponseException is not thrown.'); + } + + + function test_applied_prevents_redirect() + { + $filter = $this->buildFilter(DefaultFilter::class, 'name=neo&scheduled'); + $filter->default(['name' => 'tank']); + + $this->assertTrue(true, 'We are testing, that \Illuminate\Http\Exceptions\HttpResponseException is not thrown.'); + } + + + function test_appendable_defaults_throws_up() + { + $this->expectException(InvalidArgumentException::class); + + $filter = $this->buildFilter(DefaultFilter::class); + $filter->default(['joe']); + } + + + function test_defaults_redirect_with_correct_query() + { + $filter = $this->buildFilter(DefaultFilter::class); + + try { + $code = 307; + $filter->default(['name' => 'neo', 'scheduled'], $code); + } catch (\Illuminate\Http\Exceptions\HttpResponseException $e) { + $response = $e->getResponse(); + + $this->assertEquals($response->getStatusCode(), $code); + $this->assertTrue($response->isRedirection()); + + $parameters = []; + parse_str(parse_url($response->headers->get('location'), PHP_URL_QUERY), $parameters); + $this->assertEquals(['name' => 'neo', 'scheduled' => ''], $parameters); + } + } +} + +class DefaultFilter extends Filter +{ + + function filterMap(): array + { + return [ + 'name' => 'name', + 'active' => ['active', 'valid'], + 'scheduled' => 'scheduled', + ]; + } + + + public function name() + { + return $this->builder; + } + + + public function active() + { + return $this->builder; + } + + + public function scheduled() + { + return $this->builder; + } +} diff --git a/tests/DefaultGenericFilterTest.php b/tests/DefaultGenericFilterTest.php new file mode 100644 index 0000000..4fff605 --- /dev/null +++ b/tests/DefaultGenericFilterTest.php @@ -0,0 +1,65 @@ +expectException(\Illuminate\Http\Exceptions\HttpResponseException::class); + $filter = $this->buildGenericFilter(GenericFilter::class, 'page=1'); + + $filter->default(['filter-id' => '1']); + } + + + function test_redirect_does_not_happen() + { + $filter = $this->buildGenericFilter(GenericFilter::class, 'filter-name=~neo'); + + $filter->default(['created_at' => now()]); + + $this->assertTrue(true, 'We are testing, that \Illuminate\Http\Exceptions\HttpResponseException is not thrown.'); + } + + + function test_applied_prevents_redirect() + { + $filter = $this->buildGenericFilter(GenericFilter::class, 'filter-name=~neo'); + $filter->default(['filter-name' => '~tank']); + + $this->assertTrue(true, 'We are testing, that \Illuminate\Http\Exceptions\HttpResponseException is not thrown.'); + } + + function test_appendable_defaults_throws_up() + { + $this->expectException(InvalidArgumentException::class); + + $filter = $this->buildGenericFilter(GenericFilter::class); + $filter->default(['joe']); + } + + + function test_defaults_redirect_with_correct_query() + { + $filter = $this->buildGenericFilter(GenericFilter::class); + + try { + $code = 307; + $filter->default(['filter-name' => '~neo', 'filter-id' => '!=1'], $code); + } catch (\Illuminate\Http\Exceptions\HttpResponseException $e) { + $response = $e->getResponse(); + + $this->assertEquals($response->getStatusCode(), $code); + $this->assertTrue($response->isRedirection()); + + $parameters = []; + parse_str(parse_url($response->headers->get('location'), PHP_URL_QUERY), $parameters); + $this->assertEquals(['filter-name' => '~neo', 'filter-id' => '!=1'], $parameters); + } + } +} diff --git a/tests/FilterTest.php b/tests/FilterTest.php new file mode 100644 index 0000000..35eaf7d --- /dev/null +++ b/tests/FilterTest.php @@ -0,0 +1,48 @@ +buildFilter(Filter::class); + + $expected = ['new' => '', 'active' => '', 'scheduled' => '']; + $this->assertEquals($expected, $filter->appendableDefaults(['active', 'new', 'scheduled'])); + } + + + function test_available_filters() + { + $filter = $this->buildFilter(Filter::class); + $this->assertEquals(['active', 'new', 'scheduled'], $filter->availableFilters()); + } + + + function test_filter_is_applied_once() + { + $filter = $this->buildFilter(Filter::class, 'new&scheduled'); + $this->assertEquals('select * where "recent" = \'1\'', $this->dumpQuery($filter->apply($this->builder))); + $this->resetBuilder(); + } + + + function test_filter_defined_but_not_implemented() + { + $this->expectException(InvalidArgumentException::class); + + $filter = $this->buildFilter(Filter::class, 'active'); + $filter->apply($this->builder); + } +} diff --git a/tests/GenericFilterTest.php b/tests/GenericFilterTest.php index aa5b080..b129a38 100644 --- a/tests/GenericFilterTest.php +++ b/tests/GenericFilterTest.php @@ -3,6 +3,8 @@ namespace Kyslik\LaravelFilterable\Test; use Carbon\Carbon; +use Kyslik\LaravelFilterable\Test\Stubs\GenericFilter; +use Kyslik\LaravelFilterable\Test\Stubs\UserFilter; class GenericFilterTest extends TestCase { @@ -23,6 +25,26 @@ public function setUp() config()->set('filterable.prefix', $this->prefix); } + function test_appendable_defaults_returns_correct_array() + { + $filter = $this->buildGenericFilter(GenericFilter::class); + + $expected = ['name' => '', 'f-id' => '1']; + $this->assertEquals($expected, $filter->appendableDefaults(['name', 'f-id' => '1'])); + } + + function test_available_filters() + { + $filter = $this->buildGenericFilter(GenericFilter::class); + $this->assertEquals(['f-id', 'f-name', 'f-created_at', 'name'], $filter->availableFilters()); + } + + function test_filter_is_applied_once() + { + $filter = $this->buildGenericFilter(GenericFilter::class, 'name=one&name=neo&f-id=1&f-id=2'); + $this->assertEquals('select * where "name" = \'neo\' and "id" = \'2\'', $this->dumpQuery($filter->apply($this->builder))); + $this->resetBuilder(); + } function test_grouping_operator_is_determined() { @@ -30,7 +52,7 @@ function test_grouping_operator_is_determined() $anonymous = function ($query, $expected, $default) { config()->set('filterable.default_grouping_operator', $default); - $filter = $this->buildFilter('grouping-operator='.$query); + $filter = $this->buildGenericFilter(UserFilter::class, 'grouping-operator='.$query); $this->assertEquals($filter->getGroupingOperator(), $expected); }; @@ -237,7 +259,7 @@ function test_generic_filter_boolean_equals_not_false() function test_generic_filter_timestamp_equals() { - $now = Carbon::now(); + $now = Carbon::now(); $this->assertQuery('select * where "created_at" = \''.$now->toDateTimeString().'\'', [ $this->prefix.'created_at' => 't='.$now->timestamp, @@ -247,7 +269,7 @@ function test_generic_filter_timestamp_equals() function test_generic_filter_timestamp_not_equals() { - $now = Carbon::now(); + $now = Carbon::now(); $this->assertQuery('select * where "created_at" != \''.$now->toDateTimeString().'\'', [ $this->prefix.'created_at' => 't!='.$now->timestamp, @@ -257,7 +279,7 @@ function test_generic_filter_timestamp_not_equals() function test_generic_filter_timestamp_less_than() { - $now = Carbon::now(); + $now = Carbon::now(); $this->assertQuery('select * where "created_at" > \''.$now->toDateTimeString().'\'', [ $this->prefix.'created_at' => 't>'.$now->timestamp, ]); @@ -266,7 +288,7 @@ function test_generic_filter_timestamp_less_than() function test_generic_filter_timestamp_greater_than() { - $now = Carbon::now(); + $now = Carbon::now(); $this->assertQuery('select * where "created_at" < \''.$now->toDateTimeString().'\'', [ $this->prefix.'created_at' => 't<'.$now->timestamp, ]); @@ -275,7 +297,7 @@ function test_generic_filter_timestamp_greater_than() function test_generic_filter_timestamp_equals_or_less_than() { - $now = Carbon::now(); + $now = Carbon::now(); $this->assertQuery('select * where "created_at" >= \''.$now->toDateTimeString().'\'', [ $this->prefix.'created_at' => 't>='.$now->timestamp, ]); @@ -284,7 +306,7 @@ function test_generic_filter_timestamp_equals_or_less_than() function test_generic_filter_timestamp_equals_or_greater_than() { - $now = Carbon::now(); + $now = Carbon::now(); $this->assertQuery('select * where "created_at" <= \''.$now->toDateTimeString().'\'', [ $this->prefix.'created_at' => 't<='.$now->timestamp, ]); @@ -293,37 +315,41 @@ function test_generic_filter_timestamp_equals_or_greater_than() function test_generic_filter_timestamp_between() { - $now = Carbon::now(); - $then = Carbon::now()->addDay(); - - $this->assertQuery('select * where "created_at" between \''.$now->toDateTimeString().'\' and \''.$then->toDateTimeString().'\'', [ - $this->prefix.'created_at' => 't><'.$now->timestamp.','.$then->timestamp, - ]); - - $this->assertQuery('select * where "created_at" between \''.$now->toDateTimeString().'\' and \''.$then->toDateTimeString().'\'', [ - $this->prefix.'created_at' => 't><'.$then->timestamp.','.$now->timestamp, - ]); + $now = Carbon::now(); + $then = Carbon::now()->addDay(); + + $this->assertQuery('select * where "created_at" between \''.$now->toDateTimeString().'\' and \''.$then->toDateTimeString().'\'', + [ + $this->prefix.'created_at' => 't><'.$now->timestamp.','.$then->timestamp, + ]); + + $this->assertQuery('select * where "created_at" between \''.$now->toDateTimeString().'\' and \''.$then->toDateTimeString().'\'', + [ + $this->prefix.'created_at' => 't><'.$then->timestamp.','.$now->timestamp, + ]); } function test_generic_filter_timestamp_not_between() { - $now = Carbon::now(); - $then = Carbon::now()->addDay(); - - $this->assertQuery('select * where "created_at" not between \''.$now->toDateTimeString().'\' and \''.$then->toDateTimeString().'\'', [ - $this->prefix.'created_at' => 't!><'.$now->timestamp.','.$then->timestamp, - ]); - - $this->assertQuery('select * where "created_at" not between \''.$now->toDateTimeString().'\' and \''.$then->toDateTimeString().'\'', [ - $this->prefix.'created_at' => 't!><'.$then->timestamp.','.$now->timestamp, - ]); + $now = Carbon::now(); + $then = Carbon::now()->addDay(); + + $this->assertQuery('select * where "created_at" not between \''.$now->toDateTimeString().'\' and \''.$then->toDateTimeString().'\'', + [ + $this->prefix.'created_at' => 't!><'.$now->timestamp.','.$then->timestamp, + ]); + + $this->assertQuery('select * where "created_at" not between \''.$now->toDateTimeString().'\' and \''.$then->toDateTimeString().'\'', + [ + $this->prefix.'created_at' => 't!><'.$then->timestamp.','.$now->timestamp, + ]); } private function assertQuery($expectedQuery, $params) { - $filter = $this->buildFilter(http_build_query($params)); + $filter = $this->buildGenericFilter(UserFilter::class, http_build_query($params)); $this->assertEquals($expectedQuery, $this->dumpQuery($filter->apply($this->builder))); $this->resetBuilder(); } diff --git a/tests/JoinTest.php b/tests/JoinSupportTest.php similarity index 93% rename from tests/JoinTest.php rename to tests/JoinSupportTest.php index df583a1..6ff0aac 100644 --- a/tests/JoinTest.php +++ b/tests/JoinSupportTest.php @@ -2,7 +2,9 @@ namespace Kyslik\LaravelFilterable\Test; -class JoinTest extends TestCase +use Kyslik\LaravelFilterable\Test\Stubs\RoleFilter; + +class JoinSupportTest extends TestCase { function test_single_join() @@ -63,7 +65,7 @@ function test_join_though_multiple_tables_and_other_filter() private function assertJoinQuery($expectedQuery, $builderQuery) { - $filter = $this->buildCustomFilter(http_build_query($builderQuery)); + $filter = $this->buildFilter(RoleFilter::class, http_build_query($builderQuery)); $this->assertEquals($expectedQuery, $this->dumpQuery($filter->apply($this->builder))); $this->resetBuilder(); } diff --git a/tests/RequestMacrosTest.php b/tests/RequestMacrosTest.php new file mode 100644 index 0000000..65d162d --- /dev/null +++ b/tests/RequestMacrosTest.php @@ -0,0 +1,78 @@ +buildFilter(RequestMacroFilter::class, 'active&new'); + $this->assertTrue(resolve(Request::class)->hasAnyFilter($filter)); + } + + + function test_has_any_filter_returns_false() + { + $filter = $this->buildFilter(RequestMacroFilter::class, 'page=1'); + $this->assertFalse(resolve(Request::class)->hasAnyFilter($filter)); + } + + + function test_has_any_filter_determines_the_filter_and_returns_true() + { + $filter = $this->buildFilter(MacroCallingFilter::class, 'active&new'); + $this->assertTrue($filter->callHasAnyFilter()); + } + + + function test_has_any_filter_determines_the_filter_and_returns_false() + { + $filter = $this->buildFilter(MacroCallingFilter::class, 'page=1'); + $this->assertFalse($filter->callHasAnyFilter()); + } + + + function test_has_any_filter_throws_up_when_filter_is_not_provided() + { + $this->expectException(InvalidArgumentException::class); + resolve(Request::class)->hasAnyFilter(); + } +} + +class RequestMacroFilter extends \Kyslik\LaravelFilterable\Filter +{ + + /** + * @return array ex: ['method-name', 'another-method' => 'alias', 'yet-another-method' => ['alias-one', 'alias-two]] + */ + function filterMap(): array + { + return ['active' => 'active', 'recent' => ['new', 'scheduled']]; + } + + + function active() + { + return $this->builder; + } + + + function recent() + { + return $this->builder; + } + +} + +class MacroCallingFilter extends RequestMacroFilter +{ + + public function callHasAnyFilter() + { + return $this->request->hasAnyFilter(); + } +} \ No newline at end of file diff --git a/tests/Stubs/Filter.php b/tests/Stubs/Filter.php new file mode 100644 index 0000000..e3184a1 --- /dev/null +++ b/tests/Stubs/Filter.php @@ -0,0 +1,29 @@ +builder->where('recent', 1); + } + + + // For testing purposes only. + public function appendableDefaults(array $defaults): array + { + return parent::appendableDefaults($defaults); + } + + + /** + * @return array ex: ['method-name', 'another-method' => 'alias', 'yet-another-method' => ['alias-one', 'alias-two]] + */ + function filterMap(): array + { + return ['active', 'recent' => ['new', 'scheduled']]; + } +} + diff --git a/tests/Stubs/GenericFilter.php b/tests/Stubs/GenericFilter.php new file mode 100644 index 0000000..45ae0f5 --- /dev/null +++ b/tests/Stubs/GenericFilter.php @@ -0,0 +1,37 @@ + ['name'], + ]; + } + + + public function name($name) + { + return $this->builder->where('name', $name); + } +} \ No newline at end of file diff --git a/tests/Stubs/RoleFilter.php b/tests/Stubs/RoleFilter.php index f3f0f4e..ad51dc9 100644 --- a/tests/Stubs/RoleFilter.php +++ b/tests/Stubs/RoleFilter.php @@ -10,17 +10,6 @@ class RoleFilter extends Filter use JoinSupport; - protected $filterables = [ - 'id', - 'user_id', - 'role', - 'created_at', - 'updated_at', - 'deleted_at', - 'active', - 'published', - ]; - function filterMap(): array { @@ -32,7 +21,6 @@ function filterMap(): array 'permission' => ['permission'], 'permissionType' => ['permission-type'], 'permissionTypeActive' => ['permission-type-active'], - ]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 764d482..4f5a59a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,8 +6,6 @@ use Illuminate\Http\Request; use Kyslik\LaravelFilterable\FilterableServiceProvider; use Kyslik\LaravelFilterable\Generic\Templater; -use Kyslik\LaravelFilterable\Test\Stubs\UserFilter; -use Kyslik\LaravelFilterable\Test\Stubs\RoleFilter; use Orchestra\Testbench\TestCase as Orchestra; abstract class TestCase extends Orchestra @@ -45,21 +43,24 @@ protected function resetBuilder() } - protected function buildFilter($requestQuery) + protected function buildGenericFilter(string $filter, $requestQuery = '') { /** @var Request $request */ $request = resolve(Request::class)->create('http://test.dev?'.$requestQuery); + $this->app->instance(Request::class, $request); - return new UserFilter($request, resolve(Templater::class)); + return new $filter($request, resolve(Templater::class)); } - protected function buildCustomFilter($requestQuery) + protected function buildFilter(string $filter, $requestQuery = '') { /** @var Request $request */ $request = resolve(Request::class)->create('http://test.dev?'.$requestQuery); - return new RoleFilter($request); + $this->app->instance(Request::class, $request); + + return new $filter($request); }