• 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

85.84
/src/GraphQl/State/Processor/NormalizeProcessor.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\Processor;
15

16
use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait;
17
use ApiPlatform\GraphQl\Serializer\ItemNormalizer;
18
use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface;
19
use ApiPlatform\Metadata\CollectionOperationInterface;
20
use ApiPlatform\Metadata\DeleteOperationInterface;
21
use ApiPlatform\Metadata\GraphQl\Mutation;
22
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
23
use ApiPlatform\Metadata\GraphQl\Subscription;
24
use ApiPlatform\Metadata\Operation;
25
use ApiPlatform\State\Pagination\HasNextPagePaginatorInterface;
26
use ApiPlatform\State\Pagination\Pagination;
27
use ApiPlatform\State\Pagination\PaginatorInterface;
28
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
29
use ApiPlatform\State\ProcessorInterface;
30
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
31

32
/**
33
 * Transforms the data to a GraphQl json format. It uses the Symfony Normalizer then performs changes according to the type of operation.
34
 */
35
final class NormalizeProcessor implements ProcessorInterface
36
{
37
    use IdentifierTrait;
38

39
    public function __construct(private readonly NormalizerInterface $normalizer, private readonly SerializerContextBuilderInterface $serializerContextBuilder, private readonly Pagination $pagination)
40
    {
UNCOV
41
    }
534✔
42

43
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): ?array
44
    {
UNCOV
45
        if (!$operation instanceof GraphQlOperation) {
111✔
46
            return $data;
×
47
        }
48

UNCOV
49
        return $this->getData($data, $operation, $uriVariables, $context);
111✔
50
    }
51

52
    /**
53
     * @param array<string, mixed> $uriVariables
54
     * @param array<string, mixed> $context
55
     *
56
     * @return array<string, mixed>
57
     */
58
    private function getData(mixed $itemOrCollection, GraphQlOperation $operation, array $uriVariables = [], array $context = []): ?array
59
    {
UNCOV
60
        if (!($operation->canSerialize() ?? true)) {
111✔
61
            if ($operation instanceof CollectionOperationInterface) {
3✔
62
                if ($this->pagination->isGraphQlEnabled($operation, $context)) {
1✔
63
                    return 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ?
1✔
64
                        $this->getDefaultCursorBasedPaginatedData() :
1✔
65
                        $this->getDefaultPageBasedPaginatedData();
1✔
66
                }
67

68
                return [];
×
69
            }
70

71
            if ($operation instanceof Mutation) {
2✔
72
                return $this->getDefaultMutationData($context);
1✔
73
            }
74

75
            if ($operation instanceof Subscription) {
1✔
76
                return $this->getDefaultSubscriptionData($context);
×
77
            }
78

79
            return null;
1✔
80
        }
81

UNCOV
82
        $normalizationContext = $this->serializerContextBuilder->create($operation->getClass(), $operation, $context, normalization: true);
108✔
83

UNCOV
84
        $data = null;
108✔
UNCOV
85
        if (!$operation instanceof CollectionOperationInterface) {
108✔
UNCOV
86
            if ($operation instanceof Mutation && $operation instanceof DeleteOperationInterface) {
72✔
87
                $data = ['id' => $this->getIdentifierFromOperation($operation, $context['args'] ?? [])];
×
88
            } else {
UNCOV
89
                $data = $this->normalizer->normalize($itemOrCollection, ItemNormalizer::FORMAT, $normalizationContext);
72✔
90
            }
91
        }
92

UNCOV
93
        if ($operation instanceof CollectionOperationInterface && is_iterable($itemOrCollection)) {
108✔
UNCOV
94
            if (!$this->pagination->isGraphQlEnabled($operation, $context)) {
49✔
UNCOV
95
                $data = [];
5✔
UNCOV
96
                foreach ($itemOrCollection as $index => $object) {
5✔
UNCOV
97
                    $data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext);
4✔
98
                }
99
            } else {
UNCOV
100
                $data = 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ?
46✔
UNCOV
101
                    $this->serializeCursorBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context) :
44✔
102
                    $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context);
