• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 6067528200

04 Sep 2023 12:12AM UTC coverage: 36.875% (-21.9%) from 58.794%
6067528200

Pull #5791

github

web-flow
Merge 64157e578 into d09cfc9d2
Pull Request #5791: fix: strip down any sql function name

3096 of 3096 new or added lines in 205 files covered. (100.0%)

9926 of 26918 relevant lines covered (36.87%)

6.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/GraphQl/Type/TypeBuilder.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
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\GraphQl\Type;
15

16
use ApiPlatform\GraphQl\Serializer\ItemNormalizer;
17
use ApiPlatform\Metadata\CollectionOperationInterface;
18
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
19
use ApiPlatform\Metadata\GraphQl\Mutation;
20
use ApiPlatform\Metadata\GraphQl\Operation;
21
use ApiPlatform\Metadata\GraphQl\Query;
22
use ApiPlatform\Metadata\GraphQl\Subscription;
23
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
24
use ApiPlatform\State\Pagination\Pagination;
25
use GraphQL\Type\Definition\EnumType;
26
use GraphQL\Type\Definition\InputObjectType;
27
use GraphQL\Type\Definition\InterfaceType;
28
use GraphQL\Type\Definition\NonNull;
29
use GraphQL\Type\Definition\ObjectType;
30
use GraphQL\Type\Definition\Type as GraphQLType;
31
use Psr\Container\ContainerInterface;
32
use Symfony\Component\PropertyInfo\Type;
33

34
/**
35
 * Builds the GraphQL types.
36
 *
37
 * @author Alan Poulain <contact@alanpoulain.eu>
38
 */
