Passed
Push — master ( ca4eed...492557 )
by Aleksei
04:26 queued 28s
created

EntityReader::withSort()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 9
ccs 0
cts 7
cp 0
crap 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Cycle\Data\Reader;
6
7
use Closure;
8
use Cycle\Database\Query\SelectQuery;
9
use Cycle\ORM\Select;
10
use Cycle\ORM\Select\QueryBuilder;
11
use Generator;
12
use InvalidArgumentException;
13
use RuntimeException;
14
use Yiisoft\Data\Reader\DataReaderInterface;
15
use Yiisoft\Data\Reader\FilterHandlerInterface;
16
use Yiisoft\Data\Reader\FilterInterface;
17
use Yiisoft\Data\Reader\Sort;
18
use Yiisoft\Yii\Cycle\Data\Reader\Cache\CachedCollection;
19
use Yiisoft\Yii\Cycle\Data\Reader\Cache\CachedCount;
20
use Yiisoft\Yii\Cycle\Data\Reader\Processor\QueryBuilderProcessor;
21
22
/**
23
 * @template TKey as array-key
24
 * @template TValue as array|object
25
 *
26
 * @implements DataReaderInterface<TKey, TValue>
27
 */
28
final class EntityReader implements DataReaderInterface
29
{
30
    private Select|SelectQuery $query;
31
    private ?int $limit = null;
32
    private ?int $offset = null;
33
    private ?Sort $sorting = null;
34
    private ?FilterInterface $filter = null;
35
    private CachedCount $countCache;
36
    private CachedCollection $itemsCache;
37
    private CachedCollection $oneItemCache;
38
    /** @var FilterHandlerInterface[]|QueryBuilderProcessor[] */
39
    private array $filterHandlers = [];
40
41
    public function __construct(Select|SelectQuery $query)
42
    {
43
        $this->query = clone $query;
44
        $this->countCache = new CachedCount($this->query);
45
        $this->itemsCache = new CachedCollection();
46
        $this->oneItemCache = new CachedCollection();
47
        $this->setFilterHandlers(
48
            new Processor\All(),
49
            new Processor\Any(),
50
            new Processor\Equals(),
51
            new Processor\GreaterThan(),
52
            new Processor\GreaterThanOrEqual(),
53
            new Processor\In(),
54
            new Processor\LessThan(),
55
            new Processor\LessThanOrEqual(),
56
            new Processor\Like(),
57
            // new Processor\Not()
58
        );
59
    }
60
61
    public function getSort(): ?Sort
62
    {
63
        return $this->sorting;
64
    }
65
66
    /**
67
     * @psalm-mutation-free
68
     */
69
    public function withLimit(int $limit): static
70
    {
71
        if ($limit < 0) {
72
            throw new InvalidArgumentException('$limit must not be less than 0.');
73
        }
74
        $new = clone $this;
75
        if ($new->limit !== $limit) {
76
            $new->limit = $limit;
77
            $new->itemsCache = new CachedCollection();
78
        }
79
        return $new;
80
    }
81
82
    /**
83
     * @psalm-mutation-free
84
     */
85
    public function withOffset(int $offset): static
86
    {
87
        $new = clone $this;
88
        if ($new->offset !== $offset) {
89
            $new->offset = $offset;
90
            $new->itemsCache = new CachedCollection();
91
        }
92
        return $new;
93
    }
94
95
    /**
96
     * @psalm-mutation-free
97
     */
98
    public function withSort(?Sort $sort): static
99
    {
100
        $new = clone $this;
101
        if ($new->sorting !== $sort) {
102
            $new->sorting = $sort;
103
            $new->itemsCache = new CachedCollection();
104
            $new->oneItemCache = new CachedCollection();
105
        }
106
        return $new;
107
    }
108
109
    /**
110
     * @psalm-mutation-free
111
     */
112
    public function withFilter(FilterInterface $filter): static
113
    {
114
        $new = clone $this;
115
        if ($new->filter !== $filter) {
116
            $new->filter = $filter;
117
            $new->itemsCache = new CachedCollection();
118
            $new->oneItemCache = new CachedCollection();
119
        }
120
        return $new;
121
    }
122
123
    /**
124
     * @psalm-mutation-free
125
     */
126
    public function withFilterHandlers(FilterHandlerInterface ...$filterHandlers): static
127
    {
128
        $new = clone $this;
129
        /** @psalm-suppress ImpureMethodCall */
130
        $new->setFilterHandlers(...$filterHandlers);
131
        /** @psalm-suppress ImpureMethodCall */
132
        $new->resetCountCache();
133
        $new->itemsCache = new CachedCollection();
134
        $new->oneItemCache = new CachedCollection();
135
        return $new;
136
    }
137
138
    public function count(): int
139
    {
140
        return $this->countCache->getCount();
141
    }
142
143
    public function read(): iterable
144
    {
145
        if ($this->itemsCache->getCollection() === null) {
146
            $query = $this->buildSelectQuery();
147
            $this->itemsCache->setCollection($query->fetchAll());
148
        }
149
        return $this->itemsCache->getCollection();
150
    }
151
152
    public function readOne(): null|array|object
153
    {
154
        if (!$this->oneItemCache->isCollected()) {
155
            $item = $this->itemsCache->isCollected()
156
                // get first item from cached collection
157
                ? $this->itemsCache->getGenerator()->current()
158
                // read data with limit 1
159
                : $this->withLimit(1)->getIterator()->current();
160
            $this->oneItemCache->setCollection($item === null ? [] : [$item]);
161
        }
162
163
        return $this->oneItemCache->getGenerator()->current();
164
    }
165
166
    /**
167
     * Get Iterator without caching
168
     */
169
    public function getIterator(): Generator
170
    {
171
        yield from $this->itemsCache->getCollection() ?? $this->buildSelectQuery()->getIterator();
172
    }
173
174
    public function getSql(): string
175
    {
176
        $query = $this->buildSelectQuery();
177
        return (string)($query instanceof Select ? $query->buildQuery() : $query);
178
    }
179
180
    private function setFilterHandlers(FilterHandlerInterface ...$filterHandlers): void
181
    {
182
        $handlers = [];
183
        foreach ($filterHandlers as $filterHandler) {
184
            if ($filterHandler instanceof QueryBuilderProcessor) {
185
                $handlers[$filterHandler->getOperator()] = $filterHandler;
186
            }
187
        }
188
        $this->filterHandlers = array_merge($this->filterHandlers, $handlers);
189
    }
190
191
    private function buildSelectQuery(): SelectQuery|Select
192
    {
193
        $newQuery = clone $this->query;
194
        if ($this->offset !== null) {
195
            $newQuery->offset($this->offset);
196
        }
197
        if ($this->sorting !== null) {
198
            $newQuery->orderBy($this->normalizeSortingCriteria($this->sorting->getCriteria()));
199
        }
200
        if ($this->limit !== null) {
201
            $newQuery->limit($this->limit);
202
        }
203
        if ($this->filter !== null) {
204
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
205
        }
206
        return $newQuery;
207
    }
208
209
    private function makeFilterClosure(FilterInterface $filter): Closure
210
    {
211
        return function (QueryBuilder $select) use ($filter) {
212
            $filterArray = $filter->toCriteriaArray();
213
            $operation = array_shift($filterArray);
214
            $arguments = $filterArray;
215
216
            if (!array_key_exists($operation, $this->filterHandlers)) {
217
                throw new RuntimeException(sprintf('Filter operator "%s" is not supported.', $operation));
218
            }
219
            /** @psalm-var QueryBuilderProcessor $handler */
220
            $handler = $this->filterHandlers[$operation];
221
            $select->where(...$handler->getAsWhereArguments($arguments, $this->filterHandlers));
222
        };
223
    }
224
225
    private function resetCountCache(): void
226
    {
227
        $newQuery = clone $this->query;
228
        if ($this->filter !== null) {
229
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
230
        }
231
        $this->countCache = new CachedCount($newQuery);
232
    }
233
234
    private function normalizeSortingCriteria(array $criteria): array
235
    {
236
        foreach ($criteria as $field => $direction) {
237
            if (is_int($direction)) {
238
                $direction = match ($direction) {
239
                    SORT_DESC => 'DESC',
240
                    default => 'ASC',
241
                };
242
            }
243
            $criteria[$field] = $direction;
244
        }
245
246
        return $criteria;
247
    }
248
}
249