{"action":"opened","issue":{"url":"https://api.github.com/repos/php-cds/php-cds/issues/33","repository_url":"https://api.github.com/repos/php-cds/php-cds","labels_url":"https://api.github.com/repos/php-cds/php-cds/issues/33/labels{/name}","comments_url":"https://api.github.com/repos/php-cds/php-cds/issues/33/comments","events_url":"https://api.github.com/repos/php-cds/php-cds/issues/33/events","html_url":"https://github.com/php-cds/php-cds/issues/33","id":155278461,"number":33,"title":"[RFC] CDS-io-stream","user":{"login":"TorbenKoehn","id":1403556,"avatar_url":"https://avatars.githubusercontent.com/u/1403556?v=3","gravatar_id":"","url":"https://api.github.com/users/TorbenKoehn","html_url":"https://github.com/TorbenKoehn","followers_url":"https://api.github.com/users/TorbenKoehn/followers","following_url":"https://api.github.com/users/TorbenKoehn/following{/other_user}","gists_url":"https://api.github.com/users/TorbenKoehn/gists{/gist_id}","starred_url":"https://api.github.com/users/TorbenKoehn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TorbenKoehn/subscriptions","organizations_url":"https://api.github.com/users/TorbenKoehn/orgs","repos_url":"https://api.github.com/users/TorbenKoehn/repos","events_url":"https://api.github.com/users/TorbenKoehn/events{/privacy}","received_events_url":"https://api.github.com/users/TorbenKoehn/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2016-05-17T14:46:00Z","updated_at":"2016-05-17T14:46:00Z","closed_at":null,"body":"\r\n# CDS-io-stream\r\n\r\nThis document describes a basic stream-implementation that takes PSR-7's approach a step deeper into core-mechanics of PHP-based web applications.\r\n\r\nIt is directly based on **CDS-io**, so you might read that one first.\r\n\r\n## Why?\r\n\r\nWith 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.\r\n\r\nRight 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`\r\n\r\n\r\nBasically the StreamInterface stays exactly as it is defined in PSR-7 since it is good as it is.\r\nWhat is changed is the namespace it belongs to.\r\n\r\nFurthermore, 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.\r\n\r\nAlso, the `SeekableInterface`, which specifies that an entity can be set to specific positions, e.g. array pointers or stream-positions.\r\n\r\nIn 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.\r\n\r\nThe actual `StreamInterface` implements them all.\r\n\r\n\r\nThe classes here use american-english naming (`Readable` instead of `Readeable`, `Writable` instead of `Writeable`) since they are shorter and easier to write and remember.\r\n\r\n\r\n## The `ReadableInterface`\r\n\r\nThis interfaces adds most basic checks to verify, that the passed object is _actually_ readable and contains content.\r\n\r\n```php\r\n<?php\r\n\r\nnamespace Cds\\Io\\Stream;\r\n\r\nuse Cds\\Io\\InputInterface;\r\n\r\ninterface ReadableInterface implements InputInterface\r\n{\r\n\r\n\t/**\r\n     * Returns true if the object is at the end of the contents.\r\n     *\r\n     * @return bool\r\n     */\r\n    public function eof();\r\n\r\n\t/**\r\n     * Returns whether or not the objects contents are readable.\r\n     *\r\n     * @return bool\r\n     */\r\n    public function isReadable();\r\n\r\n    /**\r\n     * Returns the remaining contents in a string\r\n     *\r\n     * @return string\r\n     * @throws \\RuntimeException if unable to read.\r\n     * @throws \\RuntimeException if error occurs while reading.\r\n     */\r\n    public function getContents();\r\n}\r\n```\r\n\r\nThese 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.\r\n\r\n\r\n## The `WritableInterface`\r\n\r\nThis interface adds most basic checks to verify, that the passed is _actually_ writable.\r\n\r\n```php\r\n<?php\r\n\r\nnamespace Cds\\Io\\Stream;\r\n\r\nuse Cds\\Io\\WritableInterface;\r\n\r\ninterface WritableInterface implements OutputInterface\r\n{\r\n\t\r\n    /**\r\n     * Returns whether or not the object-contents are writable.\r\n     *\r\n     * @return bool\r\n     */\r\n    public function isWritable();\r\n}\r\n```\r\n\r\nThis 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`.\r\n\r\nOne 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)\r\n\r\n\r\n## The `SeekableInterface`\r\n\r\nThis interface gives an object the ability to appear _seekable_, you can rewind it and move forward or backward to specific positions in your message.\r\n\r\n```php\r\n<?php\r\n\r\nnamespace Cds\\Io\\Stream;\r\n\r\ninterface SeekableInterface\r\n{\r\n\r\n    /**\r\n     * Get the size of the objects contents if known.\r\n     *\r\n     * @return int|null Returns the size in bytes if known, or null if unknown.\r\n     */\r\n    public function getSize();\r\n\t\r\n    /**\r\n     * Returns the current position of the object's read/write pointer\r\n     *\r\n     * @return int Position of the file pointer\r\n     * @throws \\RuntimeException on error.\r\n     */\r\n    public function tell();\r\n\r\n    /**\r\n     * Returns whether or not the object is seekable.\r\n     *\r\n     * @return bool\r\n     */\r\n    public function isSeekable();\r\n\r\n    /**\r\n     * Seek to a position in the object.\r\n     *\r\n     * @see http://www.php.net/manual/en/function.fseek.php\r\n     * @param int $offset Stream offset\r\n     * @param int $whence Specifies how the cursor position will be calculated\r\n     *     based on the seek offset. Valid values are identical to the built-in\r\n     *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to\r\n     *     offset bytes SEEK_CUR: Set position to current location plus offset\r\n     *     SEEK_END: Set position to end-of-stream plus offset.\r\n     * @throws \\RuntimeException on failure.\r\n     */\r\n    public function seek($offset, $whence = SEEK_SET);\r\n\r\n    /**\r\n     * Seek to the beginning of the stream.\r\n     *\r\n     * If the stream is not seekable, this method will raise an exception;\r\n     * otherwise, it will perform a seek(0).\r\n     *\r\n     * @see seek()\r\n     * @see http://www.php.net/manual/en/function.fseek.php\r\n     * @throws \\RuntimeException on failure.\r\n     */\r\n    public function rewind();\r\n}\r\n```\r\n\r\nThe `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;`)\r\n\r\n\r\n## The `StreamInterface`\r\n\r\nThis 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](http://www.php-fig.org/psr/psr-7/).\r\n\r\nNotice that the `StreamInterface` now implements all above sub-interfaces and with that is also part of `InputInterface` as well as `OutputInterface`.\r\n\r\n```php\r\n<?php\r\nnamespace Psr\\Http\\Message;\r\n\r\n/**\r\n * Describes a data stream.\r\n *\r\n * Typically, an instance will wrap a PHP stream; this interface provides\r\n * a wrapper around the most common operations, including serialization of\r\n * the entire stream to a string.\r\n */\r\ninterface StreamInterface implements ReadableInterface, WritableInterface, SeekableInterface\r\n{\r\n\r\n    /**\r\n     * Closes the stream and any underlying resources.\r\n     *\r\n     * @return void\r\n     */\r\n    public function close();\r\n\r\n    /**\r\n     * Separates any underlying resources from the stream.\r\n     *\r\n     * After the stream has been detached, the stream is in an unusable state.\r\n     *\r\n     * @return resource|null Underlying PHP stream, if any\r\n     */\r\n    public function detach();\r\n\r\n    /**\r\n     * Get stream metadata as an associative array or retrieve a specific key.\r\n     *\r\n     * The keys returned are identical to the keys returned from PHP's\r\n     * stream_get_meta_data() function.\r\n     *\r\n     * @see http://php.net/manual/en/function.stream-get-meta-data.php\r\n     * @param string $key Specific metadata to retrieve.\r\n     * @return array|mixed|null Returns an associative array if no key is\r\n     *     provided. Returns a specific key value if a key is provided and the\r\n     *     value is found, or null if the key is not found.\r\n     */\r\n    public function getMetadata($key = null);\r\n\r\n    /**\r\n     * Reads all data from the stream into a string, from the beginning to end.\r\n     *\r\n     * This method MUST attempt to seek to the beginning of the stream before\r\n     * reading data and read the stream until the end is reached.\r\n     *\r\n     * Warning: This could attempt to load a large amount of data into memory.\r\n     *\r\n     * This method MUST NOT raise an exception in order to conform with PHP's\r\n     * string casting operations.\r\n     *\r\n     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring\r\n     * @return string\r\n     */\r\n    public function __toString();\r\n}\r\n```\r\n\r\n\r\n## Example Implementations\r\n\r\n### A fully-featured `StreamInterface`-implementation \r\n\r\nYes, this is copied from a PSR-7 implementation, completely.\r\n\r\n\r\n```php\r\n<?php\r\n\r\nnamespace Example;\r\n\r\nuse InvalidArgumentException;\r\nuse Cds\\Io\\StreamInterface;\r\nuse RuntimeException;\r\n\r\n/**\r\n * Class Stream\r\n *\r\n * @package Example\r\n */\r\nclass Stream implements StreamInterface\r\n{\r\n\r\n    /**\r\n     * The default stream mode\r\n     */\r\n    const DEFAULT_MODE = 'rb+';\r\n\r\n    /**\r\n     * The current stream context (file resource)\r\n     *\r\n     * @var resource\r\n     */\r\n    private $context;\r\n\r\n    /**\r\n     * The mode this file has been opened with\r\n     *\r\n     * @var string\r\n     */\r\n    private $mode;\r\n\r\n    /**\r\n     * An array of meta data information\r\n     *\r\n     * @var array\r\n     */\r\n    private $metadata;\r\n\r\n\r\n    /**\r\n     * Stream constructor.\r\n     *\r\n     * @param object|string|resource $context\r\n     * @param null $mode\r\n     */\r\n    public function __construct($context, $mode = null)\r\n    {\r\n\r\n        $this->context = $context;\r\n        $this->mode = $mode ? $mode : self::DEFAULT_MODE;\r\n\r\n        //Allow support for Psr\\Http\\Message\\UriInterface and other __toString objects\r\n        if (is_object($context) && method_exists($context, '__toString'))\r\n            $this->context = (string)$this->context;\r\n\r\n        if (is_string($this->context))\r\n            $this->context = fopen($this->context, $this->mode);\r\n\r\n        if (!is_resource($this->context))\r\n            throw new InvalidArgumentException(\r\n                \"Argument 1 needs to be resource or path/URI\"\r\n            );\r\n\r\n        $this->metadata = stream_get_meta_data($this->context);\r\n    }\r\n\r\n    /**\r\n     *\r\n     */\r\n    public function __destruct()\r\n    {\r\n\r\n        $this->close();\r\n    }\r\n\r\n\r\n    /**\r\n     * @return resource\r\n     */\r\n    public function getContext()\r\n    {\r\n\r\n        return $this->context;\r\n    }\r\n\r\n    /**\r\n     * @return string\r\n     */\r\n    public function getMode()\r\n    {\r\n\r\n        return $this->mode;\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function close()\r\n    {\r\n\r\n        if (!$this->context) {\r\n\r\n            return;\r\n        }\r\n\r\n        $context = $this->detach();\r\n        fclose($context);\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function detach()\r\n    {\r\n\r\n        $context = $this->context;\r\n        $this->context = null;\r\n        $this->metadata = null;\r\n\r\n        return $context;\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function getSize()\r\n    {\r\n\r\n        if ($this->context === null)\r\n            return null;\r\n\r\n        $stat = fstat($this->context);\r\n\r\n        return $stat['size'];\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function tell()\r\n    {\r\n\r\n        $result = ftell($this->context);\r\n        return $result;\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function eof()\r\n    {\r\n\r\n        if (!$this->context)\r\n            return true;\r\n\r\n        return feof($this->context);\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function isSeekable()\r\n    {\r\n\r\n        if (!$this->context)\r\n            return false;\r\n\r\n        return $this->getMetadata('seekable') ? true : false;\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function seek($offset, $whence = \\SEEK_SET)\r\n    {\r\n\r\n        if (!$this->isSeekable())\r\n            throw new RuntimeException(\r\n                \"Stream is not seekable\"\r\n            );\r\n\r\n        fseek($this->context, $offset, $whence);\r\n\r\n        return true;\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function rewind()\r\n    {\r\n\r\n        return $this->seek(0);\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function isWritable()\r\n    {\r\n\r\n        if (!$this->context)\r\n            return false;\r\n\r\n        $mode = $this->getMetadata('mode');\r\n        return (strstr($mode, 'w') || strstr($mode, 'x') || strstr($mode, 'c') || strstr($mode, '+'));\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function write($string)\r\n    {\r\n\r\n        if (!$this->isWritable())\r\n            throw new RuntimeException(\r\n                \"Stream is not writable\"\r\n            );\r\n\r\n        return fwrite($this->context, $string);\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function isReadable()\r\n    {\r\n\r\n        if (!$this->context)\r\n            return false;\r\n\r\n        $mode = $this->getMetadata('mode');\r\n        return (strstr($mode, 'r') || strstr($mode, '+'));\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function read($length)\r\n    {\r\n\r\n        if (!$this->isReadable())\r\n            throw new RuntimeException(\r\n                \"Stream is not readable\"\r\n            );\r\n\r\n        return fread($this->context, $length);\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function getContents()\r\n    {\r\n\r\n        if (!$this->isReadable())\r\n            throw new RuntimeException(\r\n                \"Stream is not readable\"\r\n            );\r\n\r\n        return stream_get_contents($this->context);\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function getMetadata($key = null)\r\n    {\r\n\r\n        if ($key === null)\r\n            return $this->metadata;\r\n\r\n        if (!isset($this->metadata[$key]))\r\n            return null;\r\n\r\n        return $this->metadata[$key];\r\n    }\r\n\r\n    /**\r\n     * {@inheritdoc}\r\n     */\r\n    public function __toString()\r\n    {\r\n\r\n        if (!$this->isReadable())\r\n            return '';\r\n\r\n        if ($this->isSeekable())\r\n            $this->rewind();\r\n\r\n        return $this->getContents();\r\n    }\r\n\r\n    /**\r\n     *\r\n     */\r\n    private function __clone() {}\r\n}\r\n```\r\n\r\n#### Advantages\r\n\r\nMost advantages of a `StreamInterface` are covered in the PSR-7 spec right now.\r\n\r\nUtilizing all our CDS-io interfaces, a Logger implementing `OutputInterface` could be initialized like this:\r\n\r\n```php\r\n$logger = new Logger(new Stream('logs/some-log.log'));\r\n\r\n$logger->log('Hey, something happened!');\r\n```\r\n\r\nNow that we're not bound to HTTP anymore, these interfaces can expand fully:\r\n\r\n```php\r\n/* Imagining $client is an instance of TcpSocket implements StreamInterface */\r\n$client = $this->acceptClient();\r\n\r\n$logger = new Logger($client);\r\n\r\n$logger->log('Doing some action...'); //Log message is automatically pushed to the connected client\r\n```\r\n\r\n### A small parser based on the 'ReadableInterface` only\r\n\r\nImagine a parser (e.g. something like the [Tale Reader](https://raw.githubusercontent.com/Talesoft/tale-reader/master/Reader.php)) 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.\r\n\r\n`read()` could return single tokens in form of strings until there is no content anymore.\r\n\r\n```php\r\n//Example code missing right now, take the Tale Reader example above\r\n```\r\n\r\n\r\n### Redirecting Application-stack output\r\n\r\n```php\r\n//Usual way\r\n$app->run(new HttpInput(), new HttpOutput());\r\n\r\n//or\r\n$app->run(new CliInput(), new CliOutput());\r\n\r\n//fetch the response in a string\r\n$app->run(new WhateverInput(), $ms = new MemoryStream()); //where MemoryStream is just new Stream(fopen('php://memory'));\r\n\r\necho $ms;\r\n```\r\n\r\n\r\n~~~ Torben Köhn <tk@talesoft.io>"}}