Passed
Pull Request — main (#89)
by Tom
03:10
created

ResolveEntityFactory   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 219
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 1
Metric Value
eloc 128
c 5
b 0
f 1
dl 0
loc 219
rs 9.52
wmc 36

4 Methods

Rating   Name   Duplication   Size   Complexity  
C buildFilterArray() 0 55 14
A get() 0 21 1
F buildPagination() 0 116 20
A __construct() 0 5 1
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\Event\FilterQueryBuilder;
9
use ApiSkeletons\Doctrine\GraphQL\Type\Entity;
10
use ApiSkeletons\Doctrine\QueryBuilder\Filter\Applicator;
11
use Closure;
12
use Doctrine\ORM\EntityManager;
13
use Doctrine\ORM\QueryBuilder;
14
use Doctrine\ORM\Tools\Pagination\Paginator;
15
use GraphQL\Type\Definition\ResolveInfo;
16
use League\Event\EventDispatcher;
17
18
use function base64_decode;
19
use function base64_encode;
20
use function implode;
21
use function strrpos;
22
use function substr;
23
24
class ResolveEntityFactory
25
{
26
    public function __construct(
27
        protected Config $config,
28
        protected EntityManager $entityManager,
29
        protected EventDispatcher $eventDispatcher,
30
    ) {
31
    }
32
33
    public function get(Entity $entity, string $eventName): Closure
34
    {
35
        return function ($objectValue, array $args, $context, ResolveInfo $info) use ($entity, $eventName) {
36
            $entityClass = $entity->getEntityClass();
37
            // Resolve top level filters
38
            $filterTypes = $args['filter'] ?? [];
39
40
            $queryBuilderFilter = (new Applicator($this->entityManager, $entityClass))
41
                ->setEntityAlias('entity');
42
            $queryBuilder       = $queryBuilderFilter($this->buildFilterArray($filterTypes))
43
                ->select('entity');
44
45
            return $this->buildPagination(
46
                filterTypes: $filterTypes,
47
                queryBuilder: $queryBuilder,
48
                aliasMap: $queryBuilderFilter->getEntityAliasMap(),
49
                eventName: $eventName,
50
                objectValue: $objectValue,
51
                args: $args,
52
                context: $context,
53
                info: $info,
54
            );
55
        };
56
    }
57
58
    /**
59
     * @param string[] $filterTypes
60
     *
61
     * @return mixed[]
62
     */
63
    private function buildFilterArray(array $filterTypes): array
64
    {
65
        $filterArray = [];
66
67
        foreach ($filterTypes as $field => $value) {
68
            // Pagination is handled elsewhere
69
            switch ($field) {
70
                case '_first':
71
                case '_after':
72
                case '_last':
73
                case '_before':
74
                    continue 2;
75
            }
76
77
            // Handle other fields as $field_$type: $value
78
            // Get right-most _text
79
            $filter = substr($field, strrpos($field, '_') + 1);
80
81
            // Special case for eq `field: value`
82
            if (strrpos($field, '_') === false) {
83
                // Handle field:value
84
                $filterArray[$field . '|eq'] = (string) $value;
85
            } else {
86
                $field = substr($field, 0, strrpos($field, '_'));
87
88
                switch ($filter) {
89
                    case 'contains':
90
                        $filterArray[$field . '|like'] = $value;
91
                        break;
92
                    case 'startswith':
93
                        $filterArray[$field . '|startswith'] = $value;
94
                        break;
95
                    case 'endswith':
96
                        $filterArray[$field . '|endswith'] = $value;
97
                        break;
98
                    case 'isnull':
99
                        $filterArray[$field . '|isnull'] = 'true';
100
                        break;
101
                    case 'between':
102
                        $filterArray[$field . '|between'] = $value['from'] . ',' . $value['to'];
103
                        break;
104
                    case 'in':
105
                        $filterArray[$field . '|in'] = implode(',', $value);
0 ignored issues
show
Bug introduced by
$value of type string is incompatible with the type array expected by parameter $pieces of implode(). ( Ignorable by Annotation )

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

105
                        $filterArray[$field . '|in'] = implode(',', /** @scrutinizer ignore-type */ $value);
Loading history...
106
                        break;
107
                    case 'notin':
108
                        $filterArray[$field . '|notin'] = implode(',', $value);
109
                        break;
110
                    default:
111
                        $filterArray[$field . '|' . $filter] = (string) $value;
112
                        break;
113
                }
114
            }
115
        }
116
117
        return $filterArray;
118
    }
119
120
    /**
121
     * @param string[] $filterTypes
122
     * @param mixed[]  $aliasMap
123
     * @param mixed[]  $args
124
     *
125
     * @return mixed[]
126
     */
127
    public function buildPagination(
128
        array $filterTypes,
129
        QueryBuilder $queryBuilder,
130
        array $aliasMap,
131
        string $eventName,
132
        mixed $objectValue,
133
        array $args,
134
        mixed $context,
135
        ResolveInfo $info,
136
    ): array {
137
        $first  = 0;
138
        $after  = 0;
139
        $last   = 0;
140
        $before = 0;
141
        $offset = 0;
142
143
        foreach ($filterTypes as $field => $value) {
144
            switch ($field) {
145
                case '_first':
146
                    $first = $value;
147
                    break;
148
                case '_after':
149
                    $after = (int) base64_decode($value, true) + 1;
150
                    break;
151
                case '_last':
152
                    $last = $value;
153
                    break;
154
                case '_before':
155
                    $before = (int) base64_decode($value, true);
156
                    break;
157
            }
158
        }
159
160
        $limit         = $this->config->getLimit();
161
        $adjustedLimit = $first ?: $last ?: $limit;
162
        if ($adjustedLimit < $limit) {
163
            $limit = $adjustedLimit;
164
        }
165
166
        if ($after) {
167
            $offset = $after;
168
        } elseif ($before) {
169
            $offset = $before - $limit;
170
        }
171
172
        if ($offset < 0) {
173
            $limit += $offset;
174
            $offset = 0;
175
        }
176
177
        if ($offset) {
178
            $queryBuilder->setFirstResult($offset);
179
        }
180
181
        if ($limit) {
182
            $queryBuilder->setMaxResults($limit);
0 ignored issues
show
Bug introduced by
It seems like $limit can also be of type string; however, parameter $maxResults of Doctrine\ORM\QueryBuilder::setMaxResults() does only seem to accept integer|null, 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

182
            $queryBuilder->setMaxResults(/** @scrutinizer ignore-type */ $limit);
Loading history...
183
        }
184
185
        /**
186
         * Fire the event dispatcher using the passed event name.
187
         * Include all resolve variables.
188
         */
189
190
        $this->eventDispatcher->dispatch(
191
            new FilterQueryBuilder(
192
                queryBuilder: $queryBuilder,
193
                entityAliasMap: $aliasMap,
194
                eventName: $eventName,
195
                objectValue: $objectValue,
196
                args: $args,
197
                context: $context,
198
                info: $info,
199
            ),
200
        );
201
202
        $paginator = new Paginator($queryBuilder->getQuery());
203
        $itemCount = $paginator->count();
204
205
        if ($last && ! $before) {
206
            $offset = $itemCount - $last;
207
            $queryBuilder->setFirstResult($offset);
208
            $paginator = new Paginator($queryBuilder->getQuery());
209
        }
210
211
        $edges       = [];
212
        $index       = 0;
213
        $lastCursor  = base64_encode((string) 0);
214
        $firstCursor = null;
215
        foreach ($paginator->getQuery()->getResult() as $result) {
216
            $cursor = base64_encode((string) ($index + $offset));
217
218
            $edges[] = [
219
                'node' => $result,
220
                'cursor' => $cursor,
221
            ];
222
223
            $lastCursor = $cursor;
224
            if (! $firstCursor) {
225
                $firstCursor = $cursor;
226
            }
227
228
            $index++;
229
        }
230
231
        $endCursor   = $paginator->count() ? $paginator->count() - 1 : 0;
232
        $startCursor = base64_encode((string) 0);
233
        $endCursor   = base64_encode((string) $endCursor);
234
235
        return [
236
            'edges' => $edges,
237
            'totalCount' => $paginator->count(),
238
            'pageInfo' => [
239
                'endCursor' => $endCursor,
240
                'startCursor' => $startCursor,
241
                'hasNextPage' => $endCursor !== $lastCursor,
242
                'hasPreviousPage' => $firstCursor !== null && $startCursor !== $firstCursor,
243
            ],
244
        ];
245
    }
246
}
247