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

api-platform / core / 14980015570

12 May 2025 06:41PM UTC coverage: 26.309% (+2.6%) from 23.685%
14980015570

Pull #7140

github

web-flow
Merge 1e6b14143 into 202c60fcb
Pull Request #7140: Fix: PHPize HTTP cache headers

0 of 1 new or added line in 1 file covered. (0.0%)

4614 existing lines in 185 files now uncovered.

13550 of 51504 relevant lines covered (26.31%)

71.73 hits per line

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

69.67
/src/GraphQl/Type/TypeConverter.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\Metadata\Exception\InvalidArgumentException;
17
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
18
use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException;
19
use ApiPlatform\Metadata\GraphQl\Operation;
20
use ApiPlatform\Metadata\GraphQl\Query;
21
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
22
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
23
use ApiPlatform\Metadata\Util\TypeHelper;
24
use GraphQL\Error\SyntaxError;
25
use GraphQL\Language\AST\ListTypeNode;
26
use GraphQL\Language\AST\NamedTypeNode;
27
use GraphQL\Language\AST\NonNullTypeNode;
28
use GraphQL\Language\AST\TypeNode;
29
use GraphQL\Language\Parser;
30
use GraphQL\Type\Definition\NullableType;
31
use GraphQL\Type\Definition\Type as GraphQLType;
32
use Symfony\Component\PropertyInfo\Type as LegacyType;
33
use Symfony\Component\TypeInfo\Type;
34
use Symfony\Component\TypeInfo\Type\CollectionType;
35
use Symfony\Component\TypeInfo\Type\ObjectType;
36
use Symfony\Component\TypeInfo\TypeIdentifier;
37

38
/**
39
 * Converts a type to its GraphQL equivalent.
40
 *
41
 * @author Alan Poulain <contact@alanpoulain.eu>
42
 */
43
final class TypeConverter implements TypeConverterInterface
44
{
45
    public function __construct(private readonly ContextAwareTypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory)
46
    {
47
    }
308✔
48

49
    /**
50
     * {@inheritdoc}
51
     */
52
    public function convertType(LegacyType $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth): GraphQLType|string|null
53
    {
UNCOV
54
        trigger_deprecation('api-platform/graphql', '4.2', 'The "%s()" method is deprecated, use "%s::convertPhpType()" instead.', __METHOD__, self::class);
×
55

UNCOV
56
        switch ($type->getBuiltinType()) {
×
UNCOV
57
            case LegacyType::BUILTIN_TYPE_BOOL:
×
UNCOV
58
                return GraphQLType::boolean();
×
UNCOV
59
            case LegacyType::BUILTIN_TYPE_INT:
×
UNCOV
60
                return GraphQLType::int();
×
UNCOV
61
            case LegacyType::BUILTIN_TYPE_FLOAT:
×
UNCOV
62
                return GraphQLType::float();
×
UNCOV
63
            case LegacyType::BUILTIN_TYPE_STRING:
×
UNCOV
64
                return GraphQLType::string();
×
UNCOV
65
            case LegacyType::BUILTIN_TYPE_ARRAY:
×
UNCOV
66
            case LegacyType::BUILTIN_TYPE_ITERABLE:
×
UNCOV
67
                if ($resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth)) {
×
UNCOV
68
                    return $resourceType;
×
69
                }
70

UNCOV
71
                return 'Iterable';
×
72
            case LegacyType::BUILTIN_TYPE_OBJECT:
×
UNCOV
73
                if (is_a($type->getClassName(), \DateTimeInterface::class, true)) {
×
UNCOV
74
                    return GraphQLType::string();
×
75
                }
76

UNCOV
77
                return $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth);
×
78
            default:
UNCOV
79
                return null;
×
80
        }
81
    }
82

83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function convertPhpType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth): GraphQLType|string|null
87
    {
88
        if ($type->isIdentifiedBy(TypeIdentifier::BOOL)) {
304✔
UNCOV
89
            return GraphQLType::boolean();
278✔
90
        }
91

92
        if ($type->isIdentifiedBy(TypeIdentifier::INT)) {
304✔
93
            return GraphQLType::int();
298✔
94
        }
95

96
        if ($type->isIdentifiedBy(TypeIdentifier::FLOAT)) {
304✔
UNCOV
97
            return GraphQLType::float();
278✔
98
        }
99

100
        if ($type->isIdentifiedBy(TypeIdentifier::STRING, \DateTimeInterface::class)) {
304✔
101
            return GraphQLType::string();
298✔
102
        }
103

104
        if ($type->isIdentifiedBy(TypeIdentifier::ARRAY, TypeIdentifier::ITERABLE)) {
304✔
105
            if ($resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth)) {
194✔
106
                return $resourceType;
24✔
107
            }
108

UNCOV
109
            return 'Iterable';
180✔
110
        }
