Passed
Push — master ( bc229f...cc7bc9 )
by Alexander
03:55 queued 01:21
created

IterableDataReader::withSort()   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\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\All;
17
use Yiisoft\Data\Reader\Iterable\FilterHandler\Any;
18
use Yiisoft\Data\Reader\Iterable\FilterHandler\Between;
19
use Yiisoft\Data\Reader\Iterable\FilterHandler\Equals;
20
use Yiisoft\Data\Reader\Iterable\FilterHandler\EqualsEmpty;
21
use Yiisoft\Data\Reader\Iterable\FilterHandler\EqualsNull;
22
use Yiisoft\Data\Reader\Iterable\FilterHandler\GreaterThan;
23
use Yiisoft\Data\Reader\Iterable\FilterHandler\GreaterThanOrEqual;
24
use Yiisoft\Data\Reader\Iterable\FilterHandler\In;
25
use Yiisoft\Data\Reader\Iterable\FilterHandler\LessThan;
26
use Yiisoft\Data\Reader\Iterable\FilterHandler\LessThanOrEqual;
27
use Yiisoft\Data\Reader\Iterable\FilterHandler\Like;
28
use Yiisoft\Data\Reader\Iterable\FilterHandler\Not;
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 105
    public function __construct(protected iterable $data)
70
    {
71 105
        $this->iterableFilterHandlers = $this->withFilterHandlers(
72 105
            new All(),
73 105
            new Any(),
74 105
            new Between(),
75 105
            new Equals(),
76 105
            new EqualsEmpty(),
77 105
            new EqualsNull(),
78 105
            new GreaterThan(),
79 105
            new GreaterThanOrEqual(),
80 105
            new In(),
81 105
            new LessThan(),
82 105
            new LessThanOrEqual(),
83 105
            new Like(),
84 105
            new Not()
85 105
        )->iterableFilterHandlers;
86
    }
87
88 105
    public function withFilterHandlers(FilterHandlerInterface ...$iterableFilterHandlers): static
89
    {
90 105
        $new = clone $this;
91 105
        $handlers = [];
92
93 105
        foreach ($iterableFilterHandlers as $iterableFilterHandler) {
94 105
            if (!$iterableFilterHandler instanceof IterableFilterHandlerInterface) {
95 1
                $message = sprintf(
96 1
                    '%s::withFilterHandlers() accepts instances of %s only.',
97 1
                    static::class,
98 1
                    IterableFilterHandlerInterface::class
99 1
                );
100 1
                throw new DataReaderException($message);
101
            }
102 105
            $handlers[$iterableFilterHandler->getOperator()] = $iterableFilterHandler;
103
        }
104
105 105
        $new->iterableFilterHandlers = array_merge($this->iterableFilterHandlers, $handlers);
106 105
        return $new;
107
    }
108
109 28
    public function withFilter(?FilterInterface $filter): static
110
    {
111 28
        $new = clone $this;
112 28
        $new->filter = $filter;
113 28
        return $new;
114
    }
115
116 43
    public function withLimit(int $limit): static
117
    {
118 43
        if ($limit < 0) {
119 1
            throw new InvalidArgumentException('The limit must not be less than 0.');
120
        }
121
122 42
        $new = clone $this;
123 42
        $new->limit = $limit;
124 42
        return $new;
125
    }
126
127 7
    public function withOffset(int $offset): static
128
    {
129 7
        $new = clone $this;
130 7
        $new->offset = $offset;
131 7
        return $new;
132
    }
133
134 48
    public function withSort(?Sort $sort): static
135
    {
136 48
        $new = clone $this;
137 48
        $new->sort = $sort;
138 48
        return $new;
139
    }
140
141
    /**
142
     * @psalm-return Generator<array-key, TValue, mixed, void>
143
     */
144 3
    public function getIterator(): Generator
145
    {
146 3
        yield from $this->read();
147
    }
148
149 41
    public function getSort(): ?Sort
150
    {
151 41
        return $this->sort;
152
    }
153
154 21
    public function count(): int
155
    {
156 21
        return count($this->read());
157
    }
158
159
    /**
160
     * @psalm-return array<TKey, TValue>
161
     */
162 76
    public function read(): array
163
    {
164 76
        $data = [];
165 76
        $skipped = 0;
166 76
        $filter = $this->filter?->toCriteriaArray();
167 76
        $sortedData = $this->sort === null ? $this->data : $this->sortItems($this->data, $this->sort);
168
169 76
        foreach ($sortedData as $key => $item) {
170
            // Do not return more than limit items.
171 71
            if ($this->limit > 0 && count($data) === $this->limit) {
172 20
                break;
173
            }
174
175
            // Skip offset items.
176 71
            if ($skipped < $this->offset) {
177 4
                ++$skipped;
178 4
                continue;
179
            }
180
181
            // Filter items.
182 71
            if ($filter === null || $this->matchFilter($item, $filter)) {
183 69
                $data[$key] = $item;
184
            }
185
        }
186
187 74
        return $data;
188
    }
189
190 3
    public function readOne(): array|object|null
191
    {
192 3
        return $this
193 3
            ->withLimit(1)
194 3
            ->getIterator()
195 3
            ->current();
196
    }
197
198
    /**
199
     * Return whether an item matches iterable filter.
200
     *
201
     * @param array|object $item Item to check.
202
     * @param array $filter Filter.
203
     *
204
     * @return bool Whether an item matches iterable filter.
205
     */
206 33
    protected function matchFilter(array|object $item, array $filter): bool
207
    {
208 33
        $operation = array_shift($filter);
209 33
        $arguments = $filter;
210
211 33
        if (!is_string($operation)) {
212 8
            throw new RuntimeException(
213 8
                sprintf(
214 8
                    'The operator should be string. The %s is received.',
215 8
                    get_debug_type($operation),
216 8
                )
217 8
            );
218
        }
219
220 25
        if ($operation === '') {
221 1
            throw new RuntimeException('The operator string cannot be empty.');
222
        }
223
224 24
        $processor = $this->iterableFilterHandlers[$operation] ?? null;
225
226 24
        if ($processor === null) {
227 1
            throw new RuntimeException(sprintf('Operation "%s" is not supported.', $operation));
228
        }
229
230 23
        return $processor->match($item, $arguments, $this->iterableFilterHandlers);
231
    }
232
233
    /**
234
     * Sorts data items according to the given sort definition.
235
     *
236
     * @param iterable $items The items to be sorted.
237
     * @param Sort $sort The sort definition.
238
     *
239
     * @return array The sorted items.
240
     *
241
     * @psalm-param iterable<TKey, TValue> $items
242
     * @psalm-return iterable<TKey, TValue>
243
     */
244 38
    private function sortItems(iterable $items, Sort $sort): iterable
245
    {
246 38
        $criteria = $sort->getCriteria();
247
248 38
        if ($criteria !== []) {
249 38
            $items = $this->iterableToArray($items);
250 38
            uasort(
251 38
                $items,
252 38
                static function (array|object $itemA, array|object $itemB) use ($criteria) {
253 30
                    foreach ($criteria as $key => $order) {
254
                        /** @psalm-var mixed $valueA */
255 30
                        $valueA = ArrayHelper::getValue($itemA, $key);
256
                        /** @psalm-var mixed $valueB */
257 30
                        $valueB = ArrayHelper::getValue($itemB, $key);
258
259 30
                        if ($valueB === $valueA) {
260
                            continue;
261
                        }
262
263 30
                        return ($valueA > $valueB xor $order === SORT_DESC) ? 1 : -1;
264
                    }
265
266
                    return 0;
267 38
                }
268 38
            );
269
        }
270
271 38
        return $items;
272
    }
273
274
    /**
275
     * Convert iterable to array.
276
     *
277
     * @param iterable $iterable Iterable to convert.
278
     * @psalm-param iterable<TKey, TValue> $iterable
279
     *
280
     * @return array Resulting array.
281
     * @psalm-return array<TKey, TValue>
282
     */
283 38
    private function iterableToArray(iterable $iterable): array
284
    {
285 38
        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...
286
    }
287
}
288