diff --git a/spec/Stream/BufferedStreamSpec.php b/spec/Stream/BufferedStreamSpec.php new file mode 100644 index 0000000..c1cbba6 --- /dev/null +++ b/spec/Stream/BufferedStreamSpec.php @@ -0,0 +1,184 @@ +beConstructedWith($stream); + + $stream->getSize()->willReturn(null); + } + + public function it_is_castable_to_string(StreamInterface $stream) + { + $eofCounter = 0; + $stream->eof()->will(function () use(&$eofCounter) { + return (++$eofCounter > 1); + }); + + $stream->read(8192)->willReturn('foo'); + + $this->__toString()->shouldReturn('foo'); + } + + public function it_detachs(StreamInterface $stream) + { + $stream->eof()->willReturn(true); + $stream->read(8192)->willReturn(''); + $stream->close()->shouldBeCalled(); + + $this->detach()->shouldBeResource(); + $this->detach()->shouldBeNull(); + } + + public function it_gets_size(StreamInterface $stream) + { + $stream->eof()->willReturn(false); + $this->getSize()->shouldReturn(null); + + $eofCounter = 0; + $stream->eof()->will(function () use(&$eofCounter) { + return (++$eofCounter > 1); + }); + + $stream->read(8192)->willReturn('foo'); + + $this->getContents()->shouldReturn('foo'); + $this->getSize()->shouldReturn(3); + } + + public function it_tells(StreamInterface $stream) + { + $this->tell()->shouldReturn(0); + + $eofCounter = 0; + $stream->eof()->will(function () use(&$eofCounter) { + return (++$eofCounter > 1); + }); + + $stream->read(8192)->willReturn('foo'); + $this->getContents()->shouldReturn('foo'); + $this->tell()->shouldReturn(3); + } + + public function it_eof(StreamInterface $stream) + { + // Case when underlying is false + $stream->eof()->willReturn(false); + $this->eof()->shouldReturn(false); + + // Case when sync and underlying is true + $stream->eof()->willReturn(true); + $this->eof()->shouldReturn(true); + + // Case not sync but underlying is true + $eofCounter = 0; + $stream->eof()->will(function () use(&$eofCounter) { + return (++$eofCounter > 1); + }); + + $stream->read(8192)->willReturn('foo'); + + $this->getContents()->shouldReturn('foo'); + $this->seek(0); + + $stream->eof()->willReturn(true); + $this->eof()->shouldReturn(false); + } + + public function it_is_seekable(StreamInterface $stream) + { + $this->isSeekable()->shouldReturn(true); + } + + public function it_seeks(StreamInterface $stream) + { + $this->seek(0); + $this->tell()->shouldReturn(0); + + $eofCounter = 0; + $stream->eof()->will(function () use(&$eofCounter) { + return (++$eofCounter > 1); + }); + + $stream->read(8192)->willReturn('foo'); + + $this->getContents()->shouldReturn('foo'); + $this->seek(2); + $this->tell()->shouldReturn(2); + } + + public function it_rewinds(StreamInterface $stream) + { + $this->rewind(); + $this->tell()->shouldReturn(0); + + $eofCounter = 0; + $stream->eof()->will(function () use(&$eofCounter) { + return (++$eofCounter > 1); + }); + + $stream->read(8192)->willReturn('foo'); + + $this->getContents()->shouldReturn('foo'); + $this->tell()->shouldReturn(3); + $this->rewind(); + $this->tell()->shouldReturn(0); + } + + public function it_is_not_writable(StreamInterface $stream) + { + $this->isWritable()->shouldReturn(false); + $this->shouldThrow('\RuntimeException')->duringWrite('foo'); + } + + public function it_is_readable(StreamInterface $stream) + { + $this->isReadable()->shouldReturn(true); + } + + public function it_reads(StreamInterface $stream) + { + $eofCounter = 0; + $stream->eof()->will(function () use(&$eofCounter) { + return (++$eofCounter > 1); + }); + + $stream->read(3)->willReturn('foo'); + $this->read(3)->shouldReturn('foo'); + + $stream->read(3)->willReturn('bar'); + $this->read(3)->shouldReturn('bar'); + + $this->rewind(); + $this->read(4)->shouldReturn('foob'); + + $stream->read(3)->willReturn('baz'); + $this->read(5)->shouldReturn('arbaz'); + } + + public function it_get_contents(StreamInterface $stream) + { + $eofCounter = 0; + $stream->eof()->will(function () use(&$eofCounter) { + return (++$eofCounter > 1); + }); + + $stream->read(8192)->willReturn('foo'); + + $this->getContents()->shouldReturn('foo'); + $this->eof()->shouldReturn(true); + } + + public function it_get_metadatas(StreamInterface $stream) + { + $this->getMetadata()->shouldBeArray(); + $this->getMetadata('unexistant')->shouldBeNull(); + $this->getMetadata('stream_type')->shouldReturn('TEMP'); + } +} diff --git a/src/Stream/BufferedStream.php b/src/Stream/BufferedStream.php new file mode 100644 index 0000000..1eac974 --- /dev/null +++ b/src/Stream/BufferedStream.php @@ -0,0 +1,270 @@ +stream = $stream; + $this->size = $stream->getSize(); + + if ($useFileBuffer) { + $this->resource = fopen('php://temp/maxmemory:'.$memoryBuffer, 'rw+'); + } else { + $this->resource = fopen('php://memory', 'rw+'); + } + + if (false === $this->resource) { + throw new \RuntimeException('Cannot create a resource over temp or memory implementation'); + } + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + try { + $this->rewind(); + + return $this->getContents(); + } catch (\Throwable $throwable) { + return ''; + } catch (\Exception $exception) { // Layer to be BC with PHP 5, remove this when we only support PHP 7+ + return ''; + } + } + + /** + * {@inheritdoc} + */ + public function close() + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot close on a detached stream'); + } + + $this->stream->close(); + fclose($this->resource); + } + + /** + * {@inheritdoc} + */ + public function detach() + { + if (null === $this->resource) { + return; + } + + // Force reading the remaining data of the stream + $this->getContents(); + + $resource = $this->resource; + $this->stream->close(); + $this->stream = null; + $this->resource = null; + + return $resource; + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + if (null === $this->resource) { + return; + } + + if (null === $this->size && $this->stream->eof()) { + return $this->written; + } + + return $this->size; + } + + /** + * {@inheritdoc} + */ + public function tell() + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot tell on a detached stream'); + } + + return ftell($this->resource); + } + + /** + * {@inheritdoc} + */ + public function eof() + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot call eof on a detached stream'); + } + + // We are at the end only when both our resource and underlying stream are at eof + return $this->stream->eof() && (ftell($this->resource) === $this->written); + } + + /** + * {@inheritdoc} + */ + public function isSeekable() + { + return null !== $this->resource; + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot seek on a detached stream'); + } + + fseek($this->resource, $offset, $whence); + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot rewind on a detached stream'); + } + + rewind($this->resource); + } + + /** + * {@inheritdoc} + */ + public function isWritable() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function write($string) + { + throw new \RuntimeException('Cannot write on this stream'); + } + + /** + * {@inheritdoc} + */ + public function isReadable() + { + return null !== $this->resource; + } + + /** + * {@inheritdoc} + */ + public function read($length) + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot read on a detached stream'); + } + + $read = ''; + + // First read from the resource + if (ftell($this->resource) !== $this->written) { + $read = fread($this->resource, $length); + } + + $bytesRead = strlen($read); + + if ($bytesRead < $length) { + $streamRead = $this->stream->read($length - $bytesRead); + + // Write on the underlying stream what we read + $this->written += fwrite($this->resource, $streamRead); + $read .= $streamRead; + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function getContents() + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot read on a detached stream'); + } + + $read = ''; + + while (!$this->eof()) { + $read .= $this->read(8192); + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = null) + { + if (null === $this->resource) { + if (null === $key) { + return []; + } + + return; + } + + $metadata = stream_get_meta_data($this->resource); + + if (null === $key) { + return $metadata; + } + + if (!array_key_exists($key, $metadata)) { + return; + } + + return $metadata[$key]; + } +}