Passed
Push — master ( 1d5f02...796f61 )
by Alan
05:53
created

FieldsBuilder::mergeFilterArgs()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 10
nc 8
nop 4
dl 0
loc 20
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\GraphQl\Type;
15
16
use ApiPlatform\Core\GraphQl\Resolver\Factory\ResolverFactoryInterface;
17
use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface;
18
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
19
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
20
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
21
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
22
use Doctrine\Common\Inflector\Inflector;
23
use GraphQL\Type\Definition\InputObjectType;
24
use GraphQL\Type\Definition\Type as GraphQLType;
25
use GraphQL\Type\Definition\WrappingType;
26
use Psr\Container\ContainerInterface;
27
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
28
use Symfony\Component\PropertyInfo\Type;
29
30
/**
31
 * Builds the GraphQL fields.
32
 *
33
 * @experimental
34
 *
35
 * @author Alan Poulain <[email protected]>
36
 */
37
final class FieldsBuilder implements FieldsBuilderInterface
38
{
39
    private $propertyNameCollectionFactory;
40
    private $propertyMetadataFactory;
41
    private $resourceMetadataFactory;
42
    private $typesContainer;
43
    private $typeBuilder;
44
    private $typeConverter;
45
    private $itemResolverFactory;
46
    private $collectionResolverFactory;
47
    private $itemMutationResolverFactory;
48
    private $filterLocator;
49
    private $paginationEnabled;
50
51
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, TypesContainerInterface $typesContainer, TypeBuilderInterface $typeBuilder, TypeConverterInterface $typeConverter, ResolverFactoryInterface $itemResolverFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, ContainerInterface $filterLocator, bool $paginationEnabled)
52
    {
53
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
54
        $this->propertyMetadataFactory = $propertyMetadataFactory;
55
        $this->resourceMetadataFactory = $resourceMetadataFactory;
56
        $this->typesContainer = $typesContainer;
57
        $this->typeBuilder = $typeBuilder;
58
        $this->typeConverter = $typeConverter;
59
        $this->itemResolverFactory = $itemResolverFactory;
60
        $this->collectionResolverFactory = $collectionResolverFactory;
61
        $this->itemMutationResolverFactory = $itemMutationResolverFactory;
62
        $this->filterLocator = $filterLocator;
63
        $this->paginationEnabled = $paginationEnabled;
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function getNodeQueryFields(): array
70
    {
71
        return [
72
            'type' => $this->typeBuilder->getNodeInterface(),
73
            'args' => [
74
                'id' => ['type' => GraphQLType::nonNull(GraphQLType::id())],
75
            ],
76
            'resolve' => ($this->itemResolverFactory)(),
77
        ];
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function getQueryFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $queryName, $itemConfiguration, $collectionConfiguration): array
84
    {
85
        $queryFields = [];
86
        $shortName = $resourceMetadata->getShortName();
87
        $fieldName = lcfirst('query' === $queryName ? $shortName : $queryName.$shortName);
88
89
        $deprecationReason = $resourceMetadata->getGraphqlAttribute($queryName, 'deprecation_reason', '', true);
90
91
        if (false !== $itemConfiguration && $fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $queryName, null)) {
92
            $itemConfiguration['args'] = $itemConfiguration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]];
93
94
            $queryFields[$fieldName] = array_merge($fieldConfiguration, $itemConfiguration);
95
        }
96
97
        if (false !== $collectionConfiguration && $fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $queryName, null)) {
98
            $queryFields[Inflector::pluralize($fieldName)] = array_merge($fieldConfiguration, $collectionConfiguration);
99
        }
100
101
        return $queryFields;
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function getMutationFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $mutationName): array
108
    {
109
        $mutationFields = [];
110
        $shortName = $resourceMetadata->getShortName();
111
        $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass);
112
        $deprecationReason = $resourceMetadata->getGraphqlAttribute($mutationName, 'deprecation_reason', '', true);
113
114
        if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, ucfirst("{$mutationName}s a $shortName."), $deprecationReason, $resourceType, $resourceClass, false, null, $mutationName)) {
115
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, null, $deprecationReason, $resourceType, $resourceClass, true, null, $mutationName)];
116
117
            if (!$this->typeBuilder->isCollection($resourceType)) {
118
                $fieldConfiguration['resolve'] = ($this->itemMutationResolverFactory)($resourceClass, null, $mutationName);
119
            }
120
        }
121
122
        $mutationFields[$mutationName.$resourceMetadata->getShortName()] = $fieldConfiguration ?? [];
123
124
        return $mutationFields;
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130
    public function getResourceObjectTypeFields(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input, ?string $queryName, ?string $mutationName, int $depth = 0, ?array $ioMetadata = null): array
