Passed
Pull Request — main (#126)
by Tom
05:09 queued 02:17
created

ResolveCollectionFactory::buildPagination()   D

Complexity

Conditions 11
Paths 384

Size

Total Lines 90
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 54
c 2
b 0
f 0
nc 384
nop 5
dl 0
loc 90
rs 4.3503

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace ApiSkeletons\Doctrine\GraphQL\Resolve;
6
7
use ApiSkeletons\Doctrine\GraphQL\Config;
8
use ApiSkeletons\Doctrine\GraphQL\Criteria\Filters as FiltersDef;
9
use ApiSkeletons\Doctrine\GraphQL\Event\FilterCriteria;
10
use ApiSkeletons\Doctrine\GraphQL\Metadata\Metadata;
11
use ApiSkeletons\Doctrine\GraphQL\Type\Entity;
12
use ApiSkeletons\Doctrine\GraphQL\Type\TypeManager;
13
use Closure;
14
use Doctrine\Common\Collections\Criteria;
15
use Doctrine\Common\Util\ClassUtils;
16
use Doctrine\ORM\EntityManager;
17
use Doctrine\ORM\Mapping\ClassMetadata;
18
use Doctrine\ORM\PersistentCollection;
19
use GraphQL\Type\Definition\ResolveInfo;
20
use League\Event\EventDispatcher;
21
22
use function base64_decode;
23
use function base64_encode;
24
use function count;
25
26
class ResolveCollectionFactory
27
{
28
    public function __construct(
29
        protected EntityManager $entityManager,
30
        protected Config $config,
31
        protected FieldResolver $fieldResolver,
32
        protected TypeManager $typeManager,
33
        protected EventDispatcher $eventDispatcher,
34
        protected Metadata $metadata,
35
    ) {
36
    }
37
38
    public function parseValue(ClassMetadata $metadata, string $field, mixed $value): mixed
39
    {
40
        /** @psalm-suppress UndefinedDocblockClass */
41
        $fieldMapping = $metadata->getFieldMapping($field);
42
        $graphQLType  = $this->typeManager->get($fieldMapping['type']);
43
44
        return $graphQLType->parseValue($graphQLType->serialize($value));
45
    }
46
47
    /** @param mixed[] $value */
48
    public function parseArrayValue(ClassMetadata $metadata, string $field, array $value): mixed
49
    {
50
        foreach ($value as $key => $val) {
51
            $value[$key] = $this->parseValue($metadata, $field, $val);
52
        }
53
54
        return $value;
55
    }
56
57
    public function get(Entity $entity): Closure
58
    {
59
        return function ($source, array $args, $context, ResolveInfo $info) {
60
            $fieldResolver = $this->fieldResolver;
61
            $collection    = $fieldResolver($source, $args, $context, $info);
62
63
            $collectionMetadata = $this->entityManager->getMetadataFactory()
64
                ->getMetadataFor(
65
                    (string) $this->entityManager->getMetadataFactory()
66
                        ->getMetadataFor(ClassUtils::getRealClass($source::class))
67
                        ->getAssociationTargetClass($info->fieldName),
68
                );
69
70
            $metadataConfig = $this->metadata->getMetadataConfig();
71
            $entityClass    = ClassUtils::getRealClass($source::class);
72
73
            return $this->buildPagination(
74
                $args['pagination'] ?? [],
75
                $collection,
76
                $this->buildCriteria($args['filter'] ?? [], $collectionMetadata),
77
                $metadataConfig[$entityClass]['fields'][$info->fieldName]['filterCriteriaEventName'],
78
                $source,
79
                $args,
80
                $context,
81
                $info,
82
            );
83
        };
84
    }
85
86
    /** @param mixed[] $filter */
87
    private function buildCriteria(array $filter, ClassMetadata $collectionMetadata): Criteria
88
    {
89
        $orderBy  = [];
90
        $criteria = Criteria::create();
91
92
        foreach ($filter as $field => $filters) {
93
            foreach ($filters as $filter => $value) {
94
                switch ($filter) {
95
                    case FiltersDef::IN:
96
                    case FiltersDef::NOTIN:
97
                        $value = $this->parseArrayValue($collectionMetadata, $field, $value);
98
                        $criteria->andWhere($criteria->expr()->$filter($field, $value));
99
                        break;
100
                    case FiltersDef::ISNULL:
101
                        $criteria->andWhere($criteria->expr()->$filter($field));
102
                        break;
103
                    case FiltersDef::BETWEEN:
104
                        $value = $this->parseArrayValue($collectionMetadata, $field, $value);
105
106
                        $criteria->andWhere($criteria->expr()->gte($field, $value['from']));
107
                        $criteria->andWhere($criteria->expr()->lte($field, $value['to']));
108
                        break;
109
                    case FiltersDef::SORT:
110
                        $orderBy[$field] = $value;
111
                        break;
112
                    default:
113
                        $value = $this->parseValue($collectionMetadata, $field, $value);
114
                        $criteria->andWhere($criteria->expr()->$filter($field, $value));
115
                        break;
116
                }
117
            }
118
        }
119
120
        if (! empty($orderBy)) {
121
            $criteria->orderBy($orderBy);
122
        }
123
124
        return $criteria;
125
    }
126
127
    /**
128
     * @param mixed[] $pagination
129
     *
130
     * @return mixed[]
131
     */
132
    private function buildPagination(
133
        array $pagination,
134
        PersistentCollection $collection,
135
        Criteria $criteria,
136
        string|null $filterCriteriaEventName,
137
        mixed ...$resolve,
138
    ): array {
139
        $paginationFields = [
140
            'first' => 0,
141
            'last' => 0,
142
            'after' => 0,
143
            'before' => 0,
144
        ];
145
146
        // Pagination
147
        foreach ($pagination as $field => $value) {
148
            switch ($field) {
149
                case 'after':
150
                    $paginationFields[$field] = (int) base64_decode($value, true) + 1;
151
                    break;
152
                case 'before':
153
                    $paginationFields[$field] = (int) base64_decode($value, true);
154
                    break;
155
                default:
156
                    $paginationFields[$field] = $value;
157
                    $first                    = $value;
0 ignored issues
show
Unused Code introduced by
The assignment to $first is dead and can be removed.
Loading history...
158
                    break;
159
            }
160
        }
161
162
        $itemCount = count($collection->matching($criteria));
163
164
        $offsetAndLimit = $this->calculateOffsetAndLimit($paginationFields, $itemCount);
165
        if ($offsetAndLimit['offset']) {
166
            $criteria->setFirstResult($offsetAndLimit['offset']);
167
        }
168
169
        if ($offsetAndLimit['limit']) {
170
            $criteria->setMaxResults($offsetAndLimit['limit']);
171
        }
172
173
        /**
174
         * Fire the event dispatcher using the passed event name.
175
         */
176
        if ($filterCriteriaEventName) {
177
            $this->eventDispatcher->dispatch(
178
                new FilterCriteria(
179
                    $criteria,
180
                    $filterCriteriaEventName,
181
                    ...$resolve,
182
                ),
183
            );
184
        }
185
186
        // Fetch slice of collection
187
        $items = $collection->matching($criteria);
188
189
        $edges       = [];
190
        $index       = 0;
191
        $lastCursor  = base64_encode((string) 0);
192
        $firstCursor = null;
193
        foreach ($items as $item) {
194
            $cursor = base64_encode((string) ($index + $offsetAndLimit['offset']));
195
196
            $edges[] = [
197
                'node' => $item,
198
                'cursor' => $cursor,
199
            ];
200
201
            $lastCursor = $cursor;
202
            if (! $firstCursor) {
203
                $firstCursor = $cursor;
204
            }
205
206
            $index++;
207
        }
208
209
        $endCursor   = $itemCount ? $itemCount - 1 : 0;
210
        $startCursor = base64_encode((string) 0);
211
        $endCursor   = base64_encode((string) $endCursor);
212
213
        // Return entities
214
        return [
215
            'edges' => $edges,
216
            'totalCount' => $itemCount,
217
            'pageInfo' => [
218
                'endCursor' => $endCursor,
219
                'startCursor' => $startCursor,
220
                'hasNextPage' => $endCursor !== $lastCursor,
221
                'hasPreviousPage' => $firstCursor !== null && $startCursor !== $firstCursor,
222
            ],
223
        ];
224
    }
225
226
    /**
227
     * @param array<string, int> $paginationFields
228
     *
229
     * @return array<string, int>
230
     */
231
    protected function calculateOffsetAndLimit(array $paginationFields, int $itemCount): array
232
    {
233
        $offset = 0;
234
235
        $limit         = $this->config->getLimit();
236
        $adjustedLimit = $paginationFields['first'] ?: $paginationFields['last'] ?: $limit;
237
238
        if ($adjustedLimit < $limit) {
239
            $limit = $adjustedLimit;
240
        }
241
242
        if ($paginationFields['after']) {
243
            $offset = $paginationFields['after'];
244
        } elseif ($paginationFields['before']) {
245
            $offset = $paginationFields['before'] - $limit;
246
        }
247
248
        if ($offset < 0) {
249
            $limit += $offset;
250
            $offset = 0;
251
        }
252
253
        if ($paginationFields['last'] && ! $paginationFields['before']) {
254
            $offset = $itemCount - $paginationFields['last'];
255
        }
256
257
        return [
258
            'offset' => $offset,
259
            'limit'  => $limit,
260
        ];
261
    }
262
}
263