Passed
Push — master ( e33381...9472d4 )
by Alexander
05:13 queued 03:40
created

IterableDataReader::withOffset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Data\Reader\Iterable;
6
7
use Generator;
8
use Traversable;
9
use Yiisoft\Arrays\ArraySorter;
10
use Yiisoft\Data\Reader\DataReaderInterface;
11
use Yiisoft\Data\Reader\Filter\FilterInterface;
12
use Yiisoft\Data\Reader\Iterable\Processor\All;
13
use Yiisoft\Data\Reader\Iterable\Processor\Any;
14
use Yiisoft\Data\Reader\Iterable\Processor\Equals;
15
use Yiisoft\Data\Reader\Iterable\Processor\GreaterThan;
16
use Yiisoft\Data\Reader\Iterable\Processor\GreaterThanOrEqual;
17
use Yiisoft\Data\Reader\Iterable\Processor\In;
18
use Yiisoft\Data\Reader\Iterable\Processor\LessThan;
19
use Yiisoft\Data\Reader\Iterable\Processor\LessThanOrEqual;
20
use Yiisoft\Data\Reader\Iterable\Processor\Like;
21
use Yiisoft\Data\Reader\Iterable\Processor\Not;
22
use Yiisoft\Data\Reader\Filter\FilterProcessorInterface;
23
use Yiisoft\Data\Reader\Iterable\Processor\IterableProcessorInterface;
24
use Yiisoft\Data\Reader\Sort;
25
26
/**
27
 * @psalm-immutable
28
 *
29
 * @template TKey as array-key
30
 * @template TValue
31
 *
32
 * @template-implements DataReaderInterface<TKey, TValue>
33
 */
34
class IterableDataReader implements DataReaderInterface
35
{
36
    /**
37
     * @psalm-var iterable<TKey, TValue>
38
     */
39
    protected iterable $data;
40
    private ?Sort $sort = null;
41
    private ?FilterInterface $filter = null;
42
    private ?int $limit = null;
43
    private int $offset = 0;
44
45
    private array $filterProcessors = [];
46
47
    /**
48
     * psalm-param iterable<TKey, TValue> $data
49
     */
50 78
    public function __construct(iterable $data)
51
    {
52 78
        $this->data = $data;
53 78
        $this->filterProcessors = $this->withFilterProcessors(
54 78
            new All(),
55 78
            new Any(),
56 78
            new Equals(),
57 78
            new GreaterThan(),
58 78
            new GreaterThanOrEqual(),
59 78
            new In(),
60 78
            new LessThan(),
61 78
            new LessThanOrEqual(),
62 78
            new Like(),
63 78
            new Not()
64 78
        )->filterProcessors;
65 78
    }
66
67 37
    public function withSort(?Sort $sort): self
68
    {
69 37
        $new = clone $this;
70 37
        $new->sort = $sort;
71 37
        return $new;
72
    }
73
74 31
    public function getSort(): ?Sort
75
    {
76 31
        return $this->sort;
77
    }
78
79
    /**
80
     * Sorts data items according to the given sort definition.
81
     * @param iterable $items the items to be sorted
82
     * @param Sort $sort the sort definition
83
     * @return iterable the sorted items
84
     */
85 33
    private function sortItems(iterable $items, Sort $sort): iterable
86
    {
87 33
        $criteria = $sort->getCriteria();
88 33
        if ($criteria !== []) {
89 33
            $items = $this->iterableToArray($items);
90 33
            ArraySorter::multisort($items, array_keys($criteria), array_values($criteria));
91
        }
92
93 33
        return $items;
94
    }
95
96 20
    protected function matchFilter(array $item, array $filter): bool
97
    {
98 20
        $operation = array_shift($filter);
99 20
        $arguments = $filter;
100
101 20
        $processor = $this->filterProcessors[$operation] ?? null;
102 20
        if ($processor === null) {
103 1
            throw new \RuntimeException(sprintf('Operation "%s" is not supported', $operation));
104
        }
105
        /* @var $processor IterableProcessorInterface */
106 19
        return $processor->match($item, $arguments, $this->filterProcessors);
107
    }
108
109 23
    public function withFilter(?FilterInterface $filter): self
110
    {
111 23
        $new = clone $this;
112 23
        $new->filter = $filter;
113 23
        return $new;
114
    }
115
116 37
    public function withLimit(int $limit): self
117
    {
118 37
        if ($limit < 0) {
119
            throw new \InvalidArgumentException('$limit must not be less than 0.');
120
        }
121 37
        $new = clone $this;
122 37
        $new->limit = $limit;
123 37
        return $new;
124
    }
125
126 65
    public function read(): array
127
    {
128 65
        $filter = null;
129 65
        if ($this->filter !== null) {
130
            /** @psalm-suppress ImpureMethodCall */
131 22
            $filter = $this->filter->toArray();
132
        }
133
134 65
        $data = [];
135 65
        $skipped = 0;
136
137 65
        $sortedData = $this->sort === null
138 32
            ? $this->data
139 65
            : $this->sortItems($this->data, $this->sort);
140
141 65
        foreach ($sortedData as $item) {
142
            // do not return more than limit items
143 60
            if ($this->limit !== null && count($data) === $this->limit) {
144 16
                break;
145
            }
146
147
            // skip offset items
148 60
            if ($skipped < $this->offset) {
149 4
                ++$skipped;
150 4
                continue;
151
            }
152
153
            // filter items
154 60
            if ($filter === null || $this->matchFilter($item, $filter)) {
155 59
                $data[] = $item;
156
            }
157
        }
158
159 64
        return $data;
160
    }
161
162 2
    public function readOne()
163
    {
164
        /** @psalm-suppress ImpureMethodCall */
165 2
        return $this->withLimit(1)->getIterator()->current();
166
    }
167
168
    /**
169
     * @psalm-return Generator<TValue>
170
     */
171 2
    public function getIterator(): Generator
172
    {
173 2
        yield from $this->read();
174
    }
175
176 6
    public function withOffset(int $offset): self
177
    {
178 6
        $new = clone $this;
179 6
        $new->offset = $offset;
180 6
        return $new;
181
    }
182
183 17
    public function count(): int
184
    {
185 17
        return count($this->read());
186
    }
187
188 33
    private function iterableToArray(iterable $iterable): array
189
    {
190 33
        return $iterable instanceof Traversable ? iterator_to_array($iterable, true) : (array)$iterable;
191
    }
192
193 78
    public function withFilterProcessors(FilterProcessorInterface ...$filterProcessors): self
194
    {
195 78
        $new = clone $this;
196 78
        $processors = [];
197 78
        foreach ($filterProcessors as $filterProcessor) {
198 78
            if ($filterProcessor instanceof IterableProcessorInterface) {
199 78
                $processors[$filterProcessor->getOperator()] = $filterProcessor;
200
            }
201
        }
202 78
        $new->filterProcessors = array_merge($this->filterProcessors, $processors);
203 78
        return $new;
204
    }
205
}
206