Passed
Push — master ( 010f69...c40d9e )
by Aleksei
02:39
created

EntityReader::withFilter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 10
ccs 8
cts 8
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
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 10
            $new->resetCountCache();
119
        }
120 10
        return $new;
121
    }
122
123
    /**
124
     * @psalm-mutation-free
125
     */
126 1
    public function withFilterHandlers(FilterHandlerInterface ...$filterHandlers): static
127
    {
128 1
        $new = clone $this;
129
        /** @psalm-suppress ImpureMethodCall */
130 1
        $new->setFilterHandlers(...$filterHandlers);
131
        /** @psalm-suppress ImpureMethodCall */
132 1
        $new->resetCountCache();
133 1
        $new->itemsCache = new CachedCollection();
134 1
        $new->oneItemCache = new CachedCollection();
135 1
        return $new;
136
    }
137
138 3
    public function count(): int
139
    {
140 3
        return $this->countCache->getCount();
141
    }
142
143 10
    public function read(): iterable
144
    {
145 10
        if ($this->itemsCache->getCollection() === null) {
146 10
            $query = $this->buildSelectQuery();
147 10
            $this->itemsCache->setCollection($query->fetchAll());
148
        }
149 10
        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
    public function getIterator(): Generator
170
    {
171
        yield from $this->itemsCache->getCollection() ?? $this->buildSelectQuery()->getIterator();
172
    }
173
174 1
    public function getSql(): string
175
    {
176 1
        $query = $this->buildSelectQuery();
177 1
        return (string)($query instanceof Select ? $query->buildQuery() : $query);
178
    }
179
180 9
    private function setFilterHandlers(FilterHandlerInterface ...$filterHandlers): void
181
    {
182 9
        $handlers = [];
183 9
        foreach ($filterHandlers as $filterHandler) {
184 9
            if ($filterHandler instanceof QueryBuilderFilterHandler) {
185 9
                $handlers[$filterHandler->getOperator()] = $filterHandler;
186
            }
187
        }
188 9
        $this->filterHandlers = array_merge($this->filterHandlers, $handlers);
189
    }
190
191 9
    private function buildSelectQuery(): SelectQuery|Select
192
    {
193 9
        $newQuery = clone $this->query;
194 9
        if ($this->offset !== null) {
195
            $newQuery->offset($this->offset);
196
        }
197 9
        if ($this->sorting !== null) {
198
            $newQuery->orderBy($this->normalizeSortingCriteria($this->sorting->getCriteria()));
199
        }
200 9
        if ($this->limit !== null) {
201
            $newQuery->limit($this->limit);
202
        }
203 9
        if ($this->filter !== null) {
204 9
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
205
        }
206 9
        return $newQuery;
207
    }
208
209 9
    private function makeFilterClosure(FilterInterface $filter): Closure
210
    {
211 9
        return function (QueryBuilder $select) use ($filter) {
212 9
            $filterArray = $filter->toCriteriaArray();
213 9
            $operation = array_shift($filterArray);
214 9
            $arguments = $filterArray;
215
216 9
            if (!array_key_exists($operation, $this->filterHandlers)) {
217
                throw new RuntimeException(sprintf('Filter operator "%s" is not supported.', $operation));
218
            }
219
            /** @var QueryBuilderFilterHandler $handler */
220 9
            $handler = $this->filterHandlers[$operation];
221 9
            $select->where(...$handler->getAsWhereArguments($arguments, $this->filterHandlers));
222 9
        };
223
    }
224
225 9
    private function resetCountCache(): void
226
    {
227 9
        $newQuery = clone $this->query;
228 9
        if ($this->filter !== null) {
229 9
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
230
        }
231 9
        $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