Completed
Pull Request — master (#309)
by ignace nyamagana
04:57
created

AbstractCsv   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 419
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 2

Test Coverage

Coverage 98.31%

Importance

Changes 0
Metric Value
dl 0
loc 419
ccs 116
cts 118
cp 0.9831
rs 9.0399
c 0
b 0
f 0
wmc 42
lcom 2
cbo 2

26 Methods

Rating   Name   Duplication   Size   Complexity  
A setOutputBOM() 0 6 1
A output() 0 12 2
A sendHeaders() 0 23 4
A setDelimiter() 0 15 3
A setEnclosure() 0 15 3
A setEscape() 0 15 3
A addStreamFilter() 0 13 2
A __construct() 0 6 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 hasStreamFilter() 0 4 1
A chunk() 0 17 4
A __toString() 0 4 1
A getContent() 0 9 2
A resetProperties() 0 3 1

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
/**
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
     */
92
    protected $escape = '\\';
93
94
    /**
95
     * The CSV document.
96
     *
97
     * @var SplFileObject|Stream
98
     */
99
    protected $document;
100
101
    /**
102
     * New instance.
103
     *
104
     * @param SplFileObject|Stream $document The CSV Object instance
105
     */
106 33
    protected function __construct($document)
107
    {
108 33
        $this->document = $document;
109 33
        list($this->delimiter, $this->enclosure, $this->escape) = $this->document->getCsvControl();
110 33
        $this->resetProperties();
111 33
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 36
    public function __destruct()
117
    {
118 36
        unset($this->document);
119 36
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 3
    public function __clone()
125
    {
126 3
        throw new Exception(sprintf('An object of class %s cannot be cloned', get_class($this)));
127
    }
128
129
    /**
130
     * Return a new instance from a SplFileObject.
131
     *
132
     * @return static
133
     */
134 36
    public static function createFromFileObject(SplFileObject $file)
135
    {
136 36
        return new static($file);
137
    }
138
139
    /**
140
     * Return a new instance from a PHP resource stream.
141
     *
142
     * @param resource $stream
143
     *
144
     * @return static
145
     */
146 3
    public static function createFromStream($stream)
147
    {
148 3
        return new static(new Stream($stream));
149
    }
150
151
    /**
152
     * Return a new instance from a string.
153
     *
154
     * @return static
155
     */
156 6
    public static function createFromString(string $content)
157
    {
158 6
        return new static(Stream::createFromString($content));
159
    }
160
161
    /**
162
     * Return a new instance from a file path.
163
     *
164
     * @param resource|null $context the resource context
165
     *
166
     * @return static
167
     */
168 3
    public static function createFromPath(string $path, string $open_mode = 'r+', $context = null)
169
    {
170 3
        return new static(Stream::createFromPath($path, $open_mode, $context));
171
    }
172
173
    /**
174
     * Returns the current field delimiter.
175
     */
176 15
    public function getDelimiter(): string
177
    {
178 15
        return $this->delimiter;
179
    }
180
181
    /**
182
     * Returns the current field enclosure.
183
     */
184 3
    public function getEnclosure(): string
185
    {
186 3
        return $this->enclosure;
187
    }
188
189
    /**
190
     * Returns the current field escape character.
191
     */
192 3
    public function getEscape(): string
193
    {
194 3
        return $this->escape;
195
    }
196
197
    /**
198
     * Returns the BOM sequence in use on Output methods.
199
     */
200 3
    public function getOutputBOM(): string
201
    {
202 3
        return $this->output_bom;
203
    }
204
205
    /**
206
     * Returns the BOM sequence of the given CSV.
207
     */
208 24
    public function getInputBOM(): string
209
    {
210 24
        if (null !== $this->input_bom) {
211 3
            return $this->input_bom;
212
        }
213
214 24
        $this->document->setFlags(SplFileObject::READ_CSV);
215 24
        $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
216 24
        $this->document->rewind();
217 24
        $this->input_bom = bom_match(implode(',', (array) $this->document->current()));
218
219 24
        return $this->input_bom;
220
    }
221
222
    /**
223
     * Returns the stream filter mode.
224
     */
225 3
    public function getStreamFilterMode(): int
226
    {
227 3
        return $this->stream_filter_mode;
228
    }
229
230
    /**
231
     * Tells whether the stream filter capabilities can be used.
232
     */
233 6
    public function supportsStreamFilter(): bool
234
    {
235 6
        return $this->document instanceof Stream;
236
    }
237
238
    /**
239
     * Tell whether the specify stream filter is attach to the current stream.
240
     */
241 3
    public function hasStreamFilter(string $filtername): bool
242
    {
243 3
        return $this->stream_filters[$filtername] ?? false;
244
    }
245
246
    /**
247
     * Retuns the CSV document as a Generator of string chunk.
248
     *
249
     * @param int $length number of bytes read
250
     *
251
     * @throws Exception if the number of bytes is lesser than 1
252
     */
253 15
    public function chunk(int $length): Generator
254
    {
255 15
        if ($length < 1) {
256 3
            throw new Exception(sprintf('%s() expects the length to be a positive integer %d given', __METHOD__, $length));
257
        }
258
259 12
        $input_bom = $this->getInputBOM();
260 12
        $this->document->rewind();
261 12
        $this->document->fseek(strlen($input_bom));
262 12
        foreach (str_split($this->output_bom.$this->document->fread($length), $length) as $chunk) {
263 12
            yield $chunk;
264
        }
265
266 12
        while ($this->document->valid()) {
267 7
            yield $this->document->fread($length);
268
        }
269 12
    }
270
271
    /**
272
     * DEPRECATION WARNING! This method will be removed in the next major point release.
273
     *
274
     * @deprecated deprecated since version 9.1.0
275
     * @see AbstractCsv::getContent
276
     *
277
     * Retrieves the CSV content
278
     */
279 3
    public function __toString(): string
280
    {
281 3
        return $this->getContent();
282
    }
283
284
    /**
285
     * Retrieves the CSV content.
286
     */
287 18
    public function getContent(): string
288
    {
289 18
        $raw = '';
290 18
        foreach ($this->chunk(8192) as $chunk) {
291 18
            $raw .= $chunk;
292
        }
293
294 18
        return $raw;
295
    }
296
297
    /**
298
     * Outputs all data on the CSV file.
299
     *
300
     *
301
     * @return int Returns the number of characters read from the handle
302
     *             and passed through to the output.
303
     */
304 9
    public function output(string $filename = null): int
305
    {
306 9
        if (null !== $filename) {
307 9
            $this->sendHeaders($filename);
308
        }
309 6
        $input_bom = $this->getInputBOM();
310 6
        $this->document->rewind();
311 6
        $this->document->fseek(strlen($input_bom));
312 6
        echo $this->output_bom;
313
314 6
        return strlen($this->output_bom) + $this->document->fpassthru();
315
    }
316
317
    /**
318
     * Send the CSV headers.
319
     *
320
     * Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition
321
     *
322
     * @throws Exception if the submitted header is invalid according to RFC 6266
323
     *
324
     * @see https://tools.ietf.org/html/rfc6266#section-4.3
325
     */
326 9
    protected function sendHeaders(string $filename)
327
    {
328 9
        if (strlen($filename) != strcspn($filename, '\\/')) {
329 3
            throw new Exception('The filename cannot contain the "/" and "\\" characters.');
330
        }
331
332 6
        $flag = FILTER_FLAG_STRIP_LOW;
333 6
        if (strlen($filename) !== mb_strlen($filename)) {
334 3
            $flag |= FILTER_FLAG_STRIP_HIGH;
335
        }
336
337 6
        $filenameFallback = str_replace('%', '', filter_var($filename, FILTER_SANITIZE_STRING, $flag));
338
339 6
        $disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filenameFallback));
340 6
        if ($filename !== $filenameFallback) {
341 3
            $disposition .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
342
        }
343
344 6
        header('Content-Type: text/csv');
345 6
        header('Content-Transfer-Encoding: binary');
346 6
        header('Content-Description: File Transfer');
347 6
        header('Content-Disposition: '.$disposition);
348 6
    }
349
350
    /**
351
     * Sets the field delimiter.
352
     *
353
     * @throws Exception If the Csv control character is not one character only.
354
     *
355
     * @return static
356
     */
357 18
    public function setDelimiter(string $delimiter): self
358
    {
359 18
        if ($delimiter === $this->delimiter) {
360 9
            return $this;
361
        }
362
363 15
        if (1 === strlen($delimiter)) {
364 15
            $this->delimiter = $delimiter;
365 15
            $this->resetProperties();
366
367 15
            return $this;
368
        }
369
370 3
        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
    protected function resetProperties()
377
    {
378
    }
379
380
    /**
381
     * Sets the field enclosure.
382
     *
383
     * @throws Exception If the Csv control character is not one character only.
384
     *
385
     * @return static
386
     */
387 3
    public function setEnclosure(string $enclosure): self
388
    {
389 3
        if ($enclosure === $this->enclosure) {
390 3
            return $this;
391
        }
392
393 3
        if (1 === strlen($enclosure)) {
394 3
            $this->enclosure = $enclosure;
395 3
            $this->resetProperties();
396
397 3
            return $this;
398
        }
399
400 3
        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
     * @return static
409
     */
410 3
    public function setEscape(string $escape): self
411
    {
412 3
        if ($escape === $this->escape) {
413 3
            return $this;
414
        }
415
416 3
        if (1 === strlen($escape)) {
417 3
            $this->escape = $escape;
418 3
            $this->resetProperties();
419
420 3
            return $this;
421
        }
422
423 3
        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 9
    public function setOutputBOM(string $str): self
432
    {
433 9
        $this->output_bom = $str;
434
435 9
        return $this;
436
    }
437
438
    /**
439
     * append a stream filter.
440
     *
441
     * @param null|mixed $params
442
     *
443
     * @throws Exception If the stream filter API can not be used
444
     *
445
     * @return static
446
     */
447 15
    public function addStreamFilter(string $filtername, $params = null): self
448
    {
449 15
        if (!$this->document instanceof Stream) {
450 3
            throw new Exception('The stream filter API can not be used');
451
        }
452
453 12
        $this->document->appendFilter($filtername, $this->stream_filter_mode, $params);
454 9
        $this->stream_filters[$filtername] = true;
455 9
        $this->resetProperties();
456 9
        $this->input_bom = null;
457
458 9
        return $this;
459
    }
460
}
461