Skip to content

Commit

Permalink
Add caching of stream metadata to improve performance (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
devanych authored Jul 12, 2021
1 parent 0d4e0ed commit e71c95b
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 13 deletions.
64 changes: 51 additions & 13 deletions src/StreamTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use function ftell;
use function fwrite;
use function get_resource_type;
use function is_int;
use function is_resource;
use function is_string;
use function stream_get_contents;
Expand All @@ -38,6 +37,26 @@ trait StreamTrait
*/
private $resource;

/**
* @var int|null
*/
private ?int $size = null;

/**
* @var bool|null
*/
private ?bool $seekable = null;

/**
* @var bool|null
*/
private ?bool $writable = null;

/**
* @var bool|null
*/
private ?bool $readable = null;

/**
* Closes the stream and any underlying resources when the instance is destructed.
*/
Expand Down Expand Up @@ -90,7 +109,8 @@ public function close(): void
public function detach()
{
$resource = $this->resource;
$this->resource = null;
$this->resource = $this->size = null;
$this->seekable = $this->writable = $this->readable = false;
return $resource;
}

Expand All @@ -105,8 +125,12 @@ public function getSize(): ?int
return null;
}

if ($this->size !== null) {
return $this->size;
}

$stats = fstat($this->resource);
return isset($stats['size']) ? (int) $stats['size'] : null;
return $this->size = isset($stats['size']) ? (int) $stats['size'] : null;
}

/**
Expand All @@ -121,7 +145,7 @@ public function tell(): int
throw new RuntimeException('No resource available. Cannot tell position');
}

if (!is_int($result = ftell($this->resource))) {
if (($result = ftell($this->resource)) === false) {
throw new RuntimeException('Error occurred during tell operation');
}

Expand All @@ -145,7 +169,11 @@ public function eof(): bool
*/
public function isSeekable(): bool
{
return ($this->resource && $this->getMetadata('seekable'));
if ($this->seekable !== null) {
return $this->seekable;
}

return $this->seekable = ($this->resource && $this->getMetadata('seekable'));
}

/**
Expand Down Expand Up @@ -198,11 +226,15 @@ public function rewind(): void
*/
public function isWritable(): bool
{
if ($this->writable !== null) {
return $this->writable;
}

if (!is_string($mode = $this->getMetadata('mode'))) {
return false;
return $this->writable = false;
}

return (
return $this->writable = (
strpos($mode, 'w') !== false
|| strpos($mode, '+') !== false
|| strpos($mode, 'x') !== false
Expand All @@ -228,7 +260,9 @@ public function write($string): int
throw new RuntimeException('Stream is not writable.');
}

if (!is_int($result = fwrite($this->resource, $string))) {
$this->size = null;

if (($result = fwrite($this->resource, $string)) === false) {
throw new RuntimeException('Error writing to stream.');
}

Expand All @@ -243,11 +277,15 @@ public function write($string): int
*/
public function isReadable(): bool
{
if ($this->readable !== null) {
return $this->readable;
}

if (!is_string($mode = $this->getMetadata('mode'))) {
return false;
return $this->readable = false;
}

return (strpos($mode, 'r') !== false || strpos($mode, '+') !== false);
return $this->readable = (strpos($mode, 'r') !== false || strpos($mode, '+') !== false);
}

/**
Expand All @@ -270,7 +308,7 @@ public function read($length): string
throw new RuntimeException('Stream is not readable.');
}

if (!is_string($result = fread($this->resource, $length))) {
if (($result = fread($this->resource, $length)) === false) {
throw new RuntimeException('Error reading stream.');
}

Expand All @@ -290,7 +328,7 @@ public function getContents(): string
throw new RuntimeException('Stream is not readable.');
}

if (!is_string($result = stream_get_contents($this->resource))) {
if (($result = stream_get_contents($this->resource)) === false) {
throw new RuntimeException('Error reading stream.');
}

Expand Down Expand Up @@ -350,7 +388,7 @@ private function init($stream, string $mode): void

if (!is_resource($stream) || get_resource_type($stream) !== 'stream') {
throw new InvalidArgumentException(
'Invalid stream provided. It must be a string stream identifier or stream resource.'
'Invalid stream provided. It must be a string stream identifier or stream resource.',
);
}

Expand Down
29 changes: 29 additions & 0 deletions tests/StreamTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ public function testDetach(): void
$this->assertNull($stream->getSize());
}

public function testWriteAndGetSizeAndDetach(): void
{
$this->assertSame(0, $this->stream->getSize());

$this->stream->write('a');
$this->assertSame(1, $this->stream->getSize());

$this->stream->write('b');
$this->assertSame(2, $this->stream->getSize());

$this->stream->detach();
$this->assertNull($this->stream->getSize());
}

public function testIsReadableReturnTrue(): void
{
$stream = new Stream($this->tmpFile, 'r');
Expand Down Expand Up @@ -197,4 +211,19 @@ public function testConstructorThrowExceptionForInvalidResource(): void
$this->expectException(InvalidArgumentException::class);
new Stream(stream_context_create(['phar' => ['compress' => Phar::GZ]]));
}

public function testCacheMetadataForCoverage(): void
{
$this->assertSame(0, $this->stream->getSize());
$this->assertSame(0, $this->stream->getSize());

$this->assertTrue($this->stream->isReadable());
$this->assertTrue($this->stream->isReadable());

$this->assertTrue($this->stream->isSeekable());
$this->assertTrue($this->stream->isSeekable());

$this->assertTrue($this->stream->isWritable());
$this->assertTrue($this->stream->isWritable());
}
}

0 comments on commit e71c95b

Please sign in to comment.