Completed
Pull Request — master (#269)
by ignace nyamagana
04:24 queued 02:51
created

AbstractCsv::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 9.4285
c 0
b 0
f 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.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
    protected function __construct($document)
93 22
    {
94
        $this->document = $document;
95 22
        list($this->delimiter, $this->enclosure, $this->escape) = $this->document->getCsvControl();
96 22
    }
97 22
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function __destruct()
102 24
    {
103
        unset($this->document);
104 24
    }
105 24
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function __clone()
110 2
    {
111
        throw new Exception(sprintf('An object of class %s cannot be cloned', get_class($this)));
112 2
    }
113
114
    /**
115
     * Return a new instance from a SplFileObject
116
     *
117
     * @param SplFileObject $file
118
     *
119
     * @return static
120
     */
121
    public static function createFromFileObject(SplFileObject $file): self
122 24
    {
123
        return new static($file);
124 24
    }
125
126
    /**
127
     * Return a new instance from a PHP resource stream
128
     *
129
     * @param resource $stream
130
     *
131
     * @return static
132
     */
133
    public static function createFromStream($stream): self
134 4
    {
135
        return new static(new Stream($stream));
136 4
    }
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
    public static function createFromString(string $content): self
146 4
    {
147
        return new static(Stream::createFromString($content));
148 4
    }
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
    public static function createFromPath(string $path, string $open_mode = 'r+', $context = null): self
160 2
    {
161
        return new static(Stream::createFromPath($path, $open_mode, $context));
162 2
    }
163
164
    /**
165
     * Returns the current field delimiter
166
     *
167
     * @return string
168
     */
169
    public function getDelimiter(): string
170 10
    {
171
        return $this->delimiter;
172 10
    }
173
174
    /**
175
     * Returns the current field enclosure
176
     *
177
     * @return string
178
     */
179
    public function getEnclosure(): string
180 2
    {
181
        return $this->enclosure;
182 2
    }
183
184
    /**
185
     * Returns the current field escape character
186
     *
187
     * @return string
188
     */
189
    public function getEscape(): string
190 2
    {
191
        return $this->escape;
192 2
    }
193
194
    /**
195
     * Returns the BOM sequence in use on Output methods
196
     *
197
     * @return string
198
     */
199
    public function getOutputBOM(): string
200 2
    {
201
        return $this->output_bom;
202 2
    }
203
204
    /**
205
     * Returns the BOM sequence of the given CSV
206
     *
207
     * @return string
208
     */
209
    public function getInputBOM(): string
210 16
    {
211
        if (null !== $this->input_bom) {
212 16
            return $this->input_bom;
213 2
        }
214
215
        $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 16
220
        return $this->input_bom;
221 16
    }
222
223
    /**
224
     * Returns the stream filter mode
225
     *
226
     * @return int
227
     */
228
    public function getStreamFilterMode(): int
229 2
    {
230
        return $this->stream_filter_mode;
231 2
    }
232
233
    /**
234
     * Tells whether the stream filter capabilities can be used
235
     *
236
     * @return bool
237
     */
238
    public function supportsStreamFilter(): bool
239 4
    {
240
        return $this->document instanceof Stream;
241 4
    }
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
    public function hasStreamFilter(string $filtername): bool
251 2
    {
252
        return $this->stream_filters[$filtername] ?? false;
253 2
    }
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
    public function chunk(int $length): Generator
265 10
    {
266
        if ($length < 1) {
267 10
            throw new Exception(sprintf('%s() expects the length to be a positive integer %d given', __METHOD__, $length));
268 2
        }
269
270
        $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 8
        }
276
277
        while ($this->document->valid()) {
278 8
            yield $this->document->fread($length);
279 6
        }
280
    }
281 8
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
    public function __toString(): string
293 12
    {
294
        return $this->getContent();
295
    }
296 12
297
    /**
298
     * Retrieves the CSV content
299
     *
300
     * @return string
301
     */
302
    public function getContent(): string
303
    {
304
        $raw = '';
305
        foreach ($this->chunk(8192) as $chunk) {
306
            $raw .= $chunk;
307
        }
308 8
309
        return $raw;
310 8
    }
