Passed
Push — master ( 6c8383...fe7e74 )
by Alexander
04:38 queued 02:11
created

IterableDataReader::count()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
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\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
final 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 110
    public function __construct(private iterable $data)
70
    {
71 110
        $this->iterableFilterHandlers = $this->prepareFilterHandlers([
72 110
            new AllHandler(),
73 110
            new AnyHandler(),
74 110
            new BetweenHandler(),
75 110
            new EqualsHandler(),
76 110
            new EqualsEmptyHandler(),
77 110
            new EqualsNullHandler(),
78 110
            new GreaterThanHandler(),
79 110
            new GreaterThanOrEqualHandler(),
80 110
            new InHandler(),
81 110
            new LessThanHandler(),
82 110
            new LessThanOrEqualHandler(),
83 110
            new LikeHandler(),
84 110
            new NotHandler(),
85 110
        ]);
86
    }
87
88
    /**
89
     * @psalm-return $this
90
     */
91 4
    public function withFilterHandlers(FilterHandlerInterface ...$iterableFilterHandlers): static
92
    {
93 4
        $new = clone $this;
94 4
        $new->iterableFilterHandlers = array_merge(
95 4
            $this->iterableFilterHandlers,
96 4
            $this->prepareFilterHandlers($iterableFilterHandlers)
97 4
        );
98 3
        return $new;
99
    }
100
101
    /**
102
     * @psalm-return $this
103
     */
104 39
    public function withFilter(?FilterInterface $filter): static
105
    {
106 39
        $new = clone $this;
107 39
        $new->filter = $filter;
108 39
        return $new;
109
    }
110
111
    /**
112
     * @psalm-return $this
113
     */
114 44
    public function withLimit(int $limit): static
115
    {
116 44
        if ($limit < 0) {
117 1
            throw new InvalidArgumentException('The limit must not be less than 0.');
118
        }
119
120 43
        $new = clone $this;
121 43
        $new->limit = $limit;
122 43
        return $new;
123
    }
124
125
    /**
126
     * @psalm-return $this
127
     */
128 7
    public function withOffset(int $offset): static
129
    {
130 7
        $new = clone $this;
131 7
        $new->offset = $offset;
132 7
        return $new;
133
    }
134
135
    /**
136
     * @psalm-return $this
137
     */
138 49
    public function withSort(?Sort $sort): static
139
    {
140 49
        $new = clone $this;
141 49
        $new->sort = $sort;
142 49
        return $new;
143
    }
144
145
    /**
146
     * @psalm-return Generator<array-key, TValue, mixed, void>
147
     */
148 14
    public function getIterator(): Generator
149
    {
150 14
        yield from $this->read();
151
    }
152
153 41
    public function getSort(): ?Sort
154
    {
155 41
        return $this->sort;
156
    }
157
158 21
    public function count(): int
159
    {
160 21
        return count($this->read());
161
    }
162
163
    /**
164
     * @psalm-return array<TKey, TValue>
165
     */
166 91
    public function read(): array
167
    {
168 91
        $data = [];
169 91
        $skipped = 0;
170 91
        $filter = $this->filter?->toCriteriaArray();
171 91
        $sortedData = $this->sort === null ? $this->data : $this->sortItems($this->data, $this->sort);
172
173 91
        foreach ($sortedData as $key => $item) {
174
            // Do not return more than limit items.
175 86
            if ($this->limit > 0 && count($data) === $this->limit) {
176
                /** @infection-ignore-all Here continue === break */
177 20
                break;
178
            }
179
180
            // Skip offset items.
181 86
            if ($skipped < $this->offset) {
182 4
                ++$skipped;
183 4
                continue;
184
            }
185
186
            // Filter items.
187 86
            if ($filter === null || $this->matchFilter($item, $filter)) {
188 76
                $data[$key] = $item;
189
            }
190
        }
191
192 81
        return $data;
193
    }
194
195 14
    public function readOne(): array|object|null
196
    {
197
        /** @infection-ignore-all Any value more one in `withLimit()` will be ignored because returned `current()` */
198 14
        return $this
199 14
            ->withLimit(1)
200 14
            ->getIterator()
201 14
            ->current();
202
    }
203
204
    /**
205
     * Return whether an item matches iterable filter.
206
     *
207
     * @param array|object $item Item to check.
208
     * @param array $filter Filter.
209
     *
210
     * @return bool Whether an item matches iterable filter.
211
     */
212 37
    private function matchFilter(array|object $item, array $filter): bool
213
    {
214 37
        $operation = array_shift($filter);
215 37
        $arguments = $filter;
216
217 37
        if (!is_string($operation)) {
218 8
            throw new RuntimeException(
219 8
                sprintf(
220 8
                    'The operator should be string. The %s is received.',
221 8
                    get_debug_type($operation),
222 8
                )
223 8
            );
224
        }
225
226 29
        if ($operation === '') {
227 1
            throw new RuntimeException('The operator string cannot be empty.');
228
        }
229
230 28
        $processor = $this->iterableFilterHandlers[$operation] ?? null;
231
232 28
        if ($processor === null) {
233 1
            throw new RuntimeException(sprintf('Operation "%s" is not supported.', $operation));
234
        }
235
236 27
        return $processor->match($item, $arguments, $this->iterableFilterHandlers);
237
    }
238
239
    /**
240
     * Sorts data items according to the given sort definition.
241
     *
242
     * @param iterable $items The items to be sorted.
243
     * @param Sort $sort The sort definition.
244
     *
245
     * @return array The sorted items.
246
     *
247
     * @psalm-param iterable<TKey, TValue> $items
248
     * @psalm-return iterable<TKey, TValue>
249
     */
250 41
    private function sortItems(iterable $items, Sort $sort): iterable
251
    {
252 41
        $criteria = $sort->getCriteria();
253
254 41
        if ($criteria !== []) {
255 41
            $items = $this->iterableToArray($items);
256
            /** @infection-ignore-all */
257 41
            uasort(
258 41
                $items,
259 41
                static function (array|object $itemA, array|object $itemB) use ($criteria) {
260 33
                    foreach ($criteria as $key => $order) {
261
                        /** @psalm-var mixed $valueA */
262 33
                        $valueA = ArrayHelper::getValue($itemA, $key);
263
                        /** @psalm-var mixed $valueB */
264 33
                        $valueB = ArrayHelper::getValue($itemB, $key);
265
266 33
                        if ($valueB === $valueA) {
267 2
                            continue;
268
                        }
269
270 33
                        return ($valueA > $valueB xor $order === SORT_DESC) ? 1 : -1;
271
                    }
272
273 1
                    return 0;
274 41
                }
275 41
            );
276
        }
277
278 41
        return $items;
279
    }
280
281
    /**
282
     * @param FilterHandlerInterface[] $filterHandlers
283
     *
284
     * @return IterableFilterHandlerInterface[]
285
     * @psalm-return array<string, IterableFilterHandlerInterface>
286
     */
287 110
    private function prepareFilterHandlers(array $filterHandlers): array
288
    {
289 110
        $result = [];
290
291 110
        foreach ($filterHandlers as $filterHandler) {
292 110
            if (!$filterHandler instanceof IterableFilterHandlerInterface) {
293 1
                throw new DataReaderException(
294 1
                    sprintf(
295 1
                        '%s::withFilterHandlers() accepts instances of %s only.',
296 1
                        static::class,
297 1
                        IterableFilterHandlerInterface::class
298 1
                    )
299 1
                );
300
            }
301 110
            $result[$filterHandler->getOperator()] = $filterHandler;
302
        }
303
304 110
        return $result;
305
    }
306
307
    /**
308
     * Convert iterable to array.
309
     *
310
     * @param iterable $iterable Iterable to convert.
311
     *
312
     * @psalm-param iterable<TKey, TValue> $iterable
313
     *
314
     * @return array Resulting array.
315
     * @psalm-return array<TKey, TValue>
316
     */
317 41
    private function iterableToArray(iterable $iterable): array
318
    {
319 41
        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...
320
    }
321
}
322