Completed
Push — master ( a98188...ba1378 )
by ignace nyamagana
07:22 queued 04:55
created

AbstractCsv::chunk()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 4
nop 1
dl 0
loc 18
ccs 14
cts 14
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
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.0.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 League\Csv\Exception\LogicException;
19
use League\Csv\Exception\RuntimeException;
20
use SplFileObject;
21
use function League\Csv\bom_match;
22
23
/**
24
 *  An abstract class to enable CSV document loading.
25
 *
26
 * @package League.csv
27
 * @since   4.0.0
28
 * @author  Ignace Nyamagana Butera <[email protected]>
29
 *
30
 */
31
abstract class AbstractCsv
32
{
33
    use ValidatorTrait;
34
35
    /**
36
     * The CSV document
37
     *
38
     * @var StreamIterator|SplFileObject
39
     */
40
    protected $document;
41
42
    /**
43
     * the field delimiter (one character only)
44
     *
45
     * @var string
46
     */
47
    protected $delimiter = ',';
48
49
    /**
50
     * the field enclosure character (one character only)
51
     *
52
     * @var string
53
     */
54
    protected $enclosure = '"';
55
56
    /**
57
     * the field escape character (one character only)
58
     *
59
     * @var string
60
     */
61
    protected $escape = '\\';
62
63
    /**
64
     * The CSV document BOM sequence
65
     *
66
     * @var string|null
67
     */
68
    protected $input_bom = null;
69
70
    /**
71
     * The Output file BOM character
72
     *
73
     * @var string
74
     */
75
    protected $output_bom = '';
76
77
    /**
78
     * collection of stream filters
79
     *
80
     * @var array
81
     */
82
    protected $stream_filters = [];
83
84
    /**
85
     * The stream filter mode (read or write)
86
     *
87
     * @var int
88
     */
89
    protected $stream_filter_mode;
90
91
    /**
92
     * New instance
93
     *
94
     * @param SplFileObject|StreamIterator $document The CSV Object instance
95
     */
96 198
    protected function __construct($document)
97
    {
98 198
        $this->document = $document;
99 198
    }
100
101
    /**
102
     * @inheritdoc
103
     */
104 2
    public function __clone()
105
    {
106 2
        throw new LogicException('An object of class '.get_class($this).' cannot be cloned');
107
    }
108
109
    /**
110
     * Return a new {@link AbstractCsv} from a SplFileObject
111
     *
112
     * @param SplFileObject $file
113
     *
114
     * @return static
115
     */
116 176
    public static function createFromFileObject(SplFileObject $file): self
117
    {
118 176
        $csv = new static($file);
119 176
        $controls = $file->getCsvControl();
120 176
        $csv->delimiter = $controls[0];
121 176
        $csv->enclosure = $controls[1];
122 176
        if (isset($controls[2])) {
123 176
            $csv->escape = $controls[2];
124
        }
125
126 176
        return $csv;
127
    }
128
129
    /**
130
     * Return a new {@link AbstractCsv} from a PHP resource stream
131
     *
132
     * @param resource $stream
133
     *
134
     * @return static
135
     */
136 12
    public static function createFromStream($stream): self
137
    {
138 12
        return new static(new StreamIterator($stream));
139
    }
140
141
    /**
142
     * Return a new {@link AbstractCsv} from a string
143
     *
144
     * @param string $str the string
145
     *
146
     * @return static
147
     */
148 22
    public static function createFromString(string $str): self
149
    {
150 22
        $stream = fopen('php://temp', 'r+');
151 22
        fwrite($stream, $str);
152
153 22
        return new static(new StreamIterator($stream));
154
    }
155
156
    /**
157
     * Return a new {@link AbstractCsv} from a file path
158
     *
159
     * @param string $path      file path
160
     * @param string $open_mode the file open mode flag
161
     *
162
     * @return static
163
     */
164 26
    public static function createFromPath(string $path, string $open_mode = 'r+'): self
165
    {
166 26
        if (!$stream = @fopen($path, $open_mode)) {
167 2
            throw new RuntimeException(error_get_last()['message']);
168
        }
169
170 24
        return new static(new StreamIterator($stream));
171
    }
172
173
    /**
174
     * Returns the current field delimiter
175
     *
176
     * @return string
177
     */
178 4
    public function getDelimiter(): string
179
    {
180 4
        return $this->delimiter;
181
    }
182
183
    /**
184
     * Returns the current field enclosure
185
     *
186
     * @return string
187
     */
188 4
    public function getEnclosure(): string
189
    {
190 4
        return $this->enclosure;
191
    }
192
193
    /**
194
     * Returns the current field escape character
195
     *
196
     * @return string
197
     */
198 4
    public function getEscape(): string
199
    {
200 4
        return $this->escape;
201
    }
202
203
    /**
204
     * Returns the BOM sequence in use on Output methods
205
     *
206
     * @return string
207
     */
208 2
    public function getOutputBOM(): string
209
    {
210 2
        return $this->output_bom;
211
    }
212
213
    /**
214
     * Returns the BOM sequence of the given CSV
215
     *
216
     * @return string
217
     */
218 144
    public function getInputBOM(): string
219
    {
220 144
        if (null === $this->input_bom) {
221 144
            $this->document->setFlags(SplFileObject::READ_CSV);
222 144
            $this->document->rewind();
223 144
            $line = $this->document->fgets();
224 144
            $this->input_bom = false === $line ? '' : bom_match($line);
225
        }
226
227 144
        return $this->input_bom;
228
    }
229
230
    /**
231
     * Tells whether the stream filter capabilities can be used
232
     *
233
     * @return bool
234
     */
235 4
    public function supportsStreamFilter(): bool
236
    {
237 4
        return $this->document instanceof StreamIterator;
238
    }
239
240
    /**
241
     * Tell whether the specify stream filter is attach to the current stream
242
     *
243
     * @return bool
244
     */
245 4
    public function hasStreamFilter(string $filtername): bool
246
    {
247 4
        return isset($this->stream_filters[$filtername]);
248
    }
249
250
    /**
251
     * Retrieves the CSV content
252
     *
253
     * @return string
254
     */
255 32
    public function __toString(): string
256
    {
257 32
        ob_start();
258 32
        $this->fpassthru();
259
260 32
        return ob_get_clean();
261
    }
262
263
    /**
264
     * Outputs all data from the CSV
265
     *
266
     * @return int Returns the number of characters read from the handle
267
     *             and passed through to the output.
268
     */
269 36
    protected function fpassthru(): int
270
    {
271 36
        $res = 0;
272 36
        $input_bom = $this->getInputBOM();
273 36
        $this->document->rewind();
274 36
        if ($input_bom != $this->output_bom) {
275 4
            $res = strlen($this->output_bom);
276 4
            $this->document->fseek(mb_strlen($input_bom));
277 4
            echo $this->output_bom;
278
        }
279
280 36
        return $res + $this->document->fpassthru();
281
    }
282
283
    /**
284
     * Outputs all data on the CSV file
285
     *
286
     * @param string $filename CSV downloaded name if present adds extra headers
287
     *
288
     * @return int Returns the number of characters read from the handle
289
     *             and passed through to the output.
290
     */
291 4
    public function output(string $filename = null): int
292
    {
293 4
        if (null !== $filename) {
294 4
            header('content-type: text/csv');
295 4
            header('content-transfer-encoding: binary');
296 4
            header('content-description: File Transfer');
297 4
            header('content-disposition: attachment; filename="'.rawurlencode($filename).'"');
298
        }
299
300 4
        return $this->fpassthru();
301
    }
302
303
    /**
304
     * Retuns the CSV document as a Generator of string chunk
305
     *
306
     * @param int $length number of bytes read
307
     *
308
     * @throws InvalidArgumentException If the length is invalid
309
     *
310
     * @return Generator
311
     */
312 4
    public function chunk(int $length): Generator
313
    {
314 4
        $length = $this->filterMinRange($length, 1, 'The length must be a positive integer');
315 2
        $input_bom = $this->getInputBOM();
316 2
        $this->document->rewind();
317 2
        if ($input_bom != $this->output_bom) {
318 2
            $this->document->fseek(strlen($input_bom));
319 2
            $chunk = $this->output_bom.$this->document->fread($length);
1 ignored issue
show
Bug introduced by
The method fread does only exist in League\Csv\StreamIterator, but not in SplFileObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
320 2
            $this->document->fflush();
321 2
            yield $chunk;
322
        }
323
324 2
        while ($this->document->valid()) {
325 2
            $chunk = $this->document->fread($length);
326 2
            $this->document->fflush();
327 2
            yield $chunk;
328
        }
329 2
    }
330
331
    /**
332
     * Sets the field delimiter
333
     *
334
     * @param string $delimiter
335
     *
336
     * @return static
337
     */
338 10
    public function setDelimiter(string $delimiter): self
339
    {
340 10
        $delimiter = $this->filterControl($delimiter, 'delimiter');
341 10
        if ($delimiter != $this->delimiter) {
342 10
            $this->delimiter = $delimiter;
343 10
            $this->resetProperties();
344
        }
345
346 10
        return $this;
347
    }
348
349
    /**
350
     * Reset dynamic CSV document properties to improve performance
351
     */
352 4
    protected function resetProperties()
353
    {
354 4
    }
355
356
    /**
357
     * Sets the field enclosure
358
     *
359
     * @param string $enclosure
360
     *
361
     * @return static
362
     */
363 2
    public function setEnclosure(string $enclosure): self
364
    {
365 2
        $enclosure = $this->filterControl($enclosure, 'enclosure');
366 2
        if ($enclosure != $this->enclosure) {
367 2
            $this->enclosure = $enclosure;
368 2
            $this->resetProperties();
369
        }
370
371 2
        return $this;
372
    }
373
374
    /**
375
     * Sets the field escape character
376
     *
377
     * @param string $escape
378
     *
379
     * @return static
380
     */
381 2
    public function setEscape(string $escape): self
382
    {
383 2
        $escape = $this->filterControl($escape, 'escape');
384 2
        if ($escape != $this->escape) {
385 2
            $this->escape = $escape;
386 2
            $this->resetProperties();
387
        }
388
389
390 2
        return $this;
391
    }
392
393
    /**
394
     * Sets the BOM sequence to prepend the CSV on output
395
     *
396
     * @param string $str The BOM sequence
397
     *
398
     * @return static
399
     */
400 8
    public function setOutputBOM(string $str): self
401
    {
402 8
        $this->output_bom = $str;
403
404 8
        return $this;
405
    }
406
407
    /**
408
     * append a stream filter
409
     *
410
     * @param string $filtername a string or an object that implements the '__toString' method
411
     * @param mixed  $params     additional parameters for the filter
412
     *
413
     * @throws LogicException If the stream filter API can not be used
414
     *
415
     * @return static
416
     */
417 16
    public function addStreamFilter(string $filtername, $params = null): self
418
    {
419 16
        if (!$this->document instanceof StreamIterator) {
420 2
            throw new LogicException('The stream filter API can not be used');
421
        }
422
423 14
        $this->stream_filters[$filtername][] = $this->document->appendFilter($filtername, $this->stream_filter_mode, $params);
424 14
        $this->resetProperties();
425 14
        $this->input_bom = null;
426
427 14
        return $this;
428
    }
429
430
    /**
431
     * The destructor
432
     */
433 198
    public function __destruct()
434
    {
435 198
        if ($this->document instanceof StreamIterator) {
436 35
            $walker = function ($filter): bool {
437 14
                return $this->document->removeFilter($filter);
1 ignored issue
show
Bug introduced by
The method removeFilter does only exist in League\Csv\StreamIterator, but not in SplFileObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
438 28
            };
439
440 56
            array_walk_recursive($this->stream_filters, $walker);
441
        }
442
443 198
        $this->document = null;
444 198
    }
445
}
446