4✔
103
            }
104
        }
105

UNCOV
106
        if (null !== $data && !\is_array($data)) {
108✔
107
            throw new \UnexpectedValueException('Expected serialized data to be a nullable array.');
×
108
        }
109

UNCOV
110
        $isMutation = $operation instanceof Mutation;
108✔
UNCOV
111
        $isSubscription = $operation instanceof Subscription;
108✔
UNCOV
112
        if ($isMutation || $isSubscription) {
108✔
113
            $wrapFieldName = lcfirst($operation->getShortName());
29✔
114

115
            return [$wrapFieldName => $data] + ($isMutation ? $this->getDefaultMutationData($context) : $this->getDefaultSubscriptionData($context));
29✔
116
        }
117

UNCOV
118
        return $data;
85✔
119
    }
120

121
    /**
122
     * @throws \LogicException
123
     * @throws \UnexpectedValueException
124
     */
125
    private function serializeCursorBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array
126
    {
UNCOV
127
        $args = $context['args'];
44✔
128

UNCOV
129
        if (!($collection instanceof PartialPaginatorInterface)) {
44✔
130
            throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s or %s.', PaginatorInterface::class, PartialPaginatorInterface::class));
×
131
        }
132

UNCOV
133
        $selection = $context['info']->getFieldSelection(1);
44✔
134

UNCOV
135
        $offset = 0;
44✔
UNCOV
136
        $totalItems = 1; // For partial pagination, always consider there is at least one item.
44✔
UNCOV
137
        $data = ['edges' => []];
44✔
UNCOV
138
        if (isset($selection['pageInfo']) || isset($selection['totalCount']) || isset($selection['edges']['cursor'])) {
44✔
139
            $nbPageItems = $collection->count();
6✔
140
            if (isset($args['after'])) {
6✔
141
                $after = base64_decode($args['after'], true);
1✔
142
                if (false === $after || '' === $args['after']) {
1✔
143
                    throw new \UnexpectedValueException('' === $args['after'] ? 'Empty cursor is invalid' : \sprintf('Cursor %s is invalid', $args['after']));
×
144
                }
145
                $offset = 1 + (int) $after;
1✔
146
            }
147

148
            if ($collection instanceof PaginatorInterface && (isset($selection['pageInfo']) || isset($selection['totalCount']))) {
6✔
149
                $totalItems = $collection->getTotalItems();
6✔
150
                if (isset($args['before'])) {
6✔
151
                    $before = base64_decode($args['before'], true);
1✔
152
                    if (false === $before || '' === $args['before']) {
1✔
153
                        throw new \UnexpectedValueException('' === $args['before'] ? 'Empty cursor is invalid' : \sprintf('Cursor %s is invalid', $args['before']));
×
154
                    }
155
                    $offset = (int) $before - $nbPageItems;
1✔
156
                }
157
                if (isset($args['last']) && !isset($args['before'])) {
6✔
158
                    $offset = $totalItems - $args['last'];
1✔
159
                }
160
            }
161

162
            $offset = max(0, $offset);
6✔
163

164
            $data = $this->getDefaultCursorBasedPaginatedData();
6✔
165
            if ((isset($selection['pageInfo']) || isset($selection['totalCount'])) && $totalItems > 0) {
6✔
166
                isset($selection['pageInfo']['startCursor']) && $data['pageInfo']['startCursor'] = base64_encode((string) $offset);
5✔
167
                $end = $offset + $nbPageItems - 1;
5✔
168
                isset($selection['pageInfo']['endCursor']) && $data['pageInfo']['endCursor'] = base64_encode((string) max($end, 0));
5✔
169
                isset($selection['pageInfo']['hasPreviousPage']) && $data['pageInfo']['hasPreviousPage'] = $offset > 0;
5✔
170
                if ($collection instanceof PaginatorInterface) {
5✔
171
                    isset($selection['totalCount']) && $data['totalCount'] = $totalItems;
5✔
172

173
                    $itemsPerPage = $collection->getItemsPerPage();
5✔
174
                    isset($selection['pageInfo']['hasNextPage']) && $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems;
5✔
175
                }
176
            }
177
        }
178

UNCOV
179
        $index = 0;
44✔
UNCOV
180
        foreach ($collection as $object) {
44✔
UNCOV
181
            $edge = [
41✔
UNCOV
182
                'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext),
41✔
UNCOV
183
            ];
41✔
UNCOV
184
            if (isset($selection['edges']['cursor'])) {
41✔
185
                $edge['cursor'] = base64_encode((string) ($index + $offset));
4✔
186
            }
UNCOV
187
            $data['edges'][$index] = $edge;
41✔
UNCOV
188
            ++$index;
41✔
189
        }
190

UNCOV
191
        return $data;
44✔
192
    }
