Completed
Push — master ( 36b8fe...63f4a0 )
by Rafael
03:24
created

PaginationDefinitionPlugin   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 53
eloc 224
dl 0
loc 395
ccs 0
cts 276
cp 0
rs 6.96
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A normalizeConfig() 0 15 6
A buildConfig() 0 22 1
B configure() 0 57 11
A __construct() 0 4 1
A createConnection() 0 54 3
A addPaginationArguments() 0 36 1
C createOrderBy() 0 77 16
C addFilters() 0 59 14

How to fix   Complexity   

Complex Class

Complex classes like PaginationDefinitionPlugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PaginationDefinitionPlugin, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*******************************************************************************
3
 *  This file is part of the GraphQL Bundle package.
4
 *
5
 *  (c) YnloUltratech <[email protected]>
6
 *
7
 *  For the full copyright and license information, please view the LICENSE
8
 *  file that was distributed with this source code.
9
 ******************************************************************************/
10
11
namespace Ynlo\GraphQLBundle\Definition\Plugin;
12
13
use GraphQL\Type\Definition\ObjectType;
14
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
15
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
16
use Ynlo\GraphQLBundle\Definition\ArgumentDefinition;
17
use Ynlo\GraphQLBundle\Definition\DefinitionInterface;
18
use Ynlo\GraphQLBundle\Definition\EnumDefinition;
19
use Ynlo\GraphQLBundle\Definition\EnumValueDefinition;
20
use Ynlo\GraphQLBundle\Definition\ExecutableDefinitionInterface;
21
use Ynlo\GraphQLBundle\Definition\FieldDefinition;
22
use Ynlo\GraphQLBundle\Definition\FieldsAwareDefinitionInterface;
23
use Ynlo\GraphQLBundle\Definition\InputObjectDefinition;
24
use Ynlo\GraphQLBundle\Definition\InterfaceDefinition;
25
use Ynlo\GraphQLBundle\Definition\ObjectDefinition;
26
use Ynlo\GraphQLBundle\Definition\QueryDefinition;
27
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint;
28
use Ynlo\GraphQLBundle\DependencyInjection\BackwardCompatibilityAwareInterface;
29
use Ynlo\GraphQLBundle\DependencyInjection\BackwardCompatibilityAwareTrait;
30
use Ynlo\GraphQLBundle\Filter\FilterFactory;
31
use Ynlo\GraphQLBundle\Model\OrderBy;
32
use Ynlo\GraphQLBundle\Query\Node\AllNodesWithPagination;
33
use Ynlo\GraphQLBundle\Type\Registry\TypeRegistry;
34
use Ynlo\GraphQLBundle\Util\FieldOptionsHelper;
35
36
/**
37
 * Convert a simple return of nodes into a paginated collection with edges
38
 */