111

112
        if ($type->isIdentifiedBy(TypeIdentifier::OBJECT)) {
304✔
113
            return $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth);
304✔
114
        }
115

UNCOV
116
        return null;
×
117
    }
118

119
    /**
120
     * {@inheritdoc}
121
     */
122
    public function resolveType(string $type): ?GraphQLType
123
    {
124
        try {
UNCOV
125
            $astTypeNode = Parser::parseType($type);
278✔
UNCOV
126
        } catch (SyntaxError $e) {
×
UNCOV
127
            throw new InvalidArgumentException(\sprintf('"%s" is not a valid GraphQL type.', $type), 0, $e);
×
128
        }
129

UNCOV
130
        if ($graphQlType = $this->resolveAstTypeNode($astTypeNode, $type)) {
278✔
UNCOV
131
            return $graphQlType;
278✔
132
        }
133

UNCOV
134
        throw new InvalidArgumentException(\sprintf('The type "%s" was not resolved.', $type));
×
135
    }
136

137
    private function getResourceType(Type|LegacyType $type, bool $input, Operation $rootOperation, string $rootResource, ?string $property, int $depth): ?GraphQLType
138
    {
139
        if ($type instanceof Type) {
304✔
140
            $isCollection = $type->isSatisfiedBy(fn ($t) => $t instanceof CollectionType);
304✔
141

142
            if ($isCollection) {
304✔
143
                $type = TypeHelper::getCollectionValueType($type);
304✔
144
            }
145

146
            /** @var class-string|null $resourceClass */
147
            $resourceClass = null;
304✔
148
            $typeIsResourceClass = function (Type $type) use (&$resourceClass): bool {
304✔
149
                return $type instanceof ObjectType && $resourceClass = $type->getClassName();
304✔
150
            };
304✔
151

152
            if (!$type->isSatisfiedBy($typeIsResourceClass)) {
304✔
UNCOV
153
                return null;
180✔
154
            }
155
        } else {
UNCOV
156
            $isCollection = $this->typeBuilder->isCollection($type);
×
UNCOV
157
            if ($isCollection && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null) {
×
UNCOV
158
                $resourceClass = $collectionValueType->getClassName();
×
159
            } else {
UNCOV
160
                $resourceClass = $type->getClassName();
×
161
            }
162

UNCOV
163
            if (null === $resourceClass) {
×
UNCOV
164
                return null;
×
165
            }
166
        }
167

168
        try {
169
            $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
304✔
UNCOV
170
        } catch (ResourceClassNotFoundException) {
×
UNCOV
171
            return null;
×
172
        }
173

174
        $hasGraphQl = false;
304✔
175
        foreach ($resourceMetadataCollection as $resourceMetadata) {
304✔
176
            if (null !== $resourceMetadata->getGraphQlOperations()) {
304✔
177
                $hasGraphQl = true;
304✔
178
                break;
304✔
179
            }
180
        }
181

182
        if (isset($resourceMetadataCollection[0]) && 'Node' === $resourceMetadataCollection[0]->getShortName()) {
304✔
UNCOV
183
            throw new \UnexpectedValueException('A "Node" resource cannot be used with GraphQL because the type is already used by the Relay specification.');
×
184
        }
185

186
        if (!$hasGraphQl) {
304✔
187
            if (is_a($resourceClass, \BackedEnum::class, true)) {
58✔
188
                $operation = null;
11✔
189
                try {
190
                    $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
11✔
191
                    $operation = $resourceMetadataCollection->getOperation();
11✔
192
                } catch (ResourceClassNotFoundException|OperationNotFoundException) {
11✔
193
                }
194
                /** @var Query $enumOperation */
195
                $enumOperation = (new Query())
11✔
196
                    ->withClass($resourceClass)
11✔
197
                    ->withShortName($operation?->getShortName() ?? (new \ReflectionClass($resourceClass))->getShortName())
11✔
198
                    ->withDescription($operation?->getDescription());
11✔
199

200
                return $this->typeBuilder->getEnumType($enumOperation);
11✔
201
            }
202

203
            return null;
58✔
204
        }
205

206
        $propertyMetadata = null;
304✔
207
        if ($property) {
304✔
208
            $context = [
198✔
209
                'normalization_groups' => $rootOperation->getNormalizationContext()['groups'] ?? null,
198✔
210
                'denormalization_groups' => $rootOperation->getDenormalizationContext()['groups'] ?? null,
198✔
211
            ];
198✔
212
            $propertyMetadata = $this->propertyMetadataFactory->create($rootResource, $property, $context);
198✔
213
        }
214

215
        if ($input && $depth > 0 && (!$propertyMetadata || !$propertyMetadata->isWritableLink())) {
304✔
216
            return GraphQLType::string();
48✔
217
        }
218

219
        $operationName = $rootOperation->getName();
304✔
220

221
        // We're retrieving the type of a property which is a relation to the root resource.
222
        if ($resourceClass !== $rootResource && $rootOperation instanceof Query) {
304✔
223
            $operationName = $isCollection ? 'collection_query' : 'item_query';
186✔
224
        }
225

226
        try {
227
            $operation = $resourceMetadataCollection->getOperation($operationName);
304✔
UNCOV
228
        } catch (OperationNotFoundException) {
11✔
229
            try {
UNCOV
230
                $operation = $resourceMetadataCollection->getOperation($isCollection ? 'collection_query' : 'item_query');
11✔
UNCOV
231
            } catch (OperationNotFoundException) {
×
UNCOV
232
                throw new OperationNotFoundException(\sprintf('A GraphQl operation named "%s" should exist on the type "%s" as we reference this type in another query.', $isCollection ? 'collection_query' : 'item_query', $resourceClass));
×
233
            }
234
        }
235
        if (!$operation instanceof Operation) {
304✔
UNCOV
236
            throw new OperationNotFoundException(\sprintf('A GraphQl operation named "%s" should exist on the type "%s" as we reference this type in another query.', $operationName, $resourceClass));
×
237
        }
238

239
        return $this->typeBuilder->getResourceObjectType($resourceMetadataCollection, $operation, $propertyMetadata, [
304✔
240
            'input' => $input,
304✔
241
            'wrapped' => false,
304✔
242
            'depth' => $depth,
304✔
243
        ]);
304✔
244
    }
