Passed
Pull Request — main (#39)
by Tom
02:16
created

ResolveCollectionFactory::parseArrayValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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