Completed
Push — master ( 9298c4...e8effc )
by Kévin
08:23 queued 12s
created

TypeBuilder   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 231
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 128
c 1
b 0
f 0
dl 0
loc 231
rs 8.96
wmc 43

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getPageBasedPaginationFields() 0 19 1
F getResourceObjectType() 0 81 29
A getResourcePaginatedCollectionType() 0 24 3
A getCursorBasedPaginationFields() 0 32 1
A getNodeInterface() 0 34 5
A isCollection() 0 3 3

How to fix   Complexity   

Complex Class

Complex classes like TypeBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TypeBuilder, and based on these observations, apply Extract Interface, too.

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\DataProvider\Pagination;
17
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
18
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
19
use GraphQL\Type\Definition\InputObjectType;
20
use GraphQL\Type\Definition\InterfaceType;
21
use GraphQL\Type\Definition\NonNull;
22
use GraphQL\Type\Definition\ObjectType;
23
use GraphQL\Type\Definition\Type as GraphQLType;
24
use Psr\Container\ContainerInterface;
25
use Symfony\Component\PropertyInfo\Type;
26
27
/**
28
 * Builds the GraphQL types.
29
 *
30
 * @experimental
31
 *
32
 * @author Alan Poulain <[email protected]>
33
 */
34
final class TypeBuilder implements TypeBuilderInterface
35
{
36
    private $typesContainer;
37
    private $defaultFieldResolver;
38
    private $fieldsBuilderLocator;
39
    private $pagination;
40
41
    public function __construct(TypesContainerInterface $typesContainer, callable $defaultFieldResolver, ContainerInterface $fieldsBuilderLocator, Pagination $pagination)
42
    {
43
        $this->typesContainer = $typesContainer;
44
        $this->defaultFieldResolver = $defaultFieldResolver;
45
        $this->fieldsBuilderLocator = $fieldsBuilderLocator;
46
        $this->pagination = $pagination;
47
    }
48
49
    /**
50
     * {@inheritdoc}
51
     */
52
    public function getResourceObjectType(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input, ?string $queryName, ?string $mutationName, bool $wrapped = false, int $depth = 0): GraphQLType
53
    {
54
        $shortName = $resourceMetadata->getShortName();
55
56
        if (null !== $mutationName) {
57
            $shortName = $mutationName.ucfirst($shortName);
58
        }
59
        if ($input) {
60
            $shortName .= 'Input';
61
        } elseif (null !== $mutationName) {
62
            if ($depth > 0) {
63
                $shortName .= 'Nested';
64
            }
65
            $shortName .= 'Payload';
66
        }
67
        if (('item_query' === $queryName || 'collection_query' === $queryName)
68
            && $resourceMetadata->getGraphqlAttribute('item_query', 'normalization_context', [], true) !== $resourceMetadata->getGraphqlAttribute('collection_query', 'normalization_context', [], true)) {
69
            if ('item_query' === $queryName) {
70
                $shortName .= 'Item';
71
            }
72
            if ('collection_query' === $queryName) {
73
                $shortName .= 'Collection';
74
            }
75
        }
76
        if ($wrapped && null !== $mutationName) {
77
            $shortName .= 'Data';
78
        }
79
80
        if ($this->typesContainer->has($shortName)) {
81
            $resourceObjectType = $this->typesContainer->get($shortName);
82
            if (!($resourceObjectType instanceof ObjectType || $resourceObjectType instanceof NonNull)) {
83
                throw new \LogicException(sprintf('Expected GraphQL type "%s" to be %s.', $shortName, implode('|', [ObjectType::class, NonNull::class])));
84
            }
85
86
            return $resourceObjectType;
87
        }
88
89
        $ioMetadata = $resourceMetadata->getGraphqlAttribute($mutationName ?? $queryName, $input ? 'input' : 'output', null, true);
0 ignored issues
show
Bug introduced by
It seems like $mutationName ?? $queryName can also be of type null; however, parameter $operationName of ApiPlatform\Core\Metadat...::getGraphqlAttribute() 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

89
        $ioMetadata = $resourceMetadata->getGraphqlAttribute(/** @scrutinizer ignore-type */ $mutationName ?? $queryName, $input ? 'input' : 'output', null, true);
Loading history...
90
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
91
            $resourceClass = $ioMetadata['class'];
92
        }
93
94
        $wrapData = !$wrapped && null !== $mutationName && !$input && $depth < 1;
95
96
        $configuration = [
97
            'name' => $shortName,
98
            'description' => $resourceMetadata->getDescription(),
99
            'resolveField' => $this->defaultFieldResolver,
100
            'fields' => function () use ($resourceClass, $resourceMetadata, $input, $mutationName, $queryName, $wrapData, $depth, $ioMetadata) {
101
                if ($wrapData) {
102
                    $queryNormalizationContext = $resourceMetadata->getGraphqlAttribute($queryName ?? '', 'normalization_context', [], true);
103
                    $mutationNormalizationContext = $resourceMetadata->getGraphqlAttribute($mutationName ?? '', 'normalization_context', [], true);
104
                    // Use a new type for the wrapped object only if there is a specific normalization context for the mutation.
105
                    // If not, use the query type in order to ensure the client cache could be used.
106
                    $useWrappedType = $queryNormalizationContext !== $mutationNormalizationContext;
107
108
                    return [
109
                        lcfirst($resourceMetadata->getShortName()) => $useWrappedType ?
110
                            $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, true, $depth) :
111
                            $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $queryName ?? 'item_query', null, true, $depth),
112
                        'clientMutationId' => GraphQLType::string(),
113
                    ];
114
                }
115
116
                $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
117
118
                $fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, $depth, $ioMetadata);