39
class PaginationDefinitionPlugin extends AbstractDefinitionPlugin implements BackwardCompatibilityAwareInterface
40
{
41
    use BackwardCompatibilityAwareTrait;
42
43
    public const ONE_TO_MANY = 'ONE_TO_MANY';
44
    public const MANY_TO_MANY = 'MANY_TO_MANY';
45
46
    /**
47
     * @var FilterFactory
48
     */
49
    protected $filterFactory;
50
51
    /**
52
     * @var int
53
     */
54
    protected $limit;
55
56
    /**
57
     * PaginationDefinitionPlugin constructor.
58
     *
59
     * @param FilterFactory $filterFactory
60
     * @param array         $config
61
     */
62
    public function __construct(FilterFactory $filterFactory, array $config = [])
63
    {
64
        $this->filterFactory = $filterFactory;
65
        $this->limit = $config['limit'] ?? 100;
66
    }
67
68
    /**
69
     * {@inheritDoc}
70
     */
71
    public function buildConfig(ArrayNodeDefinition $root): void
72
    {
73
        $config = $root
74
            ->info('Enable pagination in queries or sub-fields')
75
            ->canBeEnabled()
76
            ->children();
77
78
        /** @var NodeBuilder $rootNode */
79
        $config->scalarNode('target')
80
               ->info('Target node to properly paginate. If is possible will be auto-resolved using naming conventions')
81
               ->isRequired();
82
        $config->variableNode('filters')
83
               ->info('Filters configuration');
84
        $config->variableNode('order_by');
85
        $config->variableNode('search_fields');
86
        $config->integerNode('limit')->info('Max number of records allowed for first & last')->defaultValue($this->limit);
87
        $config->scalarNode('parent_field')
88
               ->info('When is used in sub-fields should be the field to filter by parent instance');
89
        $config->enumNode('parent_relation')
90
               ->info('When is used in sub-fields should be the type of relation with the parent field')
91
               ->defaultValue(self::ONE_TO_MANY)
92
               ->values([self::ONE_TO_MANY, self::MANY_TO_MANY]);
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     */
98
    public function normalizeConfig(DefinitionInterface $definition, $config): array
99
    {
100
        if (true === $config && $definition instanceof ExecutableDefinitionInterface) {
101
            $config = [];
102
        }
103
104
        if (\is_array($config) && !isset($config['target'])) {
105
            $config['target'] = $definition->getType();
0 ignored issues
show
Bug introduced by
The method getType() does not exist on Ynlo\GraphQLBundle\Definition\DefinitionInterface. It seems like you code against a sub-type of Ynlo\GraphQLBundle\Definition\DefinitionInterface such as Ynlo\GraphQLBundle\Definition\ArgumentDefinition or Ynlo\GraphQLBundle\Defin...ableDefinitionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
            /** @scrutinizer ignore-call */ 
106
            $config['target'] = $definition->getType();
Loading history...
106
        }
107
108
        if (false === $config) {
109
            $config = [];
110
        }
111
112
        return $config;
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    public function configure(DefinitionInterface $definition, Endpoint $endpoint, array $config): void
119
    {
120
        if (!$config) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $config of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
121
            return;
122
        }
123
124
        if (!$definition instanceof QueryDefinition && !$definition instanceof FieldDefinition) {
125
            return;
126
        }
127
128
        $target = null;
129
        if ($definition instanceof FieldDefinition) {
130
            $target = $definition->getType();
131
            // only apply pagination to inherited fields
132
            // if all interfaces has pagination enabled
133
            if ($definition->getInheritedFrom()) {
134
                foreach ($definition->getInheritedFrom() as $inheritedType) {
135
                    /** @var InterfaceDefinition $inheritedDefinition */
136
                    $inheritedDefinition = $endpoint->getType($inheritedType);
137
                    if (!$inheritedDefinition->getField($definition->getName())->hasMeta('pagination')) {
138
                        return;
139
                    }
140
                }
141
            }
142
        }
143
144
        $search = new ArgumentDefinition();
145
        $search->setName('search');
146
        $search->setType('string');
147
        $search->setNonNull(false);
148
        $search->setDescription('Search in current list by given string');
149
        $definition->addArgument($search);
150
151
        $target = $config['target'] ?? $target;
152
        if ($endpoint->hasTypeForClass($target)) {
0 ignored issues
show
Bug introduced by
It seems like $target can also be of type null; however, parameter $class of Ynlo\GraphQLBundle\Defin...oint::hasTypeForClass() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

152
        if ($endpoint->hasTypeForClass(/** @scrutinizer ignore-type */ $target)) {
Loading history...
153
            $target = $endpoint->getTypeForClass($target);
0 ignored issues
show
Bug introduced by
It seems like $target can also be of type null; however, parameter $class of Ynlo\GraphQLBundle\Defin...oint::getTypeForClass() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

153
            $target = $endpoint->getTypeForClass(/** @scrutinizer ignore-type */ $target);
Loading history...
154
        }
155
        $targetNode = $endpoint->getType($target);
156
157
        $this->addPaginationArguments($definition);
158
        $this->createOrderBy($endpoint, $definition, $targetNode);
159
160
        $connection = $this->createConnection($endpoint, $targetNode);
161
        $definition->setType($connection->getName());
162
        $definition->setList(false);
163
        $definition->setNode($target);
164
        $definition->setMeta('pagination', $config);
165
166
        if (!$definition->getResolver()) {
167
            $definition->setResolver(AllNodesWithPagination::class);
168
        }
169
170
        $this->filterFactory->build($definition, $targetNode, $endpoint);
171
172
        //deprecated, keep for BC with v1
173
        if ($this->bcConfig['filters'] ?? false) {
174
            $this->addFilters($definition, $target, $endpoint);
0 ignored issues
show
Deprecated Code introduced by
The function Ynlo\GraphQLBundle\Defin...ionPlugin::addFilters() has been deprecated: since v1.2, should use `where` instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

174
            /** @scrutinizer ignore-deprecated */ $this->addFilters($definition, $target, $endpoint);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
175
        }
176
    }
177
178
    /**
179
     * @param Endpoint            $endpoint
180
     * @param DefinitionInterface $node
181
     *
182
     * @return ObjectDefinition
183
     */
184
    private function createConnection(Endpoint $endpoint, DefinitionInterface $node): ObjectDefinition
185
    {
186
        $connection = new ObjectDefinition();
187
        $connection->setName("{$node->getName()}Connection");
188
189
        if (!$endpoint->hasType($connection->getName())) {
190
            $endpoint->addType($connection);
191
192
            $totalCount = new FieldDefinition();
193
            $totalCount->setName('totalCount');
194
            $totalCount->setType('Int');
195
            $totalCount->setNonNull(true);
196
            $connection->addField($totalCount);
197
198
            $pages = new FieldDefinition();
199
            $pages->setName('pages');
200
            $pages->setType('Int');
201
            $pages->setNonNull(true);
202
            $connection->addField($pages);
203
204
            $pageInfo = new FieldDefinition();
205
            $pageInfo->setName('pageInfo');
206
            $pageInfo->setType('PageInfo');
207
            $pageInfo->setNonNull(true);
208
            $connection->addField($pageInfo);
209
210
            $edgeObject = new ObjectDefinition();
211
            $edgeObject->setName("{$node->getName()}Edge");
212
            if (!$endpoint->hasType($edgeObject->getName())) {
213
                $endpoint->addType($edgeObject);
214
215
                $nodeField = new FieldDefinition();
216
                $nodeField->setName('node');
217
                $nodeField->setType($node->getName());
218
                $nodeField->setNonNull(true);
219
                $edgeObject->addField($nodeField);
220
221
                $cursor = new FieldDefinition();
222
                $cursor->setName('cursor');
223
                $cursor->setType('string');
224
                $cursor->setNonNull(true);
225
                $edgeObject->addField($cursor);
226
            }
227
228
            $edges = new FieldDefinition();
229
            $edges->setName('edges');
230
            $edges->setType($edgeObject->getName());
231
            $edges->setList(true);
232
            $connection->addField($edges);
233
        } else {
234
            $connection = $endpoint->getType($connection->getName());
235
        }
236
237
        return $connection;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $connection could return the type Ynlo\GraphQLBundle\Definition\DefinitionInterface which includes types incompatible with the type-hinted return Ynlo\GraphQLBundle\Definition\ObjectDefinition. Consider adding an additional type-check to rule them out.
Loading history...
238
    }
239
240
    /**
241
     * @param Endpoint                       $endpoint
242
     * @param ExecutableDefinitionInterface  $query
243
     * @param FieldsAwareDefinitionInterface $node
244
     */
245
    private function createOrderBy(Endpoint $endpoint, ExecutableDefinitionInterface $query, FieldsAwareDefinitionInterface $node)
246
    {
247
        /** @var InputObjectDefinition $orderBy */
248
        $orderBy = unserialize(serialize($endpoint->getType(OrderBy::class)), ['allowed_classes' => true]); //clone recursively
249
        $orderBy->setName("{$node->getName()}OrderBy");
250
251
        if (!$endpoint->hasType($orderBy->getName())) {
252
            $orderByFields = new EnumDefinition();
253
            $orderByFields->setName("{$node->getName()}OrderByField");
254
            $options = $query->getMeta('pagination')['order_by'] ?? ['*'];
255
            $options = FieldOptionsHelper::normalize($options);
256
257
            foreach ($node->getFields() as $field) {
258
                if (!FieldOptionsHelper::isEnabled($options, $field->getName())) {
259
                    continue;
260
                }
261
262
                //ignore if non related to entity property
263
                if ($field->getOriginType() !== \ReflectionProperty::class) {
264
                    continue;
265
                }
266
267
                //ignore if is a list
268
                if ($field->isList()) {
269
                    continue;
270
                }
271
272
                //ignore if is related to other object
273
                if ($endpoint->hasType($field->getType()) && $endpoint->getType($field->getType()) instanceof FieldsAwareDefinitionInterface) {
274
                    continue;
275
                }
276
277
                $orderByFields->addValue(new EnumValueDefinition($field->getName()));
278
            }
279
280
            //configure custom orderBy and support for children, like "parentName" => parent.name
281
            foreach ($options as $fieldName => $config) {
282
                if ('*' === $fieldName || '*' === $config || !FieldOptionsHelper::isEnabled($options, $fieldName)) {
283
                    continue;
284
                }
285
286
                if (array_key_exists($fieldName, $orderByFields->getValues())) {
287
                    continue;
288
                }
289
290
                $orderByFields->addValue(new EnumValueDefinition($fieldName, $config));
291
            }
292
293
            if ($orderByFields->getValues()) {
294
                $orderBy->getField('field')->setType($orderByFields->getName());
295
                $endpoint->addType($orderByFields);
296
                $endpoint->addType($orderBy);
297
            } else {
298
                return;
299
            }
300
        } else {
301
            $orderBy = $endpoint->getType($orderBy->getName());
302
        }
303
304
        $arg = new ArgumentDefinition();
305
        $arg->setName('order');
306
        $arg->setType($orderBy->getName());
307
        $arg->setNonNull(false);
308
        $arg->setList(true);
309
        $arg->setDescription('Ordering options for this list.');
310
        $query->addArgument($arg);
311
312
        //to keep BC
313
        if ($this->bcConfig['orderBy'] ?? false) {
314
            $arg = new ArgumentDefinition();
315
            $arg->setName('orderBy');
316
            $arg->setType(OrderBy::class);
317
            $arg->setNonNull(false);
318
            $arg->setList(true);
319
            $deprecateMessage = \is_string($this->bcConfig['orderBy']) ? $this->bcConfig['orderBy'] : '**DEPRECATED** use `order` instead.';
320
            $arg->setDescription($deprecateMessage);
321
            $query->addArgument($arg);
322
        }
323
    }
324
325
    /**
326
     * @param ExecutableDefinitionInterface $definition
327
     */
328
    private function addPaginationArguments(ExecutableDefinitionInterface $definition): void
329
    {
330
        $first = new ArgumentDefinition();
331
        $first->setName('first');
332
        $first->setType('int');
333
        $first->setNonNull(false);
334
        $first->setDescription('Returns the first *n* elements from the list.');
335
        $definition->addArgument($first);
336
337
        $last = new ArgumentDefinition();
338
        $last->setName('last');
339
        $last->setType('int');
340
        $last->setNonNull(false);
341
        $last->setDescription('Returns the last *n* elements from the list.');
342
        $definition->addArgument($last);
343
344
        $after = new ArgumentDefinition();
345
        $after->setName('after');
346
        $after->setType('string');
347
        $after->setNonNull(false);
348
        $after->setDescription('Returns the last *n* elements from the list.');
349
        $definition->addArgument($after);
350
351
        $before = new ArgumentDefinition();
352
        $before->setName('before');
353
        $before->setType('string');
354
        $before->setNonNull(false);
355
        $before->setDescription('Returns the last *n* elements from the list.');
356
        $definition->addArgument($before);
357
358
        $page = new ArgumentDefinition();
359
        $page->setName('page');
360
        $page->setType('integer');
361
        $page->setNonNull(false);
362
        $page->setDescription('Page to fetch in order to use page pagination instead of cursor based');
363
        $definition->addArgument($page);
364
    }
365
366
    /**
367
     * @param ExecutableDefinitionInterface $definition
368
     * @param string                        $targetType
369
     * @param Endpoint                      $endpoint
370
     *
371
     * @throws \ReflectionException
372
     *
373
     * @deprecated since v1.2, should use `where` instead
374
     */
375
    private function addFilters(ExecutableDefinitionInterface $definition, string $targetType, Endpoint $endpoint): void
376
    {
377
        $filterName = ucfirst($definition->getName()).'Filter';
378
        if ($endpoint->hasType($filterName)) {
379
            $filters = $endpoint->getType($filterName);
380
        } else {
381
            $filters = new InputObjectDefinition();
382
            $filters->setName($filterName);
383
            $endpoint->add($filters);
384
385
            $object = $endpoint->getType($targetType);
386
            if ($object instanceof FieldsAwareDefinitionInterface) {
387
                foreach ($object->getFields() as $field) {
388
                    if ('id' === $field->getName()
389
                        || !$field->getOriginName()
390
                        || \ReflectionProperty::class !== $field->getOriginType()) {
391
                        continue;
392
                    }
393
394
                    $filter = new FieldDefinition();
395
                    $filter->setName($field->getName());
396
                    $type = $field->getType();
397
                    if ($endpoint->hasType($type)) {
398
                        $typeDefinition = $endpoint->getType($type);
399
                        if (!$typeDefinition instanceof EnumDefinition) {
400
                            $type = 'ID';
401
                        }
402
                        $filter->setList(true);
403
                    }
404
405
                    // fields using custom object as type
406
                    // are not available for filters
407
                    if (TypeRegistry::getTypeMapp()) {
408
                        if (isset(TypeRegistry::getTypeMapp()[$type])) {
409
                            $class = TypeRegistry::getTypeMapp()[$type];
410
                            $ref = new \ReflectionClass($class);
411
                            if ($ref->isSubclassOf(ObjectType::class)) {
412
                                continue;
413
                            }
414
                        }
415
                    }
416
417
                    $filter->setType($type);
418
                    $filters->addField($filter);
419
                }
420
            }
421
        }
422
423
        if (!$filters->getFields()) {
0 ignored issues
show
Bug introduced by
The method getFields() does not exist on Ynlo\GraphQLBundle\Definition\DefinitionInterface. It seems like you code against a sub-type of Ynlo\GraphQLBundle\Definition\DefinitionInterface such as Ynlo\GraphQLBundle\Definition\ImplementorInterface or Ynlo\GraphQLBundle\Defin...wareDefinitionInterface or Ynlo\GraphQLBundle\Defin...jectDefinitionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

423
        if (!$filters->/** @scrutinizer ignore-call */ getFields()) {
Loading history...
424
            return;
425
        }
426
427
        $search = new ArgumentDefinition();
428
        $search->setName('filters');
429
        $search->setType($filters->getName());
430
        $deprecateMessage = \is_string($this->bcConfig['filters']) ? $this->bcConfig['filters'] : '**DEPRECATED** use `where` instead to filter the list.';
431
        $search->setDescription($deprecateMessage);
432
        $search->setNonNull(false);
433
        $definition->addArgument($search);
434
    }
435
}
436