Completed
Pull Request — master (#264)
by
unknown
14:20
created

Stream::isSeekable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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