Skip to content

Commit

Permalink
Implement #10
Browse files Browse the repository at this point in the history
- add `hasAnyFilter` macro on request
- improve test suite
- update readme
- update changelog
- remove Laravel 5.5 from Travis-CI configuration file
  • Loading branch information
Kyslik committed Oct 1, 2018
1 parent cc6d1df commit 8f1a8c5
Show file tree
Hide file tree
Showing 19 changed files with 631 additions and 83 deletions.
4 changes: 0 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ git:
depth: 3

env:
- version=L55
- version=L56
- version=L57

Expand All @@ -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

Expand All @@ -33,5 +31,3 @@ script: vendor/bin/phpunit
branches:
only:
- master
- L5.5-6
- L5.5-7
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 37 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
<?php
Expand All @@ -142,7 +139,7 @@ class UserFilter extends Filter

>**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;
Expand Down Expand Up @@ -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
<?php
Expand All @@ -184,7 +181,7 @@ class UserFilter extends Filter
}
```

Next, you will have to 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):
Next, you will have to 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;
Expand Down Expand Up @@ -237,6 +234,36 @@ class UserFilter extends Filter
}
```

### Additional features

#### Default filtering

In case you need to apply a filter when no filter is applied yet (determined by what query-string contains at the given request), you can use the following code in the controller:

```php
public function index(User $user, UserFilter $filter)
{
// will redirect and "apply" the `recent` and `filter-id` filters
// if not a single filter from UserFilter is applied
$filter->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
Expand Down
56 changes: 54 additions & 2 deletions src/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand All @@ -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
*/
Expand All @@ -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;
Expand All @@ -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]);
}
Expand Down
26 changes: 25 additions & 1 deletion src/FilterContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;


/**
Expand Down
6 changes: 3 additions & 3 deletions src/Filterable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
15 changes: 15 additions & 0 deletions src/FilterableServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Kyslik\LaravelFilterable;

use Illuminate\Http\Request;
use Illuminate\Support\ServiceProvider;
use Kyslik\LaravelFilterable\Exceptions\InvalidArgumentException;

class FilterableServiceProvider extends ServiceProvider
{
Expand All @@ -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());
});
}


Expand Down
Loading

0 comments on commit 8f1a8c5

Please sign in to comment.