Completed
Push — master ( 2d2097...fee32b )
by ignace nyamagana
22s queued 11s
created

ResultSet::combineHeader()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 2
nop 1
dl 0
loc 20
ccs 10
cts 10
cp 1
crap 4
rs 9.6
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * League.Csv (https://csv.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Csv;
15
16
use CallbackFilterIterator;
17
use Countable;
18
use Generator;
19
use Iterator;
20
use IteratorAggregate;
21
use JsonSerializable;
22
use LimitIterator;
23
use function array_flip;
24
use function array_search;
25
use function is_string;
26
use function iterator_count;
27
use function iterator_to_array;
28
use function sprintf;
29
30
/**
31
 * Represents the result set of a {@link Reader} processed by a {@link Statement}.
32
 */
33
class ResultSet implements Countable, IteratorAggregate, JsonSerializable
34
{
35
    /**
36
     * The CSV records collection.
37
     *
38
     * @var Iterator
39
     */
40
    protected $records;
41
42
    /**
43
     * The CSV records collection header.
44
     *
45
     * @var array
46
     */
47
    protected $header = [];
48
49
    /**
50
     * New instance.
51
     */
52 15
    public function __construct(Iterator $records, array $header)
53
    {
54 15
        $this->records = $records;
55 15
        $this->validateHeader($header);
56 15
        $this->header = $header;
57 15
    }
58
59
    /**
60
     * @throws SyntaxError if the header syntax is invalid
61
     */
62 15
    protected function validateHeader(array $header): void
63
    {
64 15
        if ($header !== array_unique(array_filter($header, 'is_string'))) {
65 3
            throw new SyntaxError('The header record must be an empty or a flat array with unique string values.');
66
        }
67 15
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 18
    public function __destruct()
73
    {
74 18
        unset($this->records);
75 18
    }
76
77
    /**
78
     * Returns the header associated with the result set.
79
     *
80
     * @return string[]
81
     */
82 3
    public function getHeader(): array
83
    {
84 3
        return $this->header;
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 21
    public function getIterator(): Generator
91
    {
92 21
        return $this->getRecords();
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 18
    public function getRecords(array $header = []): Generator
99
    {
100 18
        $this->validateHeader($header);
101 18
        $records = $this->combineHeader($header);
102 18
        foreach ($records as $offset => $value) {
103 18
            yield $offset => $value;
104
        }
105 18
    }
106
107
    /**
108
     * Combine the header to each record if present.
109
     */
110 15
    protected function combineHeader(array $header): Iterator
111
    {
112 15
        if ($header === $this->header || [] === $header) {
113 15
            return $this->records;
114
        }
115
116 3
        $field_count = count($header);
117
        $mapper = static function (array $record) use ($header, $field_count): array {
118 3
            if (count($record) != $field_count) {
119 3
                $record = array_slice(array_pad($record, $field_count, null), 0, $field_count);
120
            }
121
122
            /** @var array<string|null> $assocRecord */
123 3
            $assocRecord = array_combine($header, $record);
124
125 3
            return $assocRecord;
126 3
        };
127
128 3
        return new MapIterator($this->records, $mapper);
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134 3
    public function count(): int
135
    {
136 3
        return iterator_count($this->records);
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 3
    public function jsonSerialize(): array
143
    {
144 3
        return iterator_to_array($this->records, false);
145
    }
146
147
    /**
148
     * Returns the nth record from the result set.
149
     *
150
     * By default if no index is provided the first record of the resultet is returned
151
     *
152
     * @param int $nth_record the CSV record offset
153
     *
154
     * @throws Exception if argument is lesser than 0
155
     */
156 6
    public function fetchOne(int $nth_record = 0): array
157
    {
158 6
        if ($nth_record < 0) {
159 3
            throw new InvalidArgument(sprintf('%s() expects the submitted offset to be a positive integer or 0, %s given', __METHOD__, $nth_record));
160
        }
161
162 3
        $iterator = new LimitIterator($this->records, $nth_record, 1);
163 3
        $iterator->rewind();
164
165 3
        return (array) $iterator->current();
166
    }
167
168
    /**
169
     * Returns a single column from the next record of the result set.
170
     *
171
     * By default if no value is supplied the first column is fetch
172
     *
173
     * @param string|int $index CSV column index
174
     */
175 21
    public function fetchColumn($index = 0): Generator
176
    {
177 21
        $offset = $this->getColumnIndex($index, __METHOD__.'() expects the column index to be a valid string or integer, `%s` given');
178
        $filter = static function (array $record) use ($offset): bool {
179 12
            return isset($record[$offset]);
180 12
        };
181
182
        $select = static function (array $record) use ($offset): string {
183 9
            return $record[$offset];
184 12
        };
185
186 12
        $iterator = new MapIterator(new CallbackFilterIterator($this->records, $filter), $select);
187 12
        foreach ($iterator as $tKey => $tValue) {
188 9
            yield $tKey => $tValue;
189
        }
190 6
    }
191
192
    /**
193
     * Filter a column name against the header if any.
194
     *
195
     * @param string|int $field         the field name or the field index
196
     * @param string     $error_message the associated error message
197
     *
198
     * @return string|int
199
     */
200 30
    protected function getColumnIndex($field, string $error_message)
201
    {
202 30
        if (is_string($field)) {
203 6
            return $this->getColumnIndexByValue($field, $error_message);
204
        }
205
206 27
        return $this->getColumnIndexByKey($field, $error_message);
207
    }
208
209
    /**
210
     * Returns the selected column name.
211
     *
212
     * @throws Exception if the column is not found
213
     */
214 6
    protected function getColumnIndexByValue(string $value, string $error_message): string
215
    {
216 6
        if (false !== array_search($value, $this->header, true)) {
217 3
            return $value;
218
        }
219
220 3
        throw new InvalidArgument(sprintf($error_message, $value));
221
    }
222
223
    /**
224
     * Returns the selected column name according to its offset.
225
     *
226
     * @throws Exception if the field is invalid or not found
227
     *
228
     * @return int|string
229
     */
230 18
    protected function getColumnIndexByKey(int $index, string $error_message)
231
    {
232 18
        if ($index < 0) {
233 3
            throw new InvalidArgument($error_message);
234
        }
235
236 15
        if ([] === $this->header) {
237 9
            return $index;
238
        }
239
240 6
        $value = array_search($index, array_flip($this->header), true);
241 6
        if (false !== $value) {
242 3
            return $value;
243
        }
244
245 3
        throw new InvalidArgument(sprintf($error_message, $index));
246
    }
247
248
    /**
249
     * Returns the next key-value pairs from a result set (first
250
     * column is the key, second column is the value).
251
     *
252
     * By default if no column index is provided:
253
     * - the first column is used to provide the keys
254
     * - the second column is used to provide the value
255
     *
256
     * @param string|int $offset_index The column index to serve as offset
257
     * @param string|int $value_index  The column index to serve as value
258
     */
259 12
    public function fetchPairs($offset_index = 0, $value_index = 1): Generator
260
    {
261 12
        $offset = $this->getColumnIndex($offset_index, __METHOD__.'() expects the offset index value to be a valid string or integer, `%s` given');
262 12
        $value = $this->getColumnIndex($value_index, __METHOD__.'() expects the value index value to be a valid string or integer, `%s` given');
263
264
        $filter = static function (array $record) use ($offset): bool {
265 12
            return isset($record[$offset]);
266 12
        };
267
268
        $select = static function (array $record) use ($offset, $value): array {
269 9
            return [$record[$offset], $record[$value] ?? null];
270 12
        };
271
272 12
        $iterator = new MapIterator(new CallbackFilterIterator($this->records, $filter), $select);
273 12
        foreach ($iterator as $pair) {
274 9
            yield $pair[0] => $pair[1];
275
        }
276 12
    }
277
}
278