238 lines
6.1 KiB
PHP
238 lines
6.1 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Http\Message\Stream;
|
||
|
|
|
||
|
|
use Psr\Http\Message\StreamInterface;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Decorator to make any stream seekable.
|
||
|
|
*
|
||
|
|
* Internally it buffers an existing StreamInterface into a php://temp resource (or memory). By default it will use
|
||
|
|
* 2 megabytes of memory before writing to a temporary disk file.
|
||
|
|
*
|
||
|
|
* Due to this, very large stream can suffer performance issue (i/o slowdown).
|
||
|
|
*/
|
||
|
|
class BufferedStream implements StreamInterface
|
||
|
|
{
|
||
|
|
/** @var resource The buffered resource used to seek previous data */
|
||
|
|
private $resource;
|
||
|
|
|
||
|
|
/** @var int|null size of the stream if available */
|
||
|
|
private $size;
|
||
|
|
|
||
|
|
/** @var StreamInterface The underlying stream decorated by this class */
|
||
|
|
private $stream;
|
||
|
|
|
||
|
|
/** @var int How many bytes were written */
|
||
|
|
private $written = 0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param StreamInterface $stream Decorated stream
|
||
|
|
* @param bool $useFileBuffer Whether to use a file buffer (write to a file, if data exceed a certain size)
|
||
|
|
* by default, set this to false to only use memory
|
||
|
|
* @param int $memoryBuffer In conjunction with using file buffer, limit (in bytes) from which it begins to buffer
|
||
|
|
* the data in a file
|
||
|
|
*/
|
||
|
|
public function __construct(StreamInterface $stream, $useFileBuffer = true, $memoryBuffer = 2097152)
|
||
|
|
{
|
||
|
|
$this->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');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function __toString(): string
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
$this->rewind();
|
||
|
|
|
||
|
|
return $this->getContents();
|
||
|
|
} catch (\Throwable $throwable) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function close(): void
|
||
|
|
{
|
||
|
|
if (null === $this->resource) {
|
||
|
|
throw new \RuntimeException('Cannot close on a detached stream');
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->stream->close();
|
||
|
|
fclose($this->resource);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function detach()
|
||
|
|
{
|
||
|
|
if (null === $this->resource) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Force reading the remaining data of the stream
|
||
|
|
$this->getContents();
|
||
|
|
|
||
|
|
$resource = $this->resource;
|
||
|
|
$this->stream->close();
|
||
|
|
$this->stream = null;
|
||
|
|
$this->resource = null;
|
||
|
|
|
||
|
|
return $resource;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getSize(): ?int
|
||
|
|
{
|
||
|
|
if (null === $this->resource) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (null === $this->size && $this->stream->eof()) {
|
||
|
|
return $this->written;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->size;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function tell(): int
|
||
|
|
{
|
||
|
|
if (null === $this->resource) {
|
||
|
|
throw new \RuntimeException('Cannot tell on a detached stream');
|
||
|
|
}
|
||
|
|
|
||
|
|
$tell = ftell($this->resource);
|
||
|
|
if (false === $tell) {
|
||
|
|
throw new \RuntimeException('ftell failed');
|
||
|
|
}
|
||
|
|
|
||
|
|
return $tell;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function eof(): bool
|
||
|
|
{
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function isSeekable(): bool
|
||
|
|
{
|
||
|
|
return null !== $this->resource;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function seek(int $offset, int $whence = SEEK_SET): void
|
||
|
|
{
|
||
|
|
if (null === $this->resource) {
|
||
|
|
throw new \RuntimeException('Cannot seek on a detached stream');
|
||
|
|
}
|
||
|
|
|
||
|
|
fseek($this->resource, $offset, $whence);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function rewind(): void
|
||
|
|
{
|
||
|
|
if (null === $this->resource) {
|
||
|
|
throw new \RuntimeException('Cannot rewind on a detached stream');
|
||
|
|
}
|
||
|
|
|
||
|
|
rewind($this->resource);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function isWritable(): bool
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function write(string $string): int
|
||
|
|
{
|
||
|
|
throw new \RuntimeException('Cannot write on this stream');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function isReadable(): bool
|
||
|
|
{
|
||
|
|
return null !== $this->resource;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function read(int $length): string
|
||
|
|
{
|
||
|
|
if (null === $this->resource) {
|
||
|
|
throw new \RuntimeException('Cannot read on a detached stream');
|
||
|
|
}
|
||
|
|
if ($length < 0) {
|
||
|
|
throw new \InvalidArgumentException('Can not read a negative amount of bytes');
|
||
|
|
}
|
||
|
|
if (0 === $length) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
$read = '';
|
||
|
|
|
||
|
|
// First read from the resource
|
||
|
|
if (ftell($this->resource) !== $this->written) {
|
||
|
|
$read = fread($this->resource, $length);
|
||
|
|
}
|
||
|
|
if (false === $read) {
|
||
|
|
throw new \RuntimeException('Failed to read from resource');
|
||
|
|
}
|
||
|
|
|
||
|
|
$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;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getContents(): string
|
||
|
|
{
|
||
|
|
if (null === $this->resource) {
|
||
|
|
throw new \RuntimeException('Cannot read on a detached stream');
|
||
|
|
}
|
||
|
|
|
||
|
|
$read = '';
|
||
|
|
|
||
|
|
while (!$this->eof()) {
|
||
|
|
$read .= $this->read(8192);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $read;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getMetadata(?string $key = null)
|
||
|
|
{
|
||
|
|
if (null === $this->resource) {
|
||
|
|
if (null === $key) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$metadata = stream_get_meta_data($this->resource);
|
||
|
|
|
||
|
|
if (null === $key) {
|
||
|
|
return $metadata;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!array_key_exists($key, $metadata)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $metadata[$key];
|
||
|
|
}
|
||
|
|
}
|