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

api-platform / core / 14635100171

24 Apr 2025 06:39AM UTC coverage: 8.271% (+0.02%) from 8.252%
14635100171

Pull #6904

github

web-flow
Merge c9cefd82e into a3e5e53ea
Pull Request #6904: feat(graphql): added support for graphql subscriptions to work for actions

0 of 73 new or added lines in 3 files covered. (0.0%)

1999 existing lines in 144 files now uncovered.

13129 of 158728 relevant lines covered (8.27%)

13.6 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
    {
41
    }
647✔
42

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

49
        return $this->getData($data, $operation, $uriVariables, $context);
121✔
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
    {
60
        if (!($operation->canSerialize() ?? true)) {
121✔
UNCOV
61
            if ($operation instanceof CollectionOperationInterface) {
3✔
UNCOV
62
                if ($this->pagination->isGraphQlEnabled($operation, $context)) {
1✔
UNCOV
63
                    return 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ?
1✔
UNCOV
64
                        $this->getDefaultCursorBasedPaginatedData() :
1✔
UNCOV
65
                        $this->getDefaultPageBasedPaginatedData();
1✔
66
                }
67

68
                return [];
×
69
            }
70

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

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

UNCOV
79
            return null;
1✔
80
        }
81

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

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

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

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

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

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

118
        return $data;
95✔
119
    }
120

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

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

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

135
        $offset = 0;
46✔
136
        $totalItems = 1; // For partial pagination, always consider there is at least one item.
46✔
137
        $data = ['edges' => []];
46✔
138
        if (isset($selection['pageInfo']) || isset($selection['totalCount']) || isset($selection['edges']['cursor'])) {
46✔
UNCOV
139
            $nbPageItems = $collection->count();
6✔
UNCOV
140
            if (isset($args['after'])) {
6✔
UNCOV
141
                $after = base64_decode($args['after'], true);
1✔
UNCOV
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
                }
UNCOV
145
                $offset = 1 + (int) $after;
1✔
146
            }
147

UNCOV
148
            if ($collection instanceof PaginatorInterface && (isset($selection['pageInfo']) || isset($selection['totalCount']))) {
6✔
UNCOV
149
                $totalItems = $collection->getTotalItems();
6✔
UNCOV
150
                if (isset($args['before'])) {
6✔
UNCOV
151
                    $before = base64_decode($args['before'], true);
1✔
UNCOV
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
                    }
UNCOV
155
                    $offset = (int) $before - $nbPageItems;
1✔
156
                }
UNCOV
157
                if (isset($args['last']) && !isset($args['before'])) {
6✔
UNCOV
158
                    $offset = $totalItems - $args['last'];
1✔
159
                }
160
            }
161

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

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

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

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

191
        return $data;
46✔
192
    }
193

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

UNCOV
201
        $selection = $context['info']->getFieldSelection(1);
4✔
UNCOV
202
        if (isset($selection['paginationInfo'])) {
4✔
UNCOV
203
            $data['paginationInfo'] = [];
3✔
UNCOV
204
            if (isset($selection['paginationInfo']['itemsPerPage'])) {
3✔
UNCOV
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
                }
UNCOV
208
                $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage();
2✔
209
            }
UNCOV
210
            if (isset($selection['paginationInfo']['totalCount'])) {
3✔
UNCOV
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
                }
UNCOV
214
                $data['paginationInfo']['totalCount'] = $collection->getTotalItems();
2✔
215
            }
UNCOV
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
            }
UNCOV
222
            if (isset($selection['paginationInfo']['lastPage'])) {
3✔
UNCOV
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
                }
UNCOV
226
                $data['paginationInfo']['lastPage'] = $collection->getLastPage();
2✔
227
            }
UNCOV
228
            if (isset($selection['paginationInfo']['hasNextPage'])) {
3✔
UNCOV
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
                }
UNCOV
232
                $data['paginationInfo']['hasNextPage'] = $collection->hasNextPage();
3✔
233
            }
234
        }
235

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

UNCOV
240
        return $data;
4✔
241
    }
242

243
    private function getDefaultCursorBasedPaginatedData(): array
244
    {
UNCOV
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
    {
UNCOV
255
        return ['clientMutationId' => $context['args']['input']['clientMutationId'] ?? null];
28✔
256
    }
257

258
    private function getDefaultSubscriptionData(array $context): array
259
    {
UNCOV
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