Passed
Push — master ( 5c5da0...546803 )
by Alexander
02:27
created

IterableDataReader   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Test Coverage

Coverage 99.12%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 96
c 2
b 0
f 0
dl 0
loc 249
ccs 113
cts 114
cp 0.9912
rs 9.76
wmc 33

15 Methods

Rating   Name   Duplication   Size   Complexity  
A withFilter() 0 5 1
A withSort() 0 5 1
A getSort() 0 3 1
A readOne() 0 6 1
A withFilterHandlers() 0 8 1
A withLimit() 0 9 2
A count() 0 3 1
A withOffset() 0 5 1
A sortItems() 0 28 5
A getIterator() 0 3 1
A matchFilter() 0 25 4
A iterableToArray() 0 3 2
B read() 0 26 8
A prepareFilterHandlers() 0 18 3
A __construct() 0 16 1
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\DataReaderException;
13
use Yiisoft\Data\Reader\DataReaderInterface;
14
use Yiisoft\Data\Reader\FilterHandlerInterface;
15
use Yiisoft\Data\Reader\FilterInterface;
16
use Yiisoft\Data\Reader\Iterable\FilterHandler\AllHandler;
17
use Yiisoft\Data\Reader\Iterable\FilterHandler\AnyHandler;
18
use Yiisoft\Data\Reader\Iterable\FilterHandler\BetweenHandler;
19
use Yiisoft\Data\Reader\Iterable\FilterHandler\EqualsHandler;
20
use Yiisoft\Data\Reader\Iterable\FilterHandler\EqualsEmptyHandler;
21
use Yiisoft\Data\Reader\Iterable\FilterHandler\EqualsNullHandler;
22
use Yiisoft\Data\Reader\Iterable\FilterHandler\GreaterThanHandler;
23
use Yiisoft\Data\Reader\Iterable\FilterHandler\GreaterThanOrEqualHandler;
24
use Yiisoft\Data\Reader\Iterable\FilterHandler\InHandler;
25
use Yiisoft\Data\Reader\Iterable\FilterHandler\LessThanHandler;
26
use Yiisoft\Data\Reader\Iterable\FilterHandler\LessThanOrEqualHandler;
27
use Yiisoft\Data\Reader\Iterable\FilterHandler\LikeHandler;
28
use Yiisoft\Data\Reader\Iterable\FilterHandler\NotHandler;
29
use Yiisoft\Data\Reader\Sort;
30
31
use function array_merge;
32
use function array_shift;
33
use function count;
34
use function is_string;
35
use function iterator_to_array;
36
use function sprintf;
37
use function uasort;
38
39
/**
40
 * Iterable data reader takes an iterable data as a source and can:
41
 *
42
 * - Limit items read
43
 * - Skip N items from the beginning
44
 * - Sort items
45
 * - Form a filter criteria with {@see FilterInterface}
46
 * - Post-filter items with {@see IterableFilterHandlerInterface}
47
 *
48
 * @template TKey as array-key
49
 * @template TValue as array|object
50
 *
51
 * @implements DataReaderInterface<TKey, TValue>
52
 */
53
class IterableDataReader implements DataReaderInterface
54
{
55
    private ?Sort $sort = null;
56
    private ?FilterInterface $filter = null;
57
    private int $limit = 0;
58
    private int $offset = 0;
59
60
    /**
61
     * @psalm-var array<string, IterableFilterHandlerInterface>
62
     */
63
    private array $iterableFilterHandlers = [];
64
65
    /**
66
     * @param iterable $data Data to iterate.
67
     * @psalm-param iterable<TKey, TValue> $data
68
     */
69 108
    public function __construct(protected iterable $data)
70
    {
71 108
        $this->iterableFilterHandlers = $this->prepareFilterHandlers([
72 108
            new AllHandler(),
73 108
            new AnyHandler(),
74 108
            new BetweenHandler(),
75 108
            new EqualsHandler(),
76 108
            new EqualsEmptyHandler(),
77 108
            new EqualsNullHandler(),
78 108
            new GreaterThanHandler(),
79 108
            new GreaterThanOrEqualHandler(),
80 108
            new InHandler(),
81 108
            new LessThanHandler(),
82 108
            new LessThanOrEqualHandler(),
83 108
            new LikeHandler(),
84 108
            new NotHandler(),
85 108
        ]);
86
    }
87
88 3
    public function withFilterHandlers(FilterHandlerInterface ...$iterableFilterHandlers): static
89
    {
90 3
        $new = clone $this;
91 3
        $new->iterableFilterHandlers = array_merge(
92 3
            $this->iterableFilterHandlers,
93 3
            $this->prepareFilterHandlers($iterableFilterHandlers)
94 3
        );
95 2
        return $new;
96
    }
97
98 31
    public function withFilter(?FilterInterface $filter): static
99
    {
100 31
        $new = clone $this;
101 31
        $new->filter = $filter;
102 31
        return $new;
103
    }
104
105 43
    public function withLimit(int $limit): static
106
    {
107 43
        if ($limit < 0) {
108 1
            throw new InvalidArgumentException('The limit must not be less than 0.');
109
        }
110
111 42
        $new = clone $this;
112 42
        $new->limit = $limit;
113 42
        return $new;
114
    }
115
116 7
    public function withOffset(int $offset): static
117
    {
118 7
        $new = clone $this;
119 7
        $new->offset = $offset;
120 7
        return $new;
121
    }
122
123 48
    public function withSort(?Sort $sort): static
124
    {
125 48
        $new = clone $this;
126 48
        $new->sort = $sort;
127 48
        return $new;
128
    }
129
130
    /**
131
     * @psalm-return Generator<array-key, TValue, mixed, void>
132
     */
133 3
    public function getIterator(): Generator
134
    {
135 3
        yield from $this->read();
136
    }
137
138 41
    public function getSort(): ?Sort
139
    {
140 41
        return $this->sort;
141
    }
142
143 21
    public function count(): int
144
    {
145 21
        return count($this->read());
146
    }
147
148
    /**
149
     * @psalm-return array<TKey, TValue>
150
     */
151 81
    public function read(): array
152
    {
153 81
        $data = [];
154 81
        $skipped = 0;
155 81
        $filter = $this->filter?->toCriteriaArray();
156 81
        $sortedData = $this->sort === null ? $this->data : $this->sortItems($this->data, $this->sort);
157
158 81
        foreach ($sortedData as $key => $item) {
159
            // Do not return more than limit items.
160 76
            if ($this->limit > 0 && count($data) === $this->limit) {
161 20
                break;
162
            }
163
164
            // Skip offset items.
165 76
            if ($skipped < $this->offset) {
166 4
                ++$skipped;
167 4
                continue;
168
            }
169
170
            // Filter items.
171 76
            if ($filter === null || $this->matchFilter($item, $filter)) {
172 74
                $data[$key] = $item;
173
            }
174
        }
175
176 79
        return $data;
177
    }
178
179 3
    public function readOne(): array|object|null
180
    {
181 3
        return $this
182 3
            ->withLimit(1)
183 3
            ->getIterator()
184 3
            ->current();
185
    }
186
187
    /**
188
     * Return whether an item matches iterable filter.
189
     *
190
     * @param array|object $item Item to check.
191
     * @param array $filter Filter.
192
     *
193
     * @return bool Whether an item matches iterable filter.
194
     */
195 36
    protected function matchFilter(array|object $item, array $filter): bool
196
    {
197 36
        $operation = array_shift($filter);
198 36
        $arguments = $filter;
199
200 36
        if (!is_string($operation)) {
201 8
            throw new RuntimeException(
202 8
                sprintf(
203 8
                    'The operator should be string. The %s is received.',
204 8
                    get_debug_type($operation),
205 8
                )
206 8
            );
207
        }
208
209 28
        if ($operation === '') {
210 1
            throw new RuntimeException('The operator string cannot be empty.');
211
        }
212
213 27
        $processor = $this->iterableFilterHandlers[$operation] ?? null;
214
215 27
        if ($processor === null) {
216 1
            throw new RuntimeException(sprintf('Operation "%s" is not supported.', $operation));
217
        }
218
219 26
        return $processor->match($item, $arguments, $this->iterableFilterHandlers);
220
    }
221
222
    /**
223
     * Sorts data items according to the given sort definition.
224
     *
225
     * @param iterable $items The items to be sorted.
226
     * @param Sort $sort The sort definition.
227
     *
228
     * @return array The sorted items.
229
     *
230
     * @psalm-param iterable<TKey, TValue> $items
231
     * @psalm-return iterable<TKey, TValue>
232
     */
233 40
    private function sortItems(iterable $items, Sort $sort): iterable
234
    {
235 40
        $criteria = $sort->getCriteria();
236
237 40
        if ($criteria !== []) {
238 40
            $items = $this->iterableToArray($items);
239 40
            uasort(
240 40
                $items,
241 40
                static function (array|object $itemA, array|object $itemB) use ($criteria) {
242 32
                    foreach ($criteria as $key => $order) {
243
                        /** @psalm-var mixed $valueA */
244 32
                        $valueA = ArrayHelper::getValue($itemA, $key);
245
                        /** @psalm-var mixed $valueB */
246 32
                        $valueB = ArrayHelper::getValue($itemB, $key);
247
248 32
                        if ($valueB === $valueA) {
249 1
                            continue;
250
                        }
251
252 32
                        return ($valueA > $valueB xor $order === SORT_DESC) ? 1 : -1;
253
                    }
254
255
                    return 0;
256 40
                }
257 40
            );
258
        }
259
260 40
        return $items;
261
    }
262
263
    /**
264
     * @param FilterHandlerInterface[] $filterHandlers
265
     *
266
     * @return IterableFilterHandlerInterface[]
267
     * @psalm-return array<string, IterableFilterHandlerInterface>
268
     */
269 108
    private function prepareFilterHandlers(array $filterHandlers): array
270
    {
271 108
        $result = [];
272
273 108
        foreach ($filterHandlers as $filterHandler) {
274 108
            if (!$filterHandler instanceof IterableFilterHandlerInterface) {
275 1
                throw new DataReaderException(
276 1
                    sprintf(
277 1
                        '%s::withFilterHandlers() accepts instances of %s only.',
278 1
                        static::class,
279 1
                        IterableFilterHandlerInterface::class
280 1
                    )
281 1
                );
282
            }
283 108
            $result[$filterHandler->getOperator()] = $filterHandler;
284
        }
285
286 108
        return $result;
287
    }
288
289
    /**
290
     * Convert iterable to array.
291
     *
292
     * @param iterable $iterable Iterable to convert.
293
     *
294
     * @psalm-param iterable<TKey, TValue> $iterable
295
     *
296
     * @return array Resulting array.
297
     * @psalm-return array<TKey, TValue>
298
     */
299 40
    private function iterableToArray(iterable $iterable): array
300
    {
301 40
        return $iterable instanceof Traversable ? iterator_to_array($iterable, true) : $iterable;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $iterable instanc...able, true) : $iterable could return the type iterable which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
302
    }
303
}
304