Skip to content

Commit eda0cd6

Browse files
authored
Merge pull request #39 from lorado/feature-improve-filterable
Improve definition of filterables fields
2 parents 798217e + 0844f45 commit eda0cd6

File tree

8 files changed

+250
-12
lines changed

8 files changed

+250
-12
lines changed

README.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ query {
480480

481481
When declaring the `getFilterable` array, you can define filters for fields.
482482

483-
You can either use a closure, or give class implementing FilterInterface.
483+
You can either use a closure, an array, or give object of class implementing FilterInterface.
484484

485485
The closure (or the `FilterInterface::updateBuilder` method) is then called
486486
with:
@@ -489,17 +489,48 @@ with:
489489
* $value : the filter value
490490
* $key : the filter key
491491

492-
You can use the predefined `EqualsOrContainsFilter` like below.
492+
You also may define graphql type for you filterable input field. By default `Type::json()` is used. There are several
493+
options to define the type (all examples are listed in following code-block):
494+
495+
- if you are using class that implements `TypedFilterInterface`, returned type from method
496+
`TypedFilterInterface::getType` is used;
497+
- if you are using closure, you have to define an array with keys `type` containing type you wish and `resolver`
498+
containing closure;
499+
- if you define an array, and in `resolver` is passed an object of class with implemented `TypedFilterInterface`,
500+
then type of `TypedFilterInterface::getType` will overwrite the type in an array key `type`;
501+
- in all other situations `Type::json()` will be used as default type
502+
503+
You can also use the predefined `EqualsOrContainsFilter` like below.
493504

494505
```php
495506
public function getFilterable() {
496507
return [
497-
// Simple equality check (or "in" if value is an array)
508+
// Simple equality check (or "in" if value is an array). Type is Type::json()
498509
'id' => new EqualsOrContainsFilter(),
499-
// Customized filter
510+
511+
// Customized filter. Type is Type::json()
500512
"nameLike" => function($builder, $value) {
501513
return $builder->whereRaw('name like ?', $value);
502514
},
515+
516+
// type is Type::string()
517+
"anotherFilter" => [
518+
"type" => Type::string(),
519+
"resolver" => function($builder, $value) {
520+
return $builder->whereRaw('anotherFilter like ?', $value);
521+
}
522+
],
523+
524+
// type is what is returned from `ComplexFilter::getType()`.
525+
// This is the preffered way to define filters, as it keeps definitions code clean
526+
"complexFilter" => new ComplexFilter(),
527+
528+
// type in array will be overriden by what is returned from `ComplexFilter::getType()`.
529+
// this kind of difinition is not clear, but is implemented for backward compatibilities. Please don't use it
530+
"complexFilter2" => [
531+
"type" => Type::int(),
532+
"resolver" => new ComplexFilter()
533+
],
503534
];
504535
}
505536
```

src/Filter/TypedFilterInterface.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace StudioNet\GraphQL\Filter;
4+
5+
use GraphQL\Type\Definition\InputType;
6+
7+
/**
8+
* Abstract filter
9+
*/
10+
interface TypedFilterInterface extends FilterInterface {
11+
12+
/**
13+
* Defines GraphQL Type if current filter
14+
* @return InputType $wrappedType
15+
*/
16+
public function getType();
17+
}

src/Grammar/Grammar.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,24 @@ private function getBuilder(Builder $builder, $filter, $operator = "AND") {
4747
$whereFunc = strtolower($operator) === 'or' ? "orWhere": "where";
4848

4949
$builder->$whereFunc(function ($b) use ($filter) {
50-
if (is_callable($filter['filter'])) {
51-
$filter['filter']($b, $filter['value'], $filter['key']);
50+
$resolver = $filter['filter'];
51+
// check, if we got an array, and try fetch resolver
52+
if (is_array($filter['filter'])) {
53+
if (!key_exists('resolver', $filter['filter'])) {
54+
throw new FilterException("Invalid filter for $filter[key]");
55+
}
56+
$resolver = $filter['filter']['resolver'];
57+
}
58+
59+
// if we got simple closure, call it
60+
if (is_callable($resolver)) {
61+
$resolver($b, $filter['value'], $filter['key']);
5262
return;
5363
}
5464

55-
if ($filter['filter'] instanceof FilterInterface) {
56-
$filter['filter']->updateBuilder(
65+
// if we got instance of FilterInterface, execute it
66+
if ($resolver instanceof FilterInterface) {
67+
$resolver->updateBuilder(
5768
$b,
5869
$filter['value'],
5970
$filter['key']

src/GraphQLController.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ class GraphQLController extends Controller {
2222
* @return \Illuminate\Http\JsonResponse
2323
*/
2424
public function query(Request $request, $schema = null) {
25-
2625
$inputs = $request->all();
2726
$data = [];
2827

@@ -48,7 +47,6 @@ public function query(Request $request, $schema = null) {
4847
else {
4948
$data = $this->executeQuery($schema, $inputs);
5049
}
51-
5250
} catch (\Exception $exception) {
5351
$data = GraphQL::formatGraphQLException($exception);
5452
Log::debug($exception);

src/Support/Pipe/Eloquent/FilterPipe.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use GraphQL\Type\Definition\InputObjectType;
66
use Illuminate\Database\Eloquent\Builder;
77
use StudioNet\GraphQL\Definition\Type;
8+
use StudioNet\GraphQL\Filter\FilterInterface;
9+
use StudioNet\GraphQL\Filter\TypedFilterInterface;
810
use StudioNet\GraphQL\Grammar;
911
use StudioNet\GraphQL\Support\Definition\Definition;
1012
use StudioNet\GraphQL\Support\Pipe\Argumentable;
@@ -66,7 +68,24 @@ private function getType(Definition $definition) {
6668
$queryable = [];
6769

6870
foreach ($definition->getFilterable() as $field => $filter) {
69-
$queryable[$field] = ['type' => Type::json(), 'filter' => $filter];
71+
// define default values
72+
$fieldType = Type::json();
73+
74+
// if we got instance of TypedFilterInterface, extract type
75+
if ($filter instanceof TypedFilterInterface) {
76+
$fieldType = $filter->getType();
77+
}
78+
79+
// if we got an array, try to fetch type and resolver Function/FilterInterface
80+
if (is_array($filter)) {
81+
if ($filter['resolver'] instanceof TypedFilterInterface) {
82+
$fieldType = $filter['resolver']->getType();
83+
} else {
84+
$fieldType = array_get($filter, 'type', Type::json());
85+
}
86+
}
87+
88+
$queryable[$field] = $fieldType;
7089
}
7190

7291
return new InputObjectType([

tests/Definition/UserDefinition.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
<?php
22
namespace StudioNet\GraphQL\Tests\Definition;
33

4+
use Illuminate\Database\Eloquent\Builder;
45
use StudioNet\GraphQL\Definition\Type;
56
use StudioNet\GraphQL\Support\Definition\EloquentDefinition;
67
use StudioNet\GraphQL\Tests\Entity\User;
78
use StudioNet\GraphQL\Filter\EqualsOrContainsFilter;
89
use Illuminate\Database\Eloquent\Model;
10+
use StudioNet\GraphQL\Tests\Filters\LikeFilter;
911

1012
/**
1113
* Specify user GraphQL definition
@@ -85,9 +87,20 @@ public function getMutable() {
8587
public function getFilterable() {
8688
return [
8789
'id' => new EqualsOrContainsFilter(),
88-
"nameLike" => function ($builder, $value) {
90+
"nameLike" => function (Builder $builder, $value) {
8991
return $builder->whereRaw('name like ?', $value);
9092
},
93+
"nameLikeViaTypedFilter" => new LikeFilter('name'),
94+
"nameLikeArrayClosure" => [
95+
"type" => Type::string(),
96+
"resolver" => function (Builder $builder, $value) {
97+
return $builder->whereRaw('name like ?', $value);
98+
}
99+
],
100+
"nameLikeArrayTypedFilter" => [
101+
"type" => Type::int(), // NOTE: this type will be overridden by LikeFilter::getType()
102+
"resolver" => new LikeFilter('name')
103+
],
91104
];
92105
}
93106

tests/Filters/LikeFilter.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace StudioNet\GraphQL\Tests\Filters;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
use StudioNet\GraphQL\Definition\Type;
7+
use StudioNet\GraphQL\Filter\TypedFilterInterface;
8+
9+
class LikeFilter implements TypedFilterInterface {
10+
private $column;
11+
12+
public function __construct($column = null) {
13+
$this->column = $column;
14+
}
15+
16+
public function getType() {
17+
return Type::string();
18+
}
19+
20+
/**
21+
* {@inheritDoc}
22+
*/
23+
public function updateBuilder(Builder $builder, $value, $key) {
24+
if (!empty($this->column)) {
25+
$key = $this->column;
26+
}
27+
28+
return $builder->orWhere(
29+
$key,
30+
'LIKE',
31+
"%{$value}%"
32+
);
33+
}
34+
}

tests/GraphQLTest.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,4 +332,119 @@ public function testFiltersCustom() {
332332
);
333333
});
334334
}
335+
336+
337+
/**
338+
* Test filters : typed filter
339+
*/
340+
public function testTypedFilter() {
341+
factory(Entity\User::class)->create(['name' => 'foo']);
342+
factory(Entity\User::class)->create(['name' => 'bar']);
343+
factory(Entity\User::class)->create(['name' => 'baz']);
344+
factory(Entity\User::class)->create(['name' => 'foobar']);
345+
346+
$this->registerAllDefinitions();
347+
348+
$this->specify('test equality custom', function () {
349+
// We should only get users which name starts with 'ba'
350+
$query = <<<'EOGQL'
351+
query ($filter: UserFilter) {
352+
users(filter: $filter) {
353+
items {
354+
name
355+
}
356+
}
357+
}
358+
EOGQL;
359+
360+
$res = $this->executeGraphQL($query, [
361+
'variables' => [
362+
'filter' => [
363+
'nameLikeViaTypedFilter' => 'ba'
364+
]
365+
]
366+
]);
367+
368+
$this->assertSame(
369+
['bar','baz','foobar'],
370+
array_column($res['data']['users']['items'], 'name')
371+
);
372+
});
373+
}
374+
375+
/**
376+
* Test filters : closure filter as array
377+
*/
378+
public function testFilterAsArrayClosure() {
379+
factory(Entity\User::class)->create(['name' => 'foo']);
380+
factory(Entity\User::class)->create(['name' => 'bar']);
381+
factory(Entity\User::class)->create(['name' => 'baz']);
382+
factory(Entity\User::class)->create(['name' => 'foobar']);
383+
384+
$this->registerAllDefinitions();
385+
386+
$this->specify('test equality custom', function () {
387+
// We should only get users which name starts with 'ba'
388+
$query = <<<'EOGQL'
389+
query ($filter: UserFilter) {
390+
users(filter: $filter) {
391+
items {
392+
name
393+
}
394+
}
395+
}
396+
EOGQL;
397+
398+
$res = $this->executeGraphQL($query, [
399+
'variables' => [
400+
'filter' => [
401+
'nameLikeArrayClosure' => 'ba%'
402+
]
403+
]
404+
]);
405+
406+
$this->assertSame(
407+
['bar','baz'],
408+
array_column($res['data']['users']['items'], 'name')
409+
);
410+
});
411+
}
412+
413+
/**
414+
* Test filters : typed filter as array
415+
*/
416+
public function testFilterAsArrayTypedFilter() {
417+
factory(Entity\User::class)->create(['name' => 'foo']);
418+
factory(Entity\User::class)->create(['name' => 'bar']);
419+
factory(Entity\User::class)->create(['name' => 'baz']);
420+
factory(Entity\User::class)->create(['name' => 'foobar']);
421+
422+
$this->registerAllDefinitions();
423+
424+
$this->specify('test equality custom', function () {
425+
// We should only get users which name starts with 'ba'
426+
$query = <<<'EOGQL'
427+
query ($filter: UserFilter) {
428+
users(filter: $filter) {
429+
items {
430+
name
431+
}
432+
}
433+
}
434+
EOGQL;
435+
436+
$res = $this->executeGraphQL($query, [
437+
'variables' => [
438+
'filter' => [
439+
'nameLikeArrayTypedFilter' => 'ba'
440+
]
441+
]
442+
]);
443+
444+
$this->assertSame(
445+
['bar','baz','foobar'],
446+
array_column($res['data']['users']['items'], 'name')
447+
);
448+
});
449+
}
335450
}

0 commit comments

Comments
 (0)