39
final class TypeBuilder implements TypeBuilderInterface, TypeBuilderEnumInterface
40
{
41
    private $defaultFieldResolver;
42

43
    public function __construct(private readonly TypesContainerInterface $typesContainer, callable $defaultFieldResolver, private readonly ContainerInterface $fieldsBuilderLocator, private readonly Pagination $pagination)
44
    {
45
        $this->defaultFieldResolver = $defaultFieldResolver;
×
46
    }
47

48
    /**
49
     * {@inheritdoc}
50
     */
51
    public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType
52
    {
53
        $shortName = $operation->getShortName();
×
54
        $operationName = $operation->getName();
×
55

56
        if ($operation instanceof Mutation) {
×
57
            $shortName = $operationName.ucfirst($shortName);
×
58
        }
59

60
        if ($operation instanceof Subscription) {
×
61
            $shortName = $operationName.ucfirst($shortName).'Subscription';
×
62
        }
63

64
        if ($input) {
×
65
            if ($depth > 0) {
×
66
                $shortName .= 'Nested';
×
67
            }
68
            $shortName .= 'Input';
×
69
        } elseif ($operation instanceof Mutation || $operation instanceof Subscription) {
×
70
            if ($depth > 0) {
×
71
                $shortName .= 'Nested';
×
72
            }
73
            $shortName .= 'Payload';
×
74
        }
75

76
        if ('item_query' === $operationName || 'collection_query' === $operationName) {
×
77
            // Test if the collection/item has different groups
78
            if ($resourceMetadataCollection->getOperation($operation instanceof CollectionOperationInterface ? 'item_query' : 'collection_query')->getNormalizationContext() !== $operation->getNormalizationContext()) {
×
79
                $shortName .= $operation instanceof CollectionOperationInterface ? 'Collection' : 'Item';
×
80
            }
81
        }
82

83
        if ($wrapped && ($operation instanceof Mutation || $operation instanceof Subscription)) {
×
84
            $shortName .= 'Data';
×
85
        }
86

87
        if ($this->typesContainer->has($shortName)) {
×
88
            $resourceObjectType = $this->typesContainer->get($shortName);
×
89
            if (!($resourceObjectType instanceof ObjectType || $resourceObjectType instanceof NonNull)) {
×
90
                throw new \LogicException(sprintf('Expected GraphQL type "%s" to be %s.', $shortName, implode('|', [ObjectType::class, NonNull::class])));
×
91
            }
92

93
            return $resourceObjectType;
×
94
        }
95

96
        $ioMetadata = $input ? $operation->getInput() : $operation->getOutput();
×
97
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
×
98
            $resourceClass = $ioMetadata['class'];
×
99
        }
100

101
        $wrapData = !$wrapped && ($operation instanceof Mutation || $operation instanceof Subscription) && !$input && $depth < 1;
×
102

103
        $configuration = [
×
104
            'name' => $shortName,
×
105
            'description' => $operation->getDescription(),
×
106
            'resolveField' => $this->defaultFieldResolver,
×
107
            'fields' => function () use ($resourceClass, $operation, $operationName, $resourceMetadataCollection, $input, $wrapData, $depth, $ioMetadata) {
×
108
                if ($wrapData) {
×
109
                    $queryNormalizationContext = $this->getQueryOperation($resourceMetadataCollection)?->getNormalizationContext() ?? [];
×
110

111
                    try {
112
                        $mutationNormalizationContext = $operation instanceof Mutation || $operation instanceof Subscription ? ($resourceMetadataCollection->getOperation($operationName)->getNormalizationContext() ?? []) : [];
×
113
                    } catch (OperationNotFoundException) {
×
114
                        $mutationNormalizationContext = [];
×
115
                    }
116
                    // Use a new type for the wrapped object only if there is a specific normalization context for the mutation or the subscription.
117
                    // If not, use the query type in order to ensure the client cache could be used.
118
                    $useWrappedType = $queryNormalizationContext !== $mutationNormalizationContext;
×
119

120
                    $wrappedOperationName = $operationName;
×
121

122
                    if (!$useWrappedType) {
×
123
                        $wrappedOperationName = $operation instanceof Query ? $operationName : 'item_query';
×
124
                    }
125

126
                    $wrappedOperation = $resourceMetadataCollection->getOperation($wrappedOperationName);
×
127

128
                    $fields = [
×
129
                        lcfirst($wrappedOperation->getShortName()) => $this->getResourceObjectType($resourceClass, $resourceMetadataCollection, $wrappedOperation instanceof Operation ? $wrappedOperation : null, $input, true, $depth),
×
130
                    ];
×
131

132
                    if ($operation instanceof Subscription) {
×
133
                        $fields['clientSubscriptionId'] = GraphQLType::string();
×
134
                        if ($operation->getMercure()) {
×
135
                            $fields['mercureUrl'] = GraphQLType::string();
×
136
                        }
137

138
                        return $fields;
×
139
                    }
140

141
                    return $fields + ['clientMutationId' => GraphQLType::string()];
×
142
                }
143

144
                $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
×
145
                $fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $operation, $input, $depth, $ioMetadata);
×
146

147
                if ($input && $operation instanceof Mutation && null !== $mutationArgs = $operation->getArgs()) {
×
148
                    return $fieldsBuilder->resolveResourceArgs($mutationArgs, $operation) + ['clientMutationId' => $fields['clientMutationId']];
×
149
                }
150
                if ($input && $operation instanceof Mutation && null !== $extraMutationArgs = $operation->getExtraArgs()) {
×
151
                    return $fields + $fieldsBuilder->resolveResourceArgs($extraMutationArgs, $operation);
×
152
                }
153

154
                return $fields;
×
155
            },
×
156
            'interfaces' => $wrapData ? [] : [$this->getNodeInterface()],
×
157
        ];
×
158

159
        $resourceObjectType = $input ? GraphQLType::nonNull(new InputObjectType($configuration)) : new ObjectType($configuration);
×
160
        $this->typesContainer->set($shortName, $resourceObjectType);
×
161

162
        return $resourceObjectType;
×
163
    }
164

165
    /**
166
     * {@inheritdoc}
167
     */
168
    public function getNodeInterface(): InterfaceType
