Completed
Push — grapqlops ( 8457ea )
by Kévin
02:47
created

SchemaBuilder::getQueryFields()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
c 0
b 0
f 0
rs 9.4285
cc 3
eloc 10
nc 4
nop 1
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\Bridge\Graphql\Type;
15
16
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
17
use ApiPlatform\Core\Bridge\Graphql\Resolver\CollectionResolverFactoryInterface;
18
use ApiPlatform\Core\Bridge\Graphql\Resolver\ItemResolverFactoryInterface;
19
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
20
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
21
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
22
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
23
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
24
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
25
use Doctrine\Common\Util\Inflector;
26
use GraphQL\Type\Definition\InputObjectType;
27
use GraphQL\Type\Definition\ObjectType;
28
use GraphQL\Type\Definition\Type as GraphQLType;
29
use GraphQL\Type\Definition\WrappingType;
30
use GraphQL\Type\Schema;
31
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
32
use Symfony\Component\PropertyInfo\Type;
33
34
/**
35
 * Builder of the GraphQL schema.
36
 *
37
 * @author Raoul Clais <[email protected]>
38
 * @author Alan Poulain <[email protected]>
39
 *
40
 * @internal
41
 */
42
final class SchemaBuilder implements SchemaBuilderInterface
43
{
44
    private $propertyNameCollectionFactory;
45
    private $propertyMetadataFactory;
46
    private $resourceNameCollectionFactory;
47
    private $resourceMetadataFactory;
48
    private $collectionResolverFactory;
49
    private $itemResolverFactory;
50
    private $identifiersExtractor;
51
    private $paginationEnabled;
52
    private $resourceTypesCache = [];
53
54 View Code Duplication
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, CollectionResolverFactoryInterface $collectionResolverFactory, ItemResolverFactoryInterface $itemResolverFactory, IdentifiersExtractorInterface $identifiersExtractor, bool $paginationEnabled)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
55
    {
56
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
57
        $this->propertyMetadataFactory = $propertyMetadataFactory;
58
        $this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
59
        $this->resourceMetadataFactory = $resourceMetadataFactory;
60
        $this->collectionResolverFactory = $collectionResolverFactory;
61
        $this->itemResolverFactory = $itemResolverFactory;
62
        $this->identifiersExtractor = $identifiersExtractor;
63
        $this->paginationEnabled = $paginationEnabled;
64
    }
65
66
    public function getSchema(): Schema
67
    {
68
        $queryFields = [];
69
        foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
70
            $queryFields += $this->getQueryFields($resourceClass);
71
        }
72
73
        return new Schema([
74
            'query' => new ObjectType([
75
                'name' => 'Query',
76
                'fields' => $queryFields,
77
            ]),
78
        ]);
79
    }
80
81
    /**
82
     * Gets the query fields of the schema.
83
     */
84
    private function getQueryFields(string $resourceClass): array
85
    {
86
        $queryFields = [];
87
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
88
        $shortName = $resourceMetadata->getShortName();
89
90
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass)) {
91
            $fieldConfiguration['args'] += $this->getResourceIdentifiersArgumentsConfiguration($resourceClass);
92
            $queryFields[lcfirst($shortName)] = $fieldConfiguration;
93
        }
94
95
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass)) {
96
            $queryFields[lcfirst(Inflector::pluralize($shortName))] = $fieldConfiguration;
97
        }
98
99
        return $queryFields;
100
    }
101
102
    /**
103
     * Gets the field configuration of a resource.
104
     *
105
     * @see http://webonyx.github.io/graphql-php/type-system/object-types/
106
     *
107
     * @return array|null
108
     */
109
    private function getResourceFieldConfiguration(string $fieldDescription = null, Type $type, string $rootResource, bool $isInput = false)
