Completed
Push — master ( 624e2a...46ab4e )
by ignace nyamagana
03:23 queued 02:20
created

Stream::seek()   A

Complexity

Conditions 4
Paths 3

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 15
Code Lines 9

Code Coverage

Tests 10
CRAP Score 4

Importance

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