193

194
    /**
195
     * @throws \LogicException
196
     */
197
    private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array
198
    {
199
        $data = ['collection' => []];
4✔
200

201
        $selection = $context['info']->getFieldSelection(1);
4✔
202
        if (isset($selection['paginationInfo'])) {
4✔
203
            $data['paginationInfo'] = [];
3✔
204
            if (isset($selection['paginationInfo']['itemsPerPage'])) {
3✔
205
                if (!($collection instanceof PartialPaginatorInterface)) {
2✔
206
                    throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return itemsPerPage field.', PartialPaginatorInterface::class));
×
207
                }
208
                $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage();
2✔
209
            }
210
            if (isset($selection['paginationInfo']['totalCount'])) {
3✔
211
                if (!($collection instanceof PaginatorInterface)) {
2✔
212
                    throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return totalCount field.', PaginatorInterface::class));
×
213
                }
214
                $data['paginationInfo']['totalCount'] = $collection->getTotalItems();
2✔
215
            }
216
            if (isset($selection['paginationInfo']['currentPage'])) {
3✔
217
                if (!($collection instanceof PartialPaginatorInterface)) {
×
218
                    throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return currentPage field.', PartialPaginatorInterface::class));
×
219
                }
220
                $data['paginationInfo']['currentPage'] = $collection->getCurrentPage();
×
221
            }
222
            if (isset($selection['paginationInfo']['lastPage'])) {
3✔
223
                if (!($collection instanceof PaginatorInterface)) {
2✔
224
                    throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return lastPage field.', PaginatorInterface::class));
×
225
                }
226
                $data['paginationInfo']['lastPage'] = $collection->getLastPage();
2✔
227
            }
228
            if (isset($selection['paginationInfo']['hasNextPage'])) {
3✔
229
                if (!($collection instanceof HasNextPagePaginatorInterface)) {
3✔
230
                    throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return hasNextPage field.', HasNextPagePaginatorInterface::class));
×
231
                }
232
                $data['paginationInfo']['hasNextPage'] = $collection->hasNextPage();
3✔
233
            }
234
        }
235

236
        foreach ($collection as $object) {
4✔
237
            $data['collection'][] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext);
4✔
238
        }
239

240
        return $data;
4✔
241
    }
242

243
    private function getDefaultCursorBasedPaginatedData(): array
244
    {
245
        return ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]];
7✔
246
    }
247

248
    private function getDefaultPageBasedPaginatedData(): array
249
    {
250
        return ['collection' => [], 'paginationInfo' => ['itemsPerPage' => 0., 'totalCount' => 0., 'lastPage' => 0.]];
×
251
    }
252

253
    private function getDefaultMutationData(array $context): array
254
    {
255
        return ['clientMutationId' => $context['args']['input']['clientMutationId'] ?? null];
28✔
256
    }
257

258
    private function getDefaultSubscriptionData(array $context): array
259
    {
260
        return ['clientSubscriptionId' => $context['args']['input']['clientSubscriptionId'] ?? null];
2✔
261
    }
262
}
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