119
120
                if ($input && null !== $mutationName && null !== $mutationArgs = $resourceMetadata->getGraphql()[$mutationName]['args'] ?? null) {
121
                    return $fieldsBuilder->resolveResourceArgs($mutationArgs, $mutationName, $resourceMetadata->getShortName()) + ['clientMutationId' => $fields['clientMutationId']];
122
                }
123
124
                return $fields;
125
            },
126
            'interfaces' => $wrapData ? [] : [$this->getNodeInterface()],
127
        ];
128
129
        $resourceObjectType = $input ? GraphQLType::nonNull(new InputObjectType($configuration)) : new ObjectType($configuration);
130
        $this->typesContainer->set($shortName, $resourceObjectType);
0 ignored issues
show
Bug introduced by
It seems like $shortName can also be of type null; however, parameter $id of ApiPlatform\Core\GraphQl...ntainerInterface::set() 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

130
        $this->typesContainer->set(/** @scrutinizer ignore-type */ $shortName, $resourceObjectType);
Loading history...
131
132
        return $resourceObjectType;
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function getNodeInterface(): InterfaceType
139
    {
140
        if ($this->typesContainer->has('Node')) {
141
            $nodeInterface = $this->typesContainer->get('Node');
142
            if (!$nodeInterface instanceof InterfaceType) {
143
                throw new \LogicException(sprintf('Expected GraphQL type "Node" to be %s.', InterfaceType::class));
144
            }
145
146
            return $nodeInterface;
147
        }
148
149
        $nodeInterface = new InterfaceType([
150
            'name' => 'Node',
151
            'description' => 'A node, according to the Relay specification.',
152
            'fields' => [
153
                'id' => [
154
                    'type' => GraphQLType::nonNull(GraphQLType::id()),
155
                    'description' => 'The id of this node.',
156
                ],
157
            ],
158
            'resolveType' => function ($value) {
159
                if (!isset($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
160
                    return null;
161
                }
162
163
                $shortName = (new \ReflectionClass($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]))->getShortName();
164
165
                return $this->typesContainer->has($shortName) ? $this->typesContainer->get($shortName) : null;
166
            },
167
        ]);
168
169
        $this->typesContainer->set('Node', $nodeInterface);
170
171
        return $nodeInterface;
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, string $operationName): GraphQLType
178
    {
179
        $shortName = $resourceType->name;
180
181
        if ($this->typesContainer->has("{$shortName}Connection")) {
182
            return $this->typesContainer->get("{$shortName}Connection");
183
        }
184
185
        $paginationType = $this->pagination->getGraphQlPaginationType($resourceClass, $operationName);
186
187
        $fields = 'cursor' === $paginationType ?
188
            $this->getCursorBasedPaginationFields($resourceType) :
189
            $this->getPageBasedPaginationFields($resourceType);
190
191
        $configuration = [
192
            'name' => "{$shortName}Connection",
193
            'description' => "Connection for $shortName.",
194
            'fields' => $fields,
195
        ];
196
197
        $resourcePaginatedCollectionType = new ObjectType($configuration);
198
        $this->typesContainer->set("{$shortName}Connection", $resourcePaginatedCollectionType);
199
200
        return $resourcePaginatedCollectionType;
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    public function isCollection(Type $type): bool
207
    {
208
        return $type->isCollection() && ($collectionValueType = $type->getCollectionValueType()) && null !== $collectionValueType->getClassName();
209
    }
210
211
    private function getCursorBasedPaginationFields(GraphQLType $resourceType): array
212
    {
213
        $shortName = $resourceType->name;
214
215
        $edgeObjectTypeConfiguration = [
216
            'name' => "{$shortName}Edge",
217
            'description' => "Edge of $shortName.",
218
            'fields' => [
219
                'node' => $resourceType,
220
                'cursor' => GraphQLType::nonNull(GraphQLType::string()),
221
            ],
222
        ];
223
        $edgeObjectType = new ObjectType($edgeObjectTypeConfiguration);
224
        $this->typesContainer->set("{$shortName}Edge", $edgeObjectType);
225
226
        $pageInfoObjectTypeConfiguration = [
227
            'name' => "{$shortName}PageInfo",
228
            'description' => 'Information about the current page.',
229
            'fields' => [
230
                'endCursor' => GraphQLType::string(),
231
                'startCursor' => GraphQLType::string(),
232
                'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()),
233
                'hasPreviousPage' => GraphQLType::nonNull(GraphQLType::boolean()),
234
            ],
235
        ];
236
        $pageInfoObjectType = new ObjectType($pageInfoObjectTypeConfiguration);
237
        $this->typesContainer->set("{$shortName}PageInfo", $pageInfoObjectType);
238
239
        return [
240
            'edges' => GraphQLType::listOf($edgeObjectType),
241
            'pageInfo' => GraphQLType::nonNull($pageInfoObjectType),
242
            'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
243
        ];
244
    }
245
246
    private function getPageBasedPaginationFields(GraphQLType $resourceType): array
247
    {
248
        $shortName = $resourceType->name;
249
250
        $paginationInfoObjectTypeConfiguration = [
251
            'name' => "{$shortName}PaginationInfo",
252
            'description' => 'Information about the pagination.',
253
            'fields' => [
254
                'itemsPerPage' => GraphQLType::nonNull(GraphQLType::int()),
255
                'lastPage' => GraphQLType::nonNull(GraphQLType::int()),
256
                'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
257
            ],
258
        ];
259
        $paginationInfoObjectType = new ObjectType($paginationInfoObjectTypeConfiguration);
260
        $this->typesContainer->set("{$shortName}PaginationInfo", $paginationInfoObjectType);
261
262
        return [
263
            'collection' => GraphQLType::listOf($resourceType),
264
            'paginationInfo' => GraphQLType::nonNull($paginationInfoObjectType),
265
        ];
266
    }
267
}
268