Passed
Pull Request — master (#176)
by Maxim
17:36 queued 14:52
created

EntityReader::buildSelectQuery()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 16
nop 0
dl 0
loc 16
ccs 11
cts 11
cp 1
crap 5
rs 9.6111
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 20
    public function __construct(Select|SelectQuery $query)
41
    {
42 20
        $this->query = clone $query;
43 20
        $this->countCache = new CachedCount($this->query);
44 20
        $this->itemsCache = new CachedCollection();
45 20
        $this->oneItemCache = new CachedCollection();
46 20
        $this->setFilterHandlers(
47 20
            new FilterHandler\AllHandler(),
48 20
            new FilterHandler\AnyHandler(),
49 20
            new FilterHandler\EqualsHandler(),
50 20
            new FilterHandler\GreaterThanHandler(),
51 20
            new FilterHandler\GreaterThanOrEqualHandler(),
52 20
            new FilterHandler\InHandler(),
53 20
            new FilterHandler\LessThanHandler(),
54 20
            new FilterHandler\LessThanOrEqualHandler(),
55 20
            new FilterHandler\LikeHandler(),
56 20
            // new Processor\Not()
57 20
        );
58
    }
59
60 1
    public function getSort(): ?Sort
61
    {
62 1
        return $this->sorting;
63
    }
64
65
    /**
66
     * @psalm-mutation-free
67
     */
68 6
    public function withLimit(int $limit): static
69
    {
70 6
        if ($limit < 0) {
71 1
            throw new InvalidArgumentException('$limit must not be less than 0.');
72
        }
73 5
        $new = clone $this;
74 5
        if ($new->limit !== $limit) {
75 5
            $new->limit = $limit;
76 5
            $new->itemsCache = new CachedCollection();
77
        }
78 5
        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 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 14
    public function read(): iterable
143
    {
144 14
        if ($this->itemsCache->getCollection() === null) {
145 14
            $query = $this->buildSelectQuery();
146 14
            $this->itemsCache->setCollection($query->fetchAll());
147
        }
148 14
        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 1
    public function getIterator(): Generator
169
    {
170 1
        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 20
    private function setFilterHandlers(FilterHandlerInterface ...$filterHandlers): void
180
    {
181 20
        $handlers = [];
182 20
        foreach ($filterHandlers as $filterHandler) {
183 20
            if ($filterHandler instanceof QueryBuilderFilterHandler) {
184 20
                $handlers[$filterHandler->getOperator()] = $filterHandler;
185
            }
186
        }
187 20
        $this->filterHandlers = array_merge($this->filterHandlers, $handlers);
188
    }
189
190 16
    private function buildSelectQuery(): SelectQuery|Select
191
    {
192 16
        $newQuery = clone $this->query;
193 16
        if ($this->offset !== null) {
194 2
            $newQuery->offset($this->offset);
195
        }
196 16
        if ($this->sorting !== null) {
197 1
            $newQuery->orderBy($this->normalizeSortingCriteria($this->sorting->getCriteria()));
198
        }
199 16
        if ($this->limit !== null) {
200 4
            $newQuery->limit($this->limit);
201
        }
202 16
        if ($this->filter !== null) {
203 10
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
204
        }
205 16
        return $newQuery;
206
    }
207
208 10
    private function makeFilterClosure(FilterInterface $filter): Closure
209
    {
210 10
        return function (QueryBuilder $select) use ($filter) {
211 10
            $filterArray = $filter->toCriteriaArray();
212 10
            $operation = array_shift($filterArray);
213 10
            $arguments = $filterArray;
214
215 10
            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 10
            $handler = $this->filterHandlers[$operation];
220 10
            $select->where(...$handler->getAsWhereArguments($arguments, $this->filterHandlers));
221 10
        };
222
    }
223
224 1
    private function resetCountCache(): void
225
    {
226 1
        $newQuery = clone $this->query;
227 1
        if ($this->filter !== null) {
228
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
229
        }
230 1
        $this->countCache = new CachedCount($newQuery);
231
    }
232
233 1
    private function normalizeSortingCriteria(array $criteria): array
234
    {
235 1
        foreach ($criteria as $field => $direction) {
236 1
            if (is_int($direction)) {
237 1
                $direction = match ($direction) {
238 1
                    SORT_DESC => 'DESC',
239 1
                    default => 'ASC',
240 1
                };
241
            }
242 1
            $criteria[$field] = $direction;
243
        }
244
245 1
        return $criteria;
246
    }
247
}
248