Completed
Pull Request — master (#207)
by ignace nyamagana
09:14
created

ControlsTrait::setHeader()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
cc 3
eloc 14
nc 3
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 9.0.0
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
declare(strict_types=1);
14
15
namespace League\Csv\Config;
16
17
use CallbackFilterIterator;
18
use InvalidArgumentException;
19
use League\Csv\AbstractCsv;
20
use LimitIterator;
21
use SplFileObject;
22
23
/**
24
 *  An abstract class to enable basic CSV manipulation
25
 *
26
 * @package League.csv
27
 * @since  9.0.0
28
 * @internal
29
 */
30
trait ControlsTrait
31
{
32
    use ValidatorTrait;
33
34
    /**
35
     * the field delimiter (one character only)
36
     *
37
     * @var string
38
     */
39
    protected $delimiter = ',';
40
41
    /**
42
     * the field enclosure character (one character only)
43
     *
44
     * @var string
45
     */
46
    protected $enclosure = '"';
47
48
    /**
49
     * the field escape character (one character only)
50
     *
51
     * @var string
52
     */
53
    protected $escape = '\\';
54
55
    /**
56
     * newline character
57
     *
58
     * @var string
59
     */
60
    protected $newline = "\n";
61
62
    /**
63
     * The Input file BOM character
64
     * @var string
65
     */
66
    protected $input_bom;
67
68
    /**
69
     * The Output file BOM character
70
     * @var string
71
     */
72
    protected $output_bom = '';
73
74
    /**
75
     * CSV Document header offset
76
     *
77
     * @var int|null
78
     */
79
    protected $header_offset;
80
81
    /**
82
     * CSV Document header
83
     *
84
     * @var string[]
85
     */
86
    protected $header = [];
87
88
    /**
89
     * Returns the inner CSV Document Iterator object
90
     *
91
     * @return StreamIterator|SplFileObject
92
     */
93
    abstract public function getCsvDocument();
94
95
    /**
96
     * Returns the current field delimiter
97
     *
98
     * @return string
99
     */
100
    public function getDelimiter(): string
101
    {
102
        return $this->delimiter;
103
    }
104
105
    /**
106
     * Returns the current field enclosure
107
     *
108
     * @return string
109
     */
110
    public function getEnclosure(): string
111
    {
112
        return $this->enclosure;
113
    }
114
115
    /**
116
     * Returns the current field escape character
117
     *
118
     * @return string
119
     */
120
    public function getEscape(): string
121
    {
122
        return $this->escape;
123
    }
124
125
    /**
126
     * Returns the current newline sequence characters
127
     *
128
     * @return string
129
     */
130
    public function getNewline(): string
131
    {
132
        return $this->newline;
133
    }
134
135
    /**
136
     * Returns the BOM sequence in use on Output methods
137
     *
138
     * @return string
139
     */
140
    public function getOutputBOM(): string
141
    {
142
        return $this->output_bom;
143
    }
144
145
    /**
146
     * Returns the BOM sequence of the given CSV
147
     *
148
     * @return string
149
     */
150
    public function getInputBOM(): string
151
    {
152
        if (null === $this->input_bom) {
153
            $bom = [
154
                AbstractCsv::BOM_UTF32_BE, AbstractCsv::BOM_UTF32_LE,
155
                AbstractCsv::BOM_UTF16_BE, AbstractCsv::BOM_UTF16_LE, AbstractCsv::BOM_UTF8,
156
            ];
157
            $csv = $this->getCsvDocument();
158
            $csv->setFlags(SplFileObject::READ_CSV);
159
            $csv->rewind();
160
            $line = $csv->fgets();
161
            $res  = array_filter($bom, function ($sequence) use ($line) {
162
                return strpos($line, $sequence) === 0;
163
            });
164
165
            $this->input_bom = (string) array_shift($res);
166
        }
167
168
        return $this->input_bom;
169
    }
170
171
    /**
172
     * Sets the field delimiter
173
     *
174
     * @param string $delimiter
175
     *
176
     * @throws InvalidArgumentException If $delimiter is not a single character
177
     *
178
     * @return $this
179
     */
180
    public function setDelimiter(string $delimiter): self
181
    {
182
        $this->delimiter = $this->filterControl($delimiter, 'delimiter');
183
184
        return $this;
185
    }
186
187
    /**
188
     * Detect Delimiters occurences in the CSV
189
     *
190
     * Returns a associative array where each key represents
191
     * a valid delimiter and each value the number of occurences
192
     *
193
     * @param string[] $delimiters the delimiters to consider
194
     * @param int      $nb_rows    Detection is made using $nb_rows of the CSV
195
     *
196
     * @return array
197
     */
198
    public function fetchDelimitersOccurrence(array $delimiters, int $nb_rows = 1): array
199
    {
200
        $nb_rows = $this->filterInteger($nb_rows, 1, 'The number of rows to consider must be a valid positive integer');
201
        $filter_row = function ($row) {
202
            return is_array($row) && count($row) > 1;
203
        };
204
        $delimiters = array_unique(array_filter($delimiters, function ($value) {
205
            return 1 == strlen($value);
206
        }));
207
        $csv = $this->getCsvDocument();
208
        $csv->setFlags(SplFileObject::READ_CSV);
209
        $res = [];
210
        foreach ($delimiters as $delim) {
211
            $csv->setCsvControl($delim, $this->enclosure, $this->escape);
212
            $iterator = new CallbackFilterIterator(new LimitIterator($csv, 0, $nb_rows), $filter_row);
213
            $res[$delim] = count(iterator_to_array($iterator, false), COUNT_RECURSIVE);
214
        }
215
        arsort($res, SORT_NUMERIC);
216
217
        return $res;
218
    }
219
220
    /**
221
     * Sets the field enclosure
222
     *
223
     * @param string $enclosure
224
     *
225
     * @throws InvalidArgumentException If $enclosure is not a single character
226
     *
227
     * @return $this
228
     */
229
    public function setEnclosure(string $enclosure): self
230
    {
231
        $this->enclosure = $this->filterControl($enclosure, 'enclosure');
232
233
        return $this;
234
    }
235
236
    /**
237
     * Sets the field escape character
238
     *
239
     * @param string $escape
240
     *
241
     * @throws InvalidArgumentException If $escape is not a single character
242
     *
243
     * @return $this
244
     */
245
    public function setEscape(string $escape): self
246
    {
247
        $this->escape = $this->filterControl($escape, 'escape');
248
249
        return $this;
250
    }
251
252
    /**
253
     * Sets the newline sequence characters
254
     *
255
     * @param string $newline
256
     *
257
     * @return static
258
     */
259
    public function setNewline(string $newline): self
260
    {
261
        $this->newline = (string) $newline;
262
263
        return $this;
264
    }
265
266
    /**
267
     * Sets the BOM sequence to prepend the CSV on output
268
     *
269
     * @param string $str The BOM sequence
270
     *
271
     * @return static
272
     */
273
    public function setOutputBOM(string $str): self
274
    {
275
        if (empty($str)) {
276
            $this->output_bom = '';
277
278
            return $this;
279
        }
280
281
        $this->output_bom = (string) $str;
282
283
        return $this;
284
    }
285
286
    /**
287
     * Returns the record offset used as header
288
     *
289
     * If no CSV record is used this method MUST return null
290
     *
291
     * @return int|null
292
     */
293
    public function getHeaderOffset()
294
    {
295
        return $this->header_offset;
296
    }
297
298
    /**
299
     * Returns the header
300
     *
301
     * If no CSV record is used this method MUST return an empty array
302
     *
303
     * @return string[]
304
     */
305
    public function getHeader(): array
306
    {
307
        if (null !== $this->header_offset) {
308
            $this->header = $this->filterHeader($this->getRow($this->header_offset));
309
        }
310
311
        return $this->header;
312
    }
313
314
    /**
315
     * Returns a single row from the CSV without filtering
316
     *
317
     * @param int $offset
318
     *
319
     * @throws InvalidArgumentException If the $offset is not valid or the row does not exist
320
     *
321
     * @return array
322
     */
323
    protected function getRow(int $offset): array
324
    {
325
        $csv = $this->getCsvDocument();
326
        $csv->setFlags(SplFileObject::READ_CSV);
327
        $csv->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
328
        $csv->seek($offset);
329
        $row = $csv->current();
330
        if (empty($row) || [null] === $row) {
331
            throw new InvalidArgumentException('the specified row does not exist or is empty');
332
        }
333
334
        if (0 != $offset) {
335
            return $row;
336
        }
337
338
        return $this->removeBOM($row, mb_strlen($this->getInputBOM()), $this->enclosure);
339
    }
340
341
    /**
342
     * Validates the array to be used by the fetchAssoc method
343
     *
344
     * @param array $keys
345
     *
346
     * @throws InvalidArgumentException If the submitted array fails the assertion
347
     *
348
     * @return array
349
     */
350
    protected function filterHeader(array $keys): array
351
    {
352
        if (empty($keys)) {
353
            return $keys;
354
        }
355
356
        if ($keys !== array_unique(array_filter($keys, [$this, 'isValidKey']))) {
357
            throw new InvalidArgumentException('Use a flat array with unique string values');
358
        }
359
360
        return $keys;
361
    }
362
363
    /**
364
     * Selects the array to be used as key for the fetchAssoc method
365
     *
366
     * Because of the header is represented as an array, to be valid
367
     * a header MUST contain only unique string value.
368
     *
369
     * <ul>
370
     * <li>If a array is given it will be used as the header</li>
371
     * <li>If a integer is given it will represent the offset of the record to be used as header</li>
372
     * <li>If an empty array or null is given it will mean that no header is used</li>
373
     * </ul>
374
     *
375
     * @param int|null|string[] $offset_or_keys the assoc key OR the row Index to be used
376
     *                                          as the key index
377
     *
378
     * @return $this
379
     */
380
    public function setHeader($offset_or_keys): self
381
    {
382
        if (is_array($offset_or_keys)) {
383
            $this->header = $this->filterHeader($offset_or_keys);
384
            $this->header_offset = null;
385
386
            return $this;
387
        }
388
389
        if (null === $offset_or_keys) {
390
            $this->header = [];
391
            $this->header_offset = null;
392
393
            return $this;
394
        }
395
396
        $this->header_offset = $this->filterInteger(
397
            $offset_or_keys,
398
            0,
399
            'the row index must be a positive integer, 0 or a non empty array'
400
        );
401
402
        return $this;
403
    }
404
405
    /**
406
     * Returns whether the submitted value can be used as string
407
     *
408
     * @param mixed $value
409
     *
410
     * @return bool
411
     */
412
    protected function isValidKey($value): bool
413
    {
414
        return is_scalar($value) || (is_object($value) && method_exists($value, '__toString'));
415
    }
416
}
417