Passed
Push — master ( 6c99d7...bd6ebf )
by Alexander
02:08
created

IterableDataReader   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 196
Duplicated Lines 0 %

Test Coverage

Coverage 95.6%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 81
dl 0
loc 196
ccs 87
cts 91
cp 0.956
rs 9.92
c 1
b 0
f 0
wmc 31

14 Methods

Rating   Name   Duplication   Size   Complexity  
A withSort() 0 5 1
A getSort() 0 3 1
A __construct() 0 15 1
A withFilter() 0 5 1
A readOne() 0 3 1
A withLimit() 0 8 2
A count() 0 3 1
A withOffset() 0 5 1
A getIterator() 0 3 1
A matchFilter() 0 11 2
A withFilterProcessors() 0 12 3
A iterableToArray() 0 3 2
A sortItems() 0 19 5
B read() 0 33 9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Data\Reader\Iterable;
6
7
use Generator;
8
use Traversable;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Data\Reader\DataReaderInterface;
11
use Yiisoft\Data\Reader\Filter\FilterInterface;
12
use Yiisoft\Data\Reader\Filter\FilterProcessorInterface;
13
use Yiisoft\Data\Reader\Iterable\Processor\All;
14
use Yiisoft\Data\Reader\Iterable\Processor\Any;
15
use Yiisoft\Data\Reader\Iterable\Processor\Equals;
16
use Yiisoft\Data\Reader\Iterable\Processor\GreaterThan;
17
use Yiisoft\Data\Reader\Iterable\Processor\GreaterThanOrEqual;
18
use Yiisoft\Data\Reader\Iterable\Processor\In;
19
use Yiisoft\Data\Reader\Iterable\Processor\IterableProcessorInterface;
20
use Yiisoft\Data\Reader\Iterable\Processor\LessThan;
21
use Yiisoft\Data\Reader\Iterable\Processor\LessThanOrEqual;
22
use Yiisoft\Data\Reader\Iterable\Processor\Like;
23
use Yiisoft\Data\Reader\Iterable\Processor\Not;
24
use Yiisoft\Data\Reader\Sort;
25
26
/**
27
 * @template TKey as array-key
28
 * @template TValue
29
 *
30
 * @implements DataReaderInterface<TKey, TValue>
31
 */
