Passed
Push — master ( 099506...a7575d )
by Evgeniy
02:33
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 InvalidArgumentException;
9
use RuntimeException;
10
use Traversable;
11
use Yiisoft\Arrays\ArrayHelper;
12
use Yiisoft\Data\Reader\DataReaderInterface;
13
use Yiisoft\Data\Reader\Filter\FilterInterface;
14
use Yiisoft\Data\Reader\Filter\FilterProcessorInterface;
15
use Yiisoft\Data\Reader\FilterDataValidationHelper;
16
use Yiisoft\Data\Reader\Iterable\Processor\All;
17
use Yiisoft\Data\Reader\Iterable\Processor\Any;
18
use Yiisoft\Data\Reader\Iterable\Processor\Between;
19
use Yiisoft\Data\Reader\Iterable\Processor\Equals;
20
use Yiisoft\Data\Reader\Iterable\Processor\EqualsEmpty;
21
use Yiisoft\Data\Reader\Iterable\Processor\EqualsNull;
22
use Yiisoft\Data\Reader\Iterable\Processor\GreaterThan;
23
use Yiisoft\Data\Reader\Iterable\Processor\GreaterThanOrEqual;
24
use Yiisoft\Data\Reader\Iterable\Processor\In;
25
use Yiisoft\Data\Reader\Iterable\Processor\IterableProcessorInterface;
26
use Yiisoft\Data\Reader\Iterable\Processor\LessThan;
27
use Yiisoft\Data\Reader\Iterable\Processor\LessThanOrEqual;
28
use Yiisoft\Data\Reader\Iterable\Processor\Like;
29
use Yiisoft\Data\Reader\Iterable\Processor\Not;
30
use Yiisoft\Data\Reader\Sort;
31
32
use function array_merge;
33
use function array_shift;
34
use function count;
35
use function is_string;
36
use function iterator_to_array;
37
use function sprintf;
38
use function uasort;
39
40
/**
41
 * @template TKey as array-key
42
 * @template TValue
43
 *
44
 * @implements DataReaderInterface<TKey, TValue>
45
 */
