Skip to content

Commit 6657c14

Browse files
committed
Custom filtering operators may define a custom field
Previously custom operators were injected in schema at the same level of standard fields. That was never the intent, as we wanted to be able to defined custom fields for group custom operators, or add custom operator to existing fields.
1 parent 527c49c commit 6657c14

File tree

10 files changed

+207
-67
lines changed

10 files changed

+207
-67
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ use GraphQL\Doctrine\Annotation as API;
411411
*
412412
* @ORM\Entity
413413
* @API\Filters({
414-
* @API\Filter(field="custom", operator="GraphQLTests\Doctrine\Blog\Filtering\Search", type="string")
414+
* @API\Filter(field="custom", operator="GraphQLTests\Doctrine\Blog\Filtering\SearchOperatorType", type="string")
415415
* })
416416
*/
417417
final class Post extends AbstractModel

src/Factory/Type/FilterTypeFactory.php

Lines changed: 101 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
*/
3030
final class FilterTypeFactory extends AbstractTypeFactory
3131
{
32+
/**
33+
* @var Filter[][]
34+
*/
35+
private $customOperators;
36+
3237
/**
3338
* Create an InputObjectType from a Doctrine entity
3439
*
@@ -166,37 +171,45 @@ private function getConditionFieldsType(string $className, string $typeName): In
166171
'name' => $typeName . 'ConditionFields',
167172
'description' => 'Type to specify conditions on fields',
168173
'fields' => function () use ($className, $typeName) {
169-
$standardFilters = [];
174+
$filters = [];
170175
$metadata = $this->entityManager->getClassMetadata($className);
171176

172-
// Get all entity scalar fields
177+
// Get custom operators
178+
$this->customOperators = [];
179+
$this->readCustomOperatorsFromAnnotation($metadata->reflClass);
180+
181+
// Get all scalar fields
173182
foreach ($metadata->fieldMappings as $mapping) {
174183
/** @var LeafType $leafType */
175184
$leafType = $this->types->get($mapping['type']);
176185
$fieldName = $mapping['fieldName'];
186+
$operators = $this->getOperators($fieldName, $leafType, false);
177187

178-
$field = [
179-
'name' => $fieldName,
180-
'type' => $this->getFieldType($typeName, $fieldName, $leafType, false),
181-
];
182-
$standardFilters[] = $field;
188+
$filters[] = $this->getFieldConfiguration($typeName, $fieldName, $operators);
183189
}
184190

185-
// Get all entity collection fields
191+
// Get all collection fields
186192
foreach ($metadata->associationMappings as $mapping) {
187193
$fieldName = $mapping['fieldName'];
194+
$operators = $this->getOperators($fieldName, Type::id(), $metadata->isCollectionValuedAssociation($fieldName));
188195

189-
$field = [
190-
'name' => $fieldName,
191-
'type' => $this->getFieldType($typeName, $fieldName, Type::id(), $metadata->isCollectionValuedAssociation($fieldName)),
192-
];
193-
$standardFilters[] = $field;
196+
$filters[] = $this->getFieldConfiguration($typeName, $fieldName, $operators);
194197
}
195198

196-
// Get custom fields
197-
$customFilters = $this->getCustomFiltersFromAnnotation($metadata->reflClass);
199+
// Get all custom fields defined by custom operators
200+
foreach ($this->customOperators as $fieldName => $customOperators) {
201+
$operators = [];
202+
/** @var Filter $customOperator */
203+
foreach ($customOperators as $customOperator) {
204+
/** @var LeafType $leafType */
205+
$leafType = $this->types->get($customOperator->type);
206+
$operators[$customOperator->operator] = $leafType;
207+
}
208+
209+
$filters[] = $this->getFieldConfiguration($typeName, $fieldName, $operators);
210+
}
198211

199-
return array_merge($standardFilters, $customFilters);
212+
return $filters;
200213
},
201214
]);
202215

@@ -206,16 +219,72 @@ private function getConditionFieldsType(string $className, string $typeName): In
206219
}
207220

