Passed
Push — master ( 6a1165...134c40 )
by Alexander
01:57
created

SelectDataReader::read()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 8
ccs 0
cts 6
cp 0
crap 6
rs 10
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
    public function withLimit(int $limit): self
74
    {
75
        $clone = clone $this;
76
        $clone->setLimit($limit);
77
        return $clone;
78
    }
79
80
    public function withOffset(int $offset): self
81
    {
82
        $clone = clone $this;
83
        $clone->setOffset($offset);
84
        return $clone;
85
    }
86
87
    public function withSort(?Sort $sorting): self
88
    {
89
        $clone = clone $this;
90
        $clone->setSort($sorting);
91
        return $clone;
92
    }
93
94
    public function withFilter(FilterInterface $filter): self
95
    {
96
        $clone = clone $this;
97
        $clone->setFilter($filter);
98
        return $clone;
99
    }
100
101
    public function withFilterProcessors(FilterProcessorInterface ...$filterProcessors): self
102
    {
103
        $clone = clone $this;
104
        $clone->setFilterProcessors(...$filterProcessors);
105
        $clone->resetCountCache();
106
        $clone->itemsCache = new CachedCollection();
107
        $clone->oneItemCache = new CachedCollection();
108
        return $clone;
109
    }
110
111
    public function count(): int
112
    {
113
        return $this->countCache->getCount();
114
    }
115
116
    public function read(): iterable
117
    {
118
        if ($this->itemsCache->getCollection() !== null) {
119
            return $this->itemsCache->getCollection();
120
        }
121
        $query = $this->buildQuery();
122
        $this->itemsCache->setCollection($query->fetchAll());
123
        return $this->itemsCache->getCollection();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->itemsCache->getCollection() targeting Yiisoft\Yii\Cycle\DataRe...ection::getCollection() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug Best Practice introduced by
The expression return $this->itemsCache->getCollection() returns the type null which is incompatible with the type-hinted return iterable.
Loading history...
124
    }
125
126
    /**
127
     * @return mixed
128
     */
129
    public function readOne()
130
    {
131
        if (!$this->oneItemCache->isCollected()) {
132
            $item = $this->itemsCache->isCollected()
133
                // get first item from cached collection
134
                ? $this->itemsCache->getGenerator()->current()
135
                // read data with limit 1
136
                : $this->withLimit(1)->getIterator()->current();
137
            $this->oneItemCache->setCollection($item === null ? [] : [$item]);
138
        }
139
140
        return $this->oneItemCache->getGenerator()->current();
141
    }
142
143
    /**
144
     * Get Iterator without caching
145
     */
146
    public function getIterator(): \Generator
147
    {
148
        if ($this->itemsCache->getCollection() !== null) {
149
            yield from $this->itemsCache->getCollection();
150
        } else {
151
            yield from $this->buildQuery()->getIterator();
152
        }
153
    }
154
155
    /**
156
     * Convert to SQL string
157
     */
158
    public function __toString(): string
159
    {
160
        return $this->getSql();
161
    }
162
163
    public function getSql(): string
164
    {
165
        return $this->buildQuery()->sqlStatement();
166
    }
167
168
    private function setSort(?Sort $sorting): void
169
    {
170
        if ($this->sorting !== $sorting) {
171
            $this->sorting = $sorting;
172
            $this->itemsCache = new CachedCollection();
173
            $this->oneItemCache = new CachedCollection();
174
        }
175
    }
176
177
    private function setLimit(?int $limit): void
178
    {
179
        if ($this->limit !== $limit) {
180
            $this->limit = $limit;
181
            $this->itemsCache = new CachedCollection();
182
        }
183
    }
184
185
    private function setOffset(?int $offset): void
186
    {
187
        if ($this->offset !== $offset) {
188
            $this->offset = $offset;
189
            $this->itemsCache = new CachedCollection();
190
        }
191
    }
192
193
    private function setFilter(FilterInterface $filter): void
194
    {
195
        if ($this->filter !== $filter) {
196
            $this->filter = $filter;
197
            $this->itemsCache = new CachedCollection();
198
            $this->oneItemCache = new CachedCollection();
199
        }
200
    }
201
202
    private function setFilterProcessors(FilterProcessorInterface ...$filterProcessors): void
203
    {
204
        $processors = [];
205
        foreach ($filterProcessors as $filterProcessor) {
206
            if ($filterProcessor instanceof QueryBuilderProcessor) {
207
                $processors[$filterProcessor->getOperator()] = $filterProcessor;
208
            }
209
        }
210
        $this->filterProcessors = array_merge($this->filterProcessors, $processors);
211
    }
212
213
    /**
214
     * @return Select|SelectQuery
215
     */
216
    private function buildQuery()
217
    {
218
        $newQuery = clone $this->query;
219
        if ($this->offset !== null) {
220
            $newQuery->offset($this->offset);
221
        }
222
        if ($this->sorting !== null) {
223
            $newQuery->orderBy($this->sorting->getOrder());
224
        }
225
        if ($this->limit !== null) {
226
            $newQuery->limit($this->limit);
227
        }
228
        if ($this->filter !== null) {
229
            $newQuery->andWhere($this->makeFilterClosure());
230
        }
231
        return $newQuery;
232
    }
233
    private function makeFilterClosure(): Closure
234
    {
235
        return function (QueryBuilder $select) {
236
            $filter = $this->filter->toArray();
0 ignored issues
show
Bug introduced by
The method toArray() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

236
            /** @scrutinizer ignore-call */ 
237
            $filter = $this->filter->toArray();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
237
            $operation = array_shift($filter);
238
            $arguments = $filter;
239
240
            $processor = $this->filterProcessors[$operation] ?? null;
241
            if ($processor === null) {
242
                throw new \RuntimeException(sprintf('Filter operator "%s" is not supported.', $operation));
243
            }
244
            $select->where(...$processor->getAsWhereArguments($arguments, $this->filterProcessors));
245
        };
246
    }
247
    private function resetCountCache(): void
248
    {
249
        $newQuery = clone $this->query;
250
        if ($this->filter !== null) {
251
            $newQuery->andWhere($this->makeFilterClosure());
252
        }
253
        $this->countCache = new CachedCount($newQuery);
254
    }
255
}
256