ResolveEntityFactory::buildEdgesAndCursors()   B
last analyzed

Complexity

Conditions 7
Paths 20

Size

Total Lines 48
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 30
nc 20
nop 3
dl 0
loc 48
rs 8.5066
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ApiSkeletons\Doctrine\ORM\GraphQL\Resolve;
6
7
use ApiSkeletons\Doctrine\ORM\GraphQL\Config;
8
use ApiSkeletons\Doctrine\ORM\GraphQL\Event\QueryBuilder as QueryBuilderEvent;
9
use ApiSkeletons\Doctrine\ORM\GraphQL\Filter\QueryBuilder as QueryBuilderFilter;
10
use ApiSkeletons\Doctrine\ORM\GraphQL\Type\Entity\Entity;
11
use ArrayObject;
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
22
/**
23
 * Build a resolver for entities
24
 */
25
class ResolveEntityFactory
26
{
27
    public function __construct(
28
        protected readonly Config $config,
29
        protected readonly EntityManager $entityManager,
30
        protected readonly EventDispatcher $eventDispatcher,
31
        protected readonly ArrayObject $metadata,
32
    ) {
33
    }
34
35
    public function get(Entity $entity, string|null $eventName): Closure
36
    {
37
        return function ($objectValue, array $args, $context, ResolveInfo $info) use ($entity, $eventName) {
38
            $entityClass        = $entity->getEntityClass();
39
            $queryBuilderFilter = new QueryBuilderFilter();
40
41
            $queryBuilder = $this->entityManager->createQueryBuilder();
42
            $queryBuilder->select('entity')
43
                ->from($entityClass, 'entity');
44
45
            if (isset($args['filter'])) {
46
                $queryBuilderFilter->apply($args['filter'], $queryBuilder, $entity);
47
            }
48
49
            return $this->buildPagination(
50
                entity: $entity,
51
                queryBuilder: $queryBuilder,
52
                eventName: $eventName,
53
                objectValue: $objectValue,
54
                args: $args,
55
                context: $context,
56
                info: $info,
57
            );
58
        };
59
    }
60
61
    /** @return mixed[] */
62
    public function buildPagination(
63
        Entity $entity,
64
        QueryBuilder $queryBuilder,
65
        string|null $eventName,
66
        mixed ...$resolve,
67
    ): array {
68
        $paginationFields = [
69
            'first'  => 0,
70
            'last'   => 0,
71
            'before' => 0,
72
            'after'  => 0,
73
        ];
74
75
        if (isset($resolve['args']['pagination'])) {
76
            foreach ($resolve['args']['pagination'] as $field => $value) {
77
                $paginationFields[$field] = $value;
78
79
                if ($field === 'after') {
80
                    $paginationFields[$field] = (int) base64_decode($value, true) + 1;
81
                }
82
83
                if ($field !== 'before') {
84
                    continue;
85
                }
86
87
                $paginationFields[$field] = (int) base64_decode($value, true);
88
            }
89
        }
90
91
        $offsetAndLimit = $this->calculateOffsetAndLimit($entity, $paginationFields);
92
93
        /**
94
         * Fire the event dispatcher using the passed event name.
95
         * Include all resolve variables.
96
         */
97
        if ($eventName) {
98
            $this->eventDispatcher->dispatch(
99
                new QueryBuilderEvent(
100
                    $eventName,
101
                    $queryBuilder,
102
                    (int) $offsetAndLimit['offset'],
103
                    (int) $offsetAndLimit['limit'],
104
                    ...$resolve,
105
                ),
106
            );
107
        }
108
109
        if ($offsetAndLimit['offset']) {
110
            $queryBuilder->setFirstResult($offsetAndLimit['offset']);
111
        }
112
113
        if ($offsetAndLimit['limit']) {
114
            $queryBuilder->setMaxResults($offsetAndLimit['limit']);
115
        }
116
117
        $edgesAndCursors = $this->buildEdgesAndCursors($queryBuilder, $offsetAndLimit, $paginationFields);
118
119
        return [
120
            'edges' => $edgesAndCursors['edges'],
121
            'totalCount' => $edgesAndCursors['totalCount'],
122
            'pageInfo' => [
123
                'endCursor' => $edgesAndCursors['cursors']['last'],
124
                'startCursor' => $edgesAndCursors['cursors']['start'],
125
                'hasNextPage' => $edgesAndCursors['cursors']['end'] !== $edgesAndCursors['cursors']['last'],
126
                'hasPreviousPage' => $edgesAndCursors['cursors']['start'] !== base64_encode((string) 0),
127
            ],
128
        ];
129
    }
130
131
    /**
132
     * @param array<string, int> $offsetAndLimit
133
     * @param array<string, int> $paginationFields
134
     *
135
     * @return array<string, mixed>
136
     */
137
    protected function buildEdgesAndCursors(QueryBuilder $queryBuilder, array $offsetAndLimit, array $paginationFields): array
138
    {
139
        $index   = 0;
140
        $edges   = [];
141
        $cursors = [
142
            'start' => base64_encode((string) 0),
143
            'first' => null,
144
            'last'  => base64_encode((string) 0),
145
        ];
146
147
        $paginator = new Paginator($queryBuilder->getQuery());
148
        $itemCount = $paginator->count();
149
150
        // Rebuild paginator if needed
151
        if ($paginationFields['last'] && ! $paginationFields['before']) {
152
            $offsetAndLimit['offset'] = $itemCount - $paginationFields['last'];
153
            $queryBuilder->setFirstResult($offsetAndLimit['offset']);
154
            $paginator = new Paginator($queryBuilder->getQuery());
155
        }
156
157
        $startCursor = null;
158
        foreach ($paginator->getQuery()->getResult() as $result) {
159
            $cursors['last'] = base64_encode((string) ($index + $offsetAndLimit['offset']));
160
161
            $edges[] = [
162
                'node' => $result,
163
                'cursor' => $cursors['last'],
164
            ];
165
166
            if (! $startCursor) {
167
                $startCursor = $cursors['last'];
168
            }
169
170
            if (! $cursors['first']) {
171
                $cursors['first'] = $cursors['last'];
172
            }
173
174
            $index++;
175
        }
176
177
        $endIndex         = $paginator->count() ? $paginator->count() - 1 : 0;
178
        $cursors['end']   = base64_encode((string) $endIndex);
179
        $cursors['start'] = $startCursor ?? $cursors['start'];
180
181
        return [
182
            'cursors'    => $cursors,
183
            'edges'      => $edges,
184
            'totalCount' => $paginator->count(),
185
        ];
186
    }
187
188
    /**
189
     * @param array<string, int> $paginationFields
190
     *
191
     * @return array<string, int>
192
     */
193
    protected function calculateOffsetAndLimit(Entity $entity, array $paginationFields): array
194
    {
195
        $offset = 0;
196
197
        $limit = $this->metadata[$entity->getEntityClass()]['limit'];
198
199
        if (! $limit) {
200
            $limit = $this->config->getLimit();
201
        }
202
203
        $adjustedLimit = $paginationFields['first'] ?: $paginationFields['last'] ?: $limit;
204
        if ($adjustedLimit < $limit) {
205
            $limit = $adjustedLimit;
206
        }
207
208
        if ($paginationFields['after']) {
209
            $offset = $paginationFields['after'];
210
        } elseif ($paginationFields['before']) {
211
            $offset = $paginationFields['before'] - $limit;
212
        }
213
214
        if ($offset < 0) {
215
            $limit += $offset;
216
            $offset = 0;
217
        }
218
219
        return [
220
            'offset' => $offset,
221
            'limit'  => $limit,
222
        ];
223
    }
224
}
225