ResolveCollectionFactory::buildPagination()   B
last analyzed

Complexity

Conditions 8
Paths 80

Size

Total Lines 67
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 34
nc 80
nop 7
dl 0
loc 67
rs 8.1315
c 1
b 0
f 0

How to fix   Long Method   

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\ORM\GraphQL\Resolve;
6
7
use ApiSkeletons\Doctrine\ORM\GraphQL\Config;
8
use ApiSkeletons\Doctrine\ORM\GraphQL\Event\Criteria as CriteriaEvent;
9
use ApiSkeletons\Doctrine\ORM\GraphQL\Filter\Filters;
10
use ApiSkeletons\Doctrine\ORM\GraphQL\Type\Entity\Entity;
11
use ApiSkeletons\Doctrine\ORM\GraphQL\Type\Entity\EntityTypeContainer;
12
use ApiSkeletons\Doctrine\ORM\GraphQL\Type\TypeContainer;
13
use ArrayObject;
14
use Closure;
15
use Doctrine\Common\Collections\Collection;
16
use Doctrine\Common\Collections\Criteria;
17
use Doctrine\ORM\EntityManager;
18
use Doctrine\ORM\PersistentCollection;
19
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
20
use GraphQL\Type\Definition\ResolveInfo;
21
use League\Event\EventDispatcher;
22
23
use function array_flip;
24
use function base64_decode;
25
use function base64_encode;
26
use function count;
27
use function in_array;
28
29
/**
30
 * Build a resolver for collections
31
 */
