EntityReader::withSort()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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