Stream detects read/write more accurately; fix issues after detach()
This commit is contained in:
parent
fe780e6b92
commit
a7b08ad8a3
|
|
@ -6,7 +6,10 @@ use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
class Stream implements StreamInterface
|
class Stream implements StreamInterface
|
||||||
{
|
{
|
||||||
/** @var resource */
|
private const READABLE_MODES = ['r', 'r+', 'w+', 'a+', 'x+', 'c+'];
|
||||||
|
private const WRITABLE_MODES = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
|
||||||
|
|
||||||
|
/** @var resource|null */
|
||||||
private $resource;
|
private $resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,18 +49,16 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function __toString()
|
public function __toString()
|
||||||
{
|
{
|
||||||
$string = "";
|
|
||||||
try {
|
try {
|
||||||
if ($this->isSeekable()) {
|
if ($this->isSeekable()) {
|
||||||
rewind($this->resource);
|
$this->rewind();
|
||||||
}
|
}
|
||||||
$string = $this->getContents();
|
return $this->getContents();
|
||||||
// @codeCoverageIgnoreStart
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// @codeCoverageIgnoreEnd
|
// Silence exceptions in order to conform with PHP's string casting
|
||||||
// Silence exceptions in order to conform with PHP's string casting operations.
|
// operations.
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
return $string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -67,6 +68,10 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$resource = $this->resource;
|
$resource = $this->resource;
|
||||||
fclose($resource);
|
fclose($resource);
|
||||||
$this->resource = null;
|
$this->resource = null;
|
||||||
|
|
@ -93,6 +98,10 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function getSize()
|
public function getSize()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$statistics = fstat($this->resource);
|
$statistics = fstat($this->resource);
|
||||||
if ($statistics && $statistics["size"]) {
|
if ($statistics && $statistics["size"]) {
|
||||||
return $statistics["size"];
|
return $statistics["size"];
|
||||||
|
|
@ -108,6 +117,10 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function tell()
|
public function tell()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new \RuntimeException("Unable to retrieve current position of detached stream.");
|
||||||
|
}
|
||||||
|
|
||||||
$position = ftell($this->resource);
|
$position = ftell($this->resource);
|
||||||
if ($position === false) {
|
if ($position === false) {
|
||||||
throw new \RuntimeException("Unable to retrieve current position of file pointer.");
|
throw new \RuntimeException("Unable to retrieve current position of file pointer.");
|
||||||
|
|
@ -122,6 +135,10 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function eof()
|
public function eof()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return feof($this->resource);
|
return feof($this->resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,6 +149,10 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function isSeekable()
|
public function isSeekable()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->getMetadata("seekable") == 1;
|
return $this->getMetadata("seekable") == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,6 +170,10 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function seek($offset, $whence = SEEK_SET)
|
public function seek($offset, $whence = SEEK_SET)
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new \RuntimeException("Unable to seek detached stream.");
|
||||||
|
}
|
||||||
|
|
||||||
$result = -1;
|
$result = -1;
|
||||||
if ($this->isSeekable()) {
|
if ($this->isSeekable()) {
|
||||||
$result = fseek($this->resource, $offset, $whence);
|
$result = fseek($this->resource, $offset, $whence);
|
||||||
|
|
@ -170,12 +195,16 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function rewind()
|
public function rewind()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new \RuntimeException("Unable to seek detached stream.");
|
||||||
|
}
|
||||||
|
|
||||||
$result = false;
|
$result = false;
|
||||||
if ($this->isSeekable()) {
|
if ($this->isSeekable()) {
|
||||||
$result = rewind($this->resource);
|
$result = rewind($this->resource);
|
||||||
}
|
}
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
throw new \RuntimeException("Unable to seek to position.");
|
throw new \RuntimeException("Unable to rewind.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,8 +215,12 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function isWritable()
|
public function isWritable()
|
||||||
{
|
{
|
||||||
$mode = $this->getMetadata("mode");
|
if ($this->resource === null) {
|
||||||
return $mode[0] !== "r" || strpos($mode, "+") !== false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = $this->getBasicMode();
|
||||||
|
return in_array($mode, self::WRITABLE_MODES);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -199,6 +232,10 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function write($string)
|
public function write($string)
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new \RuntimeException("Unable to write to detached stream.");
|
||||||
|
}
|
||||||
|
|
||||||
$result = false;
|
$result = false;
|
||||||
if ($this->isWritable()) {
|
if ($this->isWritable()) {
|
||||||
$result = fwrite($this->resource, $string);
|
$result = fwrite($this->resource, $string);
|
||||||
|
|
@ -216,8 +253,12 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function isReadable()
|
public function isReadable()
|
||||||
{
|
{
|
||||||
$mode = $this->getMetadata("mode");
|
if ($this->resource === null) {
|
||||||
return strpos($mode, "r") !== false || strpos($mode, "+") !== false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = $this->getBasicMode();
|
||||||
|
return in_array($mode, self::READABLE_MODES);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -232,6 +273,10 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function read($length)
|
public function read($length)
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new \RuntimeException("Unable to read to detached stream.");
|
||||||
|
}
|
||||||
|
|
||||||
$result = false;
|
$result = false;
|
||||||
if ($this->isReadable()) {
|
if ($this->isReadable()) {
|
||||||
$result = fread($this->resource, $length);
|
$result = fread($this->resource, $length);
|
||||||
|
|
@ -251,6 +296,10 @@ class Stream implements StreamInterface
|
||||||
*/
|
*/
|
||||||
public function getContents()
|
public function getContents()
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
throw new \RuntimeException("Unable to read to detached stream.");
|
||||||
|
}
|
||||||
|
|
||||||
$result = false;
|
$result = false;
|
||||||
if ($this->isReadable()) {
|
if ($this->isReadable()) {
|
||||||
$result = stream_get_contents($this->resource);
|
$result = stream_get_contents($this->resource);
|
||||||
|
|
@ -268,13 +317,17 @@ class Stream implements StreamInterface
|
||||||
* stream_get_meta_data() function.
|
* stream_get_meta_data() function.
|
||||||
*
|
*
|
||||||
* @link http://php.net/manual/en/function.stream-get-meta-data.php
|
* @link http://php.net/manual/en/function.stream-get-meta-data.php
|
||||||
* @param string $key Specific metadata to retrieve.
|
* @param string|null $key Specific metadata to retrieve.
|
||||||
* @return array|mixed|null Returns an associative array if no key is
|
* @return array|mixed|null Returns an associative array if no key is
|
||||||
* provided. Returns a specific key value if a key is provided and the
|
* provided. Returns a specific key value if a key is provided and the
|
||||||
* value is found, or null if the key is not found.
|
* value is found, or null if the key is not found.
|
||||||
*/
|
*/
|
||||||
public function getMetadata($key = null)
|
public function getMetadata($key = null)
|
||||||
{
|
{
|
||||||
|
if ($this->resource === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$metadata = stream_get_meta_data($this->resource);
|
$metadata = stream_get_meta_data($this->resource);
|
||||||
if ($key === null) {
|
if ($key === null) {
|
||||||
return $metadata;
|
return $metadata;
|
||||||
|
|
@ -282,4 +335,14 @@ class Stream implements StreamInterface
|
||||||
return $metadata[$key];
|
return $metadata[$key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string Mode for the resource reduced to only the characters
|
||||||
|
* r, w, a, x, c, and + needed to determine readable and writeable status.
|
||||||
|
*/
|
||||||
|
private function getBasicMode()
|
||||||
|
{
|
||||||
|
$mode = $this->getMetadata('mode') ?? '';
|
||||||
|
return preg_replace('/[^rwaxc+]/', '', $mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,14 @@ class StreamTest extends TestCase
|
||||||
private $resourceDevNull;
|
private $resourceDevNull;
|
||||||
private $content = "Hello, world!";
|
private $content = "Hello, world!";
|
||||||
|
|
||||||
public function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->resource = fopen("php://memory", "w+");
|
$this->resource = fopen("php://memory", "w+");
|
||||||
$this->resourceDevNull = fopen("/dev/null", "r");
|
$this->resourceDevNull = fopen("/dev/null", "r");
|
||||||
fwrite($this->resource, $this->content);
|
fwrite($this->resource, $this->content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
if (is_resource($this->resource)) {
|
if (is_resource($this->resource)) {
|
||||||
fclose($this->resource);
|
fclose($this->resource);
|
||||||
|
|
@ -269,4 +269,119 @@ class StreamTest extends TestCase
|
||||||
["c+", true, true]
|
["c+", true, true]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// After Detach
|
||||||
|
|
||||||
|
public function testAfterDetachToStringReturnsEmptyString(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->assertEquals('', (string) $stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachCloseDoesNothing(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$stream->close();
|
||||||
|
$this->assertTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachDetachReturnsNull(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->assertNull($stream->detach());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachGetSizeReturnsNull(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->assertNull($stream->getSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachTellThrowsRuntimeException(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$stream->tell();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachEofReturnsTrue(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->assertTrue($stream->eof());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachIsSeekableReturnsFalse(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->assertFalse($stream->isSeekable());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachSeekThrowsRuntimeException(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$stream->seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachRewindThrowsRuntimeException(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$stream->rewind();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachIsWritableReturnsFalse(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->assertFalse($stream->isWritable());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachWriteThrowsRuntimeException(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$stream->write('bork');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachIsReadableReturnsFalse(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->assertFalse($stream->isReadable());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachReadThrowsRuntimeException(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$stream->read(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachGetContentsThrowsRuntimeException(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$stream->getContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAfterDetachGetMetadataReturnsNull(): void
|
||||||
|
{
|
||||||
|
$stream = new Stream($this->resource);
|
||||||
|
$stream->detach();
|
||||||
|
$this->assertNull($stream->getMetadata());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue