Passed
Push — main ( 4aba17...588674 )
by Tom
54s queued 12s
created

ResolveEntityFactory::buildEdgesAndCursors()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 42
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 26
c 1
b 0
f 0
nc 12
nop 3
dl 0
loc 42
rs 8.8817
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\FilterQueryBuilder;
10
use ApiSkeletons\Doctrine\GraphQL\Type\Entity;
11
use ApiSkeletons\Doctrine\QueryBuilder\Filter\Applicator;
12
use Closure;
13
use Doctrine\ORM\EntityManager;
14
use Doctrine\ORM\QueryBuilder;
15
use Doctrine\ORM\Tools\Pagination\Paginator;
16
use GraphQL\Type\Definition\ResolveInfo;
17
use League\Event\EventDispatcher;
18
19
use function base64_decode;
20
use function base64_encode;
21
use function implode;
22
23
class ResolveEntityFactory
24
{
25
    public function __construct(
26
        protected Config $config,
27
        protected EntityManager $entityManager,
28
        protected EventDispatcher $eventDispatcher,
29
    ) {
30
    }
31
32
    public function get(Entity $entity, string $eventName): Closure
33
    {
34
        return function ($objectValue, array $args, $context, ResolveInfo $info) use ($entity, $eventName) {
35
            $entityClass = $entity->getEntityClass();
36
37
            $queryBuilderFilter = (new Applicator($this->entityManager, $entityClass))
38
                ->setEntityAlias('entity');
39
            $queryBuilder       = $queryBuilderFilter($this->buildFilterArray($args['filter'] ?? []))
40
                ->select('entity');
41
42
            return $this->buildPagination(
43
                queryBuilder: $queryBuilder,
44
                aliasMap: $queryBuilderFilter->getEntityAliasMap(),
45
                eventName: $eventName,
46
                objectValue: $objectValue,
47
                args: $args,
48
                context: $context,
49
                info: $info,
50
            );
51
        };
52
    }
53
54
    /**
55
     * @param mixed[] $filterTypes
56
     *
57
     * @return mixed[]
58
     */
59
    private function buildFilterArray(array $filterTypes): array
60
    {
61
        $filterArray = [];
62
63
        foreach ($filterTypes as $field => $filters) {
64
            foreach ($filters as $filter => $value) {
65
                switch ($filter) {
66
                    case FiltersDef::CONTAINS:
67
                        $filterArray[$field . '|like'] = $value;
68
                        break;
69
                    case FiltersDef::STARTSWITH:
70
                        $filterArray[$field . '|startswith'] = $value;
71
                        break;
72
                    case FiltersDef::ENDSWITH:
73
                        $filterArray[$field . '|endswith'] = $value;
74
                        break;
75
                    case FiltersDef::ISNULL:
76
                        $filterArray[$field . '|isnull'] = 'true';
77
                        break;
78
                    case FiltersDef::BETWEEN:
79
                        $filterArray[$field . '|between'] = $value['from'] . ',' . $value['to'];
80
                        break;
81
                    case FiltersDef::IN:
82
                        $filterArray[$field . '|in'] = implode(',', $value);
83
                        break;
84
                    case FiltersDef::NOTIN:
85
                        $filterArray[$field . '|notin'] = implode(',', $value);
86
                        break;
87
                    default:
88
                        $filterArray[$field . '|' . $filter] = (string) $value;
89
                        break;
90
                }
91
            }
92
        }
93
94
        return $filterArray;
95
    }
96
97
    /**
98
     * @param mixed[] $aliasMap
99
     *
100
     * @return mixed[]
101
     */
102
    public function buildPagination(
103
        QueryBuilder $queryBuilder,
104
        array $aliasMap,
105
        string $eventName,
106
        mixed ...$resolve,
107
    ): array {
108
        $paginationFields = [
109
            'first'  => 0,
110
            'last'   => 0,
111
            'before' => 0,
112
            'after'  => 0,
113
        ];
114
115
        if (isset($resolve['args']['pagination'])) {
116
            foreach ($resolve['args']['pagination'] as $field => $value) {
117
                switch ($field) {
118
                    case 'after':
119
                        $paginationFields[$field] = (int) base64_decode($value, true) + 1;
120
                        break;
121
                    case 'before':
122
                        $paginationFields[$field] = (int) base64_decode($value, true);
123
                        break;
124
                    default:
125
                        $paginationFields[$field] = $value;
126
                        break;
127
                }
128
            }
129
        }
130
131
        $offsetAndLimit = $this->calculateOffsetAndLimit($paginationFields);
132
133
        if ($offsetAndLimit['offset']) {
134
            $queryBuilder->setFirstResult($offsetAndLimit['offset']);
135
        }
136
137
        if ($offsetAndLimit['limit']) {
138
            $queryBuilder->setMaxResults($offsetAndLimit['limit']);
139
        }
140
141
        /**
142
         * Fire the event dispatcher using the passed event name.
143
         * Include all resolve variables.
144
         */
145
        $this->eventDispatcher->dispatch(
146
            new FilterQueryBuilder(
147
                $queryBuilder,
148
                $aliasMap,
149
                $eventName,
150
                ...$resolve,
151
            ),
152
        );
153
154
        $edgesAndCursors = $this->buildEdgesAndCursors($queryBuilder, $offsetAndLimit, $paginationFields);
155
156
        return [
157
            'edges' => $edgesAndCursors['edges'],
158
            'totalCount' => $edgesAndCursors['totalCount'],
159
            'pageInfo' => [
160
                'endCursor' => $edgesAndCursors['cursors']['end'],
161
                'startCursor' => $edgesAndCursors['cursors']['start'],
162
                'hasNextPage' => $edgesAndCursors['cursors']['end'] !== $edgesAndCursors['cursors']['last'],
163
                'hasPreviousPage' => $edgesAndCursors['cursors']['first'] !== null
164
                    && $edgesAndCursors['cursors']['start'] !== $edgesAndCursors['cursors']['first'],
165
            ],
166
        ];
167
    }
168
169
    /**
170
     * @param array<string, int> $offsetAndLimit
171
     * @param array<string, int> $paginationFields
172
     *
173
     * @return array<string, mixed>
174
     */
175
    protected function buildEdgesAndCursors(QueryBuilder $queryBuilder, array $offsetAndLimit, array $paginationFields): array
176
    {
177
        $index   = 0;
178
        $edges   = [];
179
        $cursors = [
180
            'start' => base64_encode((string) 0),
181
            'first' => null,
182
            'last'  => base64_encode((string) 0),
183
        ];
184
185
        $paginator = new Paginator($queryBuilder->getQuery());
186
        $itemCount = $paginator->count();
187
188
        // Rebuild paginator if needed
189
        if ($paginationFields['last'] && ! $paginationFields['before']) {
190
            $offsetAndLimit['offset'] = $itemCount - $paginationFields['last'];
191
            $queryBuilder->setFirstResult($offsetAndLimit['offset']);
192
            $paginator = new Paginator($queryBuilder->getQuery());
193
        }
194
195
        foreach ($paginator->getQuery()->getResult() as $result) {
196
            $cursors['last'] = base64_encode((string) ($index + $offsetAndLimit['offset']));
197
198
            $edges[] = [
199
                'node' => $result,
200
                'cursor' => $cursors['last'],
201
            ];
202
203
            if (! $cursors['first']) {
204
                $cursors['first'] = $cursors['last'];
205
            }
206
207
            $index++;
208
        }
209
210
        $endIndex       = $paginator->count() ? $paginator->count() - 1 : 0;
211
        $cursors['end'] = base64_encode((string) $endIndex);
212
213
        return [
214
            'cursors'    => $cursors,
215
            'edges'      => $edges,
216
            'totalCount' => $paginator->count(),
217
        ];
218
    }
219
220
    /**
221
     * @param array<string, int> $paginationFields
222
     *
223
     * @return array<string, int>
224
     */
225
    protected function calculateOffsetAndLimit(array $paginationFields): array
226
    {
227
        $offset = 0;
228
        $limit  = $this->config->getLimit();
229
230
        $adjustedLimit = $paginationFields['first'] ?: $paginationFields['last'] ?: $limit;
231
        if ($adjustedLimit < $limit) {
232
            $limit = $adjustedLimit;
233
        }
234
235
        if ($paginationFields['after']) {
236
            $offset = $paginationFields['after'];
237
        } elseif ($paginationFields['before']) {
238
            $offset = $paginationFields['before'] - $limit;
239
        }
240
241
        if ($offset < 0) {
242
            $limit += $offset;
243
            $offset = 0;
244
        }
245
246
        return [
247
            'offset' => $offset,
248
            'limit'  => $limit,
249
        ];
250
    }
251
}
252