Completed
Push — master ( 9b124f...eaf240 )
by richard
02:30
created

Stream::getOption()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
rs 9.2
cc 4
eloc 10
nc 5
nop 1
1
<?php
2
3
namespace Almendra\Http\Psr\Messages;
4
5
use Psr\Http\Message\StreamInterface;
6
7
/**
8
 * Represent a message stream
9
 *
10
 * @package Almendra\Http
11
 */
12
class Stream implements StreamInterface
13
{
14
15
    /**
16
     * @var stream body / the resource
17
     */
18
    protected $body;
19
20
    /**
21
     * @var The stream uri, if any
22
     */
23
    protected $uri;
24
25
    /**
26
     * @var Is the stream seekable?
27
     */
28
    protected $seekable;
29
30
    /**
31
     * @var Is the stream readable?
32
     */
33
    protected $readable;
34
35
    /**
36
     * @var Is the stream writable?
37
     */
38
    protected $writable;
39
40
    /**
41
     * @var The stream metadata, if any
42
     */
43
    protected $metaData = [];
44
45
    /**
46
     * @var The overriding options (size, uri, etc.)
47
     */
48
    protected $options = [];
49
50
    /**
51
     * @var Default output format
52
     */
53
    protected $defaultFormat; // 'JSON'
54
55
    /**
56
     * @var The accepted overriding options
57
     */
58
    protected $overridingOptions = [
59
        'size',
60
        'uri',
61
        ];
62
63
    /** @var array Hash of readable and writable stream types */
64
    private static $readWriteHash = [
65
        'read' => [
66
            'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
67
            'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
68
            'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
69
            'x+t' => true, 'c+t' => true, 'a+' => true
70
        ],
71
        'write' => [
72
            'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
73
            'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
74
            'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
75
            'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true
76
        ]
77
    ];
78
79
    /**
80
     * Sets up the resource.
81
     *
82
     * @param resource $stream
83
     * @param array $options
84
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
85
     */
86
    public function __construct($stream, $metaData = [], $options = [])
87
    {
88
        if (!is_resource($stream)) {
89
            throw new \InvalidArgumentException("Stream must be a resource.");
90
        }
91
92
        $this -> setBody($stream);
93
        $this -> setMetadata($stream, $metaData);
94
        $this -> setOptions($options);
95
        $this -> setOperations();
96
    }
97
98
    /**
99
     * Sets the stream metadata.
100
     *
101
     * @param resource $stream         The stream
102
     * @param array $userData         The overriding user metadata
103
     * @return void
104
     */
105
    protected function setMetadata($stream, array $userData = [])
106
    {
107
        $meta = stream_get_meta_data($stream);
108
        $this -> metaData = array_merge($meta, $userData);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_merge($meta, $userData) of type array is incompatible with the declared type object<Almendra\Http\Psr\Messages\The> of property $metaData.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
109
    }
110
111
    /**
112
     * Sets the options such as the size, effectively overriding those from the stream.
113
     *
114
     * @param array $options         The overriding options
115
     * @return void
116
     */
117
    protected function setOptions(array $options)
118
    {
119
        foreach ($this -> overridingOptions as $name) {
120
            if (isset($options[$name])) {
121
                $this -> options[$name] = $options[$name];
122
            }
123
        }
124
    }
125
126
    /**
127
     * Reads all data from the stream into a string, from the beginning to end.
128
     *
129
     * This method MUST attempt to seek to the beginning of the stream before
130
     * reading data and read the stream until the end is reached.
131
     *
132
     * Warning: This could attempt to load a large amount of data into memory.
133
     *
134
     * This method MUST NOT raise an exception in order to conform with PHP's
135
     * string casting operations.
136
     *
137
     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
138
     * @return string
139
     */
140
    public function __toString()
141
    {
142
        // protect against exception
143
        try {
144
            $this -> seek(0);
145
146
            $result = (string) stream_get_contents($this -> body);
147
            if ($this -> isJsonable() && ($this -> defaultFormat === 'JSON')) {
148
                $result = json_encode($result, JSON_PRETTY_PRINT);
149
            }
150
        } catch (Exception $e) {
0 ignored issues
show
Bug introduced by
The class Almendra\Http\Psr\Messages\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
151
            $result = '';
152
        }
153
154
        return $result;
155
    }
156
157
    /**
158
     * Closes the stream and any underlying resources.
159
     *
160
     * @return void
161
     */
162
    public function close()
163
    {
164
        if (!isset($this -> body)) {
165
            return;
166
        }
167
168
        fclose($this -> body);
169
        $this -> detach();
170
    }
171
172
    /**
173
     * Separates any underlying resources from the stream.
174
     *
175
     * After the stream has been detached, the stream is in an unusable state.
176
     *
177
     * @return resource|null Underlying PHP stream, if any
178
     */
179
    public function detach()
180
    {
181
        if (!isset($this -> body)) {
182
            return null;
183
        }
184
185
        $this -> options = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type object<Almendra\Http\Psr\Messages\The> of property $options.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
186
        $this -> metaData = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type object<Almendra\Http\Psr\Messages\The> of property $metaData.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
187
        $this -> unsetOperations();
188
189
        $result = $this -> body;
190
        unset($this -> body);
191
192
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result; (Almendra\Http\Psr\Messages\Stream) is incompatible with the return type declared by the interface Psr\Http\Message\StreamInterface::detach of type resource|null.

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...
193
    }
194
195
    /**
196
     * Get the size of the stream if known.
197
     *
198
     * @return int|null Returns the size in bytes if known, or null if unknown.
199
     */
200
    public function getSize()
201
    {
202
        if (!isset($this -> options['size'])) {
203
            try {
204
                $fstat = fstat($this -> body);
205
                if (!isset($fstat['size'])) {
206
                    return null;
207
                }
208
209
                $this -> options['size'] = $fstat['size'];
210
            } catch (Exception $e) {
0 ignored issues
show
Bug introduced by
The class Almendra\Http\Psr\Messages\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
211
                return null; // fails to determine the size
212
            }
213
        }
214
215
        return $this -> options['size'];
216
    }
217
218
    /**
219
     * Returns the current position of the file read/write pointer
220
     *
221
     * @return int Position of the file pointer
222
     * @throws \RuntimeException on error.
223
     */
224
    public function tell()
225
    {
226
        if (!$result = ftell($this -> body)) {
227
            throw new \RuntimeException("Cannot determine the file pointer position.");
228
        }
229
230
        return $result;
231
    }
232
233
    /**
234
     * Returns true if the stream is at the end of the stream.
235
     *
236
     * @return bool
237
     */
238
    public function eof()
239
    {
240
        // should use feof
241
        return (!$this->body || feof($this->body));
242
    }
243
244
    /**
245
     * Returns whether or not the stream is seekable.
246
     *
247
     * @return bool
248
     */
249
    public function isSeekable()
250
    {
251
        return $this -> seekable;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->seekable; (Almendra\Http\Psr\Messages\Is) is incompatible with the return type declared by the interface Psr\Http\Message\StreamInterface::isSeekable of type boolean.

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...
252
    }
253
254
    /**
255
     * Seek to a position in the stream.
256
     *
257
     * @link http://www.php.net/manual/en/function.fseek.php
258
     * @param int $offset Stream offset
259
     * @param int $whence Specifies how the cursor position will be calculated
260
     *     based on the seek offset. Valid values are identical to the built-in
261
     *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
262
     *     offset bytes SEEK_CUR: Set position to current location plus offset
263
     *     SEEK_END: Set position to end-of-stream plus offset.
264
     * @throws \RuntimeException on failure.
265
     */
266
    public function seek($offset, $whence = SEEK_SET)
267
    {
268
        if (!$this -> isSeekable()) {
269
            throw new \RuntimeException("The stream is not seekable.");
270
        }
271
272
        if (fseek($this -> body, $offset, $whence) === -1) {
273
            throw new \RuntimeException("Could not seek stream position.");
274
        }
275
    }
276
277
    /**
278
     * Seek to the beginning of the stream.
279
     *
280
     * If the stream is not seekable, this method will raise an exception;
281
     * otherwise, it will perform a seek(0).
282
     *
283
     * @see seek()
284
     * @link http://www.php.net/manual/en/function.fseek.php
285
     * @throws \RuntimeException on failure.
286
     */
287
    public function rewind()
288
    {
289
        $this -> seek(0);
290
    }
291
292
    /**
293
     * Returns whether or not the stream is writable.
294
     *
295
     * @return bool
296
     */
297
    public function isWritable()
298
    {
299
        return $this -> writable;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->writable; (Almendra\Http\Psr\Messages\Is) is incompatible with the return type declared by the interface Psr\Http\Message\StreamInterface::isWritable of type boolean.

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...
300
    }
301
302
    /**
303
     * Write data to the stream.
304
     *
305
     * @param string $string The string that is to be written.
306
     * @return int Returns the number of bytes written to the stream.
307
     * @throws \RuntimeException on failure.
308
     */
309
    public function write($string)
310
    {
311
        if (!$this -> isWritable()) {
312
            throw new \RuntimeException("Stream is not writable.");
313
        }
314
315
        $this -> options['size'] = null;
316
        $result = fwrite($this -> body, $string);
317
        if (false === $result) {
318
            throw new \RuntimeException("Error while writing to stream.");
319
        }
320
321
        return $result;
322
    }
323
324
    /**
325
     * Returns whether or not the stream is readable.
326
     *
327
     * @return bool
328
     */
329
    public function isReadable()
330
    {
331
        return $this -> readable;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->readable; (Almendra\Http\Psr\Messages\Is) is incompatible with the return type declared by the interface Psr\Http\Message\StreamInterface::isReadable of type boolean.

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...
332
    }
333
334
    /**
335
     * Read data from the stream.
336
     *
337
     * @param int $length Read up to $length bytes from the object and return
338
     *     them. Fewer than $length bytes may be returned if underlying stream
339
     *     call returns fewer bytes.
340
     * @return string Returns the data read from the stream, or an empty string
341
     *     if no bytes are available.
342
     * @throws \RuntimeException if an error occurs.
343
     */
344
    public function read($length)
345
    {
346
        if (!$this -> isReadable()) {
347
            throw new \RuntimeException("The stream is not readable.");
348
        }
349
350
        return fread($this -> body, $length);
351
    }
352
353
    /**
354
     * Returns the remaining contents in a string
355
     *
356
     * @return string
357
     * @throws \RuntimeException if unable to read or an error occurs while
358
     *     reading.
359
     */
360
    public function getContents()
361
    {
362
        if (!$result = fread($this -> body, $this -> getSize())) {
363
            throw new \RuntimeException("Unable to read or error while reading the stream.");
364
        }
365
366
        return $result;
367
    }
368
369
    /**
370
     * Get stream metadata as an associative array or retrieve a specific key.
371
     *
372
     * The keys returned are identical to the keys returned from PHP's
373
     * stream_get_meta_data() function.
374
     *
375
     * @link http://php.net/manual/en/function.stream-get-meta-data.php
376
     * @param string $key Specific metadata to retrieve.
377
     * @return array|mixed|null Returns an associative array if no key is
378
     *     provided. Returns a specific key value if a key is provided and the
379
     *     value is found, or null if the key is not found.
380
     */
381
    public function getMetadata($key = null)
382
    {
383
        if (!$key) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $key of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
384
            return [];
385
        }
386
387
        if (isset($this -> metaData)) {
388
            $meta = $this -> metaData;
389
        } else {
390
            $meta = stream_get_meta_data($this -> getBody());
391
        }
392
393
        return isset($meta[$key]) ? $meta[$key] : null;
394
    }
