Passed
Push — master ( 951d77...1f27ac )
by Alexander
02:35
created

IterableDataReader::matchFilter()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 4
nop 2
dl 0
loc 25
ccs 16
cts 16
cp 1
crap 4
rs 9.8333
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\DataReaderInterface;
13
use Yiisoft\Data\Reader\Filter\FilterInterface;
14
use Yiisoft\Data\Reader\Filter\FilterHandlerInterface;
15
use Yiisoft\Data\Reader\FilterDataValidationHelper;
16
use Yiisoft\Data\Reader\Iterable\Handler\All;
17
use Yiisoft\Data\Reader\Iterable\Handler\Any;
18
use Yiisoft\Data\Reader\Iterable\Handler\Between;
19
use Yiisoft\Data\Reader\Iterable\Handler\Equals;
20
use Yiisoft\Data\Reader\Iterable\Handler\EqualsEmpty;
21
use Yiisoft\Data\Reader\Iterable\Handler\EqualsNull;
22
use Yiisoft\Data\Reader\Iterable\Handler\GreaterThan;
23
use Yiisoft\Data\Reader\Iterable\Handler\GreaterThanOrEqual;
24
use Yiisoft\Data\Reader\Iterable\Handler\In;
25
use Yiisoft\Data\Reader\Iterable\Handler\IterableHandlerInterface;
26
use Yiisoft\Data\Reader\Iterable\Handler\LessThan;
27
use Yiisoft\Data\Reader\Iterable\Handler\LessThanOrEqual;
28
use Yiisoft\Data\Reader\Iterable\Handler\Like;
29
use Yiisoft\Data\Reader\Iterable\Handler\Not;
30
use Yiisoft\Data\Reader\Sort;
31
32
use function array_merge;
33
use function array_shift;
34
use function count;
35
use function is_string;
36
use function iterator_to_array;
37
use function sprintf;
38
use function uasort;
39
40
/**
41
 * @template TKey as array-key
42
 * @template TValue as array|object
43
 *
44
 * @implements DataReaderInterface<TKey, TValue>
45
 */
