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

api-platform / core / 7588293000

19 Jan 2024 07:26PM UTC coverage: 63.892% (+0.08%) from 63.81%
7588293000

push

github

soyuka
Merge 3.2

0 of 100 new or added lines in 2 files covered. (0.0%)

5 existing lines in 5 files now uncovered.

17017 of 26634 relevant lines covered (63.89%)

31.31 hits per line

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

0.95
/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\Pagination;
26
use ApiPlatform\State\Pagination\PaginatorInterface;
27
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
28
use ApiPlatform\State\ProcessorInterface;
29
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
30

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

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

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

48
        return $this->getData($data, $operation, $uriVariables, $context);
×
49
    }
50

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

67
                return [];
×
68
            }
69

70
            if ($operation instanceof Mutation) {
×
71
                return $this->getDefaultMutationData($context);
×
72
            }
73

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

78
            return null;
×
79
        }
80

81
        $normalizationContext = $this->serializerContextBuilder->create($operation->getClass(), $operation, $context, normalization: true);
×
82

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

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

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

109
        $isMutation = $operation instanceof Mutation;
×
110
        $isSubscription = $operation instanceof Subscription;
×
111
        if ($isMutation || $isSubscription) {
×
112
            $wrapFieldName = lcfirst($operation->getShortName());
×
113

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

117
        return $data;
×
118
    }
119

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

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

NEW
132
        $selection = $context['info']->getFieldSelection(1);
×
133

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

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

NEW
161
            $offset = max(0, $offset);
×
162

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

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

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

190
        return $data;
×
191
    }
192

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

NEW
200
        $selection = $context['info']->getFieldSelection(1);
×
NEW
201
        if (isset($selection['paginationInfo'])) {
×
NEW
202
            $data['paginationInfo'] = [];
×
NEW
203
            if (isset($selection['paginationInfo']['itemsPerPage'])) {
×
NEW
204
                if (!($collection instanceof PartialPaginatorInterface)) {
×
NEW
205
                    throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return itemsPerPage field.', PartialPaginatorInterface::class));
×
206
                }
NEW
207
                $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage();
×
208
            }
NEW
209
            if (isset($selection['paginationInfo']['totalCount'])) {
×
NEW
210
                if (!($collection instanceof PaginatorInterface)) {
×
NEW
211
                    throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return totalCount field.', PaginatorInterface::class));
×
212
                }
NEW
213
                $data['paginationInfo']['totalCount'] = $collection->getTotalItems();
×
214
            }
NEW
215
            if (isset($selection['paginationInfo']['lastPage'])) {
×
NEW
216
                if (!($collection instanceof PaginatorInterface)) {
×
NEW
217
                    throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s to return lastPage field.', PaginatorInterface::class));
×
218
                }
NEW
219
                $data['paginationInfo']['lastPage'] = $collection->getLastPage();
×
220
            }
221
        }
222

223
        foreach ($collection as $object) {
×
224
            $data['collection'][] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext);
×
225
        }
226

227
        return $data;
×
228
    }
229

230
    private function getDefaultCursorBasedPaginatedData(): array
231
    {
232
        return ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]];
×
233
    }
234

235
    private function getDefaultPageBasedPaginatedData(): array
236
    {
237
        return ['collection' => [], 'paginationInfo' => ['itemsPerPage' => 0., 'totalCount' => 0., 'lastPage' => 0.]];
×
238
    }
239

240
    private function getDefaultMutationData(array $context): array
241
    {
242
        return ['clientMutationId' => $context['args']['input']['clientMutationId'] ?? null];
×
243
    }
244

245
    private function getDefaultSubscriptionData(array $context): array
246
    {
247
        return ['clientSubscriptionId' => $context['args']['input']['clientSubscriptionId'] ?? null];
×
248
    }
249
}
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