CDS-io-stream
This document describes a basic stream-implementation that takes PSR-7's approach a step deeper into core-mechanics of PHP-based web applications.
It is directly based on CDS-io, so you might read that one first.
Why?
With PSR-7 we got a nice StreamInterface that did everything that streams can and should do. Sadly, Streams aren't only used in HTTP contexts, streams are also wildly used in console-only applications and in file- and network-operations of any kind.
Right now, if you want to create a good Stream-wrapper, you have to require the full PSR-7 spec, even if you only want one interface: The StreamInterface
Basically the StreamInterface stays exactly as it is defined in PSR-7 since it is good as it is.
What is changed is the namespace it belongs to.
Furthermore, PSR-io-stream defines three new interfaces. ReadableInterface and WritableInterface, which define a common approach to split streams into their respective parts, reading and writing. These interfaces are coupled to CDS-io's InputInterface and OutputInterface, so that e.g. File-Streams can be passed to any kind of output-mechanism utilizing the CDS-io interfaces.
Also, the SeekableInterface, which specifies that an entity can be set to specific positions, e.g. array pointers or stream-positions.
In the interfaces below, notice that no new methods are added. All of the methods below are methods of PSR-7 StreamInterface, split into smaller sub-groups.
The actual StreamInterface implements them all.
The classes here use american-english naming (Readable instead of Readeable, Writable instead of Writeable) since they are shorter and easier to write and remember.
The ReadableInterface
This interfaces adds most basic checks to verify, that the passed object is actually readable and contains content.
<?php
namespace Cds\Io\Stream;
use Cds\Io\InputInterface;
interface ReadableInterface implements InputInterface
{
/**
* Returns true if the object is at the end of the contents.
*
* @return bool
*/
public function eof();
/**
* Returns whether or not the objects contents are readable.
*
* @return bool
*/
public function isReadable();
/**
* Returns the remaining contents in a string
*
* @return string
* @throws \RuntimeException if unable to read.
* @throws \RuntimeException if error occurs while reading.
*/
public function getContents();
}
These are the important reading-methods of the PSR-7 StreamInterface. The actual read($length) method is provided through OutputInterface and is also compatible to current PSR-7. Examples further below.
The WritableInterface
This interface adds most basic checks to verify, that the passed is actually writable.
<?php
namespace Cds\Io\Stream;
use Cds\Io\WritableInterface;
interface WritableInterface implements OutputInterface
{
/**
* Returns whether or not the object-contents are writable.
*
* @return bool
*/
public function isWritable();
}
This one is a bit shorter, as we have less stuff to check (write doesn't depend on eof as an example). The actual write($message)-method is provided through the OutputInterface.
One might think that having ReadableInterface->isReadable and WritableInterface->isWritable is redundant, but you can have objects that implement both and then keep an internal information about if they're actually readable or writable (e.g. fopen-streams with a mode you don't know until you fetch its metadata)
The SeekableInterface
This interface gives an object the ability to appear seekable, you can rewind it and move forward or backward to specific positions in your message.
<?php
namespace Cds\Io\Stream;
interface SeekableInterface
{
/**
* Get the size of the objects contents if known.
*
* @return int|null Returns the size in bytes if known, or null if unknown.
*/
public function getSize();
/**
* Returns the current position of the object's read/write pointer
*
* @return int Position of the file pointer
* @throws \RuntimeException on error.
*/
public function tell();
/**
* Returns whether or not the object is seekable.
*
* @return bool
*/
public function isSeekable();
/**
* Seek to a position in the object.
*
* @see http://www.php.net/manual/en/function.fseek.php
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated
* based on the seek offset. Valid values are identical to the built-in
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
* offset bytes SEEK_CUR: Set position to current location plus offset
* SEEK_END: Set position to end-of-stream plus offset.
* @throws \RuntimeException on failure.
*/
public function seek($offset, $whence = SEEK_SET);
/**
* Seek to the beginning of the stream.
*
* If the stream is not seekable, this method will raise an exception;
* otherwise, it will perform a seek(0).
*
* @see seek()
* @see http://www.php.net/manual/en/function.fseek.php
* @throws \RuntimeException on failure.
*/
public function rewind();
}
The SEEK_*-constants are used for any kind of seek-operation in this case, regardless if we're using files or anything else. One might define those as constants on their own classes to give them another name (const DIRECTION_FORWARD = \SEEK_CUR;)
The StreamInterface
This one is basically the rest of the methods that are not defined in the above interfaces. For a larger explanation of PSR-7 visit the official page.
Notice that the StreamInterface now implements all above sub-interfaces and with that is also part of InputInterface as well as OutputInterface.
<?php
namespace Psr\Http\Message;
/**
* Describes a data stream.
*
* Typically, an instance will wrap a PHP stream; this interface provides
* a wrapper around the most common operations, including serialization of
* the entire stream to a string.
*/
interface StreamInterface implements ReadableInterface, WritableInterface, SeekableInterface
{
/**
* Closes the stream and any underlying resources.
*
* @return void
*/
public function close();
/**
* Separates any underlying resources from the stream.
*
* After the stream has been detached, the stream is in an unusable state.
*
* @return resource|null Underlying PHP stream, if any
*/
public function detach();
/**
* Get stream metadata as an associative array or retrieve a specific key.
*
* The keys returned are identical to the keys returned from PHP's
* stream_get_meta_data() function.
*
* @see http://php.net/manual/en/function.stream-get-meta-data.php
* @param string $key Specific metadata to retrieve.
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata($key = null);
/**
* Reads all data from the stream into a string, from the beginning to end.
*
* This method MUST attempt to seek to the beginning of the stream before
* reading data and read the stream until the end is reached.
*
* Warning: This could attempt to load a large amount of data into memory.
*
* This method MUST NOT raise an exception in order to conform with PHP's
* string casting operations.
*
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
* @return string
*/
public function __toString();
}
Example Implementations
A fully-featured StreamInterface-implementation
Yes, this is copied from a PSR-7 implementation, completely.
<?php
namespace Example;
use InvalidArgumentException;
use Cds\Io\StreamInterface;
use RuntimeException;
/**
* Class Stream
*
* @package Example
*/
class Stream implements StreamInterface
{
/**
* The default stream mode
*/
const DEFAULT_MODE = 'rb+';
/**
* The current stream context (file resource)
*
* @var resource
*/
private $context;
/**
* The mode this file has been opened with
*
* @var string
*/
private $mode;
/**
* An array of meta data information
*
* @var array
*/
private $metadata;
/**
* Stream constructor.
*
* @param object|string|resource $context
* @param null $mode
*/
public function __construct($context, $mode = null)
{
$this->context = $context;
$this->mode = $mode ? $mode : self::DEFAULT_MODE;
//Allow support for Psr\Http\Message\UriInterface and other __toString objects
if (is_object($context) && method_exists($context, '__toString'))
$this->context = (string)$this->context;
if (is_string($this->context))
$this->context = fopen($this->context, $this->mode);
if (!is_resource($this->context))
throw new InvalidArgumentException(
"Argument 1 needs to be resource or path/URI"
);
$this->metadata = stream_get_meta_data($this->context);
}
/**
*
*/
public function __destruct()
{
$this->close();
}
/**
* {@inheritdoc}
*/
public function close()
{
if (!$this->context) {
return;
}
$context = $this->detach();
fclose($context);
}
/**
* {@inheritdoc}
*/
public function detach()
{
$context = $this->context;
$this->context = null;
$this->metadata = null;
return $context;
}
/**
* {@inheritdoc}
*/
public function getSize()
{
if ($this->context === null)
return null;
$stat = fstat($this->context);
return $stat['size'];
}
/**
* {@inheritdoc}
*/
public function tell()
{
$result = ftell($this->context);
return $result;
}
/**
* {@inheritdoc}
*/
public function eof()
{
if (!$this->context)
return true;
return feof($this->context);
}
/**
* {@inheritdoc}
*/
public function isSeekable()
{
if (!$this->context)
return false;
return $this->getMetadata('seekable') ? true : false;
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = \SEEK_SET)
{
if (!$this->isSeekable())
throw new RuntimeException(
"Stream is not seekable"
);
fseek($this->context, $offset, $whence);
return true;
}
/**
* {@inheritdoc}
*/
public function rewind()
{
return $this->seek(0);
}
/**
* {@inheritdoc}
*/
public function isWritable()
{
if (!$this->context)
return false;
$mode = $this->getMetadata('mode');
return (strstr($mode, 'w') || strstr($mode, 'x') || strstr($mode, 'c') || strstr($mode, '+'));
}
/**
* {@inheritdoc}
*/
public function write($string)
{
if (!$this->isWritable())
throw new RuntimeException(
"Stream is not writable"
);
return fwrite($this->context, $string);
}
/**
* {@inheritdoc}
*/
public function isReadable()
{
if (!$this->context)
return false;
$mode = $this->getMetadata('mode');
return (strstr($mode, 'r') || strstr($mode, '+'));
}
/**
* {@inheritdoc}
*/
public function read($length)
{
if (!$this->isReadable())
throw new RuntimeException(
"Stream is not readable"
);
return fread($this->context, $length);
}
/**
* {@inheritdoc}
*/
public function getContents()
{
if (!$this->isReadable())
throw new RuntimeException(
"Stream is not readable"
);
return stream_get_contents($this->context);
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = null)
{
if ($key === null)
return $this->metadata;
if (!isset($this->metadata[$key]))
return null;
return $this->metadata[$key];
}
/**
* {@inheritdoc}
*/
public function __toString()
{
if (!$this->isReadable())
return '';
if ($this->isSeekable())
$this->rewind();
return $this->getContents();
}
/**
*
*/
private function __clone() {}
}
Advantages
Most advantages of a StreamInterface are covered in the PSR-7 spec right now.
Utilizing all our CDS-io interfaces, a Logger implementing OutputInterface could be initialized like this:
$logger = new Logger(new Stream('logs/some-log.log'));
$logger->log('Hey, something happened!');
Now that we're not bound to HTTP anymore, these interfaces can expand fully:
/* Imagining $client is an instance of TcpSocket implements StreamInterface */
$client = $this->acceptClient();
$logger = new Logger($client);
$logger->log('Doing some action...'); //Log message is automatically pushed to the connected client
A small parser based on the 'ReadableInterface` only
Imagine a parser (e.g. something like the Tale Reader) that only has a reading role. It can implement ReadableInterface, provide read() and can from then on be used as input all over your application.
read() could return single tokens in form of strings until there is no content anymore.
//Example code missing right now, take the Tale Reader example above
Redirecting Application-stack output
//Usual way
$app->run(new HttpInput(), new HttpOutput());
//or
$app->run(new CliInput(), new CliOutput());
//fetch the response in a string
$app->run(new WhateverInput(), $ms = new MemoryStream()); //where MemoryStream is just new Stream(fopen('php://memory'));
echo $ms;
CDS-io-stream
This document describes a basic stream-implementation that takes PSR-7's approach a step deeper into core-mechanics of PHP-based web applications.
It is directly based on CDS-io, so you might read that one first.
Why?
With PSR-7 we got a nice
StreamInterfacethat did everything that streams can and should do. Sadly, Streams aren't only used in HTTP contexts, streams are also wildly used in console-only applications and in file- and network-operations of any kind.Right now, if you want to create a good Stream-wrapper, you have to require the full PSR-7 spec, even if you only want one interface: The
StreamInterfaceBasically the StreamInterface stays exactly as it is defined in PSR-7 since it is good as it is.
What is changed is the namespace it belongs to.
Furthermore, PSR-io-stream defines three new interfaces.
ReadableInterfaceandWritableInterface, which define a common approach to split streams into their respective parts,readingandwriting. These interfaces are coupled toCDS-io'sInputInterfaceandOutputInterface, so that e.g. File-Streams can be passed to any kind of output-mechanism utilizing the CDS-io interfaces.Also, the
SeekableInterface, which specifies that an entity can be set to specific positions, e.g. array pointers or stream-positions.In the interfaces below, notice that no new methods are added. All of the methods below are methods of PSR-7
StreamInterface, split into smaller sub-groups.The actual
StreamInterfaceimplements them all.The classes here use american-english naming (
Readableinstead ofReadeable,Writableinstead ofWriteable) since they are shorter and easier to write and remember.The
ReadableInterfaceThis interfaces adds most basic checks to verify, that the passed object is actually readable and contains content.
These are the important
reading-methods of the PSR-7StreamInterface. The actualread($length)method is provided throughOutputInterfaceand is also compatible to current PSR-7. Examples further below.The
WritableInterfaceThis interface adds most basic checks to verify, that the passed is actually writable.
This one is a bit shorter, as we have less stuff to check (
writedoesn't depend oneofas an example). The actualwrite($message)-method is provided through theOutputInterface.One might think that having
ReadableInterface->isReadableandWritableInterface->isWritableis redundant, but you can have objects that implement both and then keep an internal information about if they're actually readable or writable (e.g.fopen-streams with a mode you don't know until you fetch its metadata)The
SeekableInterfaceThis interface gives an object the ability to appear seekable, you can rewind it and move forward or backward to specific positions in your message.
The
SEEK_*-constants are used for any kind of seek-operation in this case, regardless if we're using files or anything else. One might define those as constants on their own classes to give them another name (const DIRECTION_FORWARD = \SEEK_CUR;)The
StreamInterfaceThis one is basically the rest of the methods that are not defined in the above interfaces. For a larger explanation of PSR-7 visit the official page.
Notice that the
StreamInterfacenow implements all above sub-interfaces and with that is also part ofInputInterfaceas well asOutputInterface.Example Implementations
A fully-featured
StreamInterface-implementationYes, this is copied from a PSR-7 implementation, completely.
Advantages
Most advantages of a
StreamInterfaceare covered in the PSR-7 spec right now.Utilizing all our CDS-io interfaces, a Logger implementing
OutputInterfacecould be initialized like this:Now that we're not bound to HTTP anymore, these interfaces can expand fully:
A small parser based on the 'ReadableInterface` only
Imagine a parser (e.g. something like the Tale Reader) that only has a reading role. It can implement
ReadableInterface, provideread()and can from then on be used as input all over your application.read()could return single tokens in form of strings until there is no content anymore.//Example code missing right now, take the Tale Reader example aboveRedirecting Application-stack output