ResolveEntityFactory   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 234
Duplicated Lines 0 %

Importance

Changes 12
Bugs 0 Features 1
Metric Value
eloc 125
c 12
b 0
f 1
dl 0
loc 234
rs 9.68
wmc 34

6 Methods

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