Completed
Push — master ( 9d01ab...7708dc )
by ignace nyamagana
03:22
created

AbstractCsv::resetProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 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.1.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;
16
17
use Generator;
18
use SplFileObject;
19
use function League\Csv\bom_match;
20
21
/**
22
 * An abstract class to enable CSV document loading.
23
 *
24
 * @package League.csv
25
 * @since   4.0.0
26
 * @author  Ignace Nyamagana Butera <[email protected]>
27
 */
28
abstract class AbstractCsv implements ByteSequence
29
{
30
    /**
31
     * The stream filter mode (read or write)
32
     *
33
     * @var int
34
     */
35
    protected $stream_filter_mode;
36
37
38
    /**
39
     * collection of stream filters
40
     *
41
     * @var bool[]
42
     */
43
    protected $stream_filters = [];
44
45
    /**
46
     * The CSV document BOM sequence
47
     *
48
     * @var string|null
49
     */
50
    protected $input_bom = null;
51
52
    /**
53
     * The Output file BOM character
54
     *
55
     * @var string
56
     */
57
    protected $output_bom = '';
58
59
    /**
60
     * the field delimiter (one character only)
61
     *
62
     * @var string
63
     */
64
    protected $delimiter = ',';
65
66
    /**
67
     * the field enclosure character (one character only)
68
     *
69
     * @var string
70
     */
71
    protected $enclosure = '"';
72
73
    /**
74
     * the field escape character (one character only)
75
     *
76
     * @var string
77
     */
78
    protected $escape = '\\';
79
80
    /**
81
     * The CSV document
82
     *
83
     * @var SplFileObject|Stream
84
     */
85
    protected $document;
86
87
    /**
88
     * New instance
89
     *
90
     * @param SplFileObject|Stream $document The CSV Object instance
91
     */
92 22
    protected function __construct($document)
93
    {
94 22
        $this->document = $document;
95 22
        list($this->delimiter, $this->enclosure, $this->escape) = $this->document->getCsvControl();
96 22
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101 24
    public function __destruct()
102
    {
103 24
        unset($this->document);
104 24
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109 2
    public function __clone()
110
    {
111 2
        throw new Exception(sprintf('An object of class %s cannot be cloned', get_class($this)));
112
    }
113
114
    /**
115
     * Return a new instance from a SplFileObject
116
     *
117
     * @param SplFileObject $file
118
     *
119
     * @return static
120
     */
121 24
    public static function createFromFileObject(SplFileObject $file): self
122
    {
123 24
        return new static($file);
124
    }
125
126
    /**
127
     * Return a new instance from a PHP resource stream
128
     *
129
     * @param resource $stream
130
     *
131
     * @return static
132
     */
133 4
    public static function createFromStream($stream): self
134
    {
135 4
        return new static(new Stream($stream));
136
    }
137
138
    /**
139
     * Return a new instance from a string
140
     *
141
     * @param string $content the CSV document as a string
142
     *
143
     * @return static
144
     */
145 4
    public static function createFromString(string $content): self
146
    {
147 4
        return new static(Stream::createFromString($content));
148
    }
149
150
    /**
151
     * Return a new instance from a file path
152
     *
153
     * @param string        $path      file path
154
     * @param string        $open_mode the file open mode flag
155
     * @param resource|null $context   the resource context
156
     *
157
     * @return static
158
     */
159 2
    public static function createFromPath(string $path, string $open_mode = 'r+', $context = null): self
160
    {
161 2
        return new static(Stream::createFromPath($path, $open_mode, $context));
162
    }
163
164
    /**
165
     * Returns the current field delimiter
166
     *
167
     * @return string
168
     */
169 10
    public function getDelimiter(): string
170
    {
171 10
        return $this->delimiter;
172
    }
173
174
    /**
175
     * Returns the current field enclosure
176
     *
177
     * @return string
178
     */
179 2
    public function getEnclosure(): string
180
    {
181 2
        return $this->enclosure;
182
    }
183
184
    /**
185
     * Returns the current field escape character
186
     *
187
     * @return string
188
     */
189 2
    public function getEscape(): string
190
    {
191 2
        return $this->escape;
192
    }
193
194
    /**
195
     * Returns the BOM sequence in use on Output methods
196
     *
197
     * @return string
198
     */
199 2
    public function getOutputBOM(): string
200
    {
201 2
        return $this->output_bom;
202
    }
203
204
    /**
205
     * Returns the BOM sequence of the given CSV
206
     *
207
     * @return string
208
     */
209 16
    public function getInputBOM(): string
210
    {
211 16
        if (null !== $this->input_bom) {
212 2
            return $this->input_bom;
213
        }
214
215 16
        $this->document->setFlags(SplFileObject::READ_CSV);
216 16
        $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
217 16
        $this->document->rewind();
218 16
        $this->input_bom = bom_match(implode(',', (array) $this->document->current()));
219
220 16
        return $this->input_bom;
221
    }
222
223
    /**
224
     * Returns the stream filter mode
225
     *
226
     * @return int
227
     */
228 2
    public function getStreamFilterMode(): int
229
    {
230 2
        return $this->stream_filter_mode;
231
    }
232
233
    /**
234
     * Tells whether the stream filter capabilities can be used
235
     *
236
     * @return bool
237
     */
238 4
    public function supportsStreamFilter(): bool
239
    {
240 4
        return $this->document instanceof Stream;
241
    }
242
243
    /**
244
     * Tell whether the specify stream filter is attach to the current stream
245
     *
246
     * @param string $filtername
247
     *
248
     * @return bool
249
     */
250 2
    public function hasStreamFilter(string $filtername): bool
251
    {
252 2
        return $this->stream_filters[$filtername] ?? false;
253
    }
254
255
    /**
256
     * Retuns the CSV document as a Generator of string chunk
257
     *
258
     * @param int $length number of bytes read
259
     *
260
     * @throws Exception if the number of bytes is lesser than 1
261
     *
262
     * @return Generator
263
     */
264 10
    public function chunk(int $length): Generator
265
    {
266 10
        if ($length < 1) {
267 2
            throw new Exception(sprintf('%s() expects the length to be a positive integer %d given', __METHOD__, $length));
268
        }
269
270 8
        $input_bom = $this->getInputBOM();
271 8
        $this->document->rewind();
272 8
        $this->document->fseek(strlen($input_bom));
273 8
        foreach (str_split($this->output_bom.$this->document->fread($length), $length) as $chunk) {
274 8
            yield $chunk;
275
        }
276
277 8
        while ($this->document->valid()) {
278 6
            yield $this->document->fread($length);
279
        }
280 8
    }
281
282
    /**
283
     * DEPRECATION WARNING! This method will be removed in the next major point release
284
     *
285
     * @deprecated deprecated since version 9.1.0
286
     * @see AbstractCsv::getContent
287
     *
288
     * Retrieves the CSV content
289
     *
290
     * @return string
291
     */
292 2
    public function __toString(): string
293
    {
294 2
        return $this->getContent();
295
    }
296
297
    /**
298
     * Retrieves the CSV content
299
     *
300
     * @return string
301
     */
302 12
    public function getContent(): string
303
    {
304 12
        $raw = '';
305 12
        foreach ($this->chunk(8192) as $chunk) {
306 12
            $raw .= $chunk;
307
        }
308
309 12
        return $raw;
310
    }
311
312
    /**
313
     * Outputs all data on the CSV file
314
     *
315
     * @param string $filename CSV downloaded name if present adds extra headers
316
     *
317
     * @return int Returns the number of characters read from the handle
318
     *             and passed through to the output.
319
     */
320 6
    public function output(string $filename = null): int
321
    {
322 6
        if (null !== $filename) {
323 6
            $this->sendHeaders($filename);
324
        }
325 4
        $input_bom = $this->getInputBOM();
326 4
        $this->document->rewind();
327 4
        $this->document->fseek(strlen($input_bom));
328 4
        echo $this->output_bom;
329
330 4
        return strlen($this->output_bom) + $this->document->fpassthru();
331
    }
332
333
    /**
334
     * Send the CSV headers
335
     *
336
     * Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition
337
     *
338
     * @param string|null $filename CSV disposition name
339
     *
340
     * @throws Exception if the submitted header is invalid according to RFC 6266
341
     *
342
     * @see https://tools.ietf.org/html/rfc6266#section-4.3
343
     */
344 6
    protected function sendHeaders(string $filename)
345
    {
346 6
        if (strlen($filename) != strcspn($filename, '\\/')) {
347 2
            throw new Exception('The filename cannot contain the "/" and "\\" characters.');
348
        }
349
350 4
        $flag = FILTER_FLAG_STRIP_LOW;
351 4
        if (strlen($filename) !== mb_strlen($filename)) {
352 2
            $flag |= FILTER_FLAG_STRIP_HIGH;
353
        }
354
355 4
        $filenameFallback = str_replace('%', '', filter_var($filename, FILTER_SANITIZE_STRING, $flag));
356
357 4
        $disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filenameFallback));
358 4
        if ($filename !== $filenameFallback) {
359 2
            $disposition .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
360
        }
361
362 4
        header('Content-Type: text/csv');
363 4
        header('Content-Transfer-Encoding: binary');
364 4
        header('Content-Description: File Transfer');
365 4
        header('Content-Disposition: '.$disposition);
366 4
    }
367
368
    /**
369
     * Sets the field delimiter
370
     *
371
     * @param string $delimiter
372
     *
373
     * @throws Exception If the Csv control character is not one character only.
374
     *
375
     * @return static
376
     */
377 12
    public function setDelimiter(string $delimiter): self
378
    {
379 12
        if ($delimiter === $this->delimiter) {
380 6
            return $this;
381
        }
382
383 10
        if (1 === strlen($delimiter)) {
384 10
            $this->delimiter = $delimiter;
385 10
            $this->resetProperties();
386
387 10
            return $this;
388
        }
389
390 2
        throw new Exception(sprintf('%s() expects delimiter to be a single character %s given', __METHOD__, $delimiter));
391
    }
392
393
    /**
394
     * Reset dynamic object properties to improve performance
395
     */
396 4
    protected function resetProperties()
397
    {
398 4
    }
399
400
    /**
401
     * Sets the field enclosure
402
     *
403
     * @param string $enclosure
404
     *
405
     * @throws Exception If the Csv control character is not one character only.
406
     *
407
     * @return static
408
     */
409 2
    public function setEnclosure(string $enclosure): self
410
    {
411 2
        if ($enclosure === $this->enclosure) {
412 2
            return $this;
413
        }
414
415 2
        if (1 === strlen($enclosure)) {
416 2
            $this->enclosure = $enclosure;
417 2
            $this->resetProperties();
418
419 2
            return $this;
420
        }
421
422 2
        throw new Exception(sprintf('%s() expects enclosure to be a single character %s given', __METHOD__, $enclosure));
423
    }
424
425
    /**
426
     * Sets the field escape character
427
     *
428
     * @param string $escape
429
     *
430
     * @throws Exception If the Csv control character is not one character only.
431
     *
432
     * @return static
433
     */
434 2
    public function setEscape(string $escape): self
435
    {
436 2
        if ($escape === $this->escape) {
437 2
            return $this;
438
        }
439
440 2
        if (1 === strlen($escape)) {
441 2
            $this->escape = $escape;
442 2
            $this->resetProperties();
443
444 2
            return $this;
445
        }
446
447 2
        throw new Exception(sprintf('%s() expects escape to be a single character %s given', __METHOD__, $escape));
448
    }
449
450
    /**
451
     * Sets the BOM sequence to prepend the CSV on output
452
     *
453
     * @param string $str The BOM sequence
454
     *
455
     * @return static
456
     */
457 6
    public function setOutputBOM(string $str): self
458
    {
459 6
        $this->output_bom = $str;
460
461 6
        return $this;
462
    }
463
464
    /**
465
     * append a stream filter
466
     *
467
     * @param string $filtername a string or an object that implements the '__toString' method
468
     * @param mixed  $params     additional parameters for the filter
469
     *
470
     * @throws Exception If the stream filter API can not be used
471
     *
472
     * @return static
473
     */
474 10
    public function addStreamFilter(string $filtername, $params = null): self
475
    {
476 10
        if (!$this->document instanceof Stream) {
477 2
            throw new Exception('The stream filter API can not be used');
478
        }
479
480 8
        $this->document->appendFilter($filtername, $this->stream_filter_mode, $params);
481 6
        $this->stream_filters[$filtername] = true;
482 6
        $this->resetProperties();
483 6
        $this->input_bom = null;
484
485 6
        return $this;
486
    }
487
}
488