Completed
Push — master ( 55fd80...78bdb9 )
by Sergei
11:47 queued 11:47
created

IterableDataReader::withOffset()   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\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 102
    public function __construct(iterable $data)
67
    {
68 102
        $this->data = $data;
69 102
        $this->filterProcessors = $this->withFilterProcessors(
70 102
            new All(),
71 102
            new Any(),
72 102
            new Between(),
73 102
            new Equals(),
74 102
            new EqualsEmpty(),
75 102
            new EqualsNull(),
76 102
            new GreaterThan(),
77 102
            new GreaterThanOrEqual(),
78 102
            new In(),
79 102
            new LessThan(),
80 102
            new LessThanOrEqual(),
81 102
            new Like(),
82 102
            new Not()
83 102
        )->filterProcessors;
84
    }
85
86 102
    public function withFilterProcessors(FilterProcessorInterface ...$filterProcessors): self
87
    {
88 102
        $new = clone $this;
89 102
        $processors = [];
90
91 102
        foreach ($filterProcessors as $filterProcessor) {
92 102
            if ($filterProcessor instanceof IterableProcessorInterface) {
93 102
                $processors[$filterProcessor->getOperator()] = $filterProcessor;
94
            }
95
        }
96
97 102
        $new->filterProcessors = array_merge($this->filterProcessors, $processors);
98 102
        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 42
    public function withLimit(int $limit): self
109
    {
110 42
        if ($limit < 0) {
111 1
            throw new InvalidArgumentException('The limit must not be less than 0.');
112
        }
113
114 41
        $new = clone $this;
115 41
        $new->limit = $limit;
116 41
        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 47
    public function withSort(?Sort $sort): self
127
    {
128 47
        $new = clone $this;
129 47
        $new->sort = $sort;
130 47
        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 40
    public function getSort(): ?Sort
142
    {
143 40
        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 3
        return $this->withLimit(1)->getIterator()->current();
189
    }
190
191 32
    protected function matchFilter(array $item, array $filter): bool
192
    {
193 32
        $operation = array_shift($filter);
194 32
        $arguments = $filter;
195
196 32
        if (!is_string($operation)) {
197 8
            throw new RuntimeException(sprintf(
198
                'The operator should be string. The %s is received.',
199 8
                FilterDataValidationHelper::getValueType($operation),
200
            ));
201
        }
202
203 24
        if ($operation === '') {
204 1
            throw new RuntimeException('The operator string cannot be empty.');
205
        }
206
207 23
        $processor = $this->filterProcessors[$operation] ?? null;
208
209 23
        if ($processor === null) {
210 1
            throw new RuntimeException(sprintf('Operation "%s" is not supported.', $operation));
211
        }
212
213 22
        return $processor->match($item, $arguments, $this->filterProcessors);
214
    }
215
216
    /**
217
     * Sorts data items according to the given sort definition.
218
     *
219
     * @param iterable $items The items to be sorted.
220
     * @param Sort $sort The sort definition.
221
     *
222
     * @return iterable The sorted items.
223
     */
224 38
    private function sortItems(iterable $items, Sort $sort): iterable
225
    {
226 38
        $criteria = $sort->getCriteria();
227
228 38
        if ($criteria !== []) {
229 38
            $items = $this->iterableToArray($items);
230 38
            uasort(
231
                $items,
232
                /**
233
                 * @param mixed $itemA
234
                 * @param mixed $itemB
235
                 */
236 38
                static function ($itemA, $itemB) use ($criteria) {
237 30
                    foreach ($criteria as $key => $order) {
238
                        /** @psalm-suppress MixedArgument, MixedAssignment */
239 30
                        $valueA = ArrayHelper::getValue($itemA, $key);
240
                        /** @psalm-suppress MixedArgument, MixedAssignment */
241 30
                        $valueB = ArrayHelper::getValue($itemB, $key);
242
243 30
                        if ($valueB === $valueA) {
244
                            continue;
245
                        }
246
247 30
                        return ($valueA > $valueB xor $order === SORT_DESC) ? 1 : -1;
248
                    }
249
250
                    return 0;
251
                }
252
            );
253
        }
254
255 38
        return $items;
256
    }
257
258 38
    private function iterableToArray(iterable $iterable): array
259
    {
260
        /** @psalm-suppress RedundantCast */
261 38
        return $iterable instanceof Traversable ? iterator_to_array($iterable, true) : (array) $iterable;
262
    }
263
}
264