ResolveEntityFactory::get()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 16
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 22
rs 9.7333
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
        if ($offsetAndLimit['offset']) {
94
            $queryBuilder->setFirstResult($offsetAndLimit['offset']);
95
        }
96
97
        if ($offsetAndLimit['limit']) {
98
            $queryBuilder->setMaxResults($offsetAndLimit['limit']);
99
        }
100
101
        /**
102
         * Fire the event dispatcher using the passed event name.
103
         * Include all resolve variables.
104
         */
105
        if ($eventName) {
106
            $this->eventDispatcher->dispatch(
107
                new QueryBuilderEvent(
108
                    $queryBuilder,
109
                    $eventName,
110
                    ...$resolve,
111
                ),
112
            );
113
        }
114
115
        $edgesAndCursors = $this->buildEdgesAndCursors($queryBuilder, $offsetAndLimit, $paginationFields);
116
117
        return [
118
            'edges' => $edgesAndCursors['edges'],
119
            'totalCount' => $edgesAndCursors['totalCount'],
120
            'pageInfo' => [
121
                'endCursor' => $edgesAndCursors['cursors']['end'],
122
                'startCursor' => $edgesAndCursors['cursors']['start'],
123
                'hasNextPage' => $edgesAndCursors['cursors']['end'] !== $edgesAndCursors['cursors']['last'],
124
                'hasPreviousPage' => $edgesAndCursors['cursors']['first'] !== null
125
                    && $edgesAndCursors['cursors']['start'] !== $edgesAndCursors['cursors']['first'],
126
            ],
127
        ];
128
    }
129
130
    /**
131
     * @param array<string, int> $offsetAndLimit
132
     * @param array<string, int> $paginationFields
133
     *
134
     * @return array<string, mixed>
135
     */
136
    protected function buildEdgesAndCursors(QueryBuilder $queryBuilder, array $offsetAndLimit, array $paginationFields): array
137
    {
138
        $index   = 0;
139
        $edges   = [];
140
        $cursors = [
141
            'start' => base64_encode((string) 0),
142
            'first' => null,
143
            'last'  => base64_encode((string) 0),
144
        ];
145
146
        $paginator = new Paginator($queryBuilder->getQuery());
147
        $itemCount = $paginator->count();
148
149
        // Rebuild paginator if needed
150
        if ($paginationFields['last'] && ! $paginationFields['before']) {
151
            $offsetAndLimit['offset'] = $itemCount - $paginationFields['last'];
152
            $queryBuilder->setFirstResult($offsetAndLimit['offset']);
153
            $paginator = new Paginator($queryBuilder->getQuery());
154
        }
155
156
        foreach ($paginator->getQuery()->getResult() as $result) {
157
            $cursors['last'] = base64_encode((string) ($index + $offsetAndLimit['offset']));
158
159
            $edges[] = [
160
                'node' => $result,
161
                'cursor' => $cursors['last'],
162
            ];
163
164
            if (! $cursors['first']) {
165
                $cursors['first'] = $cursors['last'];
166
            }
167
168
            $index++;
169
        }
170
171
        $endIndex       = $paginator->count() ? $paginator->count() - 1 : 0;
172
        $cursors['end'] = base64_encode((string) $endIndex);
173
174
        return [
175
            'cursors'    => $cursors,
176
            'edges'      => $edges,
177
            'totalCount' => $paginator->count(),
178
        ];
179
    }
180
181
    /**
182
     * @param array<string, int> $paginationFields
183
     *
184
     * @return array<string, int>
185
     */
186
    protected function calculateOffsetAndLimit(Entity $entity, array $paginationFields): array
187
    {
188
        $offset = 0;
189
190
        $limit = $this->metadata[$entity->getEntityClass()]['limit'];
191
192
        if (! $limit) {
193
            $limit = $this->config->getLimit();
194
        }
195
196
        $adjustedLimit = $paginationFields['first'] ?: $paginationFields['last'] ?: $limit;
197
        if ($adjustedLimit < $limit) {
198
            $limit = $adjustedLimit;
199
        }
200
201
        if ($paginationFields['after']) {
202
            $offset = $paginationFields['after'];
203
        } elseif ($paginationFields['before']) {
204
            $offset = $paginationFields['before'] - $limit;
205
        }
206
207
        if ($offset < 0) {
208
            $limit += $offset;
209
            $offset = 0;
210
        }
211
212
        return [
213
            'offset' => $offset,
214
            'limit'  => $limit,
215
        ];
216
    }
217
}
218