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

api-platform / core / 6978607092

24 Nov 2023 08:43AM UTC coverage: 36.966% (-0.5%) from 37.474%
6978607092

push

github

web-flow
fix: errors bc with rfc_7807_compliant_errors false (#5974)

* fix: errors bc with rfc_7807_compliant_errors false

70 of 140 new or added lines in 19 files covered. (50.0%)

38 existing lines in 9 files now uncovered.

10179 of 27536 relevant lines covered (36.97%)

13.69 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\Pagination;
24
use ApiPlatform\State\Pagination\PaginatorInterface;
25
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
26
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
27

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

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

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

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

57
                return [];
×
58
            }
59

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

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

68
            return null;
×
69
        }
70

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

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

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

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

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

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

105
        return $data;
×
106
    }
107

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

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

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

131
        if ($collection instanceof PaginatorInterface) {
×
132
            $totalItems = $collection->getTotalItems();
×
133

134
            if (isset($args['before'])) {
×
135
                $before = base64_decode($args['before'], true);
×
136
                if (false === $before || '' === $args['before']) {
×
137
                    throw new \UnexpectedValueException('' === $args['before'] ? 'Empty cursor is invalid' : sprintf('Cursor %s is invalid', $args['before']));
×
138
                }
139
                $offset = (int) $before - $nbPageItems;
×
140
            }
141
            if (isset($args['last']) && !isset($args['before'])) {
×
142
                $offset = $totalItems - $args['last'];
×
143
            }
144
        }
145

146
        $offset = 0 > $offset ? 0 : $offset;
×
147

148
        $data = $this->getDefaultCursorBasedPaginatedData();
×
149
        if ($totalItems > 0) {
×
150
            $data['pageInfo']['startCursor'] = base64_encode((string) $offset);
×
151
            $end = $offset + $nbPageItems - 1;
×
152
            $data['pageInfo']['endCursor'] = base64_encode((string) ($end >= 0 ? $end : 0));
×
153
            $data['pageInfo']['hasPreviousPage'] = $offset > 0;
×
154
            if ($collection instanceof PaginatorInterface) {
×
155
                $data['totalCount'] = $totalItems;
×
156
                $itemsPerPage = $collection->getItemsPerPage();
×
157
                $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems;
×
158
            }
159
        }
160

161
        $index = 0;
×
162
        foreach ($collection as $object) {
×
163
            $data['edges'][$index] = [
×
164
                'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext),
×
165
                'cursor' => base64_encode((string) ($index + $offset)),
×
166
            ];
×
167
            ++$index;
×
168
        }
169

170
        return $data;
×
171
    }
172

173
    /**
174
     * @throws \LogicException
175
     */
176
    private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext): array
177
    {
178
        if (!($collection instanceof PaginatorInterface)) {
×
179
            throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s.', PaginatorInterface::class));
×
180
        }
181

182
        $data = $this->getDefaultPageBasedPaginatedData();
×
183
        $data['paginationInfo']['totalCount'] = $collection->getTotalItems();
×
184
        $data['paginationInfo']['lastPage'] = $collection->getLastPage();
×
185
        $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage();
×
186

187
        foreach ($collection as $object) {
×
188
            $data['collection'][] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext);
×
189
        }
190

191
        return $data;
×
192
    }
193

194
    private function getDefaultCursorBasedPaginatedData(): array
195
    {
196
        return ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]];
×
197
    }
198

199
    private function getDefaultPageBasedPaginatedData(): array
200
    {
201
        return ['collection' => [], 'paginationInfo' => ['itemsPerPage' => 0., 'totalCount' => 0., 'lastPage' => 0.]];
×
202
    }
203

204
    private function getDefaultMutationData(array $context): array
205
    {
206
        return ['clientMutationId' => $context['args']['input']['clientMutationId'] ?? null];
×
207
    }
208

209
    private function getDefaultSubscriptionData(array $context): array
210
    {
211
        return ['clientSubscriptionId' => $context['args']['input']['clientSubscriptionId'] ?? null];
×
212
    }
213
}
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

© 2026 Coveralls, Inc