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

ControlsTrait::getInputEncoding()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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