46
class IterableDataReader implements DataReaderInterface
47
{
48
    /**
49
     * @psalm-var iterable<TKey, TValue>
50
     */
51
    protected iterable $data;
52
    private ?Sort $sort = null;
53
    private ?FilterInterface $filter = null;
54
    private int $limit = 0;
55
    private int $offset = 0;
56
57
    /**
58
     * @psalm-var array<string, FilterProcessorInterface&IterableProcessorInterface>
59
     */
60
    private array $filterProcessors = [];
61
62
    /**
63
     * @psalm-param iterable<TKey, TValue> $data
64
     */
65 81
    public function __construct(iterable $data)
66
    {
67 81
        $this->data = $data;
68 81
        $this->filterProcessors = $this->withFilterProcessors(
69 81
            new All(),
70 81
            new Any(),
71 81
            new Between(),
72 81
            new Equals(),
73 81
            new EqualsEmpty(),
74 81
            new EqualsNull(),
75 81
            new GreaterThan(),
76 81
            new GreaterThanOrEqual(),
77 81
            new In(),
78 81
            new LessThan(),
79 81
            new LessThanOrEqual(),
80 81
            new Like(),
81 81
            new Not()
82 81
        )->filterProcessors;
83 81
    }
84
85 81
    public function withFilterProcessors(FilterProcessorInterface ...$filterProcessors): self
86
    {
87 81
        $new = clone $this;
88 81
        $processors = [];
89
90 81
        foreach ($filterProcessors as $filterProcessor) {
91 81
            if ($filterProcessor instanceof IterableProcessorInterface) {
92 81
                $processors[$filterProcessor->getOperator()] = $filterProcessor;
93
            }
94
        }
95
96 81
        $new->filterProcessors = array_merge($this->filterProcessors, $processors);
97 81
        return $new;
98
    }
99
100 24
    public function withFilter(?FilterInterface $filter): self
101
    {
102 24
        $new = clone $this;
103 24
        $new->filter = $filter;
104 24
        return $new;
105
    }
106
107 38
    public function withLimit(int $limit): self
108
    {
109 38
        if ($limit < 0) {
110 1
            throw new InvalidArgumentException('The limit must not be less than 0.');
111
        }
112
113 37
        $new = clone $this;
114 37
        $new->limit = $limit;
115 37
        return $new;
116
    }
117
118 7
    public function withOffset(int $offset): self
119
    {
120 7
        $new = clone $this;
121 7
        $new->offset = $offset;
122 7
        return $new;
123
    }
124
125 40
    public function withSort(?Sort $sort): self
126
    {
127 40
        $new = clone $this;
128 40
        $new->sort = $sort;
129 40
        return $new;
130
    }
131
132
    /**
133
     * @psalm-return Generator<TValue>
134
     */
135 3
    public function getIterator(): Generator
136
    {
137 3
        yield from $this->read();
138
    }
139
140 33
    public function getSort(): ?Sort
141
    {
142 33
        return $this->sort;
143
    }
144
145 17
    public function count(): int
146
    {
147 17
        return count($this->read());
148
    }
149
150 67
    public function read(): array
151
    {
152 67
        $data = [];
153 67
        $skipped = 0;
154 67
        $filter = $this->filter === null ? null : $this->filter->toArray();
155 67
        $sortedData = $this->sort === null ? $this->data : $this->sortItems($this->data, $this->sort);
156
157
        /**
158
         * @var int|string $key
159
         * @var array $item
160
         */
161 67
        foreach ($sortedData as $key => $item) {
162
            // Do not return more than limit items.
163 62
            if ($this->limit > 0 && count($data) === $this->limit) {
164 16
                break;
165
            }
166
167
            // Skip offset items.
168 62
            if ($skipped < $this->offset) {
169 4
                ++$skipped;
170 4
                continue;
171
            }
172
173
            // Filter items.
174 62
            if ($filter === null || $this->matchFilter($item, $filter)) {
175 60
                $data[$key] = $item;
176
            }
177
        }
178
179 65
        return $data;
180
    }
181
182
    /**
183
     * @return mixed
184
     */
185 3
    public function readOne()
186
    {
187 3
        return $this->withLimit(1)->getIterator()->current();
188
    }
189
190 21
    protected function matchFilter(array $item, array $filter): bool
191
    {
192 21
        $operation = array_shift($filter);
193 21
        $arguments = $filter;
194
195 21
        if (!is_string($operation)) {
196
            throw new RuntimeException(sprintf(
197
                'The operator should be string. The %s is received.',
198
                FilterDataValidationHelper::getValueType($operation),
199
            ));
200
        }
201
202 21
        if ($operation === '') {
203 1
            throw new RuntimeException('The operator string cannot be empty.');
204
        }
205
206 20
        $processor = $this->filterProcessors[$operation] ?? null;
207
208 20
        if ($processor === null) {
209 1
            throw new RuntimeException(sprintf('Operation "%s" is not supported.', $operation));
210
        }
211
212 19
        return $processor->match($item, $arguments, $this->filterProcessors);
213
    }
214
215
    /**
216
     * Sorts data items according to the given sort definition.
217
     *
218
     * @param iterable $items The items to be sorted.
219
     * @param Sort $sort The sort definition.
220
     *
221
     * @return iterable The sorted items.
222
     */
223 34
    private function sortItems(iterable $items, Sort $sort): iterable
224
    {
225 34
        $criteria = $sort->getCriteria();
226
227 34
        if ($criteria !== []) {
228 34
            $items = $this->iterableToArray($items);
229 34
            uasort(
230 34
                $items,
231
                /**
232
                 * @param mixed $itemA
233
                 * @param mixed $itemB
234
                 */
235 34
                static function ($itemA, $itemB) use ($criteria) {
236 26
                    foreach ($criteria as $key => $order) {
237
                        /** @psalm-suppress MixedArgument, MixedAssignment */
238 26
                        $valueA = ArrayHelper::getValue($itemA, $key);
239
                        /** @psalm-suppress MixedArgument, MixedAssignment */
240 26
                        $valueB = ArrayHelper::getValue($itemB, $key);
241
242 26
                        if ($valueB === $valueA) {
243
                            continue;
244
                        }
245
246 26
                        return ($valueA > $valueB xor $order === SORT_DESC) ? 1 : -1;
247
                    }
248
249
                    return 0;
250 34
                }
251
            );
252
        }
253
254 34
        return $items;
255
    }
256
257 34
    private function iterableToArray(iterable $iterable): array
258
    {
259
        /** @psalm-suppress RedundantCast */
260 34
        return $iterable instanceof Traversable ? iterator_to_array($iterable, true) : (array) $iterable;
261
    }
262
}
263