110
    {
111
        try {
112
            $graphqlType = $this->convertType($type, $isInput);
113
            $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType() : $graphqlType;
114
            $isInternalGraphqlType = in_array($graphqlWrappedType, GraphQLType::getInternalTypes(), true);
115
            if ($isInternalGraphqlType) {
116
                $className = '';
117
            } else {
118
                $className = $type->isCollection() ? $type->getCollectionValueType()->getClassName() : $type->getClassName();
119
            }
120
121
            $args = [];
122
            if ($this->paginationEnabled && !$isInternalGraphqlType && $type->isCollection() && !$isInput) {
123
                $args = [
124
                    'first' => [
125
                        'type' => GraphQLType::int(),
126
                        'description' => 'Returns the first n elements from the list.',
127
                    ],
128
                    'after' => [
129
                        'type' => GraphQLType::string(),
130
                        'description' => 'Returns the elements in the list that come after the specified cursor.',
131
                    ],
132
                ];
133
            }
134
135
            if ($isInternalGraphqlType || $isInput) {
136
                $resolve = null;
137
            } else {
138
                $resolve = $type->isCollection() ? $this->collectionResolverFactory->createCollectionResolver($className, $rootResource) : $this->itemResolverFactory->createItemResolver($className, $rootResource);
139
            }
140
141
            return [
142
                'type' => $graphqlType,
143
                'description' => $fieldDescription,
144
                'args' => $args,
145
                'resolve' => $resolve,
146
            ];
147
        } catch (InvalidTypeException $e) {
148
            return null;
149
        }
150
    }
151
152
    /**
153
     * Gets the field arguments of the identifier of a given resource.
154
     *
155
     * @throws \LogicException
156
     */
157 View Code Duplication
    private function getResourceIdentifiersArgumentsConfiguration(string $resourceClass): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
158
    {
159
        $arguments = [];
160
        $identifiers = $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass);
161
        foreach ($identifiers as $identifier) {
162
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $identifier);
163
            $propertyType = $propertyMetadata->getType();
164
            if (null === $propertyType) {
165
                continue;
166
            }
167
168
            $arguments[$identifier] = $this->getResourceFieldConfiguration($propertyMetadata->getDescription(), $propertyType, $resourceClass, true);
169
        }
170
        if (!$arguments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $arguments 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...
171
            throw new \LogicException("Missing identifier field for resource \"$resourceClass\".");
172
        }
173
174
        return $arguments;
175
    }
176
177
    /**
178
     * Converts a built-in type to its GraphQL equivalent.
179
     *
180
     * @throws InvalidTypeException
181
     */
182
    private function convertType(Type $type, bool $isInput = false): GraphQLType
183
    {
184
        switch ($type->getBuiltinType()) {
185
            case Type::BUILTIN_TYPE_BOOL:
186
                $graphqlType = GraphQLType::boolean();
187
                break;
188
            case Type::BUILTIN_TYPE_INT:
189
                $graphqlType = GraphQLType::int();
190
                break;
191
            case Type::BUILTIN_TYPE_FLOAT:
192
                $graphqlType = GraphQLType::float();
193
                break;
194
            case Type::BUILTIN_TYPE_STRING:
195
                $graphqlType = GraphQLType::string();
196
                break;
197
            case Type::BUILTIN_TYPE_OBJECT:
198
                if (is_a($type->getClassName(), \DateTimeInterface::class, true)) {
199
                    $graphqlType = GraphQLType::string();
200
                    break;
201
                }
202
203
                try {
204
                    $className = $type->isCollection() ? $type->getCollectionValueType()->getClassName() : $type->getClassName();
205
                    $resourceMetadata = $this->resourceMetadataFactory->create($className);
206
                } catch (ResourceClassNotFoundException $e) {
207
                    throw new InvalidTypeException();
208
                }
209
210
                $graphqlType = $this->getResourceObjectType($className, $resourceMetadata, $isInput);
211
                break;
212
            default:
213
                throw new InvalidTypeException();
214
        }
215
216
        if ($type->isCollection()) {
217
            return $this->paginationEnabled ? $this->getResourcePaginatedCollectionType($graphqlType, $isInput) : GraphQLType::listOf($graphqlType);
218
        }
219
220
        return $type->isNullable() ? $graphqlType : GraphQLType::nonNull($graphqlType);
221
    }
