Passed
Push — master ( 98fbdf...d7acb5 )
by Alexander
12:33
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\FilterProcessorInterface;
15
use Yiisoft\Data\Reader\FilterDataValidationHelper;
16
use Yiisoft\Data\Reader\Iterable\Processor\All;
17
use Yiisoft\Data\Reader\Iterable\Processor\Any;
18
use Yiisoft\Data\Reader\Iterable\Processor\Between;
19
use Yiisoft\Data\Reader\Iterable\Processor\Equals;
20
use Yiisoft\Data\Reader\Iterable\Processor\EqualsEmpty;
21
use Yiisoft\Data\Reader\Iterable\Processor\EqualsNull;
22
use Yiisoft\Data\Reader\Iterable\Processor\GreaterThan;
23
use Yiisoft\Data\Reader\Iterable\Processor\GreaterThanOrEqual;
24
use Yiisoft\Data\Reader\Iterable\Processor\In;
25
use Yiisoft\Data\Reader\Iterable\Processor\IterableProcessorInterface;
26
use Yiisoft\Data\Reader\Iterable\Processor\LessThan;
27
use Yiisoft\Data\Reader\Iterable\Processor\LessThanOrEqual;
28
use Yiisoft\Data\Reader\Iterable\Processor\Like;
29
use Yiisoft\Data\Reader\Iterable\Processor\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
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, FilterProcessorInterface&IterableProcessorInterface>
55
     */
56
    private array $filterProcessors = [];
57
58
    /**
59
     * @psalm-param iterable<TKey, TValue> $data
60
     */
61 103
    public function __construct(protected iterable $data)
62
    {
63 103
        $this->filterProcessors = $this->withFilterProcessors(
64 103
            new All(),
65 103
            new Any(),
66 103
            new Between(),
67 103
            new Equals(),
68 103
            new EqualsEmpty(),
69 103
            new EqualsNull(),
70 103
            new GreaterThan(),
71 103
            new GreaterThanOrEqual(),
72 103
            new In(),
73 103
            new LessThan(),
74 103
            new LessThanOrEqual(),
75 103
            new Like(),
76 103
            new Not()
77 103
        )->filterProcessors;
78
    }
79
80 103
    public function withFilterProcessors(FilterProcessorInterface ...$filterProcessors): self
81
    {
82 103
        $new = clone $this;
83 103
        $processors = [];
84
85 103
        foreach ($filterProcessors as $filterProcessor) {
86 103
            if ($filterProcessor instanceof IterableProcessorInterface) {
87 103
                $processors[$filterProcessor->getOperator()] = $filterProcessor;
88
            }
89
        }
90
91 103
        $new->filterProcessors = array_merge($this->filterProcessors, $processors);
92 103
        return $new;
93
    }
94
95 27
    public function withFilter(?FilterInterface $filter): self
96
    {
97 27
        $new = clone $this;
98 27
        $new->filter = $filter;
99 27
        return $new;
100
    }
101
102 43
    public function withLimit(int $limit): self
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): self
114
    {
115 7
        $new = clone $this;
116 7
        $new->offset = $offset;
117 7
        return $new;
118
    }
119
120 48
    public function withSort(?Sort $sort): self
121
    {
122 48
        $new = clone $this;
123 48
        $new->sort = $sort;
124 48
        return $new;
125
    }
126
127
    /**
128
     * @psalm-return Generator<TValue>
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 75
    public function read(): array
146
    {
147 75
        $data = [];
148 75
        $skipped = 0;
149 75
        $filter = $this->filter?->toArray();
150 75
        $sortedData = $this->sort === null ? $this->data : $this->sortItems($this->data, $this->sort);
151
152
        /**
153
         * @var int|string $key
154
         * @var array $item
155
         */
156 75
        foreach ($sortedData as $key => $item) {
157
            // Do not return more than limit items.
158 70
            if ($this->limit > 0 && count($data) === $this->limit) {
159 20
                break;
160
            }
161
162
            // Skip offset items.
163 70
            if ($skipped < $this->offset) {
164 4
                ++$skipped;
165 4
                continue;
166
            }
167
168
            // Filter items.
169 70
            if ($filter === null || $this->matchFilter($item, $filter)) {
170 68
                $data[$key] = $item;
171
            }
172
        }
173
174 73
        return $data;
175
    }
176
177
    /**
178
     * @return mixed
179
     */
180 3
    public function readOne()
181
    {
182 3
        return $this
183 3
            ->withLimit(1)
184 3
            ->getIterator()
185 3
            ->current();
186
    }
187
188 32
    protected function matchFilter(array $item, array $filter): bool
189
    {
190 32
        $operation = array_shift($filter);
191 32
        $arguments = $filter;
192
193 32
        if (!is_string($operation)) {
194 8
            throw new RuntimeException(
195 8
                sprintf(
196 8
                    'The operator should be string. The %s is received.',
197 8
                    FilterDataValidationHelper::getValueType($operation),
198 8
                )
199 8
            );
200
        }
201
202 24
        if ($operation === '') {
203 1
            throw new RuntimeException('The operator string cannot be empty.');
204
        }
205
206 23
        $processor = $this->filterProcessors[$operation] ?? null;
207
208 23
        if ($processor === null) {
209 1
            throw new RuntimeException(sprintf('Operation "%s" is not supported.', $operation));
210
        }
211
212 22
        return $processor->match($item, $arguments, $this->filterProcessors);
213
    }
214
215
    /**
216
     * Sorts data items according to the given sort definition.
217
     *
218
     * @param iterable $items The items to be sorted.
219
     * @param Sort $sort The sort definition.
220
     *
221
     * @return iterable The sorted items.
222
     */
223 38
    private function sortItems(iterable $items, Sort $sort): iterable
224
    {
225 38
        $criteria = $sort->getCriteria();
226
227 38
        if ($criteria !== []) {
228 38
            $items = $this->iterableToArray($items);
229 38
            uasort(
230 38
                $items,
231
                /**
232
                 * @param mixed $itemA
233
                 * @param mixed $itemB
234
                 */
235 38
                static function (mixed $itemA, mixed $itemB) use ($criteria) {
236 30
                    foreach ($criteria as $key => $order) {
237
                        /** @psalm-suppress MixedArgument, MixedAssignment */
238 30
                        $valueA = ArrayHelper::getValue($itemA, $key);
239
                        /** @psalm-suppress MixedArgument, MixedAssignment */
240 30
                        $valueB = ArrayHelper::getValue($itemB, $key);
241
242 30
                        if ($valueB === $valueA) {
243
                            continue;
244
                        }
245
246 30
                        return ($valueA > $valueB xor $order === SORT_DESC) ? 1 : -1;
247
                    }
248
249
                    return 0;
250 38
                }
251 38
            );
252
        }
253
254 38
        return $items;
255
    }
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