311 8
312 8
    /**
313
     * Outputs all data on the CSV file
314
     *
315 8
     * @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
    public function output(string $filename = null): int
321
    {
322
        if (null !== $filename) {
323
            $this->sendHeaders($filename);
324
        }
325
        $input_bom = $this->getInputBOM();
326 6
        $this->document->rewind();
327
        $this->document->fseek(strlen($input_bom));
328 6
        echo $this->output_bom;
329 6
330
        return strlen($this->output_bom) + $this->document->fpassthru();
331 4
    }
332 4
333 4
    /**
334 4
     * Send the CSV headers
335
     *
336 4
     * 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
    protected function sendHeaders(string $filename)
345
    {
346
        if (strlen($filename) != strcspn($filename, '\\/')) {
347
            throw new Exception('The filename cannot contain the "/" and "\\" characters.');
348
        }
349
350 6
        $flag = FILTER_FLAG_STRIP_LOW;
351
        if (strlen($filename) !== mb_strlen($filename)) {
352 6
            $flag |= FILTER_FLAG_STRIP_HIGH;
353 2
        }
354
355
        $filenameFallback = str_replace('%', '', filter_var($filename, FILTER_SANITIZE_STRING, $flag));
356 4
357 4
        $disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filenameFallback));
358 2
        if ($filename !== $filenameFallback) {
359
            $disposition .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
360
        }
361 4
362
        header('Content-Type: text/csv');
363 4
        header('Content-Transfer-Encoding: binary');
364 4
        header('Content-Description: File Transfer');
365 2
        header('Content-Disposition: '.$disposition);
366
    }
367
368 4
    /**
369 4
     * Sets the field delimiter
370 4
     *
371 4
     * @param string $delimiter
372 4
     *
373
     * @throws Exception If the Csv control character is not one character only.
374
     *
375
     * @return static
376
     */
377
    public function setDelimiter(string $delimiter): self
378
    {
379
        if ($delimiter === $this->delimiter) {
380
            return $this;
381
        }
382
383 12
        if (1 === strlen($delimiter)) {
384
            $this->delimiter = $delimiter;
385 12
            $this->resetProperties();
386 6
387
            return $this;
388
        }
389 10
390 10
        throw new Exception(sprintf('%s() expects delimiter to be a single character %s given', __METHOD__, $delimiter));
391 10
    }
392
393 10
    /**
394
     * Reset dynamic object properties to improve performance
395
     */
396 2
    protected function resetProperties()
397
    {
398
    }
399
400
    /**
401
     * Sets the field enclosure
402 4
     *
403
     * @param string $enclosure
404 4
     *
405
     * @throws Exception If the Csv control character is not one character only.
406
     *
407
     * @return static
408
     */
409
    public function setEnclosure(string $enclosure): self
410
    {
411
        if ($enclosure === $this->enclosure) {
412
            return $this;
413
        }
414
415 2
        if (1 === strlen($enclosure)) {
416
            $this->enclosure = $enclosure;
417 2
            $this->resetProperties();
418 2
419
            return $this;
420
        }
421 2
422 2
        throw new Exception(sprintf('%s() expects enclosure to be a single character %s given', __METHOD__, $enclosure));
423 2
    }
424
425 2
    /**
426
     * Sets the field escape character
427
     *
428 2
     * @param string $escape
429
     *
430
     * @throws Exception If the Csv control character is not one character only.
431
     *
432
     * @return static
433
     */
434
    public function setEscape(string $escape): self
435
    {
436
        if ($escape === $this->escape) {
437
            return $this;
438
        }
439
440 2
        if (1 === strlen($escape)) {
441
            $this->escape = $escape;
442 2
            $this->resetProperties();
443 2
444
            return $this;
445
        }
446 2
447 2
        throw new Exception(sprintf('%s() expects escape to be a single character %s given', __METHOD__, $escape));
448 2
    }
449
450 2
    /**
451
     * Sets the BOM sequence to prepend the CSV on output
452
     *
453 2
     * @param string $str The BOM sequence
454
     *
455
     * @return static
456
     */
457
    public function setOutputBOM(string $str): self
458
    {
459
        $this->output_bom = $str;
460
461
        return $this;
462
    }
463 6
464
    /**
465 6
     * append a stream filter
466
     *
467 6
     * @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
    public function addStreamFilter(string $filtername, $params = null): self
475
    {
476
        if (!$this->document instanceof Stream) {
477
            throw new Exception('The stream filter API can not be used');
478
        }
479
480 10
        $this->document->appendFilter($filtername, $this->stream_filter_mode, $params);
481
        $this->stream_filters[$filtername] = true;
482 10
        $this->resetProperties();
483 2
        $this->input_bom = null;
484
485
        return $this;
486 8
    }
487
}
488