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

api-platform / core / 9210912913

23 May 2024 03:37PM UTC coverage: 57.176% (-0.3%) from 57.512%
9210912913

Pull #6385

github

web-flow
Merge 146e374c2 into 002d8e514
Pull Request #6385: fix(symfony): Make `WriteListener` compatible with `MainController`

1 of 1 new or added line in 1 file covered. (100.0%)

59 existing lines in 15 files now uncovered.

9987 of 17467 relevant lines covered (57.18%)

36.38 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
final class SerializeStage implements SerializeStageInterface
35
{
36
    use IdentifierTrait;
37

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

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

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

58
                return [];
×
59
            }
60

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

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

69
            return null;
×
70
        }
71

72
        $normalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, true);
×
73

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

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

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

100
        if ($isMutation || $isSubscription) {
×
101
            $wrapFieldName = lcfirst($shortName);
×
102

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

106
        return $data;
×
107
    }
108

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

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

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

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

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

150
            $offset = max(0, $offset);
×
151

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

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

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

179
        return $data;
×
180
    }
181

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

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

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

222
        return $data;
×
223
    }
224

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

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

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

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