169
    {
170
        if ($this->typesContainer->has('Node')) {
×
171
            $nodeInterface = $this->typesContainer->get('Node');
×
172
            if (!$nodeInterface instanceof InterfaceType) {
×
173
                throw new \LogicException(sprintf('Expected GraphQL type "Node" to be %s.', InterfaceType::class));
×
174
            }
175

176
            return $nodeInterface;
×
177
        }
178

179
        $nodeInterface = new InterfaceType([
×
180
            'name' => 'Node',
×
181
            'description' => 'A node, according to the Relay specification.',
×
182
            'fields' => [
×
183
                'id' => [
×
184
                    'type' => GraphQLType::nonNull(GraphQLType::id()),
×
185
                    'description' => 'The id of this node.',
×
186
                ],
×
187
            ],
×
188
            'resolveType' => function ($value): ?GraphQLType {
×
189
                if (!isset($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
×
190
                    return null;
×
191
                }
192

193
                $shortName = (new \ReflectionClass($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]))->getShortName();
×
194

195
                return $this->typesContainer->has($shortName) ? $this->typesContainer->get($shortName) : null;
×
196
            },
×
197
        ]);
×
198

199
        $this->typesContainer->set('Node', $nodeInterface);
×
200

201
        return $nodeInterface;
×
202
    }
203

204
    /**
205
     * {@inheritdoc}
206
     */
207
    public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType
208
    {
209
        @trigger_error('Using getResourcePaginatedCollectionType method of TypeBuilder is deprecated since API Platform 3.1. Use getPaginatedCollectionType method instead.', \E_USER_DEPRECATED);
×
210

211
        return $this->getPaginatedCollectionType($resourceType, $operation);
×
212
    }
213

214
    /**
215
     * {@inheritdoc}
216
     */
217
    public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType
218
    {
219
        $namedType = GraphQLType::getNamedType($resourceType);
×
220
        // graphql-php 15: name() exists
221
        $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name;
×
222
        $paginationType = $this->pagination->getGraphQlPaginationType($operation);
×
223

224
        $connectionTypeKey = sprintf('%s%sConnection', $shortName, ucfirst($paginationType));
×
225
        if ($this->typesContainer->has($connectionTypeKey)) {
×
226
            return $this->typesContainer->get($connectionTypeKey);
×
227
        }
228

229
        $fields = 'cursor' === $paginationType ?
×
230
            $this->getCursorBasedPaginationFields($resourceType) :
×
231
            $this->getPageBasedPaginationFields($resourceType);
×
232

233
        $configuration = [
×
234
            'name' => $connectionTypeKey,
×
235
            'description' => sprintf("%s connection for $shortName.", ucfirst($paginationType)),
×
236
            'fields' => $fields,
×
237
        ];
×
238

239
        $resourcePaginatedCollectionType = new ObjectType($configuration);
×
240
        $this->typesContainer->set($connectionTypeKey, $resourcePaginatedCollectionType);
×
241

242
        return $resourcePaginatedCollectionType;
×
243
    }
244

245
    public function getEnumType(Operation $operation): GraphQLType
