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

api-platform / core / 9731345539

30 Jun 2024 10:33AM UTC coverage: 63.285%. Remained the same
9731345539

push

github

soyuka
test: ArrayCollection typing as list

11104 of 17546 relevant lines covered (63.29%)

52.28 hits per line

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

61.7
/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|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory)
41
    {
42
        if ($typeBuilder instanceof TypeBuilderInterface) {
3✔
43
            @trigger_error(sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED);
×
44
        }
45

46
        if ($typeBuilder instanceof TypeBuilderEnumInterface) {
3✔
47
            @trigger_error(sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.3. It has to implement "%s" instead.', TypeBuilderEnumInterface::class, ContextAwareTypeBuilderInterface::class), \E_USER_DEPRECATED);
×
48
        }
49
    }
50

51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth): GraphQLType|string|null
55
    {
56
        switch ($type->getBuiltinType()) {
3✔
57
            case Type::BUILTIN_TYPE_BOOL:
58
                return GraphQLType::boolean();
3✔
59
            case Type::BUILTIN_TYPE_INT:
60
                return GraphQLType::int();
3✔
61
            case Type::BUILTIN_TYPE_FLOAT:
62
                return GraphQLType::float();
3✔
63
            case Type::BUILTIN_TYPE_STRING:
64
                return GraphQLType::string();
3✔
65
            case Type::BUILTIN_TYPE_ARRAY:
66
            case Type::BUILTIN_TYPE_ITERABLE:
67
                if ($resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth)) {
×
68
                    return $resourceType;
×
69
                }
70

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

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

83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function resolveType(string $type): ?GraphQLType
87
    {
88
        try {
89
            $astTypeNode = Parser::parseType($type);
3✔
90
        } catch (SyntaxError $e) {
×
91
            throw new InvalidArgumentException(sprintf('"%s" is not a valid GraphQL type.', $type), 0, $e);
×
92
        }
93

94
        if ($graphQlType = $this->resolveAstTypeNode($astTypeNode, $type)) {
3✔
95
            return $graphQlType;
3✔
96
        }
97

98
        throw new InvalidArgumentException(sprintf('The type "%s" was not resolved.', $type));
×
99
    }
100

101
    private function getResourceType(Type $type, bool $input, Operation $rootOperation, string $rootResource, ?string $property, int $depth): ?GraphQLType
102
    {
103
        if (
104
            $this->typeBuilder->isCollection($type)
3✔
105
            && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
3✔
106
        ) {
107
            $resourceClass = $collectionValueType->getClassName();
3✔
108
        } else {
109
            $resourceClass = $type->getClassName();
3✔
110
        }
111

112
        if (null === $resourceClass) {
3✔
113
            return null;
×
114
        }
115

116
        try {
117
            $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
3✔
118
        } catch (ResourceClassNotFoundException) {
×
119
            return null;
×
120
        }
121

122
        $hasGraphQl = false;
3✔
123
        foreach ($resourceMetadataCollection as $resourceMetadata) {
3✔
124
            if (null !== $resourceMetadata->getGraphQlOperations()) {
3✔
125
                $hasGraphQl = true;
3✔
126
                break;
3✔
127
            }
128
        }
129

130
        if (isset($resourceMetadataCollection[0]) && 'Node' === $resourceMetadataCollection[0]->getShortName()) {
3✔
131
            throw new \UnexpectedValueException('A "Node" resource cannot be used with GraphQL because the type is already used by the Relay specification.');
×
132
        }
133

134
        if (!$hasGraphQl) {
3✔
135
            if (is_a($resourceClass, \BackedEnum::class, true)) {
×
136
                // Remove the condition in API Platform 4.
137
                if ($this->typeBuilder instanceof TypeBuilderEnumInterface || $this->typeBuilder instanceof ContextAwareTypeBuilderInterface) {
×
138
                    $operation = null;
×
139
                    try {
140
                        $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
×
141
                        $operation = $resourceMetadataCollection->getOperation();
×
142
                    } catch (ResourceClassNotFoundException|OperationNotFoundException) {
×
143
                    }
144
                    /** @var Query $enumOperation */
145
                    $enumOperation = (new Query())
×
146
                        ->withClass($resourceClass)
×
147
                        ->withShortName($operation?->getShortName() ?? (new \ReflectionClass($resourceClass))->getShortName())
×
148
                        ->withDescription($operation?->getDescription());
×
149

150
                    return $this->typeBuilder->getEnumType($enumOperation);
×
151
                }
152
            }
153

154
            return null;
×
155
        }
156

157
        $propertyMetadata = null;
3✔
158
        if ($property) {
3✔
159
            $context = [
×
160
                'normalization_groups' => $rootOperation->getNormalizationContext()['groups'] ?? null,
×
161
                'denormalization_groups' => $rootOperation->getDenormalizationContext()['groups'] ?? null,
×
162
            ];
×
163
            $propertyMetadata = $this->propertyMetadataFactory->create($rootResource, $property, $context);
×
164
        }
165

166
        if ($input && $depth > 0 && (!$propertyMetadata || !$propertyMetadata->isWritableLink())) {
3✔
167
            return GraphQLType::string();
×
168
        }
169

170
        $operationName = $rootOperation->getName();
3✔
171
        $isCollection = $this->typeBuilder->isCollection($type);
3✔
172

173
        // We're retrieving the type of a property which is a relation to the root resource.
174
        if ($resourceClass !== $rootResource && $rootOperation instanceof Query) {
3✔
175
            $operationName = $isCollection ? 'collection_query' : 'item_query';
×
176
        }
177

178
        try {
179
            $operation = $resourceMetadataCollection->getOperation($operationName);
3✔
180
        } catch (OperationNotFoundException) {
×
181
            $operation = $resourceMetadataCollection->getOperation($isCollection ? 'collection_query' : 'item_query');
×
182
        }
183
        if (!$operation instanceof Operation) {
3✔
184
            throw new OperationNotFoundException();
×
185
        }
186

187
        return $this->typeBuilder instanceof ContextAwareTypeBuilderInterface ?
3✔
188
            $this->typeBuilder->getResourceObjectType($resourceMetadataCollection, $operation, $propertyMetadata, [
3✔
189
                'input' => $input,
3✔
190
                'wrapped' => false,
3✔
191
                'depth' => $depth,
3✔
192
            ]) :
3✔
193
            $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth);
3✔
194
    }