222
223
    /**
224
     * Gets the object type of the given resource.
225
     *
226
     * @return ObjectType|InputObjectType
227
     */
228
    private function getResourceObjectType(string $resource, ResourceMetadata $resourceMetadata, bool $isInput = false)
229
    {
230
        $shortName = $resourceMetadata->getShortName().($isInput ? 'Input' : '');
231
        if (isset($this->resourceTypesCache[$shortName])) {
232
            return $this->resourceTypesCache[$shortName];
233
        }
234
235
        $configuration = [
236
            'name' => $shortName,
237
            'description' => $resourceMetadata->getDescription(),
238
            'fields' => function () use ($resource, $isInput) {
239
                return $this->getResourceObjectTypeFields($resource, $isInput);
240
            },
241
        ];
242
243
        return $this->resourceTypesCache[$shortName] = $isInput ? new InputObjectType($configuration) : new ObjectType($configuration);
244
    }
245
246
    /**
247
     * Gets the fields of the type of the given resource.
248
     */
249 View Code Duplication
    private function getResourceObjectTypeFields(string $resource, bool $isInput = false): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
250
    {
251
        $fields = [];
252
        foreach ($this->propertyNameCollectionFactory->create($resource) as $property) {
253
            $propertyMetadata = $this->propertyMetadataFactory->create($resource, $property);
254
            if (null === ($propertyType = $propertyMetadata->getType()) || !$propertyMetadata->isReadable()) {
255
                continue;
256
            }
257
258
            if ($fieldConfiguration = $this->getResourceFieldConfiguration($propertyMetadata->getDescription(), $propertyType, $resource, $isInput)) {
259
                $fields[$property] = $fieldConfiguration;
260
            }
261
        }
262
263
        return $fields;
264
    }
265
266
    /**
267
     * Gets the type of a paginated collection of the given resource type.
268
     *
269
     * @param ObjectType|InputObjectType $resourceType
270
     *
271
     * @return ObjectType|InputObjectType
272
     */
273
    private function getResourcePaginatedCollectionType($resourceType, bool $isInput = false)
274
    {
275
        $shortName = $resourceType->name.($isInput ? 'Input' : '');
276
277
        if (isset($this->resourceTypesCache["{$shortName}Connection"])) {
278
            return $this->resourceTypesCache["{$shortName}Connection"];
279
        }
280
281
        $edgeObjectTypeConfiguration = [
282
            'name' => "{$shortName}Edge",
283
            'description' => "Edge of $shortName.",
284
            'fields' => [
285
                'node' => $resourceType,
286
                'cursor' => GraphQLType::nonNull(GraphQLType::string()),
287
            ],
288
        ];
289
        $edgeObjectType = $isInput ? new InputObjectType($edgeObjectTypeConfiguration) : new ObjectType($edgeObjectTypeConfiguration);
290
        $pageInfoObjectTypeConfiguration = [
291
            'name' => "{$shortName}PageInfo",
292
            'description' => 'Information about the current page.',
293
            'fields' => [
294
                'endCursor' => GraphQLType::string(),
295
                'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()),
296
            ],
297
        ];
298
        $pageInfoObjectType = $isInput ? new InputObjectType($pageInfoObjectTypeConfiguration) : new ObjectType($pageInfoObjectTypeConfiguration);
299
300
        $configuration = [
301
            'name' => "{$shortName}Connection",
302
            'description' => "Connection for $shortName.",
303
            'fields' => [
304
                'edges' => GraphQLType::listOf($edgeObjectType),
305
                'pageInfo' => GraphQLType::nonNull($pageInfoObjectType),
306
            ],
307
        ];
308
309
        return $this->resourceTypesCache["{$shortName}Connection"] = $isInput ? new InputObjectType($configuration) : new ObjectType($configuration);
310
    }
311
}
312