246
    {
247
        $enumName = $operation->getShortName();
×
248
        if (!str_ends_with($enumName, 'Enum')) {
×
249
            $enumName = sprintf('%sEnum', $enumName);
×
250
        }
251

252
        if ($this->typesContainer->has($enumName)) {
×
253
            return $this->typesContainer->get($enumName);
×
254
        }
255

256
        /** @var FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder */
257
        $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
×
258
        $enumCases = [];
×
259
        // Remove the condition in API Platform 4.
260
        if ($fieldsBuilder instanceof FieldsBuilderEnumInterface) {
×
261
            $enumCases = $fieldsBuilder->getEnumFields($operation->getClass());
×
262
        } else {
263
            @trigger_error(sprintf('api_platform.graphql.fields_builder service implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED);
×
264
        }
265

266
        $enumConfig = [
×
267
            'name' => $enumName,
×
268
            'values' => $enumCases,
×
269
        ];
×
270
        if ($enumDescription = $operation->getDescription()) {
×
271
            $enumConfig['description'] = $enumDescription;
×
272
        }
273

274
        $enumType = new EnumType($enumConfig);
×
275
        $this->typesContainer->set($enumName, $enumType);
×
276

277
        return $enumType;
×
278
    }
279

280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function isCollection(Type $type): bool
284
    {
285
        return $type->isCollection() && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null) && null !== $collectionValueType->getClassName();
×
286
    }
287

288
    private function getCursorBasedPaginationFields(GraphQLType $resourceType): array
289
    {
290
        $namedType = GraphQLType::getNamedType($resourceType);
×
291
        // graphql-php 15: name() exists
292
        $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name;
×
293

294
        $edgeObjectTypeConfiguration = [
×
295
            'name' => "{$shortName}Edge",
×
296
            'description' => "Edge of $shortName.",
×
297
            'fields' => [
×
298
                'node' => $resourceType,
×
299
                'cursor' => GraphQLType::nonNull(GraphQLType::string()),
×
300
            ],
×
301
        ];
×
302
        $edgeObjectType = new ObjectType($edgeObjectTypeConfiguration);
×
303
        $this->typesContainer->set("{$shortName}Edge", $edgeObjectType);
×
304

305
        $pageInfoObjectTypeConfiguration = [
×
306
            'name' => "{$shortName}PageInfo",
×
307
            'description' => 'Information about the current page.',
×
308
            'fields' => [
×
309
                'endCursor' => GraphQLType::string(),
×
310
                'startCursor' => GraphQLType::string(),
×
311
                'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()),
×
312
                'hasPreviousPage' => GraphQLType::nonNull(GraphQLType::boolean()),
×
313
            ],
×
314
        ];
×
315
        $pageInfoObjectType = new ObjectType($pageInfoObjectTypeConfiguration);
×
316
        $this->typesContainer->set("{$shortName}PageInfo", $pageInfoObjectType);
×
317

318
        return [
×
319
            'edges' => GraphQLType::listOf($edgeObjectType),
×
320
            'pageInfo' => GraphQLType::nonNull($pageInfoObjectType),
×
321
            'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
×
322
        ];
×
323
    }
324

325
    private function getPageBasedPaginationFields(GraphQLType $resourceType): array
326
    {
327
        $namedType = GraphQLType::getNamedType($resourceType);
×
328
        // graphql-php 15: name() exists
329
        $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name;
×
330

331
        $paginationInfoObjectTypeConfiguration = [
×
332
            'name' => "{$shortName}PaginationInfo",
×
333
            'description' => 'Information about the pagination.',
×
334
            'fields' => [
×
335
                'itemsPerPage' => GraphQLType::nonNull(GraphQLType::int()),
×
336
                'lastPage' => GraphQLType::nonNull(GraphQLType::int()),
×
337
                'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
×
338
            ],
×
339
        ];
×
340
        $paginationInfoObjectType = new ObjectType($paginationInfoObjectTypeConfiguration);
×
341
        $this->typesContainer->set("{$shortName}PaginationInfo", $paginationInfoObjectType);
×
342

343
        return [
×
344
            'collection' => GraphQLType::listOf($resourceType),
×
345
            'paginationInfo' => GraphQLType::nonNull($paginationInfoObjectType),
×
346
        ];
×
347
    }
348

349
    private function getQueryOperation(ResourceMetadataCollection $resourceMetadataCollection): ?Operation
350
    {
351
        foreach ($resourceMetadataCollection as $resourceMetadata) {
×
352
            foreach ($resourceMetadata->getGraphQlOperations() as $operation) {
×
353
                // Filter the custom queries.
354
                if ($operation instanceof Query && !$operation->getResolver()) {
×
355
                    return $operation;
×
356
                }
357
            }
358
        }
359

360
        return null;
×
361
    }
362
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc