IterableDataReader::withFilter()   A
last analyzed

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