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

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

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

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

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

88.66
/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 GraphQL\Error\SyntaxError;
24
use GraphQL\Language\AST\ListTypeNode;
25
use GraphQL\Language\AST\NamedTypeNode;
26
use GraphQL\Language\AST\NonNullTypeNode;
27
use GraphQL\Language\AST\TypeNode;
28
use GraphQL\Language\Parser;
29
use GraphQL\Type\Definition\NullableType;
30
use GraphQL\Type\Definition\Type as GraphQLType;
31
use Symfony\Component\PropertyInfo\Type;
32

33
/**
34
 * Converts a type to its GraphQL equivalent.
35
 *
36
 * @author Alan Poulain <contact@alanpoulain.eu>
37
 */
38
final class TypeConverter implements TypeConverterInterface
39
{
40
    public function __construct(private readonly ContextAwareTypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory)
41
    {
UNCOV
42
    }
148✔
43

44
    /**
45
     * {@inheritdoc}
46
     */
47
    public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth): GraphQLType|string|null
48
    {
UNCOV
49
        switch ($type->getBuiltinType()) {
146✔
UNCOV
50
            case Type::BUILTIN_TYPE_BOOL:
133✔
51
                return GraphQLType::boolean();
133✔
UNCOV
52
            case Type::BUILTIN_TYPE_INT:
133✔
UNCOV
53
                return GraphQLType::int();
143✔
UNCOV
54
            case Type::BUILTIN_TYPE_FLOAT:
133✔
55
                return GraphQLType::float();
133✔
UNCOV
56
            case Type::BUILTIN_TYPE_STRING:
133✔
UNCOV
57
                return GraphQLType::string();
143✔
UNCOV
58
            case Type::BUILTIN_TYPE_ARRAY:
133✔
UNCOV
59
            case Type::BUILTIN_TYPE_ITERABLE:
133✔
UNCOV
60
                if ($resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth)) {
134✔
UNCOV
61
                    return $resourceType;
9✔
62
                }
63

64
                return 'Iterable';
133✔
UNCOV
65
            case Type::BUILTIN_TYPE_OBJECT:
133✔
UNCOV
66
                if (is_a($type->getClassName(), \DateTimeInterface::class, true)) {
146✔
UNCOV
67
                    return GraphQLType::string();
134✔
68
                }
69

UNCOV
70
                return $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth);
146✔
71
            default:
72
                return null;
×
73
        }
74
    }
75

76
    /**
77
     * {@inheritdoc}
78
     */
79
    public function resolveType(string $type): ?GraphQLType
80
    {
81
        try {
82
            $astTypeNode = Parser::parseType($type);
133✔
83
        } catch (SyntaxError $e) {
×
84
            throw new InvalidArgumentException(\sprintf('"%s" is not a valid GraphQL type.', $type), 0, $e);
×
85
        }
86

87
        if ($graphQlType = $this->resolveAstTypeNode($astTypeNode, $type)) {
133✔
88
            return $graphQlType;
133✔
89
        }
90

91
        throw new InvalidArgumentException(\sprintf('The type "%s" was not resolved.', $type));
×
92
    }
93

94
    private function getResourceType(Type $type, bool $input, Operation $rootOperation, string $rootResource, ?string $property, int $depth): ?GraphQLType
