Passed
Push — master ( 0c0121...d6a1fd )
by Alexander
10:21 queued 08:08
created

IterableDataReader::iterableToArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

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