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

api-platform / core / 3713134090

pending completion
3713134090

Pull #5254

github

GitHub
Merge b2ec54b3c into ac711530f
Pull Request #5254: [OpenApi] Add ApiResource::openapi and deprecate openapiContext

197 of 197 new or added lines in 5 files covered. (100.0%)

10372 of 12438 relevant lines covered (83.39%)

11.97 hits per line

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

87.78
/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
    {
39
    }
28✔
40

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

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

57
                return [];
1✔
58
            }
59

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

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

68
            return null;
1✔
69
        }
70

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

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

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

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

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

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

105
        return $data;
9✔
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'];
12✔
115

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

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

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

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

146
        $offset = 0 > $offset ? 0 : $offset;
7✔
147

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

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

170
        return $data;
7✔
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]];
8✔
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];
3✔
207
    }
208

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