208221
/**
209-
* Get the custom filters declared on the class via annotations
222+
* Get configuration for field
210223
*
211-
* @param ReflectionClass $class
224+
* @param string $typeName
225+
* @param string $fieldName
226+
* @param LeafType[] $operators
212227
*
213228
* @return array
214229
*/
215-
private function getCustomFiltersFromAnnotation(ReflectionClass $class): array
230+
private function getFieldConfiguration(string $typeName, string $fieldName, array $operators): array
216231
{
217-
$result = [];
232+
return [
233+
'name' => $fieldName,
234+
'type' => $this->getFieldType($typeName, $fieldName, $operators),
235+
];
236+
}
218237

238+
/**
239+
* Return a map of operator class name and their leaf type, including custom operator for the given fieldName
240+
*
241+
* @param string $fieldName
242+
* @param LeafType $leafType
243+
* @param bool $isCollection
244+
*
245+
* @return LeafType[] indexed by operator class name
246+
*/
247+
private function getOperators(string $fieldName, LeafType $leafType, bool $isCollection): array
248+
{
249+
if ($isCollection) {
250+
$operators = [
251+
ContainOperatorType::class => $leafType,
252+
EmptyOperatorType::class => $leafType,
253+
];
254+
} else {
255+
$operators = [
256+
BetweenOperatorType::class => $leafType,
257+
EqualOperatorType::class => $leafType,
258+
GreaterOperatorType::class => $leafType,
259+
GreaterOrEqualOperatorType::class => $leafType,
260+
InOperatorType::class => $leafType,
261+
LessOperatorType::class => $leafType,
262+
LessOrEqualOperatorType::class => $leafType,
263+
LikeOperatorType::class => $leafType,
264+
NullOperatorType::class => $leafType,
265+
];
266+
}
267+
268+
// Add custom filters if any
269+
if (isset($this->customOperators[$fieldName])) {
270+
foreach ($this->customOperators[$fieldName] as $filter) {
271+
$leafType = $this->types->get($filter->type);
272+
$operators[$filter->operator] = $leafType;
273+
}
274+
275+
unset($this->customOperators[$fieldName]);
276+
}
277+
278+
return $operators;
279+
}
280+
281+
/**
282+
* Get the custom operators declared on the class via annotations indexed by their field name
283+
*
284+
* @param ReflectionClass $class
285+
*/
286+
private function readCustomOperatorsFromAnnotation(ReflectionClass $class): void
287+
{
219288
$filters = $this->getAnnotationReader()->getClassAnnotation($class, Filters::class);
220289
if ($filters) {
221290

@@ -224,40 +293,33 @@ private function getCustomFiltersFromAnnotation(ReflectionClass $class): array
224293
$className = $filter->operator;
225294
$this->throwIfInvalidAnnotation($class, 'Filter', AbstractOperator::class, $className);
226295

227-
/** @var LeafType $leafType */
228-
$leafType = $this->types->get($filter->type);
229-
$instance = $this->types->getOperator($className, $leafType);
230-
231-
$result[] = [
232-
'name' => $filter->field,
233-
'type' => $instance,
234-
];
296+
if (!isset($this->customOperators[$filter->field])) {
297+
$this->customOperators[$filter->field] = [];
298+
}
299+
$this->customOperators[$filter->field][] = $filter;
235300
}
236301
}
237302

238303
if ($class->getParentClass()) {
239-
return array_merge($result, $this->getCustomFiltersFromAnnotation($class->getParentClass()));
304+
$this->readCustomOperatorsFromAnnotation($class->getParentClass());
240305
}
241-
242-
return $result;
243306
}
244307

