1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* CSVelte: Slender, elegant CSV for PHP. |
4
|
|
|
* |
5
|
|
|
* Inspired by Python's CSV module and Frictionless Data and the W3C's CSV |
6
|
|
|
* standardization efforts, CSVelte was written in an effort to take all the |
7
|
|
|
* suck out of working with CSV. |
8
|
|
|
* |
9
|
|
|
* @version v0.2 |
10
|
|
|
* @copyright Copyright (c) 2016 Luke Visinoni <[email protected]> |
11
|
|
|
* @author Luke Visinoni <[email protected]> |
12
|
|
|
* @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) |
13
|
|
|
*/ |
14
|
|
|
namespace CSVelte\IO; |
15
|
|
|
|
16
|
|
|
use CSVelte\Traits\IsReadable; |
17
|
|
|
use CSVelte\Traits\IsWritable; |
18
|
|
|
use CSVelte\Traits\IsSeekable; |
19
|
|
|
|
20
|
|
|
use CSVelte\Contract\Readable; |
21
|
|
|
use CSVelte\Contract\Writable; |
22
|
|
|
use CSVelte\Contract\Seekable; |
23
|
|
|
|
24
|
|
|
use \Exception; |
25
|
|
|
use \InvalidArgumentException; |
26
|
|
|
use CSVelte\Exception\NotYetImplementedException; |
27
|
|
|
use CSVelte\Exception\EndOfFileException; |
28
|
|
|
use CSVelte\Exception\IOException; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* CSVelte Stream. |
32
|
|
|
* |
33
|
|
|
* Represents a stream for input/output. Implements both readable and writable |
34
|
|
|
* interfaces so that it can be passed to either ``CSVelte\Reader`` or |
35
|
|
|
* ``CSVelte\Writer``. |
36
|
|
|
* |
37
|
|
|
* @package CSVelte |
38
|
|
|
* @subpackage CSVelte\IO |
39
|
|
|
* @copyright (c) 2016, Luke Visinoni <[email protected]> |
40
|
|
|
* @author Luke Visinoni <[email protected]> |
41
|
|
|
* @since v0.2 |
42
|
|
|
*/ |
43
|
|
|
class Stream implements Readable, Writable, Seekable |
44
|
|
|
{ |
45
|
|
|
use IsReadable, IsWritable, IsSeekable; |
46
|
|
|
/** |
47
|
|
|
* Hash of readable/writable stream open mode types. |
48
|
|
|
* |
49
|
|
|
* Mercilessly stolen from: |
50
|
|
|
* https://github.com/guzzle/streams/blob/master/src/Stream.php |
51
|
|
|
* |
52
|
|
|
* My kudos and sincere thanks go out to Michael Dowling and Graham Campbell |
53
|
|
|
* of the guzzle/streams PHP package. Thanks for the inspiration (in some cases) |
54
|
|
|
* and the not suing me for outright theft (in this case). |
55
|
|
|
* |
56
|
|
|
* @var array Hash of readable and writable stream types |
57
|
|
|
*/ |
58
|
|
|
protected static $readWriteHash = [ |
59
|
|
|
'read' => [ |
60
|
|
|
'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, |
61
|
|
|
'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, |
62
|
|
|
'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, |
63
|
|
|
'x+t' => true, 'c+t' => true, 'a+' => true, |
64
|
|
|
], |
65
|
|
|
'write' => [ |
66
|
|
|
'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, |
67
|
|
|
'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, |
68
|
|
|
'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, |
69
|
|
|
'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true, |
70
|
|
|
], |
71
|
|
|
]; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @var resource An open stream resource |
75
|
|
|
*/ |
76
|
|
|
protected $stream; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @var int The total size (in bytes) of the stream |
80
|
|
|
*/ |
81
|
|
|
protected $size; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Meta data about stream resource. |
85
|
|
|
* Just contains the return value of stream_get_meta_data. |
86
|
|
|
* @var array The return value of stream_get_meta_data |
87
|
|
|
*/ |
88
|
|
|
protected $meta; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Is stream seekable |
92
|
|
|
* @var boolean True if stream is seekable, false otherwise |
93
|
|
|
*/ |
94
|
|
|
protected $seekable; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Is stream readable |
98
|
|
|
* @var boolean True if stream is readable, false otherwise |
99
|
|
|
*/ |
100
|
|
|
protected $readable; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Is stream writable |
104
|
|
|
* @var boolean True if stream is writable, false otherwise |
105
|
|
|
*/ |
106
|
|
|
protected $writable; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Converts object/string to a usable stream |
110
|
|
|
* |
111
|
|
|
* Mercilessly stolen from: |
112
|
|
|
* https://github.com/guzzle/streams/blob/master/src/Stream.php |
113
|
|
|
* |
114
|
|
|
* My kudos and sincere thanks go out to Michael Dowling and Graham Campbell |
115
|
|
|
* of the guzzle/streams PHP package. Thanks for the inspiration (in some cases) |
116
|
|
|
* and the not suing me for outright theft (in this case). |
117
|
|
|
* |
118
|
|
|
* @param object|string The string/object to convert to a stream |
119
|
|
|
* @param array Options to pass to the newly created stream |
120
|
|
|
* @return \CSVelte\IO\Stream |
121
|
|
|
* @throws \InvalidArgumentException |
122
|
|
|
*/ |
123
|
15 |
|
public static function streamize($resource = '') |
124
|
|
|
{ |
125
|
15 |
|
$type = gettype($resource); |
126
|
|
|
|
127
|
15 |
|
if ($type == 'string') { |
128
|
15 |
|
$stream = self::open('php://temp', 'r+'); |
129
|
15 |
|
if ($resource !== '') { |
130
|
13 |
|
fwrite($stream, $resource); |
131
|
13 |
|
fseek($stream, 0); |
132
|
13 |
|
} |
133
|
15 |
|
return new self($stream); |
134
|
|
|
} |
135
|
|
|
|
136
|
1 |
|
if ($type == 'object' && method_exists($resource, '__toString')) { |
137
|
1 |
|
return self::streamize((string) $resource); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
throw new InvalidArgumentException('Invalid resource type: ' . $type); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Stream Object Constructor. |
145
|
|
|
* |
146
|
|
|
* Instantiates the stream object |
147
|
|
|
* |
148
|
|
|
* @param string|object|resource $stream Either a valid stream URI or an open |
149
|
|
|
* stream resource (using fopen, fsockopen, et al.) |
150
|
|
|
* @param string file/stream open mode as passed to native php |
151
|
|
|
* ``fopen`` function |
152
|
|
|
* @param array Stream context options array as passed to native php |
153
|
|
|
* ``stream_context_create`` function |
154
|
|
|
* @see http://php.net/manual/en/function.fopen.php |
155
|
|
|
* @see http://php.net/manual/en/function.stream-context-create.php |
156
|
|
|
*/ |
157
|
63 |
|
public function __construct($stream, $mode = null, $context = null) |
158
|
|
|
{ |
159
|
63 |
|
$this->setMetaData($this->stream = self::open($stream, $mode, $context)); |
|
|
|
|
160
|
59 |
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Stream Object Destructor. |
164
|
|
|
* |
165
|
|
|
* Closes stream connection. |
166
|
|
|
*/ |
167
|
58 |
|
public function __destruct() |
168
|
|
|
{ |
169
|
58 |
|
$this->close(); |
170
|
58 |
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Reads all data from the stream into a string, from the beginning to end. |
174
|
|
|
* |
175
|
|
|
* This method MUST attempt to seek to the beginning of the stream before |
176
|
|
|
* reading data and read the stream until the end is reached. |
177
|
|
|
* |
178
|
|
|
* Warning: This could attempt to load a large amount of data into memory. |
179
|
|
|
* |
180
|
|
|
* This method MUST NOT raise an exception in order to conform with PHP's |
181
|
|
|
* string casting operations. |
182
|
|
|
* |
183
|
|
|
* Returns the internal pointer to the position it was in once it's finished |
184
|
|
|
* |
185
|
|
|
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring |
186
|
|
|
* @return string |
187
|
|
|
*/ |
188
|
3 |
|
public function __toString() |
189
|
|
|
{ |
190
|
3 |
|
$string = ''; |
191
|
|
|
try { |
192
|
3 |
|
$pos = $this->tell(); |
193
|
3 |
|
$this->rewind(); |
194
|
3 |
|
$string .= $this->getContents(); |
195
|
3 |
|
$this->seek($pos); |
|
|
|
|
196
|
3 |
|
} catch (Exception $e) { |
197
|
|
|
// eat any exception that may be thrown... |
198
|
|
|
} |
199
|
3 |
|
return $string; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* Open a new stream URI and return stream resource. |
204
|
|
|
* |
205
|
|
|
* Pass in either a valid stream URI or a stream resource and this will |
206
|
|
|
* return a stream resource object. |
207
|
|
|
* |
208
|
|
|
* @param string|resource $stream Either stream URI or resource object |
209
|
|
|
* @param string $mode File/stream open mode (as passed to native php |
210
|
|
|
* ``fopen`` function) |
211
|
|
|
* @param array $context Stream context options array as passed to native |
212
|
|
|
* php ``stream_context_create`` function |
213
|
|
|
* @return stream resource object |
214
|
|
|
* @throws CSVelte\Exception\IOException on invalid stream uri/resource |
215
|
|
|
* @throws \InvalidArgumentException if context param is not an array |
216
|
|
|
* @see http://php.net/manual/en/function.fopen.php |
217
|
|
|
* @see http://php.net/manual/en/function.stream-context-create.php |
218
|
|
|
*/ |
219
|
61 |
|
protected static function open($stream, $mode = null, $context = null) |
220
|
|
|
{ |
221
|
61 |
|
if (is_null($mode)) $mode = 'r+b'; |
222
|
61 |
|
if (is_string($uri = $stream)) { |
223
|
57 |
|
if (is_null($context)) { |
224
|
55 |
|
$stream = @fopen($uri, $mode); |
225
|
55 |
|
} else { |
226
|
2 |
|
if (!is_array($context)) { |
227
|
1 |
|
throw new InvalidArgumentException("Invalid argument for context. Expected array, got: " . gettype($context)); |
228
|
|
|
} |
229
|
1 |
|
$context = stream_context_create($context); |
230
|
1 |
|
$stream = @fopen($uri, $mode, false, $context); |
231
|
|
|
} |
232
|
56 |
|
if (false === $stream) { |
233
|
1 |
|
throw new IOException("Invalid stream URI: " . $uri, IOException::ERR_INVALID_STREAM_URI); |
234
|
|
|
} |
235
|
55 |
|
} |
236
|
59 |
|
if (!is_resource($stream) || get_resource_type($stream) != 'stream') { |
237
|
1 |
|
throw new IOException("Expected stream resource, got: " . gettype($stream), IOException::ERR_INVALID_STREAM_RESOURCE); |
238
|
|
|
} |
239
|
58 |
|
return $stream; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Close stream resource. |
244
|
|
|
* |
245
|
|
|
* @return boolean True on success or false on failure |
246
|
|
|
*/ |
247
|
58 |
|
public function close() |
248
|
|
|
{ |
249
|
58 |
|
if (is_resource($this->stream)) { |
250
|
54 |
|
return fclose($this->stream); |
251
|
|
|
} |
252
|
6 |
|
return false; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Set stream meta data via stream resource. |
257
|
|
|
* |
258
|
|
|
* Pass in stream resource to set this object's stream metadata as returned |
259
|
|
|
* by the native php function ``stream_get_meta_data`` |
260
|
|
|
* |
261
|
|
|
* @param resource $stream Stream resource object |
262
|
|
|
* @return $this |
263
|
|
|
* @see http://php.net/manual/en/function.stream-get-meta-data.php |
264
|
|
|
*/ |
265
|
58 |
|
protected function setMetaData($stream) |
266
|
|
|
{ |
267
|
58 |
|
$this->meta = stream_get_meta_data($stream); |
268
|
58 |
|
$this->seekable = (bool) $this->meta['seekable']; |
269
|
58 |
|
$this->readable = isset(self::$readWriteHash['read'][$this->meta['mode']]); |
270
|
58 |
|
$this->writable = isset(self::$readWriteHash['write'][$this->meta['mode']]); |
271
|
58 |
|
return $this; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Get stream metadata (all or certain value). |
276
|
|
|
* |
277
|
|
|
* Get either the entire stream metadata array or a single value from it by key. |
278
|
|
|
* |
279
|
|
|
* @param string $key If set, must be one of ``stream_get_meta_data`` array keys |
280
|
|
|
* @return string|array Either a single value or whole array returned by ``stream_get_meta_data`` |
281
|
|
|
* @see http://php.net/manual/en/function.stream-get-meta-data.php |
282
|
|
|
*/ |
283
|
15 |
|
public function getMetaData($key = null) |
284
|
|
|
{ |
285
|
15 |
|
if (!$this->stream) return null; |
286
|
12 |
|
if (is_null($key)) return $this->meta; |
287
|
11 |
|
return (array_key_exists($key, $this->meta)) ? $this->meta[$key] : null; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Accessor for seekability. |
292
|
|
|
* |
293
|
|
|
* Returns true if possible to seek to a certain position within this stream |
294
|
|
|
* |
295
|
|
|
* @return boolean True if stream is seekable |
296
|
|
|
*/ |
297
|
7 |
|
public function isSeekable() |
298
|
|
|
{ |
299
|
7 |
|
return $this->seekable; |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* Accessor for readability. |
304
|
|
|
* |
305
|
|
|
* Returns true if possible to read from this stream |
306
|
|
|
* |
307
|
|
|
* @return boolean True if stream is readable |
308
|
|
|
*/ |
309
|
40 |
|
public function isReadable() |
310
|
|
|
{ |
311
|
40 |
|
return $this->readable; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* Accessor for writability. |
316
|
|
|
* |
317
|
|
|
* Returns true if possible to write to this stream |
318
|
|
|
* |
319
|
|
|
* @return boolean True if stream is writable |
320
|
|
|
*/ |
321
|
18 |
|
public function isWritable() |
322
|
|
|
{ |
323
|
18 |
|
return $this->writable; |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* Accessor for internal stream resource. |
328
|
|
|
* |
329
|
|
|
* Returns the internal stream resource pointer |
330
|
|
|
* |
331
|
|
|
* @return resource The open stream resource pointer |
332
|
|
|
*/ |
333
|
7 |
|
public function getResource() |
334
|
|
|
{ |
335
|
7 |
|
return $this->stream; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Accessor for stream URI. |
340
|
|
|
* |
341
|
|
|
* Returns the stream URI |
342
|
|
|
* |
343
|
|
|
* @return string The URI for the stream |
344
|
|
|
*/ |
345
|
14 |
|
public function getUri() |
346
|
|
|
{ |
347
|
14 |
|
return $this->getMetaData('uri'); |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Accessor for stream name. |
352
|
|
|
* |
353
|
|
|
* Alias for ``getUri()`` |
354
|
|
|
* |
355
|
|
|
* @return string The name for this stream |
356
|
|
|
*/ |
357
|
13 |
|
public function getName() |
358
|
|
|
{ |
359
|
13 |
|
return $this->getUri(); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* Separates any underlying resources from the stream. |
364
|
|
|
* |
365
|
|
|
* After the stream has been detached, the stream is in an unusable state. |
366
|
|
|
* |
367
|
|
|
* @return resource|null Underlying PHP stream, if any |
368
|
|
|
*/ |
369
|
4 |
|
public function detach() |
370
|
|
|
{ |
371
|
4 |
|
$stream = $this->stream; |
372
|
4 |
|
$this->stream = null; |
373
|
4 |
|
$this->seekable = $this->readable = $this->writable = false; |
374
|
4 |
|
return $stream; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* Get the size of the stream if known. |
379
|
|
|
* |
380
|
|
|
* @return int|null Returns the size in bytes if known, or null if unknown. |
381
|
|
|
*/ |
382
|
2 |
|
public function getSize() |
383
|
|
|
{ |
384
|
2 |
|
if (!$this->stream) return null; |
385
|
2 |
|
if (is_null($this->size)) { |
386
|
2 |
|
$stats = fstat($this->stream); |
387
|
2 |
|
if (array_key_exists('size', $stats)) { |
388
|
2 |
|
$this->size = $stats['size']; |
389
|
2 |
|
} |
390
|
2 |
|
} |
391
|
2 |
|
return $this->size; |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
/** |
395
|
|
|
* Returns the current position of the file read/write pointer |
396
|
|
|
* |
397
|
|
|
* @return int Position of the file pointer |
398
|
|
|
* @throws \RuntimeException on error. |
399
|
|
|
*/ |
400
|
4 |
|
public function tell() |
401
|
|
|
{ |
402
|
4 |
|
return $this->stream ? ftell($this->stream) : false; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Read $length bytes from stream. |
407
|
|
|
* |
408
|
|
|
* Reads $length bytes (number of characters) from the stream |
409
|
|
|
* |
410
|
|
|
* @param int $length Number of bytes to read from stream |
411
|
|
|
* @return string|boolean The data read from stream or false if at end of |
412
|
|
|
* file or some other problem. |
413
|
|
|
* @throws CSVelte\Exception\IOException |
414
|
|
|
*/ |
415
|
39 |
|
public function read($length) |
416
|
|
|
{ |
417
|
39 |
|
$this->assertIsReadable(); |
418
|
37 |
|
if ($this->eof()) return false; |
419
|
37 |
|
return fread($this->stream, $length); |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* Returns the remaining contents in a string. |
424
|
|
|
* |
425
|
|
|
* Read and return the remaining contents of the stream, beginning from |
426
|
|
|
* wherever the stream's internal pointer is when this method is called. If |
427
|
|
|
* you want the ENTIRE stream's contents, use __toString() instead. |
428
|
|
|
* |
429
|
|
|
* @param void |
430
|
|
|
* @return string The remaining contents of the file, beginning at internal |
431
|
|
|
* pointer's current location |
432
|
|
|
* @throws CSVelte\Exception\IOException |
433
|
|
|
*/ |
434
|
5 |
|
public function getContents() |
435
|
|
|
{ |
436
|
5 |
|
$buffer = ''; |
437
|
5 |
|
if ($this->isReadable()) { |
438
|
5 |
|
while ($chunk = $this->read(1024)) { |
439
|
5 |
|
$buffer .= $chunk; |
440
|
5 |
|
} |
441
|
5 |
|
} |
442
|
5 |
|
return $buffer; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* Is file pointer at the end of the stream? |
447
|
|
|
* |
448
|
|
|
* Returns true if internal pointer has reached the end of the stream. |
449
|
|
|
* |
450
|
|
|
* @return boolean True if end of stream has been reached |
451
|
|
|
*/ |
452
|
37 |
|
public function eof() |
453
|
|
|
{ |
454
|
37 |
|
return !$this->stream || feof($this->stream); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* Rewind pointer to beginning of stream. |
459
|
|
|
* |
460
|
|
|
* Rewinds the stream, meaning it returns the pointer to the beginning of the |
461
|
|
|
* stream as if it had just been initialized. |
462
|
|
|
* |
463
|
|
|
* @return boolean True on success |
464
|
|
|
*/ |
465
|
26 |
|
public function rewind() |
466
|
|
|
{ |
467
|
26 |
|
if (is_resource($this->stream)) { |
468
|
26 |
|
return rewind($this->stream); |
469
|
|
|
} |
470
|
1 |
|
} |
471
|
|
|
|
472
|
|
|
/** |
473
|
|
|
* Write to stream |
474
|
|
|
* |
475
|
|
|
* Writes a string to the stream (if it is writable) |
476
|
|
|
* |
477
|
|
|
* @param string $str The data to be written to the stream |
478
|
|
|
* @return int The number of bytes written to the stream |
479
|
|
|
* @throws CSVelte\Exception\IOException |
480
|
|
|
*/ |
481
|
18 |
|
public function write($str) |
482
|
|
|
{ |
483
|
18 |
|
$this->assertIsWritable(); |
484
|
16 |
|
return fwrite($this->stream, $str); |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* Seek to position. |
489
|
|
|
* |
490
|
|
|
* Seek to a specific position within the stream (if seekable). |
491
|
|
|
* |
492
|
|
|
* @param int $offset The position to seek to |
493
|
|
|
* @param int $whence One of three native php ``SEEK_`` constants |
494
|
|
|
* @return boolean True on success false on failure |
495
|
|
|
* @throws CSVelte\Exception\IOException |
496
|
|
|
* @see http://php.net/manual/en/function.seek.php |
497
|
|
|
*/ |
498
|
7 |
|
public function seek($offset, $whence = SEEK_SET) |
499
|
|
|
{ |
500
|
7 |
|
$this->assertIsSeekable(); |
501
|
6 |
|
return fseek($this->stream, $offset, $whence) === 0; |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
} |
505
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.