32
class IterableDataReader implements DataReaderInterface
33
{
34
    /**
35
     * @psalm-var iterable<TKey, TValue>
36
     */
37
    protected iterable $data;
38
    private ?Sort $sort = null;
39
    private ?FilterInterface $filter = null;
40
    private int $limit = 0;
41
    private int $offset = 0;
42
43
    private array $filterProcessors = [];
44
45
    /**
46
     * psalm-param iterable<TKey, TValue> $data
47
     */
48 78
    public function __construct(iterable $data)
49
    {
50 78
        $this->data = $data;
51 78
        $this->filterProcessors = $this->withFilterProcessors(
52 78
            new All(),
53 78
            new Any(),
54 78
            new Equals(),
55 78
            new GreaterThan(),
56 78
            new GreaterThanOrEqual(),
57 78
            new In(),
58 78
            new LessThan(),
59 78
            new LessThanOrEqual(),
60 78
            new Like(),
61 78
            new Not()
62 78
        )->filterProcessors;
63 78
    }
64
65
    /**
66
     * @psalm-mutation-free
67
     */
68 37
    public function withSort(?Sort $sort): self
69
    {
70 37
        $new = clone $this;
71 37
        $new->sort = $sort;
72 37
        return $new;
73
    }
74
75 31
    public function getSort(): ?Sort
76
    {
77 31
        return $this->sort;
78
    }
79
80
    /**
81
     * Sorts data items according to the given sort definition.
82
     *
83
     * @param iterable $items the items to be sorted
84
     * @param Sort $sort the sort definition
85
     *
86
     * @return iterable the sorted items
87
     */
88 33
    private function sortItems(iterable $items, Sort $sort): iterable
89
    {
90 33
        $criteria = $sort->getCriteria();
91 33
        if ($criteria !== []) {
92 33
            $items = $this->iterableToArray($items);
93 33
            uasort($items, static function ($itemA, $itemB) use ($criteria) {
94 25
                foreach ($criteria as $key => $order) {
95 25
                    $valueA = ArrayHelper::getValue($itemA, $key);
96 25
                    $valueB = ArrayHelper::getValue($itemB, $key);
97 25
                    if ($valueB === $valueA) {
98
                        continue;
99
                    }
100 25
                    return ($valueA > $valueB xor $order === SORT_DESC) ? 1 : -1;
101
                }
102
                return 0;
103 33
            });
104
        }
105
106 33
        return $items;
107
    }
108
109 20
    protected function matchFilter(array $item, array $filter): bool
110
    {
111 20
        $operation = array_shift($filter);
112 20
        $arguments = $filter;
113
114 20
        $processor = $this->filterProcessors[$operation] ?? null;
115 20
        if ($processor === null) {
116 1
            throw new \RuntimeException(sprintf('Operation "%s" is not supported', $operation));
117
        }
118
        /* @var $processor IterableProcessorInterface */
119 19
        return $processor->match($item, $arguments, $this->filterProcessors);
120
    }
121
122
    /**
123
     * @psalm-mutation-free
124
     */
125 23
    public function withFilter(?FilterInterface $filter): self
126
    {
127 23
        $new = clone $this;
128 23
        $new->filter = $filter;
129 23
        return $new;
130
    }
131
132
    /**
133
     * @psalm-mutation-free
134
     */
135 37
    public function withLimit(int $limit): self
136
    {
137 37
        if ($limit < 0) {
138
            throw new \InvalidArgumentException('$limit must not be less than 0.');
139
        }
140 37
        $new = clone $this;
141 37
        $new->limit = $limit;
142 37
        return $new;
143
    }
144
145 65
    public function read(): array
146
    {
147 65
        $filter = null;
148 65
        if ($this->filter !== null) {
149 22
            $filter = $this->filter->toArray();
150
        }
151
152 65
        $data = [];
153 65
        $skipped = 0;
154
155 65
        $sortedData = $this->sort === null
156 32
            ? $this->data
157 65
            : $this->sortItems($this->data, $this->sort);
158
159 65
        foreach ($sortedData as $key => $item) {
160
            // do not return more than limit items
161 60
            if ($this->limit > 0 && count($data) === $this->limit) {
162 16
                break;
163
            }
164
165
            // skip offset items
166 60
            if ($skipped < $this->offset) {
167 4
                ++$skipped;
168 4
                continue;
169
            }
170
171
            // filter items
172 60
            if ($filter === null || $this->matchFilter($item, $filter)) {
173 59
                $data[$key] = $item;
174
            }
175
        }
176
177 64
        return $data;
178
    }
179
180 3
    public function readOne()
181
    {
182 3
        return $this->withLimit(1)->getIterator()->current();
183
    }
184
185
    /**
186
     * @psalm-return Generator<TValue>
187
     */
188 3
    public function getIterator(): Generator
189
    {
190 3
        yield from $this->read();
191
    }
192
193
    /**
194
     * @psalm-mutation-free
195
     */
196 6
    public function withOffset(int $offset): self
197
    {
198 6
        $new = clone $this;
199 6
        $new->offset = $offset;
200 6
        return $new;
201
    }
202
203 17
    public function count(): int
204
    {
205 17
        return count($this->read());
206
    }
207
208 33
    private function iterableToArray(iterable $iterable): array
209
    {
210 33
        return $iterable instanceof Traversable ? iterator_to_array($iterable, true) : (array)$iterable;
211
    }
212
213
    /**
214
     * @psalm-mutation-free
215
     */
216 78
    public function withFilterProcessors(FilterProcessorInterface ...$filterProcessors): self
217
    {
218 78
        $new = clone $this;
219 78
        $processors = [];
220 78
        foreach ($filterProcessors as $filterProcessor) {
221 78
            if ($filterProcessor instanceof IterableProcessorInterface) {
222
                /** @psalm-suppress ImpureMethodCall */
223 78
                $processors[$filterProcessor->getOperator()] = $filterProcessor;
224
            }
225
        }
226 78
        $new->filterProcessors = array_merge($this->filterProcessors, $processors);
227 78
        return $new;
228
    }
229
}
230