Completed
Push — master ( ecd0f8...3ad52b )
by ignace nyamagana
15:08
created

AbstractCsv::chunk()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4

Importance

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