Stream::next()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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