Passed
Pull Request — master (#69)
by Aleksei
12:08
created

SelectDataReader::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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