Skip to content

Commit

Permalink
[LazyStreamReader] Add LazyStreamReader
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandre-daubois committed Jun 20, 2023
1 parent 9bd3039 commit 06f1e0b
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 64 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,22 @@ class GoogleCloudStorageLazyStreamFactory
}
}
```

### Reading lazily a stream with `LazyStreamReader`

Files are already read lazily by default: when you call `fread()`, you only fetch the number of bytes you asked for, not more.
`LazyStreamReader` does the same thing, but it also allows you to keep the stream open or not between reading operations.

For example, you may want to read a file 1MB by 1MB, and do some processing that may take some time each time. By setting the `autoClose` option to `true` when creating a new `LazyStreamReader` object, you ask to close the stream after each reading operation and open it again when the next reading operation is triggered. You'll be resumed at the same position you were in the stream before closing it.

```php
// The stream is not opened yet, in case you never need it
$stream = new \LazyStream\LazyStreamReader('https://user:pass@example.com/my-file.png', chunkSize: 1024, autoClose: true, binary: true);

// Use the stream directly in the loop
foreach ($stream as $str) {
// With auto-closing, the stream is already closed here. You can
// do any long operation, and the stream will be opened again when
// you get in the next loop iteration
}
```
70 changes: 70 additions & 0 deletions src/AbstractLazyStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace LazyStream;

use LazyStream\Exception\LazyStreamOpenException;

abstract class AbstractLazyStream
{
/**
* @param resource|null $handle
*/
protected $handle;

protected ?array $metadata = null;

public function __construct(
protected string $uri,
protected string $openingMode,
) {
}

public function __destruct()
{
$this->closeStream();
}

protected function openStream(): void
{
if (!\is_resource($this->handle)) {
$this->handle = @\fopen($this->uri, $this->openingMode);

if ($this->handle === false) {
throw new LazyStreamOpenException($this->uri, $this->openingMode);
}

$this->metadata = \stream_get_meta_data($this->handle);
}
}

protected function closeStream(): void
{
if (\is_resource($this->handle)) {
\fclose($this->handle);
}

$this->handle = null;
}

/**
* @return resource|null
*/
public function getStreamHandle()
{
return $this->handle;
}

/**
* @return array Stream meta-data array indexed by keys given in https://www.php.net/manual/en/function.stream-get-meta-data.php.
*/
public function getMetadata(): array
{
if ($this->metadata === null) {
// If metadata is null, then we never opened the stream yet
$this->openStream();
$this->closeStream();
}

return $this->metadata;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace LazyStream\Exception;

class LazyStreamWriterOpenException extends AbstractLazyStreamWriterException
class LazyStreamOpenException extends AbstractLazyStreamWriterException
{
public function __construct(string $uri, string $mode)
{
Expand Down
71 changes: 71 additions & 0 deletions src/LazyStreamReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/*
* (c) Alexandre Daubois <alex.daubois@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace LazyStream;

class LazyStreamReader extends AbstractLazyStream implements LazyStreamReaderInterface
{
private int $position = 0;

/**
* @param string $uri A valid stream URI.
* @param bool $autoClose Whether the stream should be closed between reading operations.
*/
public function __construct(
string $uri,
private int $chunkSize,
private bool $autoClose = true,
private bool $binary = false,
) {
parent::__construct($uri, $this->binary ? 'rb' : 'r');
}

public function getStreamPosition(): int
{
return $this->position;
}

public function isAutoClose(): bool
{
return $this->autoClose;
}

public function setAutoClose(bool $autoClose): void
{
$this->autoClose = $autoClose;
}

public function getIterator(): \Generator
{
yield from $this->read();
}

private function read(): \Generator
{
$this->openStream();

while (($data = \fread($this->handle, $this->chunkSize)) !== false && \strlen($data) !== 0) {
$this->position += $this->chunkSize;

if ($this->autoClose) {
$this->closeStream();
yield $data;

$this->openStream();
\fseek($this->handle, $this->position);

continue;
}

yield $data;
}

$this->closeStream();
}
}
14 changes: 14 additions & 0 deletions src/LazyStreamReaderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/*
* (c) Alexandre Daubois <alex.daubois@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace LazyStream;

interface LazyStreamReaderInterface extends \IteratorAggregate
{
}
63 changes: 8 additions & 55 deletions src/LazyStreamWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,29 @@

namespace LazyStream;

use LazyStream\Exception\LazyStreamWriterOpenException;
use LazyStream\Exception\LazyStreamOpenException;
use LazyStream\Exception\LazyStreamWriterTriggerException;

/**
* A class to write to a stream lazily. The stream is only opened when
* the `trigger()` method is called. Data to write are provided by a
* generator, allowing data to be generated on the fly if possible.
*/
class LazyStreamWriter implements LazyStreamWriterInterface
class LazyStreamWriter extends AbstractLazyStream implements LazyStreamWriterInterface
{
/**
* @param resource|null $handle
*/
private $handle;

private ?array $metadata = null;

/**
* @param string $uri A valid stream URI.
* @param \Iterator $dataProvider The data provider that will be written to the stream.
* @param string $openingMode A valid writing mode listed in https://www.php.net/manual/fr/function.fopen.php.
* @param bool $autoClose Whether the stream should be closed once the `trigger` method is done.
*/
public function __construct(
private string $uri,
string $uri,
private \Iterator $dataProvider,
private string $openingMode = 'w',
string $openingMode = 'w',
private bool $autoClose = true,
) {
}

public function __destruct()
{
$this->closeStream();
parent::__construct($uri, $openingMode);
}

public function trigger(): void
Expand All @@ -66,14 +55,6 @@ public function trigger(): void
}
}

/**
* @return resource|null
*/
public function getStreamHandle()
{
return $this->handle;
}

public function unlink(): bool
{
if (!\is_resource($this->handle)) {
Expand All @@ -85,20 +66,6 @@ public function unlink(): bool
return \unlink($this->uri);
}

/**
* @return array Stream meta-data array indexed by keys given in https://www.php.net/manual/en/function.stream-get-meta-data.php.
*/
public function getMetadata(): array
{
if ($this->metadata === null) {
// If metadata is null, then we never opened the stream yet
$this->openStream();
$this->closeStream();
}

return $this->metadata;
}

public function equals(self $other): bool
{
return $this->dataProvider === $other->dataProvider && $this->uri === $other->uri;
Expand All @@ -114,26 +81,12 @@ public function setAutoClose(bool $autoClose): void
$this->autoClose = $autoClose;
}

private function openStream(): void
{
if (!\is_resource($this->handle)) {
$this->handle = @\fopen($this->uri, $this->openingMode);

if ($this->handle === false) {
throw new LazyStreamWriterOpenException($this->uri, $this->openingMode);
}
}

$this->metadata = \stream_get_meta_data($this->handle);
}

private function closeStream(): void
protected function closeStream(): void
{
if (\is_resource($this->handle)) {
\fflush($this->handle);
\fclose($this->handle);

$this->handle = null;
}

parent::closeStream();
}
}
5 changes: 0 additions & 5 deletions src/LazyStreamWriterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,4 @@
interface LazyStreamWriterInterface
{
public function trigger(): void;

/**
* @return bool True if the stream has been unlinked, false otherwise.
*/
public function unlink(): bool;
}
Loading

0 comments on commit 06f1e0b

Please sign in to comment.