Completed
Branch releases/v0.2 (da7ff5)
by Luke
02:45
created

Stream::assertIsSeekable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
ccs 3
cts 4
cp 0.75
rs 9.4285
cc 2
eloc 3
nc 2
nop 0
crap 2.0625
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 \InvalidArgumentException;
25
use CSVelte\Exception\NotYetImplementedException;
26
use CSVelte\Exception\EndOfFileException;
27
use CSVelte\Exception\IOException;
28
29
/**
30
 * CSVelte Stream.
31
 *
32
 * Represents a stream for input/output. Implements both readable and writable
33
 * interfaces so that it can be passed to either ``CSVelte\Reader`` or
34
 * ``CSVelte\Writer``.
35
 *
36
 * @package    CSVelte
37
 * @subpackage CSVelte\IO
38
 * @copyright  (c) 2016, Luke Visinoni <[email protected]>
39
 * @author     Luke Visinoni <[email protected]>
40
 * @since      v0.2
41
 */
42
class Stream implements Readable, Writable, Seekable
43
{
44
    use IsReadable, IsWritable, IsSeekable;
45
    /**
46
     * Hash of readable/writable stream open mode types.
47
     *
48
     * Mercilessly stolen from:
49
     * https://github.com/guzzle/streams/blob/master/src/Stream.php
50
     *
51
     * My kudos and sincere thanks go out to Michael Dowling and Graham Campbell
52
     * of the guzzle/streams PHP package. Thanks for the inspiration (in some cases)
53
     * and the not suing me for outright theft (in this case).
54
     *
55
     * @var array Hash of readable and writable stream types
56
     */
57
    protected static $readWriteHash = [
58
        'read' => [
59
            'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
60
            'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
61
            'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
62
            'x+t' => true, 'c+t' => true, 'a+' => true,
63
        ],
64
        'write' => [
65
            'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
66
            'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
67
            'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
68
            'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
69
        ],
70
    ];
71
72
    /**
73
     * @var resource An open stream resource
74
     */
75
    protected $stream;
76
77
    /**
78
     * Meta data about stream resource.
79
     * Just contains the return value of stream_get_meta_data.
80
     * @var array The return value of stream_get_meta_data
81
     */
82
    protected $meta;
83
84
    /**
85
     * Is stream seekable
86
     * @var boolean True if stream is seekable, false otherwise
87
     */
88
    protected $seekable;
89
90
    /**
91
     * Is stream readable
92
     * @var boolean True if stream is readable, false otherwise
93
     */
94
    protected $readable;
95
96
    /**
97
     * Is stream writable
98
     * @var boolean True if stream is writable, false otherwise
99
     */
100
    protected $writable;
101
102
    /**
103
     * Converts object/string to a usable stream
104
     *
105
     * Mercilessly stolen from:
106
     * https://github.com/guzzle/streams/blob/master/src/Stream.php
107
     *
108
     * My kudos and sincere thanks go out to Michael Dowling and Graham Campbell
109
     * of the guzzle/streams PHP package. Thanks for the inspiration (in some cases)
110
     * and the not suing me for outright theft (in this case).
111
     *
112
     * @param string|object The string/object to convert to a stream
113
     * @param array Options to pass to the newly created stream
114
     * @return CSVelte\IO\Stream
115
     * @throws \InvalidArgumentException
116
     */
117 10
    public static function streamize($resource = '')
118
    {
119 10
        $type = gettype($resource);
120
121 10
        if ($type == 'string') {
122 10
            $stream = self::open('php://temp', 'r+');
123 10
            if ($resource !== '') {
124 9
                fwrite($stream, $resource);
125 9
                fseek($stream, 0);
126 9
            }
127 10
            return new self($stream);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new self($stream); (CSVelte\IO\Stream) is incompatible with the return type documented by CSVelte\IO\Stream::streamize of type CSVelte\IO\CSVelte\IO\Stream.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
128
        }
129
130 1
        if ($type == 'object' && method_exists($resource, '__toString')) {
131 1
            return self::streamize((string) $resource);
132
        }
133
134
        throw new InvalidArgumentException('Invalid resource type: ' . $type);
135
    }
136
137
    /**
138
     * Stream Object Constructor.
139
     *
140
     * Instantiates the stream object
141
     *
142
     * @param string|resource $stream Either a valid stream URI or an open
143
     *     stream resource (using fopen, fsockopen, et al.)
144
     * @param string file/stream open mode as passed to native php
145
     *     ``fopen`` function
146
     * @param array Stream context options array as passed to native php
147
     *     ``stream_context_create`` function
148
     * @see http://php.net/manual/en/function.fopen.php
149
     * @see http://php.net/manual/en/function.stream-context-create.php
150
     */
151 39
    public function __construct($stream, $mode = null, $context = null)
152
    {
153 39
        $this->setMetaData($this->stream = self::open($stream, $mode, $context));
154 36
    }
155
156
    /**
157
     * Stream Object Destructor.
158
     *
159
     * Closes stream connection.
160
     */
161 34
    public function __destruct()
162
    {
163 34
        $this->close();
164 34
    }
165
166
    /**
167
     * Open a new stream URI and return stream resource.
168
     *
169
     * Pass in either a valid stream URI or a stream resource and this will
170
     * return a stream resource object.
171
     *
172
     * @param string|resource $stream Either stream URI or resource object
173
     * @param string $mode File/stream open mode (as passed to native php
174
     *     ``fopen`` function)
175
     * @param array $context Stream context options array as passed to native
176
     *     php ``stream_context_create`` function
177
     * @return stream resource object
178
     * @throws CSVelte\Exception\IOException on invalid stream uri/resource
179
     * @throws \InvalidArgumentException if context param is not an array
180
     * @see http://php.net/manual/en/function.fopen.php
181
     * @see http://php.net/manual/en/function.stream-context-create.php
182
     */
183 36
    protected static function open($stream, $mode = null, $context = null)
184
    {
185 36
        if (is_null($mode)) $mode = 'r+b';
186 36
        if (is_string($uri = $stream)) {
187 32
            if (is_null($context)) {
188 32
                $stream = @fopen($uri, $mode);
189 32
            } else {
190
                if (!is_array($context)) {
191
                    throw new InvalidArgumentException("Invalid argument for context. Expected array, got: " . gettype($context));
192
                }
193
                $context = stream_context_create($context);
194
                $stream = @fopen($uri, $mode, false, $context);
195
            }
196 32
            if (false === $stream) {
197 1
                throw new IOException("Invalid stream URI: " . $uri, IOException::ERR_INVALID_STREAM_URI);
198
            }
199 31
        }
200 35
        if (!is_resource($stream) || get_resource_type($stream) != 'stream') {
201 1
            throw new IOException("Expected stream resource, got: " . gettype($stream), IOException::ERR_INVALID_STREAM_RESOURCE);
202
        }
203 34
        return $stream;
204
    }
205
206
    /**
207
     * Close stream resource.
208
     *
209
     * @return boolean True on success or false on failure
210
     */
211 34
    public function close()
212
    {
213 34
        if (is_resource($this->stream)) {
214 34
            return fclose($this->stream);
215
        }
216 2
        return false;
217
    }
218
219
    /**
220
     * Set stream meta data via stream resource.
221
     *
222
     * Pass in stream resource to set this object's stream metadata as returned
223
     * by the native php function ``stream_get_meta_data``
224
     *
225
     * @param resource $stream Stream resource object
226
     * @return $this
227
     * @see http://php.net/manual/en/function.stream-get-meta-data.php
228
     */
229 34
    protected function setMetaData($stream)
230
    {
231 34
        $this->meta = stream_get_meta_data($stream);
232 34
        $this->seekable = (bool) $this->meta['seekable'];
233 34
        $this->readable = isset(self::$readWriteHash['read'][$this->meta['mode']]);
234 34
        $this->writable = isset(self::$readWriteHash['write'][$this->meta['mode']]);
235 34
        return $this;
236
    }
237
238
    /**
239
     * Get stream metadata (all or certain value).
240
     *
241
     * Get either the entire stream metadata array or a single value from it by key.
242
     *
243
     * @param string $key If set, must be one of ``stream_get_meta_data`` array keys
244
     * @return string|array Either a single value or whole array returned by ``stream_get_meta_data``
245
     * @see http://php.net/manual/en/function.stream-get-meta-data.php
246
     */
247 5
    public function getMetaData($key = null)
248
    {
249 5
        if (is_null($key)) return $this->meta;
250 4
        return (array_key_exists($key, $this->meta)) ? $this->meta[$key] : null;
251
    }
252
253
    /**
254
     * Accessor for seekability.
255
     *
256
     * Returns true if possible to seek to a certain position within this stream
257
     *
258
     * @return boolean True if stream is seekable
259
     */
260 2
    public function isSeekable()
261
    {
262 2
        return $this->seekable;
263
    }
264
265
    /**
266
     * Accessor for readability.
267
     *
268
     * Returns true if possible to read from this stream
269
     *
270
     * @return boolean True if stream is readable
271
     */
272 22
    public function isReadable()
273
    {
274 22
        return $this->readable;
275
    }
276
277
    /**
278
     * Accessor for writability.
279
     *
280
     * Returns true if possible to write to this stream
281
     *
282
     * @return boolean True if stream is writable
283
     */
284 13
    public function isWritable()
285
    {
286 13
        return $this->writable;
287
    }
288
289
    /**
290
     * Accessor for internal stream resource.
291
     *
292
     * Returns the internal stream resource pointer
293
     *
294
     * @return resource The open stream resource pointer
295
     */
296 2
    public function getResource()
297
    {
298 2
        return $this->stream;
299
    }
300
301
    /**
302
     * Accessor for stream URI.
303
     *
304
     * Returns the stream URI
305
     *
306
     * @return string The URI for the stream
307
     */
308 4
    public function getUri()
309
    {
310 4
        return $this->getMetaData('uri');
311
    }
312
313
    /**
314
     * Accessor for stream name.
315
     *
316
     * Alias for ``getUri()``
317
     *
318
     * @return string The name for this stream
319
     */
320 3
    public function getName()
321
    {
322 3
        return $this->getUri();
1 ignored issue
show
Bug Compatibility introduced by
The expression $this->getUri(); of type string|array adds the type array to the return on line 322 which is incompatible with the return type declared by the interface CSVelte\Contract\Readable::getName of type string.
Loading history...
323
    }
324
325
    /**
326
     * Read $length bytes from stream.
327
     *
328
     * Reads $length bytes (number of characters) from the stream
329
     *
330
     * @param int $length Number of bytes to read from stream
331
     * @return string|boolean The data read from stream or false if at end of
332
     *     file or some other problem.
333
     * @throws CSVelte\Exception\IOException
334
     */
335 21
    public function read($length)
336
    {
337 21
        $this->assertIsReadable();
338 20
        if ($this->eof()) return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface CSVelte\Contract\Readable::read of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
339 20
        return fread($this->stream, $length);
340
    }
341
342
    /**
343
     * Read the entire contents of file/stream.
344
     *
345
     * Read and return the entire contents of the stream.
346
     *
347
     * @param void
348
     * @return string The entire file contents
349
     * @throws CSVelte\Exception\IOException
350
     */
351
    public function getContents()
352
    {
353
354
    }
355
356
    /**
357
     * Is file pointer at the end of the stream?
358
     *
359
     * Returns true if internal pointer has reached the end of the stream.
360
     *
361
     * @return boolean True if end of stream has been reached
362
     */
363 20
    public function eof()
364
    {
365 20
        return feof($this->stream);
366
    }
367
368
    /**
369
     * Rewind pointer to beginning of stream.
370
     *
371
     * Rewinds the stream, meaning it returns the pointer to the beginning of the
372
     * stream as if it had just been initialized.
373
     *
374
     * @return boolean True on success
375
     */
376 12
    public function rewind()
377
    {
378 12
        return rewind($this->stream);
379
    }
380
381
    /**
382
     * Write to stream
383
     *
384
     * Writes a string to the stream (if it is writable)
385
     *
386
     * @param string $str The data to be written to the stream
387
     * @return int The number of bytes written to the stream
388
     * @throws CSVelte\Exception\IOException
389
     */
390 13
    public function write($str)
391
    {
392 13
        $this->assertIsWritable();
393 13
        return fwrite($this->stream, $str);
394
    }
395
396
    /**
397
     * Seek to position.
398
     *
399
     * Seek to a specific position within the stream (if seekable).
400
     *
401
     * @param int $offset The position to seek to
402
     * @param int $whence One of three native php ``SEEK_`` constants
403
     * @return boolean True on success false on failure
404
     * @throws CSVelte\Exception\IOException
405
     * @see http://php.net/manual/en/function.seek.php
406
     */
407 2
    public function seek($offset, $whence = SEEK_SET)
408
    {
409 2
        $this->assertIsSeekable();
410 2
        return fseek($this->stream, $offset, $whence) === 0;
411
    }
412
413
}
414