95
    {
96
        if (
UNCOV
97
            $this->typeBuilder->isCollection($type)
146✔
UNCOV
98
            && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
146✔
99
        ) {
UNCOV
100
            $resourceClass = $collectionValueType->getClassName();
146✔
101
        } else {
UNCOV
102
            $resourceClass = $type->getClassName();
145✔
103
        }
104

UNCOV
105
        if (null === $resourceClass) {
146✔
106
            return null;
133✔
107
        }
108

109
        try {
UNCOV
110
            $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
146✔
111
        } catch (ResourceClassNotFoundException) {
×
112
            return null;
×
113
        }
114

UNCOV
115
        $hasGraphQl = false;
146✔
UNCOV
116
        foreach ($resourceMetadataCollection as $resourceMetadata) {
146✔
UNCOV
117
            if (null !== $resourceMetadata->getGraphQlOperations()) {
146✔
UNCOV
118
                $hasGraphQl = true;
146✔
UNCOV
119
                break;
146✔
120
            }
121
        }
122

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

UNCOV
127
        if (!$hasGraphQl) {
146✔
UNCOV
128
            if (is_a($resourceClass, \BackedEnum::class, true)) {
31✔
UNCOV
129
                $operation = null;
5✔
130
                try {
UNCOV
131
                    $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
5✔
UNCOV
132
                    $operation = $resourceMetadataCollection->getOperation();
5✔
UNCOV
133
                } catch (ResourceClassNotFoundException|OperationNotFoundException) {
5✔
134
                }
135
                /** @var Query $enumOperation */
UNCOV
136
                $enumOperation = (new Query())
5✔
UNCOV
137
                    ->withClass($resourceClass)
5✔
UNCOV
138
                    ->withShortName($operation?->getShortName() ?? (new \ReflectionClass($resourceClass))->getShortName())
5✔
UNCOV
139
                    ->withDescription($operation?->getDescription());
5✔
140

UNCOV
141
                return $this->typeBuilder->getEnumType($enumOperation);
5✔
142
            }
143

UNCOV
144
            return null;
31✔
145
        }
146

UNCOV
147
        $propertyMetadata = null;
146✔
UNCOV
148
        if ($property) {
146✔
UNCOV
149
            $context = [
95✔
UNCOV
150
                'normalization_groups' => $rootOperation->getNormalizationContext()['groups'] ?? null,
95✔
UNCOV
151
                'denormalization_groups' => $rootOperation->getDenormalizationContext()['groups'] ?? null,
95✔
UNCOV
152
            ];
95✔
UNCOV
153
            $propertyMetadata = $this->propertyMetadataFactory->create($rootResource, $property, $context);
95✔
154
        }
155

UNCOV
156
        if ($input && $depth > 0 && (!$propertyMetadata || !$propertyMetadata->isWritableLink())) {
146✔
UNCOV
157
            return GraphQLType::string();
22✔
158
        }
159

UNCOV
160
        $operationName = $rootOperation->getName();
146✔
UNCOV
161
        $isCollection = $this->typeBuilder->isCollection($type);
146✔
162

163
        // We're retrieving the type of a property which is a relation to the root resource.
UNCOV
164
        if ($resourceClass !== $rootResource && $rootOperation instanceof Query) {
146✔
UNCOV
165
            $operationName = $isCollection ? 'collection_query' : 'item_query';
90✔
166
        }
167

168
        try {
UNCOV
169
            $operation = $resourceMetadataCollection->getOperation($operationName);
146✔
170
        } catch (OperationNotFoundException) {
4✔
171
            try {
172
                $operation = $resourceMetadataCollection->getOperation($isCollection ? 'collection_query' : 'item_query');
4✔
173
            } catch (OperationNotFoundException) {
×
174
                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));
×
175
            }
176
        }
UNCOV
177
        if (!$operation instanceof Operation) {
146✔
178
            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));
×
179
        }
180

UNCOV
181
        return $this->typeBuilder->getResourceObjectType($resourceMetadataCollection, $operation, $propertyMetadata, [
146✔
UNCOV
182
            'input' => $input,
146✔
UNCOV
183
            'wrapped' => false,
146✔
UNCOV
184
            'depth' => $depth,
146✔
UNCOV
185
        ]);
146✔
186
    }
187

188
    private function resolveAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType
189
    {
190
        if ($astTypeNode instanceof NonNullTypeNode) {
133✔
191
            /** @var NullableType|null $nullableAstTypeNode */
192
            $nullableAstTypeNode = $this->resolveNullableAstTypeNode($astTypeNode->type, $fromType);
133✔
193

194
            return $nullableAstTypeNode ? GraphQLType::nonNull($nullableAstTypeNode) : null;
133✔
195
        }
196

197
        return $this->resolveNullableAstTypeNode($astTypeNode, $fromType);
133✔
198
    }
199

200
    private function resolveNullableAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType
201
    {
202
        if ($astTypeNode instanceof ListTypeNode) {
133✔
203
            /** @var TypeNode $astTypeNodeElement */
204
            $astTypeNodeElement = $astTypeNode->type;
133✔
205

206
            return GraphQLType::listOf($this->resolveAstTypeNode($astTypeNodeElement, $fromType));
133✔
207
        }
208

209
        if (!$astTypeNode instanceof NamedTypeNode) {
133✔
210
            return null;
×
211
        }
212

213
        $typeName = $astTypeNode->name->value;
133✔
214

215
        return match ($typeName) {
133✔
216
            GraphQLType::STRING => GraphQLType::string(),
133✔
217
            GraphQLType::INT => GraphQLType::int(),
133✔
218
            GraphQLType::BOOLEAN => GraphQLType::boolean(),
133✔
219
            GraphQLType::FLOAT => GraphQLType::float(),
133✔
220
            GraphQLType::ID => GraphQLType::id(),
133✔
221
            default => $this->typesContainer->has($typeName) ? $this->typesContainer->get($typeName) : null,
133✔
222
        };
133✔
223
    }
224
}
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

© 2025 Coveralls, Inc