AbstractCsv   B
last analyzed

Coupling/Cohesion

Components 2
Dependencies 2

Complexity

Total Complexity 42

Size/Duplication

Total Lines 460
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 42
lcom 2
cbo 2
dl 0
loc 460
ccs 117
cts 117
cp 1
rs 8.295
c 0
b 0
f 0

26 Methods

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