Passed
Push — master ( 273064...5c5da0 )
by Alexander
02:36
created

IterableDataReader::sortItems()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5.005

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 2
nop 2
dl 0
loc 28
ccs 16
cts 17
cp 0.9412
crap 5.005
rs 9.4888
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
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->withFilterHandlers(
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
        )->iterableFilterHandlers;
86
    }
87
88 108
    public function withFilterHandlers(FilterHandlerInterface ...$iterableFilterHandlers): static
89
    {
90 108
        $new = clone $this;
91 108
        $handlers = [];
92
93 108
        foreach ($iterableFilterHandlers as $iterableFilterHandler) {
94 108
            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 108
            $handlers[$iterableFilterHandler->getOperator()] = $iterableFilterHandler;
103
        }
104
105 108
        $new->iterableFilterHandlers = array_merge($this->iterableFilterHandlers, $handlers);
106 108
        return $new;
107
    }
108
109 31
    public function withFilter(?FilterInterface $filter): static
110
    {
111 31
        $new = clone $this;
112 31
        $new->filter = $filter;
113 31
        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 81
    public function read(): array
163
    {
164 81
        $data = [];
165 81
        $skipped = 0;
166 81
        $filter = $this->filter?->toCriteriaArray();
167 81
        $sortedData = $this->sort === null ? $this->data : $this->sortItems($this->data, $this->sort);
168
169 81
        foreach ($sortedData as $key => $item) {
170
            // Do not return more than limit items.
171 76
            if ($this->limit > 0 && count($data) === $this->limit) {
172 20
                break;
173
            }
174
175
            // Skip offset items.
176 76
            if ($skipped < $this->offset) {
177 4
                ++$skipped;
178 4
                continue;
179
            }
180
181
            // Filter items.
182 76
            if ($filter === null || $this->matchFilter($item, $filter)) {
183 74
                $data[$key] = $item;
184
            }
185
        }
186
187 79
        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 36
    protected function matchFilter(array|object $item, array $filter): bool
207
    {
208 36
        $operation = array_shift($filter);
209 36
        $arguments = $filter;
210
211 36
        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 28
        if ($operation === '') {
221 1
            throw new RuntimeException('The operator string cannot be empty.');
222
        }
223
224 27
        $processor = $this->iterableFilterHandlers[$operation] ?? null;
225
226 27
        if ($processor === null) {
227 1
            throw new RuntimeException(sprintf('Operation "%s" is not supported.', $operation));
228
        }
229
230 26
        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 40
    private function sortItems(iterable $items, Sort $sort): iterable
245
    {
246 40
        $criteria = $sort->getCriteria();
247
248 40
        if ($criteria !== []) {
249 40
            $items = $this->iterableToArray($items);
250 40
            uasort(
251 40
                $items,
252 40
                static function (array|object $itemA, array|object $itemB) use ($criteria) {
253 32
                    foreach ($criteria as $key => $order) {
254
                        /** @psalm-var mixed $valueA */
255 32
                        $valueA = ArrayHelper::getValue($itemA, $key);
256
                        /** @psalm-var mixed $valueB */
257 32
                        $valueB = ArrayHelper::getValue($itemB, $key);
258
259 32
                        if ($valueB === $valueA) {
260 1
                            continue;
261
                        }
262
263 32
                        return ($valueA > $valueB xor $order === SORT_DESC) ? 1 : -1;
264
                    }
265
266
                    return 0;
267 40
                }
268 40
            );
269
        }
270
271 40
        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 40
    private function iterableToArray(iterable $iterable): array
284
    {
285 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...
286
    }
287
}
288