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

api-platform / core / 13925245599

18 Mar 2025 02:06PM UTC coverage: 61.074% (-0.9%) from 61.973%
13925245599

Pull #7031

github

web-flow
Merge 8990d8b26 into 7cb5a6db8
Pull Request #7031: fix: header parameter should be case insensitive

3 of 4 new or added lines in 1 file covered. (75.0%)

167 existing lines in 27 files now uncovered.

11314 of 18525 relevant lines covered (61.07%)

51.02 hits per line

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

0.0
/src/GraphQl/Resolver/Stage/SerializeStage.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\Resolver\Stage;
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\GraphQl\Mutation;
21
use ApiPlatform\Metadata\GraphQl\Operation;
22
use ApiPlatform\Metadata\GraphQl\Subscription;
23
use ApiPlatform\State\Pagination\HasNextPagePaginatorInterface;
24
use ApiPlatform\State\Pagination\Pagination;
25
use ApiPlatform\State\Pagination\PaginatorInterface;
26
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
27
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
28

29
/**
30
 * Serialize stage of GraphQL resolvers.
31
 *
32
 * @author Alan Poulain <contact@alanpoulain.eu>
33
 *
34
 * @deprecated
35
 */
36
final class SerializeStage implements SerializeStageInterface
37
{
38
    use IdentifierTrait;
39

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

44
    public function __invoke(object|array|null $itemOrCollection, string $resourceClass, Operation $operation, array $context): ?array
45
    {
UNCOV
46
        $isCollection = $operation instanceof CollectionOperationInterface;
×
UNCOV
47
        $isMutation = $operation instanceof Mutation;
×
UNCOV
48
        $isSubscription = $operation instanceof Subscription;
×
UNCOV
49
        $shortName = $operation->getShortName();
×
UNCOV
50
        $operationName = $operation->getName();
×
51

UNCOV
52
        if (!($operation->canSerialize() ?? true)) {
×
53
            if ($isCollection) {
×
54
                if ($this->pagination->isGraphQlEnabled($operation, $context)) {
×
55
                    return 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ?
×
56
                        $this->getDefaultCursorBasedPaginatedData() :
×
57
                        $this->getDefaultPageBasedPaginatedData();
×
58
                }
59

60
                return [];
×
61
            }
62

63
            if ($isMutation) {
×
64
                return $this->getDefaultMutationData($context);
×
65
            }
66

67
            if ($isSubscription) {
×
68
                return $this->getDefaultSubscriptionData($context);
×
69
            }
70

71
            return null;
×
72
        }
73

UNCOV
74
        $normalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, true);
×
75

UNCOV
76
        $data = null;
×
UNCOV
77
        if (!$isCollection) {
×
UNCOV
78
            if ($isMutation && 'delete' === $operationName) {
×
79
                $data = ['id' => $this->getIdentifierFromContext($context)];
×
80
            } else {
UNCOV
81
                $data = $this->normalizer->normalize($itemOrCollection, ItemNormalizer::FORMAT, $normalizationContext);
×
82
            }
83
        }
84

UNCOV
85
        if ($isCollection && is_iterable($itemOrCollection)) {
×
UNCOV
86
            if (!$this->pagination->isGraphQlEnabled($operation, $context)) {
×
UNCOV
87
                $data = [];
×
UNCOV
88
                foreach ($itemOrCollection as $index => $object) {
×
UNCOV
89
                    $data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext);
×
90
                }
91
            } else {
92
                $data = 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ?
×
93
                    $this->serializeCursorBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context) :
×
94
                    $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context);
×
95
            }
96
        }
97

UNCOV
98
        if (null !== $data && !\is_array($data)) {
×
99
            throw new \UnexpectedValueException('Expected serialized data to be a nullable array.');
×
100
        }
101

UNCOV
102
        if ($isMutation || $isSubscription) {
×
103
            $wrapFieldName = lcfirst($shortName);
×
104

105
            return [$wrapFieldName => $data] + ($isMutation ? $this->getDefaultMutationData($context) : $this->getDefaultSubscriptionData($context));
×
106
        }
