Passed
Push — master ( 046edc...7f8fa3 )
by Alexander
02:26
created

EntityReader::read()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
ccs 0
cts 5
cp 0
crap 6
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 Countable;
9
use Cycle\ORM\Select;
10
use Cycle\ORM\Select\QueryBuilder;
11
use Generator;
12
use InvalidArgumentException;
13
use RuntimeException;
14
use Spiral\Database\Query\SelectQuery;
15
use Spiral\Pagination\PaginableInterface;
16
use Yiisoft\Data\Reader\DataReaderInterface;
17
use Yiisoft\Data\Reader\Filter\FilterInterface;
18
use Yiisoft\Data\Reader\Filter\FilterProcessorInterface;
19
use Yiisoft\Data\Reader\Sort;
20
use Yiisoft\Yii\Cycle\Data\Reader\Cache\CachedCollection;
21
use Yiisoft\Yii\Cycle\Data\Reader\Cache\CachedCount;
22
use Yiisoft\Yii\Cycle\Data\Reader\Processor\QueryBuilderProcessor;
23
24
final class EntityReader implements DataReaderInterface
25
{
26
    /** @var Select|SelectQuery */
27
    private $query;
28
    private ?int $limit = null;
29
    private ?int $offset = null;
30
    private ?Sort $sorting = null;
31
    private ?FilterInterface $filter = null;
32
    private CachedCount $countCache;
33
    private CachedCollection $itemsCache;
34
    private CachedCollection $oneItemCache;
35
    /** @var FilterProcessorInterface[]|QueryBuilderProcessor[] */
36
    private array $filterProcessors = [];
37
38
    /**
39
     * @param Select|SelectQuery $query
40
     */
41
    public function __construct($query)
42
    {
43
        if (!$query instanceof Countable) {
0 ignored issues
show
introduced by
$query is always a sub-type of Countable.
Loading history...
44
            throw new InvalidArgumentException(sprintf('Query should implement %s interface', Countable::class));
45
        }
46
        if (!$query instanceof PaginableInterface) {
0 ignored issues
show
introduced by
$query is always a sub-type of Spiral\Pagination\PaginableInterface.
Loading history...
47
            throw new InvalidArgumentException(
48
                sprintf('Query should implement %s interface', PaginableInterface::class)
49
            );
50
        }
51
        $this->query = clone $query;
52
        $this->countCache = new CachedCount($this->query);
53
        $this->itemsCache = new CachedCollection();
54
        $this->oneItemCache = new CachedCollection();
55
        $this->setFilterProcessors(
56
            new Processor\All(),
57
            new Processor\Any(),
58
            new Processor\Equals(),
59
            new Processor\GreaterThan(),
60
            new Processor\GreaterThanOrEqual(),
61
            new Processor\In(),
62
            new Processor\LessThan(),
63
            new Processor\LessThanOrEqual(),
64
            new Processor\Like(),
65
            // new Processor\Not()
66
        );
67
    }
68
69
    public function getSort(): ?Sort
70
    {
71
        return $this->sorting;
72
    }
73
74
    /**
75
     * @psalm-mutation-free
76
     */
77
    public function withLimit(int $limit): self
78
    {
79
        if ($limit < 0) {
80
            throw new InvalidArgumentException('$limit must not be less than 0.');
81
        }
82
        $new = clone $this;
83
        if ($new->limit !== $limit) {
84
            $new->limit = $limit;
85
            $new->itemsCache = new CachedCollection();
86
        }
87
        return $new;
88
    }
89
90
    /**
91
     * @psalm-mutation-free
92
     */
93
    public function withOffset(int $offset): self
94
    {
95
        $new = clone $this;
96
        if ($new->offset !== $offset) {
97
            $new->offset = $offset;
98
            $new->itemsCache = new CachedCollection();
99
        }
100
        return $new;
101
    }
102
103
    /**
104
     * @psalm-mutation-free
105
     */
106
    public function withSort(?Sort $sorting): self
107
    {
108
        $new = clone $this;
109
        if ($new->sorting !== $sorting) {
110
            $new->sorting = $sorting;
111
            $new->itemsCache = new CachedCollection();
112
            $new->oneItemCache = new CachedCollection();
113
        }
114
        return $new;
115
    }
116
117
    /**
118
     * @psalm-mutation-free
119
     */
120
    public function withFilter(FilterInterface $filter): self
121
    {
122
        $new = clone $this;
123
        if ($new->filter !== $filter) {
124
            $new->filter = $filter;
125
            $new->itemsCache = new CachedCollection();
126
            $new->oneItemCache = new CachedCollection();
127
        }
128
        return $new;
129
    }
130
131
    /**
132
     * @psalm-mutation-free
133
     */
134
    public function withFilterProcessors(FilterProcessorInterface ...$filterProcessors): self
135
    {
136
        $new = clone $this;
137
        /** @psalm-suppress ImpureMethodCall */
138
        $new->setFilterProcessors(...$filterProcessors);
139
        /** @psalm-suppress ImpureMethodCall */
140
        $new->resetCountCache();
141
        $new->itemsCache = new CachedCollection();
142
        $new->oneItemCache = new CachedCollection();
143
        return $new;
144
    }
145
146
    public function count(): int
147
    {
148
        return $this->countCache->getCount();
149
    }
150
151
    public function read(): iterable
152
    {
153
        if ($this->itemsCache->getCollection() === null) {
154
            $query = $this->buildQuery();
155
            $this->itemsCache->setCollection($query->fetchAll());
156
        }
157
        return $this->itemsCache->getCollection();
158
    }
159
160
    /**
161
     * @return mixed
162
     */
163
    public function readOne()
164
    {
165
        if (!$this->oneItemCache->isCollected()) {
166
            $item = $this->itemsCache->isCollected()
167
                // get first item from cached collection
168
                ? $this->itemsCache->getGenerator()->current()
169
                // read data with limit 1
170
                : $this->withLimit(1)->getIterator()->current();
171
            $this->oneItemCache->setCollection($item === null ? [] : [$item]);
172
        }
173
174
        return $this->oneItemCache->getGenerator()->current();
175
    }
176
177
    /**
178
     * Get Iterator without caching
179
     */
180
    public function getIterator(): Generator
181
    {
182
        yield from $this->itemsCache->getCollection() ?? $this->buildQuery()->getIterator();
183
    }
184
185
    public function getSql(): string
186
    {
187
        return $this->buildQuery()->sqlStatement();
188
    }
189
190
    private function setFilterProcessors(FilterProcessorInterface ...$filterProcessors): void
191
    {
192
        $processors = [];
193
        foreach ($filterProcessors as $filterProcessor) {
194
            if ($filterProcessor instanceof QueryBuilderProcessor) {
195
                $processors[$filterProcessor->getOperator()] = $filterProcessor;
196
            }
197
        }
198
        $this->filterProcessors = array_merge($this->filterProcessors, $processors);
199
    }
200
201
    private function buildQuery()
202
    {
203
        $newQuery = clone $this->query;
204
        if ($this->offset !== null) {
205
            $newQuery->offset($this->offset);
206
        }
207
        if ($this->sorting !== null) {
208
            $newQuery->orderBy($this->sorting->getOrder());
209
        }
210
        if ($this->limit !== null) {
211
            $newQuery->limit($this->limit);
212
        }
213
        if ($this->filter !== null) {
214
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
215
        }
216
        return $newQuery;
217
    }
218
219
    private function makeFilterClosure(FilterInterface $filter): Closure
220
    {
221
        return function (QueryBuilder $select) use ($filter) {
222
            $filterArray = $filter->toArray();
223
            $operation = array_shift($filterArray);
224
            $arguments = $filterArray;
225
226
            if (!array_key_exists($operation, $this->filterProcessors)) {
227
                throw new RuntimeException(sprintf('Filter operator "%s" is not supported.', $operation));
228
            }
229
            /** @psalm-var QueryBuilderProcessor $processor */
230
            $processor = $this->filterProcessors[$operation];
231
            $select->where(...$processor->getAsWhereArguments($arguments, $this->filterProcessors));
232
        };
233
    }
234
235
    private function resetCountCache(): void
236
    {
237
        $newQuery = clone $this->query;
238
        if ($this->filter !== null) {
239
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
240
        }
241
        $this->countCache = new CachedCount($newQuery);
242
    }
243
}
244