Passed
Pull Request — main (#105)
by Tom
02:54
created

ResolveCollectionFactory   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 189
Duplicated Lines 0 %

Importance

Changes 6
Bugs 2 Features 0
Metric Value
eloc 108
c 6
b 2
f 0
dl 0
loc 189
rs 9.68
wmc 34

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A get() 0 17 1
A parseValue() 0 6 1
A parseArrayValue() 0 7 2
B buildCriteria() 0 38 9
F buildPagination() 0 94 20
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\Type\Entity;
10
use ApiSkeletons\Doctrine\GraphQL\Type\TypeManager;
11
use Closure;
12
use Doctrine\Common\Collections\Criteria;
13
use Doctrine\Common\Util\ClassUtils;
14
use Doctrine\ORM\EntityManager;
15
use Doctrine\ORM\Mapping\ClassMetadata;
16
use Doctrine\ORM\PersistentCollection;
17
use GraphQL\Type\Definition\ResolveInfo;
18
19
use function base64_decode;
20
use function base64_encode;
21
use function count;
22
23
class ResolveCollectionFactory
24
{
25
    public function __construct(
26
        protected EntityManager $entityManager,
27
        protected Config $config,
28
        protected FieldResolver $fieldResolver,
29
        protected TypeManager $typeManager,
30
    ) {
31
    }
32
33
    public function parseValue(ClassMetadata $metadata, string $field, mixed $value): mixed
34
    {
35
        $fieldMapping = $metadata->getFieldMapping($field);
36
        $graphQLType  = $this->typeManager->get($fieldMapping['type']);
37
38
        return $graphQLType->parseValue($graphQLType->serialize($value));
39
    }
40
41
    /** @param mixed[] $value */
42
    public function parseArrayValue(ClassMetadata $metadata, string $field, array $value): mixed
43
    {
44
        foreach ($value as $key => $val) {
45
            $value[$key] = $this->parseValue($metadata, $field, $val);
46
        }
47
48
        return $value;
49
    }
50
51
    public function get(Entity $entity): Closure
52
    {
53
        return function ($source, $args, $context, ResolveInfo $resolveInfo) {
54
            $fieldResolver = $this->fieldResolver;
55
            $collection    = $fieldResolver($source, $args, $context, $resolveInfo);
56
57
            $collectionMetadata = $this->entityManager->getMetadataFactory()
58
                ->getMetadataFor(
59
                    (string) $this->entityManager->getMetadataFactory()
60
                        ->getMetadataFor(ClassUtils::getRealClass($source::class))
61
                        ->getAssociationTargetClass($resolveInfo->fieldName),
62
                );
63
64
            return $this->buildPagination(
65
                $args['pagination'] ?? [],
66
                $collection,
67
                $this->buildCriteria($args['filter'] ?? [], $collectionMetadata),
68
            );
69
        };
70
    }
71
72
    /** @param mixed[] $filter */
73
    private function buildCriteria(array $filter, ClassMetadata $collectionMetadata): Criteria
74
    {
75
        $orderBy  = [];
76
        $criteria = Criteria::create();
77
78
        foreach ($filter as $field => $filters) {
79
            foreach ($filters as $filter => $value) {
80
                switch ($filter) {
81
                    case FiltersDef::IN:
82
                    case FiltersDef::NOTIN:
83
                        $value = $this->parseArrayValue($collectionMetadata, $field, $value);
84
                        $criteria->andWhere($criteria->expr()->$filter($field, $value));
85
                        break;
86
                    case FiltersDef::ISNULL:
87
                        $criteria->andWhere($criteria->expr()->$filter($field));
88
                        break;
89
                    case FiltersDef::BETWEEN:
90
                        $value = $this->parseArrayValue($collectionMetadata, $field, $value);
91
92
                        $criteria->andWhere($criteria->expr()->gte($field, $value['from']));
93
                        $criteria->andWhere($criteria->expr()->lte($field, $value['to']));
94
                        break;
95
                    case FiltersDef::SORT:
96
                        $orderBy[$field] = $value;
97
                        break;
98
                    default:
99
                        $value = $this->parseValue($collectionMetadata, $field, $value);
100
                        $criteria->andWhere($criteria->expr()->$filter($field, $value));
101
                        break;
102
                }
103
            }
104
        }
105
106
        if (! empty($orderBy)) {
107
            $criteria->orderBy($orderBy);
108
        }
109
110
        return $criteria;
111
    }
112
113
    /**
114
     * @param mixed[] $pagination
115
     *
116
     * @return mixed[]
117
     */
118
    private function buildPagination(array $pagination, PersistentCollection $collection, Criteria $criteria): array
119
    {
120
        $first  = 0;
121
        $after  = 0;
122
        $last   = 0;
123
        $before = 0;
124
        $offset = 0;
125
126
        // Pagination
127
        foreach ($pagination as $field => $value) {
128
            switch ($field) {
129
                case 'first':
130
                    $first = $value;
131
                    break;
132
                case 'after':
133
                    $after = (int) base64_decode($value, true) + 1;
134
                    break;
135
                case 'last':
136
                    $last = $value;
137
                    break;
138
                case 'before':
139
                    $before = (int) base64_decode($value, true);
140
                    break;
141
            }
142
        }
143
144
        $limit         = $this->config->getLimit();
145
        $adjustedLimit = $first ?: $last ?: $limit;
146
        if ($adjustedLimit < $limit) {
147
            $limit = $adjustedLimit;
148
        }
149
150
        if ($after) {
151
            $offset = $after;
152
        } elseif ($before) {
153
            $offset = $before - $limit;
154
        }
155
156
        if ($offset < 0) {
157
            $limit += $offset;
158
            $offset = 0;
159
        }
160
161
        // Get total count from collection then match
162
        $itemCount = count($collection->matching($criteria));
163
164
        if ($last && ! $before) {
165
            $offset = $itemCount - $last;
166
        }
167
168
        if ($offset) {
169
            $criteria->setFirstResult($offset);
170
        }
171
172
        if ($limit) {
173
            $criteria->setMaxResults($limit);
174
        }
175
176
        // Fetch slice of collection
177
        $items = $collection->matching($criteria);
178
179
        $edges       = [];
180
        $index       = 0;
181
        $lastCursor  = base64_encode((string) 0);
182
        $firstCursor = null;
183
        foreach ($items as $result) {
184
            $cursor = base64_encode((string) ($index + $offset));
185
186
            $edges[] = [
187
                'node' => $result,
188
                'cursor' => $cursor,
189
            ];
190
191
            $lastCursor = $cursor;
192
            if (! $firstCursor) {
193
                $firstCursor = $cursor;
194
            }
195
196
            $index++;
197
        }
198
199
        $endCursor   = $itemCount ? $itemCount - 1 : 0;
200
        $startCursor = base64_encode((string) 0);
201
        $endCursor   = base64_encode((string) $endCursor);
202
203
        // Return entities
204
        return [
205
            'edges' => $edges,
206
            'totalCount' => $itemCount,
207
            'pageInfo' => [
208
                'endCursor' => $endCursor,
209
                'startCursor' => $startCursor,
210
                'hasNextPage' => $endCursor !== $lastCursor,
211
                'hasPreviousPage' => $firstCursor !== null && $startCursor !== $firstCursor,
212
            ],
213
        ];
214
    }
215
}
216