195

196
    private function resolveAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType
197
    {
198
        if ($astTypeNode instanceof NonNullTypeNode) {
3✔
199
            /** @var NullableType|null $nullableAstTypeNode */
200
            $nullableAstTypeNode = $this->resolveNullableAstTypeNode($astTypeNode->type, $fromType);
3✔
201

202
            return $nullableAstTypeNode ? GraphQLType::nonNull($nullableAstTypeNode) : null;
3✔
203
        }
204

205
        return $this->resolveNullableAstTypeNode($astTypeNode, $fromType);
3✔
206
    }
207

208
    private function resolveNullableAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType
209
    {
210
        if ($astTypeNode instanceof ListTypeNode) {
3✔
211
            /** @var TypeNode $astTypeNodeElement */
212
            $astTypeNodeElement = $astTypeNode->type;
3✔
213

214
            return GraphQLType::listOf($this->resolveAstTypeNode($astTypeNodeElement, $fromType));
3✔
215
        }
216

217
        if (!$astTypeNode instanceof NamedTypeNode) {
3✔
218
            return null;
×
219
        }
220

221
        $typeName = $astTypeNode->name->value;
3✔
222

223
        return match ($typeName) {
3✔
224
            GraphQLType::STRING => GraphQLType::string(),
3✔
225
            GraphQLType::INT => GraphQLType::int(),
3✔
226
            GraphQLType::BOOLEAN => GraphQLType::boolean(),
3✔
227
            GraphQLType::FLOAT => GraphQLType::float(),
3✔
228
            GraphQLType::ID => GraphQLType::id(),
3✔
229
            default => $this->typesContainer->has($typeName) ? $this->typesContainer->get($typeName) : null,
3✔
230
        };
3✔
231
    }
232
}
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