You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
250 lines
5.8 KiB
250 lines
5.8 KiB
<?php |
|
|
|
declare(strict_types=1); |
|
|
|
namespace GuzzleHttp\Psr7; |
|
|
|
use Psr\Http\Message\StreamInterface; |
|
|
|
/** |
|
* Reads from multiple streams, one after the other. |
|
* |
|
* This is a read-only stream decorator. |
|
*/ |
|
final class AppendStream implements StreamInterface |
|
{ |
|
/** @var StreamInterface[] Streams being decorated */ |
|
private $streams = []; |
|
|
|
/** @var bool */ |
|
private $seekable = true; |
|
|
|
/** @var int */ |
|
private $current = 0; |
|
|
|
/** @var int */ |
|
private $pos = 0; |
|
|
|
/** |
|
* @param StreamInterface[] $streams Streams to decorate. Each stream must |
|
* be readable. |
|
*/ |
|
public function __construct(array $streams = []) |
|
{ |
|
foreach ($streams as $stream) { |
|
$this->addStream($stream); |
|
} |
|
} |
|
|
|
public function __toString(): string |
|
{ |
|
try { |
|
$this->rewind(); |
|
|
|
return $this->getContents(); |
|
} catch (\Throwable $e) { |
|
if (\PHP_VERSION_ID >= 70400) { |
|
throw $e; |
|
} |
|
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); |
|
|
|
return ''; |
|
} |
|
} |
|
|
|
/** |
|
* Add a stream to the AppendStream |
|
* |
|
* @param StreamInterface $stream Stream to append. Must be readable. |
|
* |
|
* @throws \InvalidArgumentException if the stream is not readable |
|
*/ |
|
public function addStream(StreamInterface $stream): void |
|
{ |
|
if (!$stream->isReadable()) { |
|
throw new \InvalidArgumentException('Each stream must be readable'); |
|
} |
|
|
|
// The stream is only seekable if all streams are seekable |
|
if (!$stream->isSeekable()) { |
|
$this->seekable = false; |
|
} |
|
|
|
$this->streams[] = $stream; |
|
} |
|
|
|
public function getContents(): string |
|
{ |
|
return Utils::copyToString($this); |
|
} |
|
|
|
/** |
|
* Closes each attached stream. |
|
*/ |
|
public function close(): void |
|
{ |
|
$this->pos = $this->current = 0; |
|
$this->seekable = true; |
|
|
|
foreach ($this->streams as $stream) { |
|
$stream->close(); |
|
} |
|
|
|
$this->streams = []; |
|
} |
|
|
|
/** |
|
* Detaches each attached stream. |
|
* |
|
* Returns null as it's not clear which underlying stream resource to return. |
|
*/ |
|
public function detach() |
|
{ |
|
$this->pos = $this->current = 0; |
|
$this->seekable = true; |
|
|
|
foreach ($this->streams as $stream) { |
|
$stream->detach(); |
|
} |
|
|
|
$this->streams = []; |
|
|
|
return null; |
|
} |
|
|
|
public function tell(): int |
|
{ |
|
return $this->pos; |
|
} |
|
|
|
/** |
|
* Tries to calculate the size by adding the size of each stream. |
|
* |
|
* If any of the streams do not return a valid number, then the size of the |
|
* append stream cannot be determined and null is returned. |
|
*/ |
|
public function getSize(): ?int |
|
{ |
|
$size = 0; |
|
|
|
foreach ($this->streams as $stream) { |
|
$s = $stream->getSize(); |
|
if ($s === null) { |
|
return null; |
|
} |
|
$size += $s; |
|
} |
|
|
|
return $size; |
|
} |
|
|
|
public function eof(): bool |
|
{ |
|
return !$this->streams || |
|
($this->current >= count($this->streams) - 1 && |
|
$this->streams[$this->current]->eof()); |
|
} |
|
|
|
public function rewind(): void |
|
{ |
|
$this->seek(0); |
|
} |
|
|
|
/** |
|
* Attempts to seek to the given position. Only supports SEEK_SET. |
|
*/ |
|
public function seek($offset, $whence = SEEK_SET): void |
|
{ |
|
if (!$this->seekable) { |
|
throw new \RuntimeException('This AppendStream is not seekable'); |
|
} elseif ($whence !== SEEK_SET) { |
|
throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); |
|
} |
|
|
|
$this->pos = $this->current = 0; |
|
|
|
// Rewind each stream |
|
foreach ($this->streams as $i => $stream) { |
|
try { |
|
$stream->rewind(); |
|
} catch (\Exception $e) { |
|
throw new \RuntimeException('Unable to seek stream ' |
|
.$i.' of the AppendStream', 0, $e); |
|
} |
|
} |
|
|
|
// Seek to the actual position by reading from each stream |
|
while ($this->pos < $offset && !$this->eof()) { |
|
$result = $this->read(min(8096, $offset - $this->pos)); |
|
if ($result === '') { |
|
break; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Reads from all of the appended streams until the length is met or EOF. |
|
*/ |
|
public function read($length): string |
|
{ |
|
$buffer = ''; |
|
$total = count($this->streams) - 1; |
|
$remaining = $length; |
|
$progressToNext = false; |
|
|
|
while ($remaining > 0) { |
|
// Progress to the next stream if needed. |
|
if ($progressToNext || $this->streams[$this->current]->eof()) { |
|
$progressToNext = false; |
|
if ($this->current === $total) { |
|
break; |
|
} |
|
++$this->current; |
|
} |
|
|
|
$result = $this->streams[$this->current]->read($remaining); |
|
|
|
if ($result === '') { |
|
$progressToNext = true; |
|
continue; |
|
} |
|
|
|
$buffer .= $result; |
|
$remaining = $length - strlen($buffer); |
|
} |
|
|
|
$this->pos += strlen($buffer); |
|
|
|
return $buffer; |
|
} |
|
|
|
public function isReadable(): bool |
|
{ |
|
return true; |
|
} |
|
|
|
public function isWritable(): bool |
|
{ |
|
return false; |
|
} |
|
|
|
public function isSeekable(): bool |
|
{ |
|
return $this->seekable; |
|
} |
|
|
|
public function write($string): int |
|
{ |
|
throw new \RuntimeException('Cannot write to an AppendStream'); |
|
} |
|
|
|
/** |
|
* {@inheritdoc} |
|
* |
|
* @return mixed |
|
*/ |
|
public function getMetadata($key = null) |
|
{ |
|
return $key ? null : []; |
|
} |
|
}
|
|
|