107

UNCOV
108
        return $data;
×
109
    }
110

111
    /**
112
     * @throws \LogicException
113
     * @throws \UnexpectedValueException
114
     */
115
    private function serializeCursorBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array
116
    {
117
        $args = $context['args'];
×
118

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

123
        $selection = $context['info']->getFieldSelection(1);
×
124

125
        $offset = 0;
×
126
        $totalItems = 1; // For partial pagination, always consider there is at least one item.
×
127
        $data = ['edges' => []];
×
128
        if (isset($selection['pageInfo']) || isset($selection['totalCount']) || isset($selection['edges']['cursor'])) {
×
129
            $nbPageItems = $collection->count();
×
130
            if (isset($args['after'])) {
×
131
                $after = base64_decode($args['after'], true);
×
132
                if (false === $after || '' === $args['after']) {
×
133
                    throw new \UnexpectedValueException('' === $args['after'] ? 'Empty cursor is invalid' : \sprintf('Cursor %s is invalid', $args['after']));
×
134
                }
135
                $offset = 1 + (int) $after;
×
136
            }
137

138
            if ($collection instanceof PaginatorInterface && (isset($selection['pageInfo']) || isset($selection['totalCount']))) {
×
139
                $totalItems = $collection->getTotalItems();
×
140
                if (isset($args['before'])) {
×
141
                    $before = base64_decode($args['before'], true);
×
142
                    if (false === $before || '' === $args['before']) {
×
143
                        throw new \UnexpectedValueException('' === $args['before'] ? 'Empty cursor is invalid' : \sprintf('Cursor %s is invalid', $args['before']));
×
144
                    }
145
                    $offset = (int) $before - $nbPageItems;
×
146
                }
147
                if (isset($args['last']) && !isset($args['before'])) {
×
148
                    $offset = $totalItems - $args['last'];
×
149
                }
150
            }
151

152
            $offset = max(0, $offset);
×
153

154
            $data = $this->getDefaultCursorBasedPaginatedData();
×
155
            if ((isset($selection['pageInfo']) || isset($selection['totalCount'])) && $totalItems > 0) {
×
156
                isset($selection['pageInfo']['startCursor']) && $data['pageInfo']['startCursor'] = base64_encode((string) $offset);
×
157
                $end = $offset + $nbPageItems - 1;
×
158
                isset($selection['pageInfo']['endCursor']) && $data['pageInfo']['endCursor'] = base64_encode((string) max($end, 0));
×
159
                isset($selection['pageInfo']['hasPreviousPage']) && $data['pageInfo']['hasPreviousPage'] = $offset > 0;
×
160
                if ($collection instanceof PaginatorInterface) {
×
161
                    isset($selection['totalCount']) && $data['totalCount'] = $totalItems;
×
162

163
                    $itemsPerPage = $collection->getItemsPerPage();
×
164
                    isset($selection['pageInfo']['hasNextPage']) && $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems;
×
165
                }
166
            }
167
        }
168

169
        $index = 0;
×
170
        foreach ($collection as $object) {
×
171
            $edge = [
×
172
                'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext),
×
173
            ];
×
174
            if (isset($selection['edges']['cursor'])) {
×
175
                $edge['cursor'] = base64_encode((string) ($index + $offset));
×
176
            }
177
            $data['edges'][$index] = $edge;
×
178
            ++$index;
×
179
        }
180

181
        return $data;
×
182
    }
183

184
    /**
185
     * @throws \LogicException
186
     */
187
    private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array
188
    {
189
        $data = ['collection' => []];
×
190

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

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

224
        return $data;
×
225
    }
226

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

232
    private function getDefaultPageBasedPaginatedData(): array
233
    {
234
        return ['collection' => [], 'paginationInfo' => ['itemsPerPage' => 0., 'totalCount' => 0., 'lastPage' => 0., 'hasNextPage' => false]];
×
235
    }
236

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

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