Completed
Push — master ( 624e2a...46ab4e )
by ignace nyamagana
02:19
created

AbstractCsv   B

Coupling/Cohesion

Components 2
Dependencies 2

Complexity

Total Complexity 43

Size/Duplication

Total Lines 465
Duplicated Lines 0 %

Test Coverage

Coverage 97.5%

Importance

Changes 0
Metric Value
dl 0
loc 465
ccs 117
cts 120
cp 0.975
rs 8.3157
c 0
b 0
f 0
wmc 43
lcom 2
cbo 2

26 Methods

Rating   Name   Duplication   Size   Complexity  
A createFromStream() 0 4 1
A getDelimiter() 0 4 1
A getEnclosure() 0 4 1
A getEscape() 0 4 1
A getOutputBOM() 0 4 1
A supportsStreamFilter() 0 4 1
A __construct() 0 5 1
A __destruct() 0 4 1
A __clone() 0 4 1
A createFromFileObject() 0 4 1
A createFromString() 0 4 1
A createFromPath() 0 4 1
A getInputBOM() 0 13 2
A getStreamFilterMode() 0 4 1
A hasStreamFilter() 0 4 1
A chunk() 0 17 4
A __toString() 0 9 2
A getContent() 0 9 2
A output() 0 12 2
B sendHeaders() 0 23 4
A setDelimiter() 0 15 3
A resetProperties() 0 3 1
A setEnclosure() 0 15 3
A setEscape() 0 15 3
A setOutputBOM() 0 6 1
A addStreamFilter() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like AbstractCsv often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractCsv, and based on these observations, apply Extract Interface, too.

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