Passed
Push — master ( 46c3ee...445208 )
by Rafael
04:28
created

PaginationDefinitionPlugin::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

103
            /** @scrutinizer ignore-call */ 
104
            $config['target'] = $definition->getType();
Loading history...
104
        }
105
106
        if (false === $config) {
107
            $config = [];
108
        }
109
110
        return $config;
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function configure(DefinitionInterface $definition, Endpoint $endpoint, array $config): void
117
    {
118
        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...
119
            return;
120
        }
121
122
        if (!$definition instanceof QueryDefinition && !$definition instanceof FieldDefinition) {
123
            return;
124
        }
125
126
        $search = new ArgumentDefinition();
127
        $search->setName('search');
128
        $search->setType('string');
129
        $search->setNonNull(false);
130
        $search->setDescription('Search in current list by given string');
131
        $definition->addArgument($search);
132
133
        $target = null;
134
        if ($definition instanceof FieldDefinition) {
135
            $target = $definition->getType();
136
        }
137
138
        $target = $config['target'] ?? $target;
139
        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

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

140
            $target = $endpoint->getTypeForClass(/** @scrutinizer ignore-type */ $target);
Loading history...
141
        }
142
        $targetNode = $endpoint->getType($target);
143
144
        $this->addPaginationArguments($definition);
145
        $this->createOrderBy($endpoint, $definition, $targetNode);
146
147
        $connection = $this->createConnection($endpoint, $targetNode);
148
        $definition->setType($connection->getName());
149
        $definition->setList(false);
150
        $definition->setNode($target);
151
        $definition->setMeta('pagination', $config);
152
153
        if (!$definition->getResolver()) {
154
            $definition->setResolver(AllNodesWithPagination::class);
155
        }
156
157
        $this->filterFactory->build($definition, $targetNode, $endpoint);
158
159
        //deprecated, keep for BC with v1
