Completed
Pull Request — master (#309)
by ignace nyamagana
01:59
created

AbstractCsv   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 418
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 0
loc 418
ccs 117
cts 117
cp 1
rs 8.96
c 0
b 0
f 0
wmc 43
lcom 2
cbo 2

26 Methods

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