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

api-platform / core / 6067528200

04 Sep 2023 12:12AM UTC coverage: 36.875% (-21.9%) from 58.794%
6067528200

Pull #5791

github

web-flow
Merge 64157e578 into d09cfc9d2
Pull Request #5791: fix: strip down any sql function name

3096 of 3096 new or added lines in 205 files covered. (100.0%)

9926 of 26918 relevant lines covered (36.87%)

6.5 hits per line

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

1.11
/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
    }
18✔
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) :
×
101
                    $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext);
×
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

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

143
        if ($collection instanceof PaginatorInterface) {
×
144
            $totalItems = $collection->getTotalItems();
×
145

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

158
        $offset = 0 > $offset ? 0 : $offset;
×
159

160
        $data = $this->getDefaultCursorBasedPaginatedData();
×
161
        if ($totalItems > 0) {
×
162
            $data['pageInfo']['startCursor'] = base64_encode((string) $offset);
×
163
            $end = $offset + $nbPageItems - 1;
×
164
            $data['pageInfo']['endCursor'] = base64_encode((string) ($end >= 0 ? $end : 0));
×
165
            $data['pageInfo']['hasPreviousPage'] = $offset > 0;
×
166
            if ($collection instanceof PaginatorInterface) {
×
167
                $data['totalCount'] = $totalItems;
×
168
                $itemsPerPage = $collection->getItemsPerPage();
×
169
                $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems;
×
170
            }
171
        }
172

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

182
        return $data;
×
183
    }
184

185
    /**
186
     * @throws \LogicException
187
     */
188
    private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext): array
189
    {
190
        if (!($collection instanceof PaginatorInterface)) {
×
191
            throw new \LogicException(sprintf('Collection returned by the collection data provider must implement %s.', PaginatorInterface::class));
×
192
        }
193

194
        $data = $this->getDefaultPageBasedPaginatedData();
×
195
        $data['paginationInfo']['totalCount'] = $collection->getTotalItems();
×
196
        $data['paginationInfo']['lastPage'] = $collection->getLastPage();
×
197
        $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage();
×
198

199
        foreach ($collection as $object) {
×
200
            $data['collection'][] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext);
×
201
        }
202

203
        return $data;
×
204
    }
205

206
    private function getDefaultCursorBasedPaginatedData(): array
207
    {
208
        return ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]];
×
209
    }
210

211
    private function getDefaultPageBasedPaginatedData(): array
212
    {
213
        return ['collection' => [], 'paginationInfo' => ['itemsPerPage' => 0., 'totalCount' => 0., 'lastPage' => 0.]];
×
214
    }
215

216
    private function getDefaultMutationData(array $context): array
217
    {
218
        return ['clientMutationId' => $context['args']['input']['clientMutationId'] ?? null];
×
219
    }
220

221
    private function getDefaultSubscriptionData(array $context): array
222
    {
223
        return ['clientSubscriptionId' => $context['args']['input']['clientSubscriptionId'] ?? null];
×
224
    }
225
}
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