160
        if ($this->bcConfig['filters'] ?? false) {
161
            $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

161
            /** @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...
162
        }
163
    }
164
165
    /**
166
     * @param Endpoint            $endpoint
167
     * @param DefinitionInterface $node
168
     *
169
     * @return ObjectDefinition
170
     */
171
    private function createConnection(Endpoint $endpoint, DefinitionInterface $node): ObjectDefinition
172
    {
173
        $connection = new ObjectDefinition();
174
        $connection->setName("{$node->getName()}Connection");
175
176
        if (!$endpoint->hasType($connection->getName())) {
177
            $endpoint->addType($connection);
178
179
            $totalCount = new FieldDefinition();
180
            $totalCount->setName('totalCount');
181
            $totalCount->setType('Int');
182
            $totalCount->setNonNull(true);
183
            $connection->addField($totalCount);
184
185
            $pageInfo = new FieldDefinition();
186
            $pageInfo->setName('pageInfo');
187
            $pageInfo->setType('PageInfo');
188
            $pageInfo->setNonNull(true);
189
            $connection->addField($pageInfo);
190
191
            $edgeObject = new ObjectDefinition();
192
            $edgeObject->setName("{$node->getName()}Edge");
193
            if (!$endpoint->hasType($edgeObject->getName())) {
194
                $endpoint->addType($edgeObject);
195
196
                $nodeField = new FieldDefinition();
197
                $nodeField->setName('node');
198
                $nodeField->setType($node->getName());
199
                $nodeField->setNonNull(true);
200
                $edgeObject->addField($nodeField);
201
202
                $cursor = new FieldDefinition();
203
                $cursor->setName('cursor');
204
                $cursor->setType('string');
205
                $cursor->setNonNull(true);
206
                $edgeObject->addField($cursor);
207
            }
208
209
            $edges = new FieldDefinition();
210
            $edges->setName('edges');
211
            $edges->setType($edgeObject->getName());
212
            $edges->setList(true);
213
            $connection->addField($edges);
214
        } else {
215
            $connection = $endpoint->getType($connection->getName());
216
        }
217
218
        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...
219
    }
220
221
    /**
222
     * @param Endpoint                       $endpoint
223
     * @param ExecutableDefinitionInterface  $query
224
     * @param FieldsAwareDefinitionInterface $node
225
     */
226
    private function createOrderBy(Endpoint $endpoint, ExecutableDefinitionInterface $query, FieldsAwareDefinitionInterface $node)
227
    {
228
        /** @var InputObjectDefinition $orderBy */
229
        $orderBy = unserialize(serialize($endpoint->getType(OrderBy::class)), ['allowed_classes' => true]); //clone recursively
230
        $orderBy->setName("{$node->getName()}OrderBy");
231
232
        if (!$endpoint->hasType($orderBy->getName())) {
233
            $orderByFields = new EnumDefinition();
234
            $orderByFields->setName("{$node->getName()}OrderByField");
235
            $options = $query->getMeta('pagination')['order_by'] ?? ['*'];
236
            foreach ($node->getFields() as $field) {
237
                if (!FieldOptionsHelper::isEnabled($options, $field->getName())) {
238
                    continue;
239
                }
240
241
                //ignore if non related to entity property
242
                if ($field->getOriginType() !== \ReflectionProperty::class) {
243
                    continue;
244
                }
245
246
                //ignore if is a list
247
                if ($field->isList()) {
248
                    continue;
249
                }
250
251
                //ignore if is related to other object
252
                if ($endpoint->hasType($field->getType()) && $endpoint->getType($field->getType()) instanceof FieldsAwareDefinitionInterface) {
253
                    continue;
254
                }
255
256
                $orderByFields->addValue(new EnumValueDefinition($field->getName()));
257
            }
258
            if ($orderByFields->getValues()) {
259
                $orderBy->getField('field')->setType($orderByFields->getName());
260
                $endpoint->addType($orderByFields);
261
                $endpoint->addType($orderBy);
262
            } else {
263
                return;
264
            }
265
        } else {
266
            $orderBy = $endpoint->getType($orderBy->getName());
267
        }
268
269
        $arg = new ArgumentDefinition();
270
        $arg->setName('order');
271
        $arg->setType($orderBy->getName());
272
        $arg->setNonNull(false);
273
        $arg->setList(true);
274
        $arg->setDescription('Ordering options for this list.');
275
        $query->addArgument($arg);
276
277
        //to keep BC
278
        if ($this->bcConfig['orderBy'] ?? false) {
279
            $arg = new ArgumentDefinition();
280
            $arg->setName('orderBy');
281
            $arg->setType(OrderBy::class);
282
            $arg->setNonNull(false);
283
            $arg->setList(true);
284
            $deprecateMessage = \is_string($this->bcConfig['orderBy']) ? $this->bcConfig['orderBy'] : '**DEPRECATED** use `order` instead.';
285
            $arg->setDescription($deprecateMessage);
286
            $query->addArgument($arg);
287
        }
288
    }
289
290
    /**
291
     * @param ExecutableDefinitionInterface $definition
292
     */
293
    private function addPaginationArguments(ExecutableDefinitionInterface $definition): void
294
    {
295
        $first = new ArgumentDefinition();
296
        $first->setName('first');
297
        $first->setType('int');
298
        $first->setNonNull(false);
299
        $first->setDescription('Returns the first *n* elements from the list.');
300
        $definition->addArgument($first);
301
302
        $last = new ArgumentDefinition();
303
        $last->setName('last');
304
        $last->setType('int');
305
        $last->setNonNull(false);
306
        $last->setDescription('Returns the last *n* elements from the list.');
307
        $definition->addArgument($last);
308
309
        $after = new ArgumentDefinition();
310
        $after->setName('after');
311
        $after->setType('string');
312
        $after->setNonNull(false);
313
        $after->setDescription('Returns the last *n* elements from the list.');
314
        $definition->addArgument($after);
315
316
        $before = new ArgumentDefinition();
317
        $before->setName('before');
318
        $before->setType('string');
319
        $before->setNonNull(false);
320
        $before->setDescription('Returns the last *n* elements from the list.');
321
        $definition->addArgument($before);
322
    }
323
324
    /**
325
     * @param ExecutableDefinitionInterface $definition
326
     * @param string                        $targetType
327
     * @param Endpoint                      $endpoint
328
     *
329
     * @throws \ReflectionException
330
     *
331
     * @deprecated since v1.2, should use `where` instead
332
     */
333
    private function addFilters(ExecutableDefinitionInterface $definition, string $targetType, Endpoint $endpoint): void
334
    {
335
        $filterName = ucfirst($definition->getName()).'Filter';
336
        if ($endpoint->hasType($filterName)) {
337
            $filters = $endpoint->getType($filterName);
338
        } else {
339
            $filters = new InputObjectDefinition();
340
            $filters->setName($filterName);
341
            $endpoint->add($filters);
342
343
            $object = $endpoint->getType($targetType);
344
            if ($object instanceof FieldsAwareDefinitionInterface) {
345
                foreach ($object->getFields() as $field) {
346
                    if ('id' === $field->getName()
347
                        || !$field->getOriginName()
348
                        || \ReflectionProperty::class !== $field->getOriginType()) {
349
                        continue;
350
                    }
351
352
                    $filter = new FieldDefinition();
353
                    $filter->setName($field->getName());
354
                    $type = $field->getType();
355
                    if ($endpoint->hasType($type)) {
356
                        $typeDefinition = $endpoint->getType($type);
357
                        if (!$typeDefinition instanceof EnumDefinition) {
358
                            $type = 'ID';
359
                        }
360
                        $filter->setList(true);
361
                    }
362
363
                    // fields using custom object as type
364
                    // are not available for filters
365
                    if (TypeRegistry::getTypeMapp()) {
366
                        if (isset(TypeRegistry::getTypeMapp()[$type])) {
367
                            $class = TypeRegistry::getTypeMapp()[$type];
368
                            $ref = new \ReflectionClass($class);
369
                            if ($ref->isSubclassOf(ObjectType::class)) {
370
                                continue;
371
                            }
372
                        }
373
                    }
374
375
                    $filter->setType($type);
376
                    $filters->addField($filter);
377
                }
378
            }
379
        }
380
381
        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

381
        if (!$filters->/** @scrutinizer ignore-call */ getFields()) {
Loading history...
382
            return;
383
        }
384
385
        $search = new ArgumentDefinition();
386
        $search->setName('filters');
387
        $search->setType($filters->getName());
388
        $deprecateMessage = \is_string($this->bcConfig['filters']) ? $this->bcConfig['filters'] : '**DEPRECATED** use `where` instead to filter the list.';
389
        $search->setDescription($deprecateMessage);
390
        $search->setNonNull(false);
391
        $definition->addArgument($search);
392
    }
393
}
394