245

246
    private function resolveAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType
247
    {
UNCOV
248
        if ($astTypeNode instanceof NonNullTypeNode) {
278✔
249
            /** @var NullableType|null $nullableAstTypeNode */
UNCOV
250
            $nullableAstTypeNode = $this->resolveNullableAstTypeNode($astTypeNode->type, $fromType);
278✔
251

UNCOV
252
            return $nullableAstTypeNode ? GraphQLType::nonNull($nullableAstTypeNode) : null;
278✔
253
        }
254

UNCOV
255
        return $this->resolveNullableAstTypeNode($astTypeNode, $fromType);
278✔
256
    }
257

258
    private function resolveNullableAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType
259
    {
UNCOV
260
        if ($astTypeNode instanceof ListTypeNode) {
278✔
261
            /** @var TypeNode $astTypeNodeElement */
UNCOV
262
            $astTypeNodeElement = $astTypeNode->type;
278✔
263

UNCOV
264
            return GraphQLType::listOf($this->resolveAstTypeNode($astTypeNodeElement, $fromType));
278✔
265
        }
266

UNCOV
267
        if (!$astTypeNode instanceof NamedTypeNode) {
278✔
UNCOV
268
            return null;
×
269
        }
270

UNCOV
271
        $typeName = $astTypeNode->name->value;
278✔
272

UNCOV
273
        return match ($typeName) {
278✔
UNCOV
274
            GraphQLType::STRING => GraphQLType::string(),
278✔
UNCOV
275
            GraphQLType::INT => GraphQLType::int(),
278✔
UNCOV
276
            GraphQLType::BOOLEAN => GraphQLType::boolean(),
278✔
UNCOV
277
            GraphQLType::FLOAT => GraphQLType::float(),
278✔
UNCOV
278
            GraphQLType::ID => GraphQLType::id(),
278✔
UNCOV
279
            default => $this->typesContainer->has($typeName) ? $this->typesContainer->get($typeName) : null,
278✔
UNCOV
280
        };
278✔
281
    }
282
}
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