Passed
Pull Request — main (#126)
by Tom
02:58
created

ResolveCollectionFactory::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
c 0
b 0
f 0
nc 1
nop 6
dl 0
loc 8
rs 10
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...
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 20 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
158
                    break;
159
            }
160
        }
161
162
        $itemCount = count($collection->matching($criteria));
163
164
        $offsetAndLimit = $this->calculateOffsetAndLimit($paginationFields, $itemCount);
165
        if ($offsetAndLimit['offset']) {
0 ignored issues
show
introduced by
Expected 1 line after "if", found 0.
Loading history...
166
            $criteria->setFirstResult($offsetAndLimit['offset']);
167
        }
168
        if ($offsetAndLimit['limit']) {
169
            $criteria->setMaxResults($offsetAndLimit['limit']);
170
        }
171
172
        /**
173
         * Fire the event dispatcher using the passed event name.
174
         */
175
        if ($filterCriteriaEventName) {
176
            $this->eventDispatcher->dispatch(
177
                new FilterCriteria(
178
                    $criteria,
179
                    $filterCriteriaEventName,
180
                    ...$resolve,
181
                ),
182
            );
183
        }
184
185
        // Fetch slice of collection
186
        $items = $collection->matching($criteria);
187
188
        $edges       = [];
189
        $index       = 0;
190
        $lastCursor  = base64_encode((string) 0);
191
        $firstCursor = null;
192
        foreach ($items as $item) {
193
            $cursor = base64_encode((string) ($index + $offsetAndLimit['offset']));
194
195
            $edges[] = [
196
                'node' => $item,
197
                'cursor' => $cursor,
198
            ];
199
200
            $lastCursor = $cursor;
201
            if (! $firstCursor) {
202
                $firstCursor = $cursor;
203
            }
204
205
            $index++;
206
        }
207
208
        $endCursor   = $itemCount ? $itemCount - 1 : 0;
209
        $startCursor = base64_encode((string) 0);
210
        $endCursor   = base64_encode((string) $endCursor);
211
212
        // Return entities
213
        return [
214
            'edges' => $edges,
215
            'totalCount' => $itemCount,
216
            'pageInfo' => [
217
                'endCursor' => $endCursor,
218
                'startCursor' => $startCursor,
219
                'hasNextPage' => $endCursor !== $lastCursor,
220
                'hasPreviousPage' => $firstCursor !== null && $startCursor !== $firstCursor,
221
            ],
222
        ];
223
    }
224
225
    /**
226
     * @param array<string, int> $paginationFields
227
     *
228
     * @return array<string, int>
229
     */
230
    protected function calculateOffsetAndLimit(array $paginationFields, int $itemCount): array
231
    {
232
        $offset = 0;
233
234
        $limit         = $this->config->getLimit();
235
        $adjustedLimit = $paginationFields['first'] ?: $paginationFields['last'] ?: $limit;
236
237
        if ($adjustedLimit < $limit) {
238
            $limit = $adjustedLimit;
239
        }
240
241
        if ($paginationFields['after']) {
242
            $offset = $paginationFields['after'];
243
        } elseif ($paginationFields['before']) {
244
            $offset = $paginationFields['before'] - $limit;
245
        }
246
247
        if ($offset < 0) {
248
            $limit += $offset;
249
            $offset = 0;
250
        }
251
252
        if ($paginationFields['last'] && ! $paginationFields['before']) {
253
            $offset = $itemCount - $paginationFields['last'];
254
        }
255
256
        return [
257
            'offset' => $offset,
258
            'limit'  => $limit,
259
        ];
260
    }
261
}
262