395
396
    /**
397
     * Gets the value of body.
398
     *
399
     * @return mixed
400
     */
401
    public function getBody()
402
    {
403
        if (!isset($this -> body)) {
404
            return null;
405
        }
406
407
        return $this->body;
408
    }
409
410
    /**
411
     * Sets the value of body.
412
     *
413
     * @param mixed $body the body
414
     *
415
     * @return self
416
     */
417
    protected function setBody($body)
418
    {
419
        $this->body = $body;
420
421
        return $this;
422
    }
423
424
    /**
425
     * Is the stream JSONable?
426
     *
427
     * @return boolean
428
     */
429
    protected function isJsonable()
430
    {
431
        $contents = $this -> getBody();
432
        try {
433
            $contents = json_encode($contents, JSON_PRETTY_PRINT);
434
            if ($contents === null ||
435
                $contents === '') {
436
                return false;
437
            }
438
        } catch (Exception $e) { // Invalid operation
0 ignored issues
show
Bug introduced by
The class Almendra\Http\Psr\Messages\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
439
            return false;
440
        }
441
        
442
        return true;
443
    }
444
445
    protected function getOption($name)
446
    {
447
        if (!isset($this -> options[$name])) {
448
            try {
449
                $fstat = fstat($this -> body);
450
                if (!isset($fstat[$name])) {
451
                    return null;
452
                }
453
454
                $this -> options[$name] = $fstat[$name];
455
            } catch (Exception $e) {
0 ignored issues
show
Bug introduced by
The class Almendra\Http\Psr\Messages\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
456
                return null; // fails to determine the size
457
            }
458
        }
459
460
        return $this -> options[$name];
461
    }
462
463
    protected function setOperations()
464
    {
465
        $this -> seekable = $this -> getMetadata('seekable');
466
        $this -> readable = isset(self::$readWriteHash['read'][$this -> getMetadata('mode')]);
0 ignored issues
show
Documentation Bug introduced by
It seems like isset(self::$readWriteHa...->getMetadata('mode')]) of type boolean is incompatible with the declared type object<Almendra\Http\Psr\Messages\Is> of property $readable.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
467
        $this -> writable = isset(self::$readWriteHash['write'][$this -> getMetadata('mode')]);
0 ignored issues
show
Documentation Bug introduced by
It seems like isset(self::$readWriteHa...->getMetadata('mode')]) of type boolean is incompatible with the declared type object<Almendra\Http\Psr\Messages\Is> of property $writable.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
468
    }
469
470
    protected function unsetOperations()
471
    {
472
        $this -> writable = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<Almendra\Http\Psr\Messages\Is> of property $writable.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
473
        $this -> readable = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<Almendra\Http\Psr\Messages\Is> of property $readable.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
474
        $this -> seekable = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<Almendra\Http\Psr\Messages\Is> of property $seekable.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
475
    }
476
477
    /**
478
     * Free resources
479
     *
480
     * @return void
481
     */
482
    public function __destruct()
483
    {
484
        $this -> close();
485
    }
486
}
487