Completed
Pull Request — master (#178)
by ignace nyamagana
40:39
created

Records::fetchAssoc()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 9.4285
cc 2
eloc 9
nc 1
nop 1
1
<?php
2
/**
3
* This file is part of the League.csv library
4
*
5
* @license http://opensource.org/licenses/MIT
6
* @link https://github.com/thephpleague/csv/
7
* @version 8.1.1
8
* @package League.csv
9
*
10
* For the full copyright and license information, please view the LICENSE
11
* file that was distributed with this source code.
12
*/
13
namespace League\Csv;
14
15
use DomDocument;
16
use Generator;
17
use InvalidArgumentException;
18
use Iterator;
19
use IteratorAggregate;
20
use JsonSerializable;
21
use League\Csv\Config\Validator;
22
use LimitIterator;
23
use SplFileObject;
24
25
/**
26
 * A class to extract and convert data from a CSV
27
 *
28
 * @package League.csv
29
 * @since  3.0.0
30
 *
31
 */
32
class Records implements IteratorAggregate, JsonSerializable
33
{
34
    use Validator;
35
36
    /**
37
     * @var AbstractCsv
38
     */
39
    protected $csv;
40
41
    /**
42
     * @var Query
43
     */
44
    protected $query;
45
46
    /**
47
     * New Instance
48
     *
49
     * @param AbstractCsv $csv
50
     * @param Query       $query
51
     */
52
    public function __construct(AbstractCsv $csv, Query $query)
53
    {
54
        $this->csv = $csv;
55
        $this->query = $query;
56
    }
57
58
    /**
59
     * @inheritdoc
60
     */
61
    public function getIterator()
62
    {
63
        return $this->query->__invoke($this->csv);
64
    }
65
66
    /**
67
     * Returns a sequential array of all CSV lines
68
     *
69
     * The callable function will be applied to each Iterator item
70
     *
71
     * @return array
72
     */
73
    public function fetchAll()
74
    {
75
        return iterator_to_array($this->getIterator(), false);
76
    }
77
78
    /**
79
     * Returns a single row from the CSV
80
     *
81
     * By default if no offset is provided the first row of the CSV is selected
82
     *
83
     * @param int $offset the CSV row offset
84
     *
85
     * @return array
86
     */
87
    public function fetchOne($offset = 0)
88
    {
89
        $query = clone $this->query;
90
        $query->setOffset($offset)->setLimit(1);
91
        $iterator = $query($this->csv);
92
        $iterator->rewind();
93
94
        return (array) $iterator->current();
95
    }
96
97
    /**
98
     * Returns the next value from a single CSV column
99
     *
100
     * The callable function will be applied to each value to be return
101
     *
102
     * By default if no column index is provided the first column of the CSV is selected
103
     *
104
     * @param int $column_index CSV column index
105
     *
106
     * @return Iterator
107
     */
108
    public function fetchColumn($column_index = 0)
109
    {
110
        $column_index = $this->validateInteger($column_index, 0, 'the column index must be a positive integer or 0');
111
112
        $filter_column = function ($row) use ($column_index) {
113
            return isset($row[$column_index]);
114
        };
115
116
        $select_column = function ($row) use ($column_index) {
117
            return $row[$column_index];
118
        };
119
120
        $query = clone $this->query;
121
        $query->addFilter($filter_column);
122
123
        return new MapIterator($query($this->csv), $select_column);
124
    }
125
126
    /**
127
     * Fetches the next key-value pairs from a result set (first
128
     * column is the key, second column is the value).
129
     *
130
     * By default if no column index is provided:
131
     * - the first CSV column is used to provide the keys
132
     * - the second CSV column is used to provide the value
133
     *
134
     * @param int $offset_index The column index to serve as offset
135
     * @param int $value_index  The column index to serve as value
136
     *
137
     * @return Generator
138
     */
139
    public function fetchPairs($offset_index = 0, $value_index = 1)
140
    {
141
        $offset_index = $this->validateInteger($offset_index, 0, 'the offset column index must be a positive integer or 0');
142
        $value_index = $this->validateInteger($value_index, 0, 'the value column index must be a positive integer or 0');
143
        $filter_pairs = function ($row) use ($offset_index) {
144
            return isset($row[$offset_index]);
145
        };
146
        $select_pairs = function ($row) use ($offset_index, $value_index) {
147
            return [
148
                $row[$offset_index],
149
                isset($row[$value_index]) ? $row[$value_index] : null,
150
            ];
151
        };
152
153
        $query = clone $this->query;
154
        $query->addFilter($filter_pairs);
155
        foreach (new MapIterator($query($this->csv), $select_pairs) as $row) {
156
            yield $row[0] => $row[1];
157
        }
158
    }
159
160
    /**
161
     * Fetch the next row from a result set
162
     *
163
     * The rows are presented as associated arrays
164
     * The callable function will be applied to each row
165
     *
166
     * @param int|array $offset_or_keys the name for each key member OR the row Index to be
167
     *                                  used as the associated named keys
168
     *
169
     * @throws InvalidArgumentException If the submitted keys are invalid
170
     *
171
     * @return Iterator
172
     */
173
    public function fetchAssoc($offset_or_keys = 0)
174
    {
175
        $query = clone $this->query;
176
        $keys = $this->getAssocKeys($offset_or_keys, $query);
177
        $keys_count = count($keys);
178
        $combine_array = function (array $row) use ($keys, $keys_count) {
179
            if ($keys_count != count($row)) {
180
                $row = array_slice(array_pad($row, $keys_count, null), 0, $keys_count);
181
            }
182
183
            return array_combine($keys, $row);
184
        };
185
186
        return new MapIterator($query($this->csv), $combine_array);
187
    }
188
189
    /**
190
     * Selects the array to be used as key for the fetchAssoc method
191
     *
192
     * @param int|array $offset_or_keys the assoc key OR the row Index to be used
193
     *                                  as the key index
194
     *
195
     * @throws InvalidArgumentException If the row index and/or the resulting array is invalid
196
     *
197
     * @return array
198
     */
199
    protected function getAssocKeys($offset_or_keys, Query $query)
200
    {
201
        if (is_array($offset_or_keys)) {
202
            return $this->validateKeys($offset_or_keys);
203
        }
204
205
        $offset_or_keys = $this->validateInteger(
206
            $offset_or_keys,
207
            0,
208
            'the row index must be a positive integer, 0 or a non empty array'
209
        );
210
        $keys = $this->validateKeys($this->getRow($offset_or_keys));
211
        $filterOutRow = function ($row, $rowIndex) use ($offset_or_keys) {
212
            return $rowIndex != $offset_or_keys;
213
        };
214
        $query->addFilter($filterOutRow);
215
216
        return $keys;
217
    }
218
219
    /**
220
     * Validates the array to be used by the fetchAssoc method
221
     *
222
     * @param array $keys
223
     *
224
     * @throws InvalidArgumentException If the submitted array fails the assertion
225
     *
226
     * @return array
227
     */
228
    protected function validateKeys(array $keys)
229
    {
230
        if (empty($keys) || $keys !== array_unique(array_filter($keys, [$this, 'isValidKey']))) {
231
            throw new InvalidArgumentException('Use a flat array with unique string values');
232
        }
233
234
        return $keys;
235
    }
236
237
    /**
238
     * Returns whether the submitted value can be used as string
239
     *
240
     * @param mixed $value
241
     *
242
     * @return bool
243
     */
244
    protected function isValidKey($value)
245
    {
246
        return is_scalar($value) || (is_object($value) && method_exists($value, '__toString'));
247
    }
248
249
    /**
250
     * Returns a single row from the CSV without filtering
251
     *
252
     * @param int $offset
253
     *
254
     * @throws InvalidArgumentException If the $offset is not valid or the row does not exist
255
     *
256
     * @return array
257
     */
258
    protected function getRow($offset)
259
    {
260
        $fileObj = $this->csv->getIterator();
261
        $fileObj->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
262
        $iterator = new LimitIterator($fileObj, $offset, 1);
263
        $iterator->rewind();
264
        $line = $iterator->current();
265
        if (empty($line)) {
266
            throw new InvalidArgumentException('the specified row does not exist or is empty');
267
        }
268
269
        $input_bom = $this->csv->getInputBOM();
270
        if (0 === $offset && '' !== $input_bom) {
271
            $line = mb_substr($line, mb_strlen($input_bom));
272
        }
273
274
        return str_getcsv($line, $this->csv->getDelimiter(), $this->csv->getEnclosure(), $this->csv->getEscape());
275
    }
276
277
    /**
278
     * @inheritdoc
279
     */
280
    public function jsonSerialize()
281
    {
282
        return $this->fetchAll();
283
    }
284
285
    /**
286
     * Returns a HTML table representation of the CSV Table
287
     *
288
     * @param string $class_attr optional classname
289
     *
290
     * @return string
291
     */
292
    public function toHTML($class_attr = 'table-csv-data')
293
    {
294
        $doc = $this->toXML('table', 'tr', 'td');
295
        $doc->documentElement->setAttribute('class', $class_attr);
296
297
        return $doc->saveHTML($doc->documentElement);
298
    }
299
300
    /**
301
     * Transforms a CSV into a XML
302
     *
303
     * @param string $root_name XML root node name
304
     * @param string $row_name  XML row node name
305
     * @param string $cell_name XML cell node name
306
     *
307
     * @return DomDocument
308
     */
309
    public function toXML($root_name = 'csv', $row_name = 'row', $cell_name = 'cell')
310
    {
311
        $doc = new DomDocument('1.0', 'UTF-8');
312
        $root = $doc->createElement($root_name);
313
        foreach ($this->getIterator() as $row) {
314
            $rowElement = $doc->createElement($row_name);
315
            array_walk($row, function ($value) use (&$rowElement, $doc, $cell_name) {
316
                $content = $doc->createTextNode($value);
317
                $cell = $doc->createElement($cell_name);
318
                $cell->appendChild($content);
319
                $rowElement->appendChild($cell);
320
            });
321
            $root->appendChild($rowElement);
322
        }
323
        $doc->appendChild($root);
324
325
        return $doc;
326
    }
327
}
328