Completed
Push — master ( 0a7ee7...624e2a )
by ignace nyamagana
02:45
created

Stream::seek()   B

Complexity

Conditions 5
Paths 4

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 19
Code Lines 11

Code Coverage

Tests 12
CRAP Score 5

Importance

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