Passed
Pull Request — master (#176)
by Maxim
14:53 queued 11:53
created

EntityReader::withLimit()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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