131
    {
132
        $fields = [];
133
        $idField = ['type' => GraphQLType::nonNull(GraphQLType::id())];
134
        $clientMutationId = GraphQLType::string();
135
136
        if (null !== $ioMetadata && null === $ioMetadata['class']) {
137
            if ($input) {
138
                return ['clientMutationId' => $clientMutationId];
139
            }
140
141
            return [];
142
        }
143
144
        if ('delete' === $mutationName) {
145
            $fields = [
146
                'id' => $idField,
147
            ];
148
149
            if ($input) {
150
                $fields['clientMutationId'] = $clientMutationId;
151
            }
152
153
            return $fields;
154
        }
155
156
        if (!$input || 'create' !== $mutationName) {
157
            $fields['id'] = $idField;
158
        }
159
160
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
161
162
        if (null !== $resourceClass) {
163
            foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
164
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, ['graphql_operation_name' => $mutationName ?? $queryName ?? 'query']);
165
                if (
166
                    null === ($propertyType = $propertyMetadata->getType())
167
                    || (!$input && false === $propertyMetadata->isReadable())
168
                    || ($input && null !== $mutationName && false === $propertyMetadata->isWritable())
169
                ) {
170
                    continue;
171
                }
172
173
                $rootResource = $resourceClass;
174
                if (null !== $propertyMetadata->getSubresource()) {
175
                    $resourceClass = $propertyMetadata->getSubresource()->getResourceClass();
176
                    $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
177
                }
178
                if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, $property, $propertyMetadata->getDescription(), $propertyMetadata->getAttribute('deprecation_reason', ''), $propertyType, $rootResource, $input, $queryName, $mutationName, $depth)) {
179
                    $fields['id' === $property ? '_id' : $property] = $fieldConfiguration;
180
                }
181
                $resourceClass = $rootResource;
182
            }
183
        }
184
185
        if (null !== $mutationName && $input) {
186
            $fields['clientMutationId'] = $clientMutationId;
187
        }
188
189
        return $fields;
190
    }
191
192
    /**
193
     * Get the field configuration of a resource.
194
     *
195
     * @see http://webonyx.github.io/graphql-php/type-system/object-types/
196
     */
197
    private function getResourceFieldConfiguration(string $resourceClass, ResourceMetadata $resourceMetadata, ?string $property, ?string $fieldDescription, string $deprecationReason, Type $type, string $rootResource, bool $input, ?string $queryName, ?string $mutationName, int $depth = 0): ?array
198
    {
199
        try {
200
            if (null === $graphqlType = $this->convertType($type, $input, $queryName, $mutationName, $resourceClass, $property, $depth)) {
0 ignored issues
show
introduced by
The condition null === $graphqlType = ...ass, $property, $depth) is always false.
Loading history...
201
                return null;
202
            }
203
204
            $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType() : $graphqlType;
205
            $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
206
            if ($isStandardGraphqlType) {
207
                $className = '';
208
            } else {
209
                $className = $this->typeBuilder->isCollection($type) && ($collectionValueType = $type->getCollectionValueType()) ? $collectionValueType->getClassName() : $type->getClassName();
210
            }
211
212
            $args = [];
213
            if (!$input && null === $mutationName && !$isStandardGraphqlType && $this->typeBuilder->isCollection($type)) {
214
                if ($this->paginationEnabled) {
215
                    $args = [
216
                        'first' => [
217
                            'type' => GraphQLType::int(),
218
                            'description' => 'Returns the first n elements from the list.',
219
                        ],
220
                        'last' => [
221
                            'type' => GraphQLType::int(),
222
                            'description' => 'Returns the last n elements from the list.',
223
                        ],
224
                        'before' => [
225
                            'type' => GraphQLType::string(),
226
                            'description' => 'Returns the elements in the list that come before the specified cursor.',
227
                        ],
228
                        'after' => [
229
                            'type' => GraphQLType::string(),
230
                            'description' => 'Returns the elements in the list that come after the specified cursor.',
231
                        ],
232
                    ];
233
                }
234
235
                foreach ($resourceMetadata->getGraphqlAttribute($queryName ?? 'query', 'filters', [], true) as $filterId) {
236
                    if (null === $this->filterLocator || !$this->filterLocator->has($filterId)) {
237
                        continue;
238
                    }
239
240
                    foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) {
241
                        $nullable = isset($value['required']) ? !$value['required'] : true;
242
                        $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']);
243
                        $graphqlFilterType = $this->convertType($filterType, false, $queryName, $mutationName, $resourceClass, $property, $depth);
244
245
                        if ('[]' === substr($key, -2)) {
246
                            $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
247
                            $key = substr($key, 0, -2).'_list';
248
                        }
249
250
                        parse_str($key, $parsed);
251
                        if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
252
                            $parsed = [$key => ''];
253
                        }
254
                        array_walk_recursive($parsed, function (&$value) use ($graphqlFilterType) {
255
                            $value = $graphqlFilterType;
256
                        });
257
                        $args = $this->mergeFilterArgs($args, $parsed, $resourceMetadata, $key);
258
                    }
259
                }
