Passed
Pull Request — main (#88)
by Tom
02:51
created

ResolveCollectionFactory   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 128
c 2
b 0
f 0
dl 0
loc 221
rs 8.64
wmc 47

6 Methods

Rating   Name   Duplication   Size   Complexity  
D buildCriteria() 0 67 22
A get() 0 19 1
A __construct() 0 6 1
A parseValue() 0 7 1
F buildPagination() 0 94 20
A parseArrayValue() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like ResolveCollectionFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ResolveCollectionFactory, and based on these observations, apply Extract Interface, too.

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\Type\Entity;
9
use ApiSkeletons\Doctrine\GraphQL\Type\TypeManager;
10
use Closure;
11
use Doctrine\Common\Collections\Criteria;
12
use Doctrine\Common\Util\ClassUtils;
13
use Doctrine\ORM\EntityManager;
14
use Doctrine\ORM\Mapping\ClassMetadata;
15
use Doctrine\ORM\PersistentCollection;
16
use GraphQL\Type\Definition\ResolveInfo;
17
18
use function base64_decode;
19
use function base64_encode;
20
use function count;
21
use function strrpos;
22
use function substr;
23
24
class ResolveCollectionFactory
25
{
26
    public function __construct(
27
        protected EntityManager $entityManager,
28
        protected Config $config,
29
        protected FieldResolver $fieldResolver,
30
        protected TypeManager $typeManager,
31
    ) {
32
    }
33
34
    public function parseValue(ClassMetadata $metadata, string $field, mixed $value): mixed
35
    {
36
        /** @psalm-suppress UndefinedDocblockClass */
37
        $fieldMapping = $metadata->getFieldMapping($field);
38
        $graphQLType  = $this->typeManager->get($fieldMapping['type']);
39
40
        return $graphQLType->parseValue($graphQLType->serialize($value));
41
    }
42
43
    /** @param mixed[] $value */
44
    public function parseArrayValue(ClassMetadata $metadata, string $field, array $value): mixed
45
    {
46
        foreach ($value as $key => $val) {
47
            $value[$key] = $this->parseValue($metadata, $field, $val);
48
        }
49
50
        return $value;
51
    }
52
53
    public function get(Entity $entity): Closure
54
    {
55
        return function ($source, $args, $context, ResolveInfo $resolveInfo) {
56
            $filter = $args['filter'] ?? [];
57
58
            $fieldResolver = $this->fieldResolver;
59
            $collection    = $fieldResolver($source, $args, $context, $resolveInfo);
60
61
            $collectionMetadata = $this->entityManager->getMetadataFactory()
62
                ->getMetadataFor(
63
                    $this->entityManager->getMetadataFactory()
1 ignored issue
show
Bug introduced by
It seems like $this->entityManager->ge...resolveInfo->fieldName) can also be of type null; however, parameter $className of Doctrine\Persistence\Map...ctory::getMetadataFor() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

63
                    /** @scrutinizer ignore-type */ $this->entityManager->getMetadataFactory()
Loading history...
64
                        ->getMetadataFor(ClassUtils::getRealClass($source::class))
65
                        ->getAssociationTargetClass($resolveInfo->fieldName),
66
                );
67
68
            return $this->buildPagination(
69
                $filter,
70
                $collection,
71
                $this->buildCriteria($filter, $collectionMetadata),
72
            );
73
        };
74
    }
75
76
    /** @param mixed[] $filter */
77
    private function buildCriteria(array $filter, ClassMetadata $collectionMetadata): Criteria
78
    {
79
        $orderBy  = [];
80
        $criteria = Criteria::create();
81
82
        foreach ($filter as $field => $value) {
83
            // Pagination is handled elsewhere
84
            switch ($field) {
85
                case '_first':
86
                case '_after':
87
                case '_last':
88
                case '_before':
89
                    continue 2;
90
            }
91
92
            // Handle other fields as $field_$type: $value
93
            // Get right-most _text
94
            $filter = substr($field, strrpos($field, '_') + 1);
95
96
            if (strrpos($field, '_') === false) {
97
                // Special case for eq `field: value`
98
                $value = $this->parseValue($collectionMetadata, $field, $value);
99
                $criteria->andWhere($criteria->expr()->eq($field, $value));
100
            } else {
101
                $field = substr($field, 0, strrpos($field, '_'));
102
103
                // Format value type - this seems like something which should
104
                // be done in GraphQL.
105
                switch ($filter) {
106
                    case 'eq':
107
                    case 'neq':
108
                    case 'lt':
109
                    case 'lte':
110
                    case 'gt':
111
                    case 'gte':
112
                    case 'contains':
113
                    case 'startswith':
114
                    case 'endswith':
115
                        $value = $this->parseValue($collectionMetadata, $field, $value);
116
                        $criteria->andWhere($criteria->expr()->$filter($field, $value));
117
                        break;
118
                    case 'in':
119
                    case 'notin':
120
                        $value = $this->parseArrayValue($collectionMetadata, $field, $value);
121
                        $criteria->andWhere($criteria->expr()->$filter($field, $value));
122
                        break;
123
                    case 'isnull':
124
                        $criteria->andWhere($criteria->expr()->$filter($field));
125
                        break;
126
                    case 'between':
127
                        $value = $this->parseArrayValue($collectionMetadata, $field, $value);
128
129
                        $criteria->andWhere($criteria->expr()->gte($field, $value['from']));
130
                        $criteria->andWhere($criteria->expr()->lte($field, $value['to']));
131
                        break;
132
                    case 'sort':
133
                        $orderBy[$field] = $value;
134
                        break;
135
                }
136
            }
137
        }
138
139
        if (! empty($orderBy)) {
140
            $criteria->orderBy($orderBy);
141
        }
142
143
        return $criteria;
144
    }
145
146
    /**
147
     * @param mixed[] $filter
148
     *
149
     * @return mixed[]
150
     */
151
    private function buildPagination(array $filter, PersistentCollection $collection, Criteria $criteria): array
152
    {
153
        $first  = 0;
154
        $after  = 0;
155
        $last   = 0;
156
        $before = 0;
157
        $offset = 0;
158
159
        // Pagination
160
        foreach ($filter as $field => $value) {
161
            switch ($field) {
162
                case '_first':
163
                    $first = $value;
164
                    break;
165
                case '_after':
166
                    $after = (int) base64_decode($value, true) + 1;
167
                    break;
168
                case '_last':
169
                    $last = $value;
170
                    break;
171
                case '_before':
172
                    $before = (int) base64_decode($value, true);
173
                    break;
174
            }
175
        }
176
177
        $limit         = $this->config->getLimit();
178
        $adjustedLimit = $first ?: $last ?: $limit;
179
        if ($adjustedLimit < $limit) {
180
            $limit = $adjustedLimit;
181
        }
182
183
        if ($after) {
184
            $offset = $after;
185
        } elseif ($before) {
186
            $offset = $before - $limit;
187
        }
188
189
        if ($offset < 0) {
190
            $limit += $offset;
191
            $offset = 0;
192
        }
193
194
        // Get total count from collection then match
195
        $itemCount = count($collection->matching($criteria));
196
197
        if ($last && ! $before) {
198
            $offset = $itemCount - $last;
199
        }
200
201
        if ($offset) {
202
            $criteria->setFirstResult($offset);
203
        }
204
205
        if ($limit) {
206
            $criteria->setMaxResults($limit);
207
        }
208
209
        // Fetch slice of collection
210
        $items = $collection->matching($criteria);
211
212
        $edges       = [];
213
        $index       = 0;
214
        $lastCursor  = base64_encode((string) 0);
215
        $firstCursor = null;
216
        foreach ($items as $result) {
217
            $cursor = base64_encode((string) ($index + $offset));
218
219
            $edges[] = [
220
                'node' => $result,
221
                'cursor' => $cursor,
222
            ];
223
224
            $lastCursor = $cursor;
225
            if (! $firstCursor) {
226
                $firstCursor = $cursor;
227
            }
228
229
            $index++;
230
        }
231
232
        $endCursor   = $itemCount ? $itemCount - 1 : 0;
233
        $startCursor = base64_encode((string) 0);
234
        $endCursor   = base64_encode((string) $endCursor);
235
236
        // Return entities
237
        return [
238
            'edges' => $edges,
239
            'totalCount' => $itemCount,
240
            'pageInfo' => [
241
                'endCursor' => $endCursor,
242
                'startCursor' => $startCursor,
243
                'hasNextPage' => $endCursor !== $lastCursor,
244
                'hasPreviousPage' => $firstCursor !== null && $startCursor !== $firstCursor,
245
            ],
246
        ];
247
    }
248
}
249