PaginationDefinitionPlugin::configure()   B
last analyzed

Complexity

Conditions 11
Paths 27

Size

Total Lines 57
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 34
c 1
b 0
f 0
dl 0
loc 57
ccs 0
cts 44
cp 0
rs 7.3166
cc 11
nc 27
nop 3
crap 132

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\OrderBy\Common\OrderByRelatedField;
33
use Ynlo\GraphQLBundle\OrderBy\Common\OrderBySimpleField;
34
use Ynlo\GraphQLBundle\OrderBy\OrderByInterface;
35
use Ynlo\GraphQLBundle\Query\Node\AllNodesWithPagination;
36
use Ynlo\GraphQLBundle\Type\Registry\TypeRegistry;
37
use Ynlo\GraphQLBundle\Util\FieldOptionsHelper;
38
39
/**
40
 * Convert a simple return of nodes into a paginated collection with edges
41
 */
42
class PaginationDefinitionPlugin extends AbstractDefinitionPlugin implements BackwardCompatibilityAwareInterface
43
{
44
    use BackwardCompatibilityAwareTrait;
45
46
    public const ONE_TO_MANY = 'ONE_TO_MANY';
47
    public const MANY_TO_MANY = 'MANY_TO_MANY';
48
49
    /**
50
     * @var FilterFactory
51
     */
52
    protected $filterFactory;
53
54
    /**
55
     * @var int
56
     */
57
    protected $limit;
58
59
    /**
60
     * PaginationDefinitionPlugin constructor.
61
     *
62
     * @param FilterFactory $filterFactory
63
     * @param array         $config
64
     */
65
    public function __construct(FilterFactory $filterFactory, array $config = [])
66
    {
67
        $this->filterFactory = $filterFactory;
68
        $this->limit = $config['limit'] ?? 100;
69
    }
70
71
    /**
72
     * {@inheritDoc}
73
     */
74
    public function buildConfig(ArrayNodeDefinition $root): void
75
    {
76
        $config = $root
77
            ->info('Enable pagination in queries or sub-fields')
78
            ->canBeEnabled()
79
            ->children();
80
81
        /** @var NodeBuilder $rootNode */
82
        $config->scalarNode('target')
83
               ->info('Target node to properly paginate. If is possible will be auto-resolved using naming conventions')
84
               ->isRequired();
85
        $config->variableNode('filters')
86
               ->info('Filters configuration');
87
        $config->variableNode('order_by');
88
        $config->variableNode('search_fields');
89
        $config->integerNode('limit')->info('Max number of records allowed for first & last')->defaultValue($this->limit);
90
        $config->scalarNode('parent_field')
91
               ->info('When is used in sub-fields should be the field to filter by parent instance');
92
        $config->enumNode('parent_relation')
93
               ->info('When is used in sub-fields should be the type of relation with the parent field')
94
               ->defaultValue(self::ONE_TO_MANY)
95
               ->values([self::ONE_TO_MANY, self::MANY_TO_MANY]);
96
    }
97
98
    /**
99
     * {@inheritDoc}
100
     */
101
    public function normalizeConfig(DefinitionInterface $definition, $config): array
102
    {
103
        if (true === $config && $definition instanceof ExecutableDefinitionInterface) {
104
            $config = [];
105
        }
106
107
        if (\is_array($config) && !isset($config['target'])) {
108
            $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

108
            /** @scrutinizer ignore-call */ 
109
            $config['target'] = $definition->getType();
Loading history...
109
        }
110
111
        if (false === $config) {
112
            $config = [];
113
        }
114
115
        return $config;
116
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121
    public function configure(DefinitionInterface $definition, Endpoint $endpoint, array $config): void
122
    {
123
        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...
124
            return;
125
        }
126
127
        if (!$definition instanceof QueryDefinition && !$definition instanceof FieldDefinition) {
128
            return;
129
        }
130
131
        $target = null;
132
        if ($definition instanceof FieldDefinition) {
133
            $target = $definition->getType();
134
            // only apply pagination to inherited fields
135
            // if all interfaces has pagination enabled
136
            if ($definition->getInheritedFrom()) {
137
                foreach ($definition->getInheritedFrom() as $inheritedType) {
138
                    /** @var InterfaceDefinition $inheritedDefinition */
139
                    $inheritedDefinition = $endpoint->getType($inheritedType);
140
                    if (!$inheritedDefinition->getField($definition->getName())->hasMeta('pagination')) {
141
                        return;
142
                    }
143
                }
144
            }
145
        }
146
147
        $search = new ArgumentDefinition();
148
        $search->setName('search');
149
        $search->setType('string');
150
        $search->setNonNull(false);
151
        $search->setDescription('Search in current list by given string');
152
        $definition->addArgument($search);
153
154
        $target = $config['target'] ?? $target;
155
        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

155
        if ($endpoint->hasTypeForClass(/** @scrutinizer ignore-type */ $target)) {
Loading history...
156
            $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

156
            $target = $endpoint->getTypeForClass(/** @scrutinizer ignore-type */ $target);
Loading history...
157
        }
158
        $targetNode = $endpoint->getType($target);
159
160
        $this->addPaginationArguments($definition);
161
        $this->createOrderBy($endpoint, $definition, $targetNode);
162
163
        $connection = $this->createConnection($endpoint, $targetNode);
164
        $definition->setType($connection->getName());
165
        $definition->setList(false);
166
        $definition->setNode($target);
167
        $definition->setMeta('pagination', $config);
168
169
        if (!$definition->getResolver()) {
170
            $definition->setResolver(AllNodesWithPagination::class);
171
        }
172
173
        $this->filterFactory->build($definition, $targetNode, $endpoint);
174
175
        //deprecated, keep for BC with v1
176
        if ($this->bcConfig['filters'] ?? false) {
177
            $this->addFilters($definition, $target, $endpoint);
0 ignored issues
show
Bug introduced by
It seems like $target can also be of type null; however, parameter $targetType of Ynlo\GraphQLBundle\Defin...ionPlugin::addFilters() 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

177
            $this->addFilters($definition, /** @scrutinizer ignore-type */ $target, $endpoint);
Loading history...
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

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

448
        if (!$filters->/** @scrutinizer ignore-call */ getFields()) {
Loading history...
449
            return;
450
        }
451
452
        $search = new ArgumentDefinition();
453
        $search->setName('filters');
454
        $search->setType($filters->getName());
455
        $deprecateMessage = \is_string($this->bcConfig['filters']) ? $this->bcConfig['filters'] : '**DEPRECATED** use `where` instead to filter the list.';
456
        $search->setDescription($deprecateMessage);
457
        $search->setNonNull(false);
458
        $definition->addArgument($search);
459
    }
460
}
461