Completed
Pull Request — master (#309)
by ignace nyamagana
02:17
created

Stream::fread()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 SeekableIterator;
20
use SplFileObject;
21
use TypeError;
22
use const SEEK_SET;
23
use function array_keys;
24
use function array_values;
25
use function array_walk_recursive;
26
use function fclose;
27
use function feof;
28
use function fflush;
29
use function fgetcsv;
30
use function fopen;
31
use function fpassthru;
32
use function fputcsv;
33
use function fread;
34
use function fseek;
35
use function fwrite;
36
use function get_class;
37
use function get_resource_type;
38
use function gettype;
39
use function is_resource;
40
use function rewind;
41
use function sprintf;
42
use function stream_filter_append;
43
use function stream_filter_remove;
44
use function stream_get_meta_data;
45
use function strlen;
46
47
/**
48
 * An object oriented API for a CSV stream resource.
49
 *
50
 * @package  League.csv
51
 * @since    8.2.0
52
 * @author   Ignace Nyamagana Butera <[email protected]>
53
 * @internal used internally to iterate over a stream resource
54
 */
55
class Stream implements SeekableIterator
56
{
57
    /**
58
     * Attached filters.
59
     *
60
     * @var resource[]
61
     */
62
    protected $filters = [];
63
64
    /**
65
     * stream resource.
66
     *
67
     * @var resource
68
     */
69
    protected $stream;
70
71
    /**
72
     * Tell whether the stream should be closed on object destruction.
73
     *
74
     * @var bool
75
     */
76
    protected $should_close_stream = false;
77
78
    /**
79
     * Current iterator value.
80
     *
81
     * @var mixed
82
     */
83
    protected $value;
84
85
    /**
86
     * Current iterator key.
87
     *
88
     * @var int
89
     */
90
    protected $offset;
91
92
    /**
93
     * Flags for the Document.
94
     *
95
     * @var int
96
     */
97
    protected $flags = 0;
98
99
    /**
100
     * the field delimiter (one character only).
101
     *
102
     * @var string
103
     */
104
    protected $delimiter = ',';
105
106
    /**
107
     * the field enclosure character (one character only).
108
     *
109
     * @var string
110
     */
111
    protected $enclosure = '"';
112
113
    /**
114
     * the field escape character (one character only).
115
     *
116
     * @var string
117
     */
118
    protected $escape = '\\';
119
120
    /**
121
     * Tell whether the current stream is seekable;.
122
     *
123
     * @var bool
124
     */
125
    protected $is_seekable = false;
126
127
    /**
128
     * New instance.
129
     *
130
     * @param resource $resource stream type resource
131
     */
132 48
    public function __construct($resource)
133
    {
134 48
        if (!is_resource($resource)) {
135 3
            throw new TypeError(sprintf('Argument passed must be a stream resource, %s given', gettype($resource)));
136
        }
137
138 45
        if ('stream' !== ($type = get_resource_type($resource))) {
139 3
            throw new TypeError(sprintf('Argument passed must be a stream resource, %s resource given', $type));
140
        }
141
142 42
        $this->is_seekable = stream_get_meta_data($resource)['seekable'];
143 42
        $this->stream = $resource;
144 42
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function __destruct()
150
    {
151 42
        $walker = function ($filter): bool {
152 12
            return stream_filter_remove($filter);
153 42
        };
154
155 42
        array_walk_recursive($this->filters, $walker);
156
157 42
        if ($this->should_close_stream && is_resource($this->stream)) {
158 27
            fclose($this->stream);
159
        }
160
161 42
        unset($this->stream);
162 42
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 3
    public function __clone()
168
    {
169 3
        throw new Exception(sprintf('An object of class %s cannot be cloned', get_class($this)));
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175 3
    public function __debugInfo()
176
    {
177 3
        return stream_get_meta_data($this->stream) + [
178 3
            'delimiter' => $this->delimiter,
179 3
            'enclosure' => $this->enclosure,
180 3
            'escape' => $this->escape,
181 3
            'stream_filters' => array_keys($this->filters),
182
        ];
183
    }
184
185
    /**
186
     * Return a new instance from a file path.
187
     *
188
     * @param resource|null $context
189
     *
190
     * @throws Exception if the stream resource can not be created
191
     *
192
     * @return static
193
     */
194 21
    public static function createFromPath(string $path, string $open_mode = 'r', $context = null)
195
    {
196 21
        $args = [$path, $open_mode];
197 21
        if (null !== $context) {
198 3
            $args[] = false;
199 3
            $args[] = $context;
200
        }
201
202 21
        if (!$resource = @fopen(...$args)) {
203 6
            throw new Exception(sprintf('`%s`: failed to open stream: No such file or directory', $path));
204
        }
205
206 15
        $instance = new static($resource);
207 15
        $instance->should_close_stream = true;
208
209 15
        return $instance;
210
    }
211
212
    /**
213
     * Return a new instance from a string.
214
     *
215
     *
216
     * @return static
217
     */
218 15
    public static function createFromString(string $content)
219
    {
220 15
        $resource = fopen('php://temp', 'r+');
221 15
        fwrite($resource, $content);
222
223 15
        $instance = new static($resource);
224 15
        $instance->should_close_stream = true;
225
226 15
        return $instance;
227
    }
228
229
    /**
230
     * append a filter.
231
     *
232
     * @see http://php.net/manual/en/function.stream-filter-append.php
233
     *
234
     * @param  null|mixed $params
235
     * @throws Exception  if the filter can not be appended
236
     */
237 18
    public function appendFilter(string $filtername, int $read_write, $params = null)
238
    {
239 18
        $res = @stream_filter_append($this->stream, $filtername, $read_write, $params);
240 18
        if (is_resource($res)) {
241 12
            $this->filters[$filtername][] = $res;
242 12
            return;
243
        }
244
245 6
        throw new Exception(sprintf('unable to locate filter `%s`', $filtername));
246
    }
247
248
    /**
249
     * Set CSV control.
250
     *
251
     * @see http://php.net/manual/en/splfileobject.setcsvcontrol.php
252
     */
253 36
    public function setCsvControl(string $delimiter = ',', string $enclosure = '"', string $escape = '\\')
254
    {
255 36
        list($this->delimiter, $this->enclosure, $this->escape) = $this->filterControl($delimiter, $enclosure, $escape, __METHOD__);
256 36
    }
257
258
    /**
259
     * Filter Csv control characters.
260
     *
261
     *
262
     * @throws Exception If the Csv control character is not one character only.
263
     */
264 45
    protected function filterControl(string $delimiter, string $enclosure, string $escape, string $caller): array
265
    {
266 45
        $controls = ['delimiter' => $delimiter, 'enclosure' => $enclosure, 'escape' => $escape];
267 45
        foreach ($controls as $type => $control) {
268 45
            if (1 !== strlen($control)) {
269 45
                throw new Exception(sprintf('%s() expects %s to be a single character', $caller, $type));
270
            }
271
        }
272
273 36
        return array_values($controls);
274
    }
275
276
    /**
277
     * Set CSV control.
278
     *
279
     * @see http://php.net/manual/en/splfileobject.getcsvcontrol.php
280
     *
281
     * @return string[]
282
     */
283 45
    public function getCsvControl()
284
    {
285 45
        return [$this->delimiter, $this->enclosure, $this->escape];
286
    }
287
288
    /**
289
     * Set CSV stream flags.
290
     *
291
     * @see http://php.net/manual/en/splfileobject.setflags.php
292
     */
293 33
    public function setFlags(int $flags)
294
    {
295 33
        $this->flags = $flags;
296 33
    }
297
298
    /**
299
     * Write a field array as a CSV line.
300
     *
301
     * @see http://php.net/manual/en/splfileobject.fputcsv.php
302
     *
303
     * @return int|null|bool
304
     */
305 21
    public function fputcsv(array $fields, string $delimiter = ',', string $enclosure = '"', string $escape = '\\')
306
    {
307 21
        $controls = $this->filterControl($delimiter, $enclosure, $escape, __METHOD__);
308
309 12
        return fputcsv($this->stream, $fields, ...$controls);
310
    }
311
312
    /**
313
     * Get line number.
314
     *
315
     * @see http://php.net/manual/en/splfileobject.key.php
316
     *
317
     * @return int
318
     */
319 18
    public function key()
320
    {
321 18
        return $this->offset;
322
    }
323
324
    /**
325
     * Read next line.
326
     *
327
     * @see http://php.net/manual/en/splfileobject.next.php
328
     */
329 18
    public function next()
330
    {
331 18
        $this->value = false;
332 18
        $this->offset++;
333 18
    }
334
335
    /**
336
     * Rewind the file to the first line.
337
     *
338
     * @see http://php.net/manual/en/splfileobject.rewind.php
339
     *
340
     * @throws Exception if the stream resource is not seekable
341
     */
342 36
    public function rewind()
343
    {
344 36
        if (!$this->is_seekable) {
345 3
            throw new Exception('stream does not support seeking');
346
        }
347
348 33
        rewind($this->stream);
349 33
        $this->offset = 0;
350 33
        $this->value = false;
351 33
        if ($this->flags & SplFileObject::READ_AHEAD) {
352 18
            $this->current();
353
        }
354 33
    }
355
356
    /**
357
     * Not at EOF.
358
     *
359
     * @see http://php.net/manual/en/splfileobject.valid.php
360
     *
361
     * @return bool
362
     */
363 30
    public function valid()
364
    {
365 30
        if ($this->flags & SplFileObject::READ_AHEAD) {
366 18
            return $this->current() !== false;
367
        }
368
369 12
        return !feof($this->stream);
370
    }
371
372
    /**
373
     * Retrieves the current line of the file.
374
     *
375
     * @see http://php.net/manual/en/splfileobject.current.php
376
     */
377 36
    public function current()
378
    {
379 36
        if (false !== $this->value) {
380 21
            return $this->value;
381
        }
382
383 36
        $this->value = $this->getCurrentRecord();
384
385 36
        return $this->value;
386
    }
387
388
    /**
389
     * Retrieves the current line as a CSV Record.
390
     *
391
     * @return array|bool
392
     */
393 33
    protected function getCurrentRecord()
394
    {
395
        do {
396 33
            $ret = fgetcsv($this->stream, 0, $this->delimiter, $this->enclosure, $this->escape);
397 33
        } while ($this->flags & SplFileObject::SKIP_EMPTY && $ret !== false && $ret[0] === null);
398
399 33
        return $ret;
400
    }
401
402
    /**
403
     * Seek to specified line.
404
     *
405
     * @see http://php.net/manual/en/splfileobject.seek.php
406
     *
407
     * @param  int       $position
408
     * @throws Exception if the position is negative
409
     */
410 15
    public function seek($position)
411
    {
412 15
        if ($position < 0) {
413 3
            throw new Exception(sprintf('%s() can\'t seek stream to negative line %d', __METHOD__, $position));
414
        }
415
416 12
        $this->rewind();
417 9
        while ($this->key() !== $position && $this->valid()) {
418 3
            $this->current();
419 3
            $this->next();
420
        }
421
422 9
        $this->offset--;
423 9
        $this->current();
424 9
    }
425
426
    /**
427
     * Output all remaining data on a file pointer.
428
     *
429
     * @see http://php.net/manual/en/splfileobject.fpatssthru.php
430
     *
431
     * @return int
432
     */
433 3
    public function fpassthru()
434
    {
435 3
        return fpassthru($this->stream);
436
    }
437
438
    /**
439
     * Read from file.
440
     *
441
     * @see http://php.net/manual/en/splfileobject.fread.php
442
     *
443
     * @param int $length The number of bytes to read
444
     *
445
     * @return string|false
446
     */
447 12
    public function fread($length)
448
    {
449 12
        return fread($this->stream, $length);
450
    }
451
452
    /**
453
     * Seek to a position.
454
     *
455
     * @see http://php.net/manual/en/splfileobject.fseek.php
456
     *
457
     *
458
     * @throws Exception if the stream resource is not seekable
459
     *
460
     * @return int
461
     */
462 18
    public function fseek(int $offset, int $whence = SEEK_SET)
463
    {
464 18
        if (!$this->is_seekable) {
465 3
            throw new Exception('stream does not support seeking');
466
        }
467
468 15
        return fseek($this->stream, $offset, $whence);
469
    }
470
471
    /**
472
     * Write to stream.
473
     *
474
     * @see http://php.net/manual/en/splfileobject.fwrite.php
475
     *
476
     * @return int|bool
477
     */
478 3
    public function fwrite(string $str, int $length = 0)
479
    {
480 3
        if (0 === $length) {
481
            return fwrite($this->stream, $str);
482
        }
483
484 3
        return fwrite($this->stream, $str, $length);
485
    }
486
487
    /**
488
     * Flushes the output to a file.
489
     *
490
     * @see http://php.net/manual/en/splfileobject.fwrite.php
491
     *
492
     * @return bool
493
     */
494 3
    public function fflush()
495
    {
496 3
        return fflush($this->stream);
497
    }
498
}
499