Completed
Pull Request — master (#210)
by ignace nyamagana
02:21
created

AbstractCsv::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
crap 1
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 LogicException;
18
use SplFileObject;
19
20
/**
21
 *  An abstract class to enable basic CSV manipulation
22
 *
23
 * @package League.csv
24
 * @since  4.0.0
25
 * @author  Ignace Nyamagana Butera <[email protected]>
26
 *
27
 */
28
abstract class AbstractCsv
29
{
30
    use ValidatorTrait;
31
32
    /**
33
     *  UTF-8 BOM sequence
34
     */
35
    const BOM_UTF8 = "\xEF\xBB\xBF";
36
37
    /**
38
     * UTF-16 BE BOM sequence
39
     */
40
    const BOM_UTF16_BE = "\xFE\xFF";
41
42
    /**
43
     * UTF-16 LE BOM sequence
44
     */
45
    const BOM_UTF16_LE = "\xFF\xFE";
46
47
    /**
48
     * UTF-32 BE BOM sequence
49
     */
50
    const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
51
52
    /**
53
     * UTF-32 LE BOM sequence
54
     */
55
    const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
56
57
    /**
58
     * The CSV document
59
     *
60
     * @var StreamIterator|SplFileObject
61
     */
62
    protected $document;
63
64
    /**
65
     * the field delimiter (one character only)
66
     *
67
     * @var string
68
     */
69
    protected $delimiter = ',';
70
71
    /**
72
     * the field enclosure character (one character only)
73
     *
74
     * @var string
75
     */
76
    protected $enclosure = '"';
77
78
    /**
79
     * the field escape character (one character only)
80
     *
81
     * @var string
82
     */
83
    protected $escape = '\\';
84
85
    /**
86
     * The Output file BOM character
87
     * @var string
88
     */
89
    protected $output_bom = '';
90
91
    /**
92
     * collection of stream filters
93
     *
94
     * @var array
95
     */
96
    protected $stream_filters = [];
97
98
    /**
99
     * The stream filter mode (read or write)
100
     *
101
     * @var int
102
     */
103
    protected $stream_filter_mode;
104
105
    /**
106
     * New instance
107
     *
108
     * @param SplFileObject|StreamIterator $document The CSV Object instance
109
     */
110 204
    protected function __construct($document)
111
    {
112 204
        $this->document = $document;
113 204
    }
114
115
    /**
116
     * The destructor
117
     */
118 204
    public function __destruct()
119
    {
120 204
        $this->clearStreamFilter();
121 204
        $this->document = null;
122 204
    }
123
124
    /**
125
     * @inheritdoc
126
     */
127 2
    public function __clone()
128
    {
129 2
        throw new LogicException('An object of class '.get_class($this).' cannot be cloned');
130
    }
131
132
    /**
133
     * Return a new {@link AbstractCsv} from a SplFileObject
134
     *
135
     * @param SplFileObject $file
136
     *
137
     * @return static
138
     */
139 194
    public static function createFromFileObject(SplFileObject $file): self
140
    {
141 194
        $csv = new static($file);
142 194
        $controls = $file->getCsvControl();
143 194
        $csv->delimiter = $controls[0];
144 194
        $csv->enclosure = $controls[1];
145 194
        if (isset($controls[2])) {
146 194
            $csv->escape = $controls[2];
147
        }
148
149 194
        return $csv;
150
    }
151
152
    /**
153
     * Return a new {@link AbstractCsv} from a PHP resource stream
154
     *
155
     * @param resource $stream
156
     *
157
     * @return static
158
     */
159 12
    public static function createFromStream($stream): self
160
    {
161 12
        return new static(new StreamIterator($stream));
162
    }
163
164
    /**
165
     * Return a new {@link AbstractCsv} from a string
166
     *
167
     * @param string $str the string
168
     *
169
     * @return static
170
     */
171 16
    public static function createFromString(string $str): self
172
    {
173 16
        $stream = fopen('php://temp', 'r+');
174 16
        fwrite($stream, $str);
175
176 16
        return new static(new StreamIterator($stream));
177
    }
178
179
    /**
180
     * Return a new {@link AbstractCsv} from a file path
181
     *
182
     * @param string $path      file path
183
     * @param string $open_mode the file open mode flag
184
     *
185
     * @return static
186
     */
187 18
    public static function createFromPath(string $path, string $open_mode = 'r+'): self
188
    {
189 18
        if (!$stream = @fopen($path, $open_mode)) {
190 2
            throw new Exception(error_get_last()['message']);
191
        }
192
193 16
        return new static(new StreamIterator($stream));
194
    }
195
196
    /**
197
     * Returns the current field delimiter
198
     *
199
     * @return string
200
     */
201 4
    public function getDelimiter(): string
202
    {
203 4
        return $this->delimiter;
204
    }
205
206
    /**
207
     * Returns the current field enclosure
208
     *
209
     * @return string
210
     */
211 4
    public function getEnclosure(): string
212
    {
213 4
        return $this->enclosure;
214
    }
215
216
    /**
217
     * Returns the current field escape character
218
     *
219
     * @return string
220
     */
221 4
    public function getEscape(): string
222
    {
223 4
        return $this->escape;
224
    }
225
226
    /**
227
     * Returns the BOM sequence in use on Output methods
228
     *
229
     * @return string
230
     */
231 2
    public function getOutputBOM(): string
232
    {
233 2
        return $this->output_bom;
234
    }
235
236
    /**
237
     * Returns the BOM sequence of the given CSV
238
     *
239
     * @return string
240
     */
241 146
    public function getInputBOM(): string
242
    {
243
        $bom = [
244 146
            self::BOM_UTF32_BE, self::BOM_UTF32_LE,
245 146
            self::BOM_UTF16_BE, self::BOM_UTF16_LE, self::BOM_UTF8,
246
        ];
247
248 146
        $this->document->setFlags(SplFileObject::READ_CSV);
249 146
        $this->document->rewind();
250 146
        $line = $this->document->fgets();
251 146
        $res = array_filter($bom, function ($sequence) use ($line) {
252 146
            return strpos($line, $sequence) === 0;
253 146
        });
254
255 146
        return (string) array_shift($res);
256
    }
257
258
    /**
259
     * Tells whether the stream filter capabilities can be used
260
     *
261
     * @return bool
262
     */
263 4
    public function isStream(): bool
264
    {
265 4
        return $this->document instanceof StreamIterator;
266
    }
267
268
    /**
269
     * Tell whether the specify stream filter is attach to the current stream
270
     *
271
     * @return bool
272
     */
273 4
    public function hasStreamFilter(string $filtername): bool
274
    {
275 4
        return isset($this->stream_filters[$filtername]);
276
    }
277
278
    /**
279
     * Retrieves the CSV content
280
     *
281
     * @return string
282
     */
283 32
    public function __toString(): string
284
    {
285 32
        ob_start();
286 32
        $this->fpassthru();
287
288 32
        return ob_get_clean();
289
    }
290
291
    /**
292
     * Outputs all data on the CSV file
293
     *
294
     * @param string $filename CSV downloaded name if present adds extra headers
295
     *
296
     * @return int Returns the number of characters read from the handle
297
     *             and passed through to the output.
298
     */
299 4
    public function output(string $filename = null): int
300
    {
301 4
        if (null !== $filename) {
302 4
            $filename = filter_var($filename, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
303 4
            header('content-type: text/csv');
304 4
            header('content-transfer-encoding: binary');
305 4
            header('content-disposition: attachment; filename="'.rawurlencode($filename).'"');
306
        }
307
308 4
        return $this->fpassthru();
309
    }
310
311
    /**
312
     * Outputs all data from the CSV
313
     *
314
     * @return int Returns the number of characters read from the handle
315
     *             and passed through to the output.
316
     */
317 36
    protected function fpassthru(): int
318
    {
319 36
        $bom = '';
320 36
        $input_bom = $this->getInputBOM();
321 36
        if ($this->output_bom && $input_bom != $this->output_bom) {
322 4
            $bom = $this->output_bom;
323
        }
324
325 36
        $this->document->rewind();
326 36
        if ('' !== $bom) {
327 4
            $this->document->fseek(mb_strlen($input_bom));
328
        }
329 36
        echo $bom;
330 36
        $res = $this->document->fpassthru();
331
332 36
        return $res + strlen($bom);
333
    }
334
335
    /**
336
     * Sets the field delimiter
337
     *
338
     * @param string $delimiter
339
     *
340
     * @return static
341
     */
342 2
    public function setDelimiter(string $delimiter): self
343
    {
344 2
        $this->delimiter = $this->filterControl($delimiter, 'delimiter');
345 2
        $this->resetDynamicProperties();
346
347 2
        return $this;
348
    }
349
350
    /**
351
     * Reset dynamic CSV document properties to improve performance
352
     */
353 4
    protected function resetDynamicProperties()
354
    {
355 4
    }
356
357
    /**
358
     * Sets the field enclosure
359
     *
360
     * @param string $enclosure
361
     *
362
     * @return static
363
     */
364 2
    public function setEnclosure(string $enclosure): self
365
    {
366 2
        $this->enclosure = $this->filterControl($enclosure, 'enclosure');
367 2
        $this->resetDynamicProperties();
368
369 2
        return $this;
370
    }
371
372
    /**
373
     * Sets the field escape character
374
     *
375
     * @param string $escape
376
     *
377
     * @return static
378
     */
379 2
    public function setEscape(string $escape): self
380
    {
381 2
        $this->escape = $this->filterControl($escape, 'escape');
382 2
        $this->resetDynamicProperties();
383
384 2
        return $this;
385
    }
386
387
    /**
388
     * Sets the BOM sequence to prepend the CSV on output
389
     *
390
     * @param string $str The BOM sequence
391
     *
392
     * @return static
393
     */
394 6
    public function setOutputBOM(string $str): self
395
    {
396 6
        $this->output_bom = $str;
397
398 6
        return $this;
399
    }
400
401
    /**
402
     * append a stream filter
403
     *
404
     * @param string $filtername a string or an object that implements the '__toString' method
405
     *
406
     * @throws LogicException If the stream filter API can not be used
407
     *
408
     * @return static
409
     */
410 12
    public function addStreamFilter(string $filtername): self
411
    {
412 12
        if (!$this->document instanceof StreamIterator) {
413 2
            throw new LogicException('The stream filter API can not be used');
414
        }
415
416 10
        $this->stream_filters[$filtername][] = $this->document->appendFilter($filtername, $this->stream_filter_mode);
417 10
        $this->resetDynamicProperties();
418
419 10
        return $this;
420
    }
421
422
    /**
423
     * Remove all registered stream filter
424
     */
425 204
    protected function clearStreamFilter()
426
    {
427 204
        foreach (array_keys($this->stream_filters) as $filtername) {
428 10
            $this->removeStreamFilter($filtername);
429
        }
430
431 204
        $this->stream_filters = [];
432 204
    }
433
434
    /**
435
     * Remove all the stream filter with the same name
436
     *
437
     * @param string $filtername the stream filter name
438
     */
439 10
    protected function removeStreamFilter(string $filtername)
440
    {
441 10
        foreach ($this->stream_filters[$filtername] as $filter) {
442 10
            $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...
443
        }
444
445 10
        unset($this->stream_filters[$filtername]);
446 10
    }
447
}
448