diff --git a/src/Stream/StreamStream.php b/src/Stream/StreamStream.php new file mode 100644 index 0000000..bfd6c2f --- /dev/null +++ b/src/Stream/StreamStream.php @@ -0,0 +1,216 @@ +handle = $handle; + } else { + throw new \InvalidArgumentException("Expected a resource handler."); + } + } + + /** + * Reads all data from the stream into a string, from the beginning to end. + * + * This method MUST attempt to seek to the beginning of the stream before + * reading data and read the stream until the end is reached. + * + * Warning: This could attempt to load a large amount of data into memory. + * + * @return string + */ + public function __toString() + { + rewind($this->handle); + return $this->getContents(); + } + + /** + * Closes the stream and any underlying resources. + * + * @return void + */ + public function close() + { + fclose($this->handle); + } + + /** + * Separates any underlying resources from the stream. + * + * After the stream has been detached, the stream is in an unusable state. + * + * @return resource|null Underlying PHP stream, if any + */ + public function detach() + { + $stream = $this->handle; + $this->handle = null; + return $stream; + } + + /** + * Get the size of the stream if known + * + * @return int|null Returns the size in bytes if known, or null if unknown. + */ + public function getSize() + { + return null; + } + + /** + * Returns the current position of the file read/write pointer + * + * @return int|bool Position of the file pointer or false on error. + */ + public function tell() + { + return ftell($this->handle); + } + + /** + * Returns true if the stream is at the end of the stream. + * + * @return bool + */ + public function eof() + { + return feof($this->handle); + } + + /** + * Returns whether or not the stream is seekable. + * + * @return bool + */ + public function isSeekable() + { + return $this->getMetadata("seekable") == 1; + } + + /** + * Seek to a position in the stream. + * + * @link http://www.php.net/manual/en/function.fseek.php + * @param int $offset Stream offset + * @param int $whence Specifies how the cursor position will be calculated + * based on the seek offset. Valid values are identical to the built-in + * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to + * offset bytes SEEK_CUR: Set position to current location plus offset + * SEEK_END: Set position to end-of-stream plus offset. + * @return bool Returns TRUE on success or FALSE on failure. + */ + public function seek($offset, $whence = SEEK_SET) + { + fseek($this->handle, $offset, $whence); + } + + /** + * Seek to the beginning of the stream. + * + * If the stream is not seekable, this method will return FALSE, indicating + * failure; otherwise, it will perform a seek(0), and return the status of + * that operation. + * + * @see seek() + * @link http://www.php.net/manual/en/function.fseek.php + * @return bool Returns TRUE on success or FALSE on failure. + */ + public function rewind() + { + rewind($this->handle); + } + + /** + * Returns whether or not the stream is writable. + * + * @return bool + */ + public function isWritable() + { + $mode = $this->getMetadata("mode"); + return $mode[0] !== "r" || strpos($mode, "+") !== false; + } + + /** + * Write data to the stream. + * + * @param string $string The string that is to be written. + * @return int|bool Returns the number of bytes written to the stream on + * success or FALSE on failure. + */ + public function write($string) + { + return fwrite($this->handle, $string); + } + + /** + * Returns whether or not the stream is readable. + * + * @return bool + */ + public function isReadable() + { + $mode = $this->getMetadata("mode"); + return strpos($mode, "r") !== false || strpos($mode, "+") !== false; + } + + /** + * Read data from the stream. + * + * @param int $length Read up to $length bytes from the object and return + * them. Fewer than $length bytes may be returned if underlying stream + * call returns fewer bytes. + * @return string|false Returns the data read from the stream, false if + * unable to read or if an error occurs. + */ + public function read($length) + { + return fread($this->handle, $length); + } + + /** + * Returns the remaining contents in a string + * + * @return string + */ + public function getContents() + { + return stream_get_contents($this->handle); + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * + * The keys returned are identical to the keys returned from PHP's + * stream_get_meta_data() function. + * + * @link http://php.net/manual/en/function.stream-get-meta-data.php + * @param string $key Specific metadata to retrieve. + * @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 + * value is found, or null if the key is not found. + */ + public function getMetadata($key = null) + { + $metadata = stream_get_meta_data($this->handle); + if ($key === null) { + return $metadata; + } else { + return $metadata[$key]; + } + } +} diff --git a/test/tests/unit/Stream/StreamStreamTest.php b/test/tests/unit/Stream/StreamStreamTest.php new file mode 100644 index 0000000..44c74c6 --- /dev/null +++ b/test/tests/unit/Stream/StreamStreamTest.php @@ -0,0 +1,282 @@ +handle = fopen("php://memory", "w+"); + fwrite($this->handle, $this->content); + } + + public function tearDown() + { + if (is_resource($this->handle)) { + fclose($this->handle); + } + } + + /** + * @covers WellRESTed\Stream\StreamStream::__construct() + */ + public function testCreatesInstance() + { + $stream = new StreamStream($this->handle); + $this->assertNotNull($stream); + } + + /** + * @covers WellRESTed\Stream\StreamStream::__construct() + * @expectedException \InvalidArgumentException + */ + public function testThrowsExceptiondWithoutHandle() + { + new StreamStream(null); + } + + /** + * @covers WellRESTed\Stream\StreamStream::__toString() + * @uses WellRESTed\Stream\StreamStream + */ + public function testCastsToString() + { + $content = "Hello, world!"; + + $h = fopen("php://memory", "w+"); + fwrite($h, $content); + rewind($h); + + $stream = new StreamStream($h); + $this->assertEquals($content, (string) $stream); + } + + /** + * @covers WellRESTed\Stream\StreamStream::__toString() + * @uses WellRESTed\Stream\StreamStream + */ + public function testRewindsBeforeCastingToString() + { + $content = "Hello, world!"; + + $h = fopen("php://memory", "w+"); + fwrite($h, $content); + + $stream = new StreamStream($h); + $this->assertEquals($content, (string) $stream); + } + + /** + * @covers WellRESTed\Stream\StreamStream::close() + * @uses WellRESTed\Stream\StreamStream + */ + public function testClosesHandle() + { + $stream = new StreamStream($this->handle); + $stream->close(); + $this->assertFalse(is_resource($this->handle)); + } + + /** + * @covers WellRESTed\Stream\StreamStream::detach() + * @uses WellRESTed\Stream\StreamStream + */ + public function testDetachReturnsHandle() + { + $stream = new StreamStream($this->handle); + $h = $stream->detach(); + $this->assertSame($this->handle, $h); + } + + /** + * @covers WellRESTed\Stream\StreamStream::detach() + * @uses WellRESTed\Stream\StreamStream + */ + public function testDetachUnsetsInstanceVariable() + { + $stream = new StreamStream($this->handle); + $stream->detach(); + $this->assertNull($stream->detach()); + } + + /** + * @covers WellRESTed\Stream\StreamStream::getSize + * @uses WellRESTed\Stream\StreamStream + */ + public function testReturnsNullForSize() + { + $stream = new StreamStream($this->handle); + $this->assertNull($stream->getSize()); + } + + /** + * @covers WellRESTed\Stream\StreamStream::tell + * @uses WellRESTed\Stream\StreamStream + */ + public function testTellReturnsHandlePosition() + { + $stream = new StreamStream($this->handle); + fseek($this->handle, 10); + $this->assertEquals(10, $stream->tell()); + } + + /** + * @covers WellRESTed\Stream\StreamStream::eof + * @uses WellRESTed\Stream\StreamStream + */ + public function testReturnsOef() + { + $stream = new StreamStream($this->handle); + $stream->rewind(); + $this->assertFalse($stream->eof()); + $stream->getContents(); + $this->assertTrue($stream->eof()); + } + + /** + * @covers WellRESTed\Stream\StreamStream::isSeekable + * @uses WellRESTed\Stream\StreamStream + */ + public function testReadsSeekableStatusFromMetadata() + { + $stream = new StreamStream($this->handle); + $metadata = stream_get_meta_data($this->handle); + $seekable = $metadata["seekable"] == 1; + $this->assertEquals($seekable, $stream->isSeekable()); + } + + /** + * @covers WellRESTed\Stream\StreamStream::seek + * @uses WellRESTed\Stream\StreamStream + */ + public function testSeeksToPosition() + { + $stream = new StreamStream($this->handle); + $stream->seek(10); + $this->assertEquals(10, ftell($this->handle)); + } + + /** + * @covers WellRESTed\Stream\StreamStream::rewind + * @uses WellRESTed\Stream\StreamStream + */ + public function testRewindReturnsToBeginning() + { + $stream = new StreamStream($this->handle); + $stream->seek(10); + $stream->rewind(); + $this->assertEquals(0, ftell($this->handle)); + } + + /** + * @covers WellRESTed\Stream\StreamStream::write + * @uses WellRESTed\Stream\StreamStream + */ + public function testWritesToHandle() + { + $message = "\nThis is a stream."; + $stream = new StreamStream($this->handle); + $stream->write($message); + $this->assertEquals($this->content . $message, (string) $stream); + } + + /** + * @covers WellRESTed\Stream\StreamStream::read + * @uses WellRESTed\Stream\StreamStream + */ + public function testReadsFromStream() + { + $stream = new StreamStream($this->handle); + $stream->seek(7); + $string = $stream->read(5); + $this->assertEquals("world", $string); + } + + /** + * @covers WellRESTed\Stream\StreamStream::getContents + * @uses WellRESTed\Stream\StreamStream + */ + public function testReadsToEnd() + { + $stream = new StreamStream($this->handle); + $stream->seek(7); + $string = $stream->getContents(); + $this->assertEquals("world!", $string); + } + + /** + * @covers WellRESTed\Stream\StreamStream::getMetadata + * @uses WellRESTed\Stream\StreamStream + */ + public function testReturnsMetadataArray() + { + $stream = new StreamStream($this->handle); + $this->assertEquals(stream_get_meta_data($this->handle), $stream->getMetadata()); + } + + /** + * @covers WellRESTed\Stream\StreamStream::getMetadata + * @uses WellRESTed\Stream\StreamStream + */ + public function testReturnsMetadataItem() + { + $stream = new StreamStream($this->handle); + $metadata = stream_get_meta_data($this->handle); + $this->assertEquals($metadata["mode"], $stream->getMetadata("mode")); + } + + /** + * @covers WellRESTed\Stream\StreamStream::isReadable + * @uses WellRESTed\Stream\StreamStream + * @dataProvider modeProvider + */ + public function testReturnsIsReadableForReadableStreams($mode, $readable, $writeable) + { + $tmp = tempnam(sys_get_temp_dir(), "php"); + if ($mode[0] === "x") { + unlink($tmp); + } + fclose($this->handle); + $this->handle = fopen($tmp, $mode); + $stream = new StreamStream($this->handle); + $this->assertEquals($readable, $stream->isReadable()); + } + + /** + * @covers WellRESTed\Stream\StreamStream::isWritable + * @uses WellRESTed\Stream\StreamStream + * @dataProvider modeProvider + */ + public function testReturnsIsWritableForWritableStreams($mode, $readable, $writeable) + { + $tmp = tempnam(sys_get_temp_dir(), "php"); + if ($mode[0] === "x") { + unlink($tmp); + } + fclose($this->handle); + $this->handle = fopen($tmp, $mode); + $stream = new StreamStream($this->handle); + $this->assertEquals($writeable, $stream->isWritable()); + } + + public function modeProvider() + { + return [ + ["r", true, false], + ["r+", true, true], + ["w", false, true], + ["w+", true, true], + ["a", false, true], + ["a+", true, true], + ["x", false, true], + ["x+", true, true], + ["c", false, true], + ["c+", true, true] + ]; + } +}