Completed
Push — master ( 2975e7...46c3ee )
by Rafael
07:49
created

PaginationDefinitionPlugin::normalizeConfig()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 0
cts 12
cp 0
rs 9.2222
c 0
b 0
f 0
cc 6
nc 8
nop 2
crap 42
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\ExecutableDefinitionInterface;
20
use Ynlo\GraphQLBundle\Definition\FieldDefinition;
21
use Ynlo\GraphQLBundle\Definition\FieldsAwareDefinitionInterface;
22
use Ynlo\GraphQLBundle\Definition\InputObjectDefinition;
23
use Ynlo\GraphQLBundle\Definition\ObjectDefinition;
24
use Ynlo\GraphQLBundle\Definition\QueryDefinition;
25
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint;
26
use Ynlo\GraphQLBundle\Filter\FilterFactory;
27
use Ynlo\GraphQLBundle\Model\Filter\DateTimeComparisonExpression;
28
use Ynlo\GraphQLBundle\Model\Filter\FloatComparisonExpression;
29
use Ynlo\GraphQLBundle\Model\Filter\IntegerComparisonExpression;
30
use Ynlo\GraphQLBundle\Model\Filter\NodeComparisonExpression;
31
use Ynlo\GraphQLBundle\Model\Filter\StringComparisonExpression;
32
use Ynlo\GraphQLBundle\Model\OrderBy;
33
use Ynlo\GraphQLBundle\Query\Node\AllNodesWithPagination;
34
use Ynlo\GraphQLBundle\Type\Registry\TypeRegistry;
35
36
/**
37
 * Convert a simple return of nodes into a paginated collection with edges
38
 */
39
class PaginationDefinitionPlugin extends AbstractDefinitionPlugin
40
{
41
    public const ONE_TO_MANY = 'ONE_TO_MANY';
42
    public const MANY_TO_MANY = 'MANY_TO_MANY';
43
44
    /**
45
     * @var FilterFactory
46
     */
47
    protected $filterFactory;
48
49
    /**
50
     * @var int
51
     */
52
    protected $limit;
53
54
    /**
55
     * PaginationDefinitionPlugin constructor.
56
     *
57
     * @param FilterFactory $filterFactory
58
     * @param array         $config
59
     */
60
    public function __construct(FilterFactory $filterFactory, array $config = [])
61
    {
62
        $this->filterFactory = $filterFactory;
63
        $this->limit = $config['limit'] ?? 100;
64
    }
65
66
    /**
67
     * {@inheritDoc}
68
     */
69
    public function buildConfig(ArrayNodeDefinition $root): void
70
    {
71
        $config = $root
72
            ->info('Enable pagination in queries or sub-fields')
73
            ->canBeEnabled()
74
            ->children();
75
76
        /** @var NodeBuilder $rootNode */
77
        $config->scalarNode('target')
78
               ->info('Target node to properly paginate. If is possible will be auto-resolved using naming conventions')
79
               ->isRequired();
80
        $config->variableNode('filters')
81
               ->info('Filters configuration');
82
        $config->integerNode('limit')->info('Max number of records allowed for first & last')->defaultValue($this->limit);
83
        $config->scalarNode('parent_field')
84
               ->info('When is used in sub-fields should be the field to filter by parent instance');
85
        $config->enumNode('parent_relation')
86
               ->info('When is used in sub-fields should be the type of relation with the parent field')
87
               ->defaultValue(self::ONE_TO_MANY)
88
               ->values([self::ONE_TO_MANY, self::MANY_TO_MANY]);
89
    }
90
91
    /**
92
     * {@inheritDoc}
93
     */
94
    public function normalizeConfig(DefinitionInterface $definition, $config): array
95
    {
96
        if (true === $config && $definition instanceof ExecutableDefinitionInterface) {
97
            $config = [];
98
        }
99
100
        if (\is_array($config) && !isset($config['target'])) {
101
            $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

101
            /** @scrutinizer ignore-call */ 
102
            $config['target'] = $definition->getType();
Loading history...
102
        }
103
104
        if (false === $config) {
105
            $config = [];
106
        }
107
108
        return $config;
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function configure(DefinitionInterface $definition, Endpoint $endpoint, array $config): void
115
    {
116
        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...
117
            return;
118
        }
119
120
        if (!$definition instanceof QueryDefinition && !$definition instanceof FieldDefinition) {
121
            return;
122
        }
123
124
        $search = new ArgumentDefinition();
125
        $search->setName('search');
126
        $search->setType('string');
127
        $search->setNonNull(false);
128
        $search->setDescription('Search in current list by given string');
129
        $definition->addArgument($search);
130
131
        $first = new ArgumentDefinition();
132
        $first->setName('first');
133
        $first->setType('int');
134
        $first->setNonNull(false);
135
        $first->setDescription('Returns the first *n* elements from the list.');
136
        $definition->addArgument($first);
137
138
        $last = new ArgumentDefinition();
139
        $last->setName('last');
140
        $last->setType('int');
141
        $last->setNonNull(false);
142
        $last->setDescription('Returns the last *n* elements from the list.');
143
        $definition->addArgument($last);
144
145
        $after = new ArgumentDefinition();
146
        $after->setName('after');
147
        $after->setType('string');
148
        $after->setNonNull(false);
149
        $after->setDescription('Returns the last *n* elements from the list.');
150
        $definition->addArgument($after);
151
152
        $before = new ArgumentDefinition();
153
        $before->setName('before');
154
        $before->setType('string');
155
        $before->setNonNull(false);
156
        $before->setDescription('Returns the last *n* elements from the list.');
157
        $definition->addArgument($before);
158
159
        if (!$definition->hasArgument('orderBy')) {
160
            $orderBy = new ArgumentDefinition();
161
            $orderBy->setName('orderBy');
162
            $orderBy->setType(OrderBy::class);
163
            $orderBy->setNonNull(false);
164
            $orderBy->setList(true);
165
            $orderBy->setDescription('Ordering options for this list.');
166
            $definition->addArgument($orderBy);
167
        } else {
168
            //if exist move to the end
169
            $orderBy = $definition->getArgument('orderBy');
170
            $definition->removeArgument('orderBy');
171
            $definition->addArgument($orderBy);
172
        }
173
174
        $target = null;
175
        if ($definition instanceof FieldDefinition) {
176
            $target = $definition->getType();
177
        }
178
179
        $target = $config['target'] ?? $target;
180
        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

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

181
            $target = $endpoint->getTypeForClass(/** @scrutinizer ignore-type */ $target);
Loading history...
182
        }
183
184
        $connection = new ObjectDefinition();
185
        $connection->setName(ucfirst($target).'Connection');
186
187
        if (!$endpoint->hasType($connection->getName())) {
188
            $endpoint->addType($connection);
189
190
            $totalCount = new FieldDefinition();
191
            $totalCount->setName('totalCount');
192
            $totalCount->setType('Int');
193
            $totalCount->setNonNull(true);
194
            $connection->addField($totalCount);
195
196
            $pageInfo = new FieldDefinition();
197
            $pageInfo->setName('pageInfo');
198
            $pageInfo->setType('PageInfo');
199
            $pageInfo->setNonNull(true);
200
            $connection->addField($pageInfo);
201
202
            $edgeObject = new ObjectDefinition();
203
            $edgeObject->setName(ucfirst($target).'Edge');
204
            if (!$endpoint->hasType($edgeObject->getName())) {
205
                $endpoint->addType($edgeObject);
206
207
                $node = new FieldDefinition();
208
                $node->setName('node');
209
                $node->setType($target);
210
                $node->setNonNull(true);
211
                $edgeObject->addField($node);
212
213
                $cursor = new FieldDefinition();
214
                $cursor->setName('cursor');
215
                $cursor->setType('string');
216
                $cursor->setNonNull(true);
217
                $edgeObject->addField($cursor);
218
            }
219
220
            $edges = new FieldDefinition();
221
            $edges->setName('edges');
222
            $edges->setType($edgeObject->getName());
223
            $edges->setList(true);
224
            $connection->addField($edges);
225
        }
226
227
        $definition->setType($connection->getName());
228
        $definition->setList(false);
229
        $definition->setMeta('node', $target);
230
        $definition->setMeta('pagination', $config);
231
232
        if (!$definition->getResolver()) {
233
            $definition->setResolver(AllNodesWithPagination::class);
234
        }
235
        if (!$endpoint->hasType($target)) {
236
            dump($target);
237
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
238
        }
239
        $this->filterFactory->build($definition, $endpoint->getType($target), $endpoint);
240
241
        $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.1, 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

241
        /** @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...
242
    }
243
244
    /**
245
     * @param ExecutableDefinitionInterface $definition
246
     * @param string                        $targetType
247
     * @param Endpoint                      $endpoint
248
     *
249
     * @throws \ReflectionException
250
     */
251
    private function addWhere(ExecutableDefinitionInterface $definition, string $targetType, Endpoint $endpoint): void
0 ignored issues
show
Unused Code introduced by
The parameter $targetType is not used and could be removed. ( Ignorable by Annotation )

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

251
    private function addWhere(ExecutableDefinitionInterface $definition, /** @scrutinizer ignore-unused */ string $targetType, Endpoint $endpoint): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The method addWhere() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
252
    {
253
        $whereName = ucfirst($definition->getName()).'Condition';
254
        if ($endpoint->hasType($whereName)) {
255
            $wheres = $endpoint->getType($whereName);
256
        } else {
257
            $wheres = new InputObjectDefinition();
258
            $wheres->setName($whereName);
259
            $endpoint->add($wheres);
260
261
            $field = new FieldDefinition();
262
            $field->setName('createdAt');
263
            $field->setType(DateTimeComparisonExpression::class);
264
            $wheres->addField($field);
265
266
            $field = new FieldDefinition();
267
            $field->setName('name');
268
            $field->setType(StringComparisonExpression::class);
269
            $wheres->addField($field);
270
271
            $field = new FieldDefinition();
272
            $field->setName('age');
273
            $field->setType(IntegerComparisonExpression::class);
274
            $wheres->addField($field);
275
276
            $field = new FieldDefinition();
277
            $field->setName('credits');
278
            $field->setType(FloatComparisonExpression::class);
279
            $wheres->addField($field);
280
281
            $field = new FieldDefinition();
282
            $field->setName('categories');
283
            $field->setType(NodeComparisonExpression::class);
284
            $wheres->addField($field);
285
        }
286
287
        if (!$wheres->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

287
        if (!$wheres->/** @scrutinizer ignore-call */ getFields()) {
Loading history...
288
            return;
289
        }
290
291
        $where = new ArgumentDefinition();
292
        $where->setName('where');
293
        $where->setType($wheres->getName());
294
        $where->setNonNull(false);
295
        $where->setDescription('Filter the list using conditions');
296
297
        $definition->addArgument($where);
298
    }
299
300
    /**
301
     * @param ExecutableDefinitionInterface $definition
302
     * @param string                        $targetType
303
     * @param Endpoint                      $endpoint
304
     *
305
     * @throws \ReflectionException
306
     *
307
     * @deprecated since v1.1, should use `where` instead
308
     */
309
    private function addFilters(ExecutableDefinitionInterface $definition, string $targetType, Endpoint $endpoint): void
310
    {
311
        $filterName = ucfirst($definition->getName()).'Filter';
312
        if ($endpoint->hasType($filterName)) {
313
            $filters = $endpoint->getType($filterName);
314
        } else {
315
            $filters = new InputObjectDefinition();
316
            $filters->setName($filterName);
317
            $endpoint->add($filters);
318
319
            $object = $endpoint->getType($targetType);
320
            if ($object instanceof FieldsAwareDefinitionInterface) {
321
                foreach ($object->getFields() as $field) {
322
                    if ('id' === $field->getName()
323
                        || !$field->getOriginName()
324
                        || \ReflectionProperty::class !== $field->getOriginType()) {
325
                        continue;
326
                    }
327
328
                    $filter = new FieldDefinition();
329
                    $filter->setName($field->getName());
330
                    $type = $field->getType();
331
                    if ($endpoint->hasType($type)) {
332
                        $typeDefinition = $endpoint->getType($type);
333
                        if (!$typeDefinition instanceof EnumDefinition) {
334
                            $type = 'ID';
335
                        }
336
                        $filter->setList(true);
337
                    }
338
339
                    // fields using custom object as type
340
                    // are not available for filters
341
                    if (TypeRegistry::getTypeMapp()) {
342
                        if (isset(TypeRegistry::getTypeMapp()[$type])) {
343
                            $class = TypeRegistry::getTypeMapp()[$type];
344
                            $ref = new \ReflectionClass($class);
345
                            if ($ref->isSubclassOf(ObjectType::class)) {
346
                                continue;
347
                            }
348
                        }
349
                    }
350
351
                    $filter->setType($type);
352
                    $filters->addField($filter);
353
                }
354
            }
355
        }
356
357
        if (!$filters->getFields()) {
358
            return;
359
        }
360
361
        $search = new ArgumentDefinition();
362
        $search->setName('filters');
363
        $search->setType($filters->getName());
364
        $search->setDescription('**DEPRECATED** use `where` instead to filter the list.');
365
        $search->setNonNull(false);
366
        $definition->addArgument($search);
367
    }
368
}
369