260
                $args = $this->convertFilterArgsToTypes($args);
261
            }
262
263
            if ($isStandardGraphqlType || $input) {
264
                $resolve = null;
265
            } elseif ($this->typeBuilder->isCollection($type)) {
266
                $resolve = ($this->collectionResolverFactory)($className, $rootResource, $queryName);
267
            } else {
268
                $resolve = ($this->itemResolverFactory)($className, $rootResource, $queryName);
269
            }
270
271
            return [
272
                'type' => $graphqlType,
273
                'description' => $fieldDescription,
274
                'args' => $args,
275
                'resolve' => $resolve,
276
                'deprecationReason' => $deprecationReason,
277
            ];
278
        } catch (InvalidTypeException $e) {
279
            // just ignore invalid types
280
        }
281
282
        return null;
283
    }
284
285
    private function mergeFilterArgs(array $args, array $parsed, ResourceMetadata $resourceMetadata = null, $original = ''): array
286
    {
287
        foreach ($parsed as $key => $value) {
288
            // Never override keys that cannot be merged
289
            if (isset($args[$key]) && !\is_array($args[$key])) {
290
                continue;
291
            }
292
293
            if (\is_array($value)) {
294
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
295
                if (!isset($value['#name'])) {
296
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
297
                    $value['#name'] = ($resourceMetadata ? $resourceMetadata->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
298
                }
299
            }
300
301
            $args[$key] = $value;
302
        }
303
304
        return $args;
305
    }
306
307
    private function convertFilterArgsToTypes(array $args): array
308
    {
309
        foreach ($args as $key => $value) {
310
            if (strpos($key, '.')) {
311
                // Declare relations/nested fields in a GraphQL compatible syntax.
312
                $args[str_replace('.', '_', $key)] = $value;
313
                unset($args[$key]);
314
            }
315
        }
316
317
        foreach ($args as $key => $value) {
318
            if (!\is_array($value) || !isset($value['#name'])) {
319
                continue;
320
            }
321
322
            $name = $value['#name'];
323
324
            if ($this->typesContainer->has($name)) {
325
                $args[$key] = $this->typesContainer->get($name);
326
                continue;
327
            }
328
329
            unset($value['#name']);
330
331
            $filterArgType = new InputObjectType([
332
                'name' => $name,
333
                'fields' => $this->convertFilterArgsToTypes($value),
334
            ]);
335
336
            $this->typesContainer->set($name, $filterArgType);
337
338
            $args[$key] = $filterArgType;
339
        }
340
341
        return $args;
342
    }
343
344
    /**
345
     * Converts a built-in type to its GraphQL equivalent.
346
     *
347
     * @throws InvalidTypeException
348
     */
349
    private function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, ?string $property, int $depth)
350
    {
351
        $graphqlType = $this->typeConverter->convertType($type, $input, $queryName, $mutationName, $resourceClass, $property, $depth);
352
353
        if (null === $graphqlType) {
354
            throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $type->getBuiltinType()));
355
        }
356
357
        if (\is_string($graphqlType)) {
358
            if (!$this->typesContainer->has($graphqlType)) {
359
                throw new InvalidTypeException(sprintf('The GraphQL type %s is not valid. Valid types are: %s. Have you registered this type by implementing %s?', $graphqlType, implode(', ', array_keys($this->typesContainer->all())), TypeInterface::class));
360
            }
361
362
            $graphqlType = $this->typesContainer->get($graphqlType);
363
        }
364
365
        if ($this->typeBuilder->isCollection($type)) {
366
            return $this->paginationEnabled && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType) : GraphQLType::listOf($graphqlType);
367
        }
368
369
        return $type->isNullable() || (null !== $mutationName && 'update' === $mutationName) ? $graphqlType : GraphQLType::nonNull($graphqlType);
0 ignored issues
show
Bug introduced by
$graphqlType of type GraphQL\Type\Definition\Type is incompatible with the type GraphQL\Type\Definition\NullableType expected by parameter $wrappedType of GraphQL\Type\Definition\Type::nonNull(). ( Ignorable by Annotation )

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

369
        return $type->isNullable() || (null !== $mutationName && 'update' === $mutationName) ? $graphqlType : GraphQLType::nonNull(/** @scrutinizer ignore-type */ $graphqlType);
Loading history...
370
    }
371
}
372