46
class IterableDataReader implements DataReaderInterface
47
{
48
    private ?Sort $sort = null;
49
    private ?FilterInterface $filter = null;
50
    private int $limit = 0;
51
    private int $offset = 0;
52
53
    /**
54
     * @psalm-var array<string, IterableHandlerInterface>
55
     */
56
    private array $filterHandlers = [];
57
58
    /**
59
     * @psalm-param iterable<TKey, TValue> $data
60
     */
61 104
    public function __construct(protected iterable $data)
62
    {
63 104
        $this->filterHandlers = $this->withFilterHandlers(
64 104
            new All(),
65 104
            new Any(),
66 104
            new Between(),
67 104
            new Equals(),
68 104
            new EqualsEmpty(),
69 104
            new EqualsNull(),
70 104
            new GreaterThan(),
71 104
            new GreaterThanOrEqual(),
72 104
            new In(),
73 104
            new LessThan(),
74 104
            new LessThanOrEqual(),
75 104
            new Like(),
76 104
            new Not()
77 104
        )->filterHandlers;
78
    }
79
80 104
    public function withFilterHandlers(FilterHandlerInterface ...$filterHandlers): static
81
    {
82 104
        $new = clone $this;
83 104
        $processors = [];
84
85 104
        foreach ($filterHandlers as $filterHandler) {
86 104
            if ($filterHandler instanceof IterableHandlerInterface) {
87 104
                $processors[$filterHandler->getOperator()] = $filterHandler;
88
            }
89
        }
90
91 104
        $new->filterHandlers = array_merge($this->filterHandlers, $processors);
92 104
        return $new;
93
    }
94
95 28
    public function withFilter(?FilterInterface $filter): static
96
    {
97 28
        $new = clone $this;
98 28
        $new->filter = $filter;
99 28
        return $new;
100
    }
101
102 43
    public function withLimit(int $limit): static
103
    {
104 43
        if ($limit < 0) {
105 1
            throw new InvalidArgumentException('The limit must not be less than 0.');
106
        }
107
108 42
        $new = clone $this;
109 42
        $new->limit = $limit;
110 42
        return $new;
111
    }
112
113 7
    public function withOffset(int $offset): static
114
    {
115 7
        $new = clone $this;
116 7
        $new->offset = $offset;
117 7
        return $new;
118
    }
119
120 48
    public function withSort(?Sort $sort): static
121
    {
122 48
        $new = clone $this;
123 48
        $new->sort = $sort;
124 48
        return $new;
125
    }
126
127
    /**
128
     * @psalm-return Generator<array-key, TValue, mixed, void>
129
     */
130 3
    public function getIterator(): Generator
131
    {
132 3
        yield from $this->read();
133
    }
134
135 41
    public function getSort(): ?Sort
136
    {
137 41
        return $this->sort;
138
    }
139
140 21
    public function count(): int
141
    {
142 21
        return count($this->read());
143
    }
144
145
    /**
146
     * @psalm-return array<TKey, TValue>
147
     */
148 76
    public function read(): array
149
    {
150 76
        $data = [];
151 76
        $skipped = 0;
152 76
        $filter = $this->filter?->toArray();
153 76
        $sortedData = $this->sort === null ? $this->data : $this->sortItems($this->data, $this->sort);
154
155 76
        foreach ($sortedData as $key => $item) {
156
            // Do not return more than limit items.
157 71
            if ($this->limit > 0 && count($data) === $this->limit) {
158 20
                break;
159
            }
160
161
            // Skip offset items.
162 71
            if ($skipped < $this->offset) {
163 4
                ++$skipped;
164 4
                continue;
165
            }
166
167
            // Filter items.
168 71
            if ($filter === null || $this->matchFilter($item, $filter)) {
169 69
                $data[$key] = $item;
170
            }
171
        }
172
173 74
        return $data;
174
    }
175
176 3
    public function readOne(): array|object|null
177
    {
178 3
        return $this
179 3
            ->withLimit(1)
180 3
            ->getIterator()
181 3
            ->current();
182
    }
183
184 33
    protected function matchFilter(array|object $item, array $filter): bool
185
    {
186 33
        $operation = array_shift($filter);
187 33
        $arguments = $filter;
188
189 33
        if (!is_string($operation)) {
190 8
            throw new RuntimeException(
191 8
                sprintf(
192 8
                    'The operator should be string. The %s is received.',
193 8
                    FilterDataValidationHelper::getValueType($operation),
194 8
                )
195 8
            );
196
        }
197
198 25
        if ($operation === '') {
199 1
            throw new RuntimeException('The operator string cannot be empty.');
200
        }
201
202 24
        $processor = $this->filterHandlers[$operation] ?? null;
203
204 24
        if ($processor === null) {
205 1
            throw new RuntimeException(sprintf('Operation "%s" is not supported.', $operation));
206
        }
207
208 23
        return $processor->match($item, $arguments, $this->filterHandlers);
209
    }
210
211
    /**
212
     * Sorts data items according to the given sort definition.
213
     *
214
     * @param iterable $items The items to be sorted.
215
     * @param Sort $sort The sort definition.
216
     *
217
     * @return array The sorted items.
218
     *
219
     * @psalm-param iterable<TKey, TValue> $items
220
     * @psalm-return iterable<TKey, TValue>
221
     */
222 38
    private function sortItems(iterable $items, Sort $sort): iterable
223
    {
224 38
        $criteria = $sort->getCriteria();
225
226 38
        if ($criteria !== []) {
227 38
            $items = $this->iterableToArray($items);
228 38
            uasort(
229 38
                $items,
230 38
                static function (array|object $itemA, array|object $itemB) use ($criteria) {
231 30
                    foreach ($criteria as $key => $order) {
232
                        /** @var mixed */
233 30
                        $valueA = ArrayHelper::getValue($itemA, $key);
234
                        /** @var mixed */
235 30
                        $valueB = ArrayHelper::getValue($itemB, $key);
236
237 30
                        if ($valueB === $valueA) {
238
                            continue;
239
                        }
240
241 30
                        return ($valueA > $valueB xor $order === SORT_DESC) ? 1 : -1;
242
                    }
243
244
                    return 0;
245 38
                }
246 38
            );
247
        }
248
249 38
        return $items;
250
    }
251
252
    /**
253
     * @param iterable<TKey, TValue> $iterable
254
     *
255
     * @return array<TKey, TValue>
256
     */
257 38
    private function iterableToArray(iterable $iterable): array
258
    {
259 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...
260
    }
261
}
262