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

api-platform / core / 17487610263

05 Sep 2025 08:12AM UTC coverage: 22.608% (+0.3%) from 22.319%
17487610263

push

github

web-flow
chore: remove @experimental flag from parameters (#7357)

12049 of 53296 relevant lines covered (22.61%)

26.21 hits per line

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

62.0
/src/GraphQl/State/Provider/ReadProvider.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\State\Provider;
15

16
use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait;
17
use ApiPlatform\GraphQl\Serializer\ItemNormalizer;
18
use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface;
19
use ApiPlatform\GraphQl\Util\ArrayTrait;
20
use ApiPlatform\Metadata\CollectionOperationInterface;
21
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
22
use ApiPlatform\Metadata\GraphQl\Mutation;
23
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
24
use ApiPlatform\Metadata\GraphQl\QueryCollection;
25
use ApiPlatform\Metadata\GraphQl\Subscription;
26
use ApiPlatform\Metadata\IriConverterInterface;
27
use ApiPlatform\Metadata\Operation;
28
use ApiPlatform\Metadata\Util\ClassInfoTrait;
29
use ApiPlatform\State\ProviderInterface;
30
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
31

32
/**
33
 * Reads data of the provided args, it is also called when reading relations in the same query.
34
 */
35
final class ReadProvider implements ProviderInterface
36
{
37
    use ArrayTrait;
38
    use ClassInfoTrait;
39
    use IdentifierTrait;
40

41
    public function __construct(private readonly ProviderInterface $provider, private readonly IriConverterInterface $iriConverter, private readonly ?SerializerContextBuilderInterface $serializerContextBuilder, private readonly string $nestingSeparator)
42
    {
43
    }
26✔
44

45
    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
46
    {
47
        if (!$operation instanceof GraphQlOperation || !($operation->canRead() ?? true)) {
26✔
48
            return $operation instanceof QueryCollection ? [] : null;
×
49
        }
50

51
        $args = $context['args'] ?? [];
26✔
52

53
        if ($this->serializerContextBuilder) {
26✔
54
            // Builtin data providers are able to use the serialization context to automatically add join clauses
55
            $context += $this->serializerContextBuilder->create($operation->getClass(), $operation, $context, true);
26✔
56
        }
57

58
        if (!$operation instanceof CollectionOperationInterface) {
26✔
59
            $identifier = $this->getIdentifierFromOperation($operation, $args);
20✔
60

61
            if (!$identifier) {
20✔
62
                return null;
2✔
63
            }
64

65
            try {
66
                $item = $this->iriConverter->getResourceFromIri($identifier, $context);
18✔
67
            } catch (ItemNotFoundException) {
×
68
                $item = null;
×
69
            }
70

71
            if ($operation instanceof Subscription || $operation instanceof Mutation) {
18✔
72
                if (null === $item) {
×
73
                    throw new NotFoundHttpException(\sprintf('Item "%s" not found.', $args['input']['id']));
×
74
                }
75

76
                if ($operation->getClass() !== $this->getObjectClass($item)) {
×
77
                    throw new \UnexpectedValueException(\sprintf('Item "%s" did not match expected type "%s".', $args['input']['id'], $operation->getShortName()));
×
78
                }
79
            }
80

81
            if (null === $item) {
18✔
82
                return $item;
×
83
            }
84

85
            if (isset($context['graphql_context']) && !enum_exists($item::class)) {
18✔
86
                $context['graphql_context']['previous_object'] = clone $item;
6✔
87
            }
88

89
            return $item;
18✔
90
        }
91

92
        if (null === ($context['root_class'] ?? null)) {
6✔
93
            return [];
×
94
        }
95

96
        $uriVariables = [];
6✔
97
        $context['filters'] = $this->getNormalizedFilters($args);
6✔
98

99
        // This is how we resolve graphql links see ApiPlatform\Doctrine\Common\State\LinksHandlerTrait, I'm wondering if we couldn't do that in an UriVariables
100
        // resolver within our ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory, this would mimic what's happening in the HTTP controller and simplify some code.
101
        $source = $context['source'];
6✔
102
        /** @var \GraphQL\Type\Definition\ResolveInfo $info */
103
        $info = $context['info'];
6✔
104
        if (isset($source[$info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY], $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
6✔
105
            $uriVariables = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY];
×
106
            $context['linkClass'] = $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY];
×
107
            $context['linkProperty'] = $info->fieldName;
×
108
        }
109

110
        return $this->provider->provide($operation, $uriVariables, $context);
6✔
111
    }
112

113
    /**
114
     * @param array<string, string|array> $args
115
     *
116
     * @return array<string, string>
117
     */
118
    private function getNormalizedFilters(array $args): array
119
    {
120
        $filters = $args;
6✔
121

122
        foreach ($filters as $name => $value) {
6✔
123
            // Prevent numeric keys like `'1'`
124
            $name = (string) $name;
2✔
125

126
            if (\is_array($value)) {
2✔
127
                if (strpos($name, '_list')) {
2✔
128
                    $name = substr($name, 0, \strlen($name) - \strlen('_list'));
×
129
                }
130

131
                // If the value contains arrays, we need to merge them for the filters to understand this syntax, proper to GraphQL to preserve the order of the arguments.
132
                if ($this->isSequentialArrayOfArrays($value)) {
2✔
133
                    $value = array_merge(...$value);
×
134
                }
135
                $filters[$name] = $this->getNormalizedFilters($value);
2✔
136
            }
137

138
            if (strpos($name, $this->nestingSeparator)) {
2✔
139
                // Gives a chance to relations/nested fields.
140
                $index = array_search($name, array_keys($filters), true);
×
141
                $filters =
×
142
                    \array_slice($filters, 0, $index + 1) +
×
143
                    [str_replace($this->nestingSeparator, '.', $name) => $value] +
×
144
                    \array_slice($filters, $index + 1);
×
145
            }
146
        }
147

148
        return $filters;
6✔
149
    }
150
}
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