32
class ResolveCollectionFactory
33
{
34
    public function __construct(
35
        protected readonly EntityManager $entityManager,
36
        protected readonly Config $config,
37
        protected readonly FieldResolver $fieldResolver,
38
        protected readonly TypeContainer $typeContainer,
39
        protected readonly EntityTypeContainer $entityTypeContainer,
40
        protected readonly EventDispatcher $eventDispatcher,
41
        protected readonly ArrayObject $metadata,
42
    ) {
43
    }
44
45
    public function get(Entity $entity): Closure
46
    {
47
        return function ($source, array $args, $context, ResolveInfo $info) use ($entity) {
48
            $fieldResolver = $this->fieldResolver;
49
            $collection    = $fieldResolver($source, $args, $context, $info);
50
51
            $defaultProxyClassNameResolver = new DefaultProxyClassNameResolver();
52
            $entityClassName               = $defaultProxyClassNameResolver->getClass($source);
53
54
            // If an alias map exists, check for an alias
55
            $targetCollectionName = $info->fieldName;
56
            if (in_array($info->fieldName, $this->entityTypeContainer->get($entityClassName)->getExtractionMap())) {
57
                $targetCollectionName = array_flip($this->entityTypeContainer
58
                    ->get($entityClassName)->getExtractionMap())[$info->fieldName] ?? $info->fieldName;
59
            }
60
61
            $targetClassName = (string) $this->entityManager->getMetadataFactory()
62
                ->getMetadataFor($entityClassName)
63
                ->getAssociationTargetClass($targetCollectionName);
64
65
            return $this->buildPagination(
66
                $entityClassName,
67
                $targetClassName,
68
                $args['pagination'] ?? [],
69
                $collection,
70
                $this->buildCriteria($args['filter'] ?? [], $entity),
71
                $this->metadata[$entityClassName]['fields'][$targetCollectionName]['criteriaEventName'],
72
                $source,
73
                $args,
74
                $context,
75
                $info,
76
            );
77
        };
78
    }
79
80
    /** @param mixed[] $filter */
81
    protected function buildCriteria(array $filter, Entity $entity): Criteria
82
    {
83
        $orderBy  = [];
84
        $criteria = Criteria::create();
85
86
        foreach ($filter as $field => $filters) {
87
            // Resolve aliases
88
            $field = array_flip($entity->getExtractionMap())[$field] ?? $field;
89
90
            foreach ($filters as $filter => $value) {
91
                switch (Filters::from($filter)) {
92
                    case Filters::ISNULL:
93
                        $criteria->andWhere($criteria->expr()->$filter($field));
94
                        break;
95
                    case Filters::BETWEEN:
96
                        $criteria->andWhere($criteria->expr()->gte($field, $value['from']));
97
                        $criteria->andWhere($criteria->expr()->lte($field, $value['to']));
98
                        break;
99
                    case Filters::SORT:
100
                        $orderBy[$field] = $value;
101
                        break;
102
                    default:
103
                        $criteria->andWhere($criteria->expr()->$filter($field, $value));
104
                        break;
105
                }
106
            }
107
        }
108
109
        if (! empty($orderBy)) {
110
            $criteria->orderBy($orderBy);
111
        }
112
113
        return $criteria;
114
    }
115
116
    /**
117
     * @param mixed[] $pagination
118
     *
119
     * @return mixed[]
120
     */
121
    protected function buildPagination(
122
        string $entityClassName,
123
        string $targetClassName,
124
        array $pagination,
125
        PersistentCollection $collection,
126
        Criteria $criteria,
127
        string|null $criteriaEventName,
128
        mixed ...$resolve,
129
    ): array {
130
        $paginationFields = [
131
            'first' => 0,
132
            'last' => 0,
133
            'after' => 0,
134
            'before' => 0,
135
        ];
136
137
        // Pagination
138
        foreach ($pagination as $field => $value) {
139
            $paginationFields[$field] = $value;
140
141
            if ($field === 'after') {
142
                $paginationFields[$field] = (int) base64_decode($value, true) + 1;
143
            }
144
145
            if ($field !== 'before') {
146
                continue;
147
            }
148
149
            $paginationFields[$field] = (int) base64_decode($value, true);
150
        }
151
152
        $itemCount = count($collection->matching($criteria));
153
154
        $offsetAndLimit = $this->calculateOffsetAndLimit($resolve[3]->fieldName, $entityClassName, $targetClassName, $paginationFields, $itemCount);
155
        if ($offsetAndLimit['offset']) {
156
            $criteria->setFirstResult($offsetAndLimit['offset']);
157
        }
158
159
        if ($offsetAndLimit['limit']) {
160
            $criteria->setMaxResults($offsetAndLimit['limit']);
161
        }
162
163
        /**
164
         * Fire the event dispatcher using the passed event name.
165
         */
166
        if ($criteriaEventName) {
167
            $this->eventDispatcher->dispatch(
168
                new CriteriaEvent(
169
                    $criteria,
170
                    $criteriaEventName,
171
                    ...$resolve,
172
                ),
173
            );
174
        }
175
176
        $edgesAndCursors = $this->buildEdgesAndCursors($collection->matching($criteria), $offsetAndLimit, $itemCount);
177
178
        // Return entities
179
        return [
180
            'edges' => $edgesAndCursors['edges'],
181
            'totalCount' => $itemCount,
182
            'pageInfo' => [
183
                'endCursor' => $edgesAndCursors['cursors']['end'],
184
                'startCursor' => $edgesAndCursors['cursors']['start'],
185
                'hasNextPage' => $edgesAndCursors['cursors']['end'] !== $edgesAndCursors['cursors']['last'],
186
                'hasPreviousPage' => $edgesAndCursors['cursors']['first'] !== null
187
                    && $edgesAndCursors['cursors']['start'] !== $edgesAndCursors['cursors']['first'],
188
            ],
189
        ];
190
    }
191
192
    /**
193
     * @param array<string, int>     $offsetAndLimit
194
     * @param Collection<int, mixed> $items
195
     *
196
     * @return array<string, mixed>
197
     */
198
    protected function buildEdgesAndCursors(Collection $items, array $offsetAndLimit, int $itemCount): array
199
    {
200
        $edges   = [];
201
        $index   = 0;
202
        $cursors = [
203
            'first' => null,
204
            'last'  => base64_encode((string) 0),
205
            'start' => base64_encode((string) 0),
206
        ];
207
208
        foreach ($items as $item) {
209
            $cursors['last'] = base64_encode((string) ($index + $offsetAndLimit['offset']));
210
211
            $edges[] = [
212
                'node' => $item,
213
                'cursor' => $cursors['last'],
214
            ];
215
216
            if (! $cursors['first']) {
217
                $cursors['first'] = $cursors['last'];
218
            }
219
220
            $index++;
221
        }
222
223
        $endIndex       = $itemCount ? $itemCount - 1 : 0;
224
        $cursors['end'] = base64_encode((string) $endIndex);
225
226
        return [
227
            'cursors' => $cursors,
228
            'edges'   => $edges,
229
        ];
230
    }
231
232
    /**
233
     * @param array<string, int> $paginationFields
234
     *
235
     * @return array<string, int>
236
     */
237
    protected function calculateOffsetAndLimit(
238
        string $associationName,
239
        string $entityClassName,
240
        string $targetClassName,
241
        array $paginationFields,
242
        int $itemCount,
243
    ): array {
244
        $offset = 0;
245
246
        $limit            = $this->metadata[$targetClassName]['limit'];
247
        $associationLimit = $this->metadata[$entityClassName]['fields'][$associationName]['limit'] ?? null;
248
249
        if ($associationLimit) {
250
            $limit = $associationLimit;
251
        }
252
253
        if (! $limit) {
254
            $limit = $this->config->getLimit();
255
        }
256
257
        $adjustedLimit = $paginationFields['first'] ?: $paginationFields['last'] ?: $limit;
258
259
        if ($adjustedLimit < $limit) {
260
            $limit = $adjustedLimit;
261
        }
262
263
        if ($paginationFields['after']) {
264
            $offset = $paginationFields['after'];
265
        } elseif ($paginationFields['before']) {
266
            $offset = $paginationFields['before'] - $limit;
267
        }
268
269
        if ($offset < 0) {
270
            $limit += $offset;
271
            $offset = 0;
272
        }
273
274
        if ($paginationFields['last'] && ! $paginationFields['before']) {
275
            $offset = $itemCount - $paginationFields['last'];
276
        }
277
278
        return [
279
            'offset' => $offset,
280
            'limit'  => $limit,
281
        ];
282
    }
283
}
284