245308
/**
246309
* Get the type for a specific field
247310
*
248311
* @param string $typeName
249312
* @param string $fieldName
250-
* @param LeafType $leafType
251-
* @param bool $isCollection
313+
* @param LeafType[] $operators
252314
*
253315
* @return InputObjectType
254316
*/
255-
private function getFieldType(string $typeName, string $fieldName, LeafType $leafType, bool $isCollection): InputObjectType
317+
private function getFieldType(string $typeName, string $fieldName, array $operators): InputObjectType
256318
{
257319
$fieldType = new InputObjectType([
258320
'name' => $typeName . 'ConditionField' . ucfirst($fieldName),
259321
'description' => 'Type to specify a condition on a specific field',
260-
'fields' => $this->getOperators($leafType, $isCollection),
322+
'fields' => $this->getOperatorConfiguration($operators),
261323
]);
262324

263325
$this->types->registerInstance($fieldType);
@@ -266,36 +328,16 @@ private function getFieldType(string $typeName, string $fieldName, LeafType $lea
266328
}
267329

268330
/**
269-
* Get standard operators for a specific leaf type
331+
* Get operators configuration for a specific leaf type
270332
*
271-
* @param LeafType $leafType
272-
* @param bool $isCollection
333+
* @param LeafType[] $operators
273334
*
274335
* @return array
275336
*/
276-
private function getOperators(LeafType $leafType, bool $isCollection): array
337+
private function getOperatorConfiguration(array $operators): array
277338
{
278-
if ($isCollection) {
279-
$operators = [
280-
ContainOperatorType::class,
281-
EmptyOperatorType::class,
282-
];
283-
} else {
284-
$operators = [
285-
BetweenOperatorType::class,
286-
EqualOperatorType::class,
287-
GreaterOperatorType::class,
288-
GreaterOrEqualOperatorType::class,
289-
InOperatorType::class,
290-
LessOperatorType::class,
291-
LessOrEqualOperatorType::class,
292-
LikeOperatorType::class,
293-
NullOperatorType::class,
294-
];
295-
}
296339
$conf = [];
297-
298-
foreach ($operators as $operator) {
340+
foreach ($operators as $operator => $leafType) {
299341
$instance = $this->types->getOperator($operator, $leafType);
300342
$field = [
301343
'name' => $this->getOperatorFieldName($operator),
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GraphQLTests\Doctrine\Blog\Filtering;
6+
7+
use Doctrine\ORM\Mapping\ClassMetadata;
8+
use Doctrine\ORM\QueryBuilder;
9+
use GraphQL\Doctrine\Definition\Operator\AbstractOperator;
10+
use GraphQL\Doctrine\Factory\UniqueNameFactory;
11+
use GraphQL\Doctrine\Types;
12+
use GraphQL\Type\Definition\LeafType;
13+
use GraphQL\Type\Definition\Type;
14+
15+
final class ModuloOperatorType extends AbstractOperator
16+
{
17+
protected function getConfiguration(Types $types, LeafType $leafType): array
18+
{
19+
return [
20+
'fields' => [
21+
[
22+
'name' => 'value',
23+
'type' => Type::nonNull(Type::int()),
24+
],
25+
],
26+
];
27+
}
28+
29+
public function getDqlCondition(UniqueNameFactory $uniqueNameFactory, ClassMetadata $metadata, QueryBuilder $queryBuilder, string $alias, string $field, array $args): string
30+
{
31+
$param = $uniqueNameFactory->createParameterName();
32+
$queryBuilder->setParameter($param, $args['value']);
33+
34+
return 'MOD(' . $alias . '.' . $field . ', :' . $param . ') = 0';
35+
}
36+
}

tests/Blog/Filtering/Search.php renamed to tests/Blog/Filtering/SearchOperatorType.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
use GraphQL\Type\Definition\LeafType;
1313
use GraphQL\Type\Definition\Type;
1414

15-
final class Search extends AbstractOperator
15+
final class SearchOperatorType extends AbstractOperator
1616
{
1717
protected function getConfiguration(Types $types, LeafType $leafType): array
1818
{
@@ -36,7 +36,7 @@ public function getDqlCondition(UniqueNameFactory $uniqueNameFactory, ClassMetad
3636
$textType = ['string', 'text'];
3737
foreach ($metadata->fieldMappings as $g) {
3838
if (in_array($g['type'], $textType, true)) {
39-
$fields[] = $alias . '.' . $g['name'];
39+
$fields[] = $alias . '.' . $g['fieldName'];
4040
}
4141
}
4242

tests/Blog/Model/AbstractModel.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
*
1414
* @ORM\MappedSuperclass
1515
* @API\Sorting({"GraphQLTests\Doctrine\Blog\Sorting\PseudoRandom"})
16+
* @API\Filters({
17+
* @API\Filter(field="id", operator="GraphQLTests\Doctrine\Blog\Filtering\ModuloOperatorType", type="int")
18+
* })
1619
*/
1720
abstract class AbstractModel
1821
{

tests/Blog/Model/Post.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* @ORM\Entity
1515
* @API\Sorting({"GraphQLTests\Doctrine\Blog\Sorting\UserName"})
1616
* @API\Filters({
17-
* @API\Filter(field="custom", operator="GraphQLTests\Doctrine\Blog\Filtering\Search", type="string")
17+
* @API\Filter(field="custom", operator="GraphQLTests\Doctrine\Blog\Filtering\SearchOperatorType", type="string")
1818
* })
1919
*/
2020
final class Post extends AbstractModel

tests/TypesTrait.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use GraphQL\Type\Definition\Type;
1313
use GraphQL\Type\Schema;
1414
use GraphQL\Utils\SchemaPrinter;
15-
use GraphQLTests\Doctrine\Blog\Filtering\Search;
1615
use GraphQLTests\Doctrine\Blog\Types\CustomType;
1716
use GraphQLTests\Doctrine\Blog\Types\DateTimeType;
1817
use GraphQLTests\Doctrine\Blog\Types\PostStatusType;
@@ -41,7 +40,6 @@ public function setUp(): void
4140
DateTime::class => DateTimeType::class,
4241
stdClass::class => CustomType::class,
4342
'PostStatus' => PostStatusType::class,
44-
Search::class,
4543
],
4644
'aliases' => [
4745
'datetime' => DateTime::class, // Declare alias for Doctrine type to be used for filters

tests/data/PostFilter.graphqls

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ enum LogicalOperator {
241241
OR
242242
}
243243

244+
input ModuloOperatorInt {
245+
value: Int!
246+
}
247+
244248
input NullOperatorBoolean {
245249
not: Boolean = false
246250
}
@@ -306,6 +310,11 @@ input PostFilterConditionFieldCreationDate {
306310
null: NullOperatorDateTime
307311
}
308312

313+
# Type to specify a condition on a specific field
314+
input PostFilterConditionFieldCustom {
315+
search: SearchOperatorString
316+
}
317+
309318
# Type to specify a condition on a specific field
310319
input PostFilterConditionFieldId {
311320
between: BetweenOperatorInt
@@ -317,6 +326,7 @@ input PostFilterConditionFieldId {
317326
lessOrEqual: LessOrEqualOperatorInt
318327
like: LikeOperatorInt
319328
null: NullOperatorInt
329+
modulo: ModuloOperatorInt
320330
}
321331

322332
# Type to specify a condition on a specific field
@@ -380,15 +390,15 @@ input PostFilterConditionFields {
380390
status: PostFilterConditionFieldStatus
381391
id: PostFilterConditionFieldId
382392
user: PostFilterConditionFieldUser
383-
custom: SearchString
393+
custom: PostFilterConditionFieldCustom
384394
}
385395

386396
# Type to specify join tables in a filter
387397
input PostFilterJoins {
388398
user: JoinOnUser
389399
}
390400

391-
input SearchString {
401+
input SearchOperatorString {
392402
term: String!
393403
}
394404

@@ -448,6 +458,7 @@ input UserFilterConditionFieldId {
448458
lessOrEqual: LessOrEqualOperatorInt
449459
like: LikeOperatorInt
450460
null: NullOperatorInt
461+
modulo: ModuloOperatorInt
451462
}
452463

453464
# Type to specify a condition on a specific field

0 commit comments

Comments
 (0)