Files
predis/src/Connection/Resource/Stream.php
T
Vladyslav Vildanov 980326d5d8 Added retry support (#1616)
* Initial work on retries

* Added retry class and test coverage

* Added support for standalone and cluster

* Make TimeoutException instance of CommunicationException

* Added pipeline, trasnaction, replication support

* Fixed broken test

* Marked test as relay-incompatible

* Marked test as relay-incompatible

* Fixed analysis errors, added missing tests

* Codestyle fixes

* Fixed test

* Update README.md

* Update README.md

* Update README.md

* Updated README.md

* Refactor retry on read and write

* Added check for timeout value

* Updated README.md

* Fixed README.md

* Codestyle changes

* Added missing coverage

* Added missing test coverage

* Removed comments

* Added retry support for Relay connection (#1620)

* Added integration test case with mocked retry

* Changed client initialisation in tests

* Marked test as relay-incompatible

---------

Co-authored-by: Pavlo Yatsukhnenko <yatsukhnenko@users.noreply.github.com>
2025-12-23 10:40:46 +02:00

336 lines
7.5 KiB
PHP

<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Connection\Resource;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
class Stream implements StreamInterface
{
/**
* @see https://www.php.net/manual/en/function.fopen.php
* @see https://www.php.net/manual/en/function.gzopen.php
*/
private const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/';
private const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/';
/**
* @var resource
*/
private $stream;
/**
* @var bool
*/
private $seekable;
/**
* @var bool
*/
private $readable;
/**
* @var bool
*/
private $writable;
/**
* @param resource $stream
* @throws InvalidArgumentException if stream is not a valid resource.
*/
public function __construct($stream)
{
if (!is_resource($stream)) {
throw new InvalidArgumentException('Given stream is not a valid resource');
}
$this->stream = $stream;
$metadata = stream_get_meta_data($this->stream);
$this->seekable = $metadata['seekable'];
$this->readable = (bool) preg_match(self::READABLE_MODES, $metadata['mode']);
$this->writable = (bool) preg_match(self::WRITABLE_MODES, $metadata['mode']);
}
/**
* {@inheritDoc}
*/
public function __toString(): string
{
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
}
/**
* {@inheritDoc}
*/
public function close(): void
{
if (isset($this->stream)) {
fclose($this->stream);
}
$this->detach();
}
/**
* {@inheritDoc}
*/
public function detach()
{
if (!isset($this->stream)) {
return null;
}
$result = $this->stream;
unset($this->stream);
$this->readable = $this->writable = $this->seekable = false;
return $result;
}
/**
* {@inheritDoc}
*/
public function getSize(): ?int
{
if (!isset($this->stream)) {
return null;
}
$stats = fstat($this->stream);
if (is_array($stats) && isset($stats['size'])) {
return $stats['size'];
}
return null;
}
/**
* {@inheritDoc}
*/
public function tell(): int
{
if (!isset($this->stream)) {
throw new RuntimeException('Stream is detached');
}
$result = ftell($this->stream);
if ($result === false) {
throw new RuntimeException('Unable to determine stream position');
}
return $result;
}
/**
* {@inheritDoc}
*/
public function eof(): bool
{
if (!isset($this->stream)) {
throw new RuntimeException('Stream is detached');
}
return feof($this->stream);
}
/**
* {@inheritDoc}
*/
public function isSeekable(): bool
{
return $this->seekable;
}
/**
* {@inheritDoc}
*/
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!isset($this->stream)) {
throw new RuntimeException('Stream is detached');
}
if (!$this->isSeekable()) {
throw new RuntimeException('Stream is not seekable');
}
if (fseek($this->stream, $offset, $whence) === -1) {
throw new RuntimeException("Unable to seek stream from offset {$offset} to whence {$whence}");
}
}
/**
* {@inheritDoc}
*/
public function rewind(): void
{
$this->seek(0);
}
/**
* {@inheritDoc}
*/
public function isWritable(): bool
{
return $this->writable;
}
/**
* {@inheritDoc}
* @throws RuntimeException
*/
public function write(string $string): int
{
if (!isset($this->stream)) {
throw new RuntimeException('Stream is detached');
}
if (!$this->isWritable()) {
throw new RuntimeException('Cannot write to a non-writable stream');
}
$result = fwrite($this->stream, $string);
if ($result === false || $result === 0) {
$metadata = $this->getMetadata();
if ($this->eof()) {
throw new RuntimeException('Connection closed by peer during write', 1);
}
if (!is_resource($this->stream)) {
throw new RuntimeException(
'Stream resource is no longer valid',
1
);
}
if (array_key_exists('timed_out', $metadata) && $metadata['timed_out']) {
throw new RuntimeException(
'Stream has been timed out',
2
);
}
throw new RuntimeException('Unable to write to stream', 1);
}
return $result;
}
/**
* {@inheritDoc}
*/
public function isReadable(): bool
{
return $this->readable;
}
/**
* {@inheritDoc}
* @param int $length If length = -1, reads a stream line by line (e.g fgets())
* @throws RuntimeException
*/
public function read(int $length): string
{
if (!isset($this->stream)) {
throw new RuntimeException('Stream is detached');
}
if (!$this->isReadable()) {
throw new RuntimeException('Cannot read from non-readable stream');
}
if ($length < -1) {
throw new RuntimeException('Length parameter cannot be negative');
}
if (0 === $length) {
return '';
}
if ($length === -1) {
$string = fgets($this->stream);
} else {
$string = fread($this->stream, $length);
}
if (false === $string) {
$metadata = $this->getMetadata();
if ($this->eof()) {
throw new RuntimeException('Connection closed by peer during read', 1);
}
if (!is_resource($this->stream)) {
throw new RuntimeException(
'Stream resource is no longer valid',
1
);
}
if (array_key_exists('timed_out', $metadata) && $metadata['timed_out']) {
throw new RuntimeException(
'Stream has been timed out',
2
);
}
throw new RuntimeException('Unable to read from stream', 1);
}
return $string;
}
/**
* {@inheritDoc}
*/
public function getContents(): string
{
if (!isset($this->stream)) {
throw new RuntimeException('Stream is detached');
}
if (!$this->isReadable()) {
throw new RuntimeException('Cannot read from non-readable stream');
}
return stream_get_contents($this->stream);
}
/**
* {@inheritDoc}
* @return mixed
*/
public function getMetadata(?string $key = null)
{
if (!isset($this->stream)) {
return null;
}
if (!$key) {
return stream_get_meta_data($this->stream);
}
$metadata = stream_get_meta_data($this->stream);
return $metadata[$key] ?? null;
}
}