Passed
Push — master ( b8d8bc...934c3a )
by Alexander
04:05 queued 01:35
created

EntityReader::read()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 2
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 7
    public function __construct(Select|SelectQuery $query)
42
    {
43 7
        $this->query = clone $query;
44 7
        $this->countCache = new CachedCount($this->query);
45 7
        $this->itemsCache = new CachedCollection();
46 7
        $this->oneItemCache = new CachedCollection();
47 7
        $this->setFilterHandlers(
48 7
            new Processor\All(),
49 7
            new Processor\Any(),
50 7
            new Processor\Equals(),
51 7
            new Processor\GreaterThan(),
52 7
            new Processor\GreaterThanOrEqual(),
53 7
            new Processor\In(),
54 7
            new Processor\LessThan(),
55 7
            new Processor\LessThanOrEqual(),
56 7
            new Processor\Like(),
57 7
            // new Processor\Not()
58 7
        );
59
    }
60
61 1
    public function getSort(): ?Sort
62
    {
63 1
        return $this->sorting;
64
    }
65
66
    /**
67
     * @psalm-mutation-free
68
     */
69 4
    public function withLimit(int $limit): static
70
    {
71 4
        if ($limit < 0) {
72
            throw new InvalidArgumentException('$limit must not be less than 0.');
73
        }
74 4
        $new = clone $this;
75 4
        if ($new->limit !== $limit) {
76 4
            $new->limit = $limit;
77 4
            $new->itemsCache = new CachedCollection();
78
        }
79 4
        return $new;
80
    }
81
82
    /**
83
     * @psalm-mutation-free
84
     */
85 1
    public function withOffset(int $offset): static
86
    {
87 1
        $new = clone $this;
88 1
        if ($new->offset !== $offset) {
89 1
            $new->offset = $offset;
90 1
            $new->itemsCache = new CachedCollection();
91
        }
92 1
        return $new;
93
    }
94
95
    /**
96
     * @psalm-mutation-free
97
     */
98 1
    public function withSort(?Sort $sort): static
99
    {
100 1
        $new = clone $this;
101 1
        if ($new->sorting !== $sort) {
102 1
            $new->sorting = $sort;
103 1
            $new->itemsCache = new CachedCollection();
104 1
            $new->oneItemCache = new CachedCollection();
105
        }
106 1
        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 2
    public function count(): int
139
    {
140 2
        return $this->countCache->getCount();
141
    }
142
143 4
    public function read(): iterable
144
    {
145 4
        if ($this->itemsCache->getCollection() === null) {
146 4
            $query = $this->buildSelectQuery();
147 4
            $this->itemsCache->setCollection($query->fetchAll());
148
        }
149 4
        return $this->itemsCache->getCollection();
150
    }
151
152 1
    public function readOne(): null|array|object
153
    {
154 1
        if (!$this->oneItemCache->isCollected()) {
155 1
            $item = $this->itemsCache->isCollected()
156 1
                // get first item from cached collection
157
                ? $this->itemsCache->getGenerator()->current()
158 1
                // read data with limit 1
159 1
                : $this->withLimit(1)->getIterator()->current();
160 1
            $this->oneItemCache->setCollection($item === null ? [] : [$item]);
161
        }
162
163 1
        return $this->oneItemCache->getGenerator()->current();
164
    }
165
166
    /**
167
     * Get Iterator without caching
168
     */
169 1
    public function getIterator(): Generator
170
    {
171 1
        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 7
    private function setFilterHandlers(FilterHandlerInterface ...$filterHandlers): void
181
    {
182 7
        $handlers = [];
183 7
        foreach ($filterHandlers as $filterHandler) {
184 7
            if ($filterHandler instanceof QueryBuilderProcessor) {
185 7
                $handlers[$filterHandler->getOperator()] = $filterHandler;
186
            }
187
        }
188 7
        $this->filterHandlers = array_merge($this->filterHandlers, $handlers);
189
    }
190
191 5
    private function buildSelectQuery(): SelectQuery|Select
192
    {
193 5
        $newQuery = clone $this->query;
194 5
        if ($this->offset !== null) {
195 1
            $newQuery->offset($this->offset);
196
        }
197 5
        if ($this->sorting !== null) {
198 1
            $newQuery->orderBy($this->normalizeSortingCriteria($this->sorting->getCriteria()));
199
        }
200 5
        if ($this->limit !== null) {
201 3
            $newQuery->limit($this->limit);
202
        }
203 5
        if ($this->filter !== null) {
204
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
205
        }
206 5
        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 1
    private function normalizeSortingCriteria(array $criteria): array
235
    {
236 1
        foreach ($criteria as $field => $direction) {
237 1
            if (is_int($direction)) {
238 1
                $direction = match ($direction) {
239 1
                    SORT_DESC => 'DESC',
240 1
                    default => 'ASC',
241 1
                };
242
            }
243 1
            $criteria[$field] = $direction;
244
        }
245
246 1
        return $criteria;
247
    }
248
}
249