├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── public ├── copy-stream.php ├── cuervo.jpg ├── generator.php ├── iterator.php └── php-output.php └── src ├── CallbackStream.php └── IteratorStream.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The BSD 2-Clause License 2 | ======================== 3 | 4 | Copyright (c) 2015, Matthew Weier O'Phinney 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSR-7 Stream Examples 2 | 3 | [PSR-7](https://github.com/php-fig/fig-standards/blob/master/proposed/http-message.md) 4 | uses `Psr\Http\Message\StreamableInterface` to represent content for a 5 | message. This has a variety of benefits, as outlined in the specification. 6 | However, for some, the model poses some conceptual challenges: 7 | 8 | - What if I want to emit a file on the server, like I might with `fpassthru()` 9 | or `stream_copy_to_stream($fileHandle, fopen('php://output'))`? 10 | - What if I want to use a callback to produce my output? 11 | - What if I want to use output buffering and/or `echo`/`printf`/etc. 12 | directly? 13 | - What if I want to iterate over a data structure and iteratively output content? 14 | 15 | These patterns are all possible with creative implementations of 16 | `StreamableInterface`. 17 | 18 | - The file [copy-stream.php](public/copy-stream.php) demonstrates how you would 19 | emit a file. 20 | - [CallbackStream](src/CallbackStream.php) and [php-output.php](public/php-output.php) 21 | demonstrate using a callback to generate and return content. 22 | - [CallbackStream](src/CallbackStream.php) and [php-output.php](public/php-output.php) 23 | also demonstrate how you might use a callback to allow direct output from your 24 | code, without first aggregating it. 25 | - [IteratorStream](src/IteratorStream.php) and the files [iterator.php](public/iterator.php) 26 | and [generator.php](public/generator.php) demonstrate using iterators and 27 | generators for creating output. 28 | 29 | In each, the assumption is that the application will short-circuit on receiving 30 | a response as a return value. Most modern frameworks do this already, and it's a 31 | guiding principle of middleware. 32 | 33 | The code in this repository uses [phly/http](https://github.com/phly/http) as 34 | the PSR-7 implementation; any PSR-7 implementation should behave similarly. 35 | 36 | ## Analyzing the code 37 | 38 | ### Emitting a file 39 | 40 | For those who are accustomed to using `readfile()`, `fpassthru()` or copying a 41 | stream into `php://output` via `stream_copy_to_stream()`, PSR-7 will look and 42 | feel different. Typically, you will not use the aforementioned techniques when 43 | building an application to work with PSR-7, as they bypass the HTTP message 44 | entirely, and delegate it to PHP itself. 45 | 46 | The problem with using these built-in PHP methods is that you cannot test your 47 | code as easily, as it now has side-effects. One major reason to adopt PSR-7 is 48 | if you want to be able to test your web-facing code without worrying about side 49 | effects. Adopting frameworks or application architectures that work with HTTP 50 | messages lets you pass in a request, and make assertions on the response. 51 | 52 | In the case of emitting a file, this means that you will: 53 | 54 | - Create a `Stream` instance, passing it the file location. 55 | - Provide appropriate headers to the response. 56 | - Provide your stream instance to the response. 57 | - Return your response. 58 | 59 | Which looks like what we have in [copy-stream.php](public/copy-stream.php): 60 | 61 | ```php 62 | $image = __DIR__ . '/cuervo.jpg'; 63 | 64 | return (new Response()) 65 | ->withHeader('Content-Type', 'image/jpeg') 66 | ->withHeader('Content-Length', (string) filesize($image)) 67 | ->withBody(new Stream($image)); 68 | return $response; 69 | ``` 70 | 71 | The assumption is that returning a response will bubble out of your application; 72 | most modern frameworks do this already, as does middleware. As such, you will 73 | typically have minimal additional overhead from the time you create the response 74 | until it's streaming your file back to the client. 75 | 76 | ### Direct output 77 | 78 | Just like the above example, for those accustomed to directly calling `echo`, or 79 | sending data directly to the `php://output` stream, PSR-7 will feel strange. 80 | However, as noted before as well, these are actions that have side effects that 81 | act as a barrier to testing and other quality assurance activities. 82 | 83 | There _is_ a way to accomodate these, however, with a little trickery: wrapping any 84 | output-emitting code in a callback, and passing this to a callback-enabled 85 | stream implementation. The [CallbackStream](src/CallbackStream.php) implementation 86 | in this repo is one potential way to accomplish it. 87 | 88 | As an example, from [php-output.php](public/php-output.php): 89 | 90 | ```php 91 | $output = new CallbackStream(function () use ($request) { 92 | printf("The requested URI was: %s
\n", $request->getUri()); 93 | return ''; 94 | }); 95 | return (new Response()) 96 | ->withHeader('Content-Type', 'text/html') 97 | ->withBody($output); 98 | ``` 99 | 100 | This has a few benefits over directly emitting output from within your 101 | web-facing code: 102 | 103 | - We can ensure our headers are sent before emitting output. 104 | - We can set a non-200 status code if desired. 105 | - We can test the various aspects of the response separately from the output. 106 | - We still get the benefits of the output buffer. 107 | 108 | As noted previously, returning a response will generally bubble out of the 109 | application immediately, making this a very viable option for emitting output 110 | directly. 111 | 112 | (Note: the callback could also aggregate content and return it as a string if 113 | desired; I wanted to demonstrate specifically how it can be used to work with 114 | output buffering.) 115 | 116 | ### Iterators and generators 117 | 118 | Ruby's Rack specification uses an iterable body for response messages, instead 119 | of a stream. In some situations, such as returning large data sets, this could 120 | be tremendously useful. Can PSR-7 accomplish it? 121 | 122 | The answer is, succinctly, yes. The [IteratorStream](src/IteratorStream.php) 123 | implementation in this repo is a rough prototype showing how it may work; usage 124 | would be as in [iterator.php](public/iterator.php): 125 | 126 | ```php 127 | $output = new IteratorStream(new ArrayObject([ 128 | "Foo!
\n", 129 | "Bar!
\n", 130 | "Baz!
\n", 131 | ])); 132 | return (new Response()) 133 | ->withHeader('Content-Type', 'text/html') 134 | ->withBody($output); 135 | ``` 136 | 137 | or, with a generator per [generator.php](public/generator.php): 138 | 139 | ```php 140 | $generator = function ($count) { 141 | while ($count) { 142 | --$count; 143 | yield(uniqid() . "
\n"); 144 | } 145 | }; 146 | 147 | $output = new IteratorStream($generator(10)); 148 | 149 | return (new Response()) 150 | ->withHeader('Content-Type', 'text/html') 151 | ->withBody($output); 152 | ``` 153 | 154 | This is a nice approach, as you can iteratively generate the data returned; if 155 | you are worried about data overhead from aggregating the data before returning 156 | it, you can always use `print` or `echo` statements instead of aggregation 157 | within the iterator stream implementation. 158 | 159 | ## Testing it out 160 | 161 | You can test it out for yourself: 162 | 163 | - Clone this repo 164 | - Run `composer install` 165 | - Run `cd public ; php -S 0:8080` in the directory, and then browse to 166 | `http://localhost:8080/{filename}`, where `{filename}` is one of: 167 | - `copy-stream.php` 168 | - `generator.php` 169 | - `iterator.php` 170 | - `php-output.php` 171 | 172 | ## Improvements 173 | 174 | This was a quick repository built to demonstrate that PSR-7 fulfills these 175 | scenarios; however, they are far from comprehensive. Some ideas: 176 | 177 | - `IteratorStream` could and likely should allow providing a separator, and 178 | potentially preamble/postfix for wrapping content. 179 | - `IteratorStream` and `CallbackStream` could be optimized to emit output 180 | directly instead of aggregating + returning, if you are worried about large 181 | data sets. 182 | - `CallbackStream` could cache the contents to allow multiple reads (though 183 | using `detach()` would allow it already). 184 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "psr/http-message": "0.9.*", 4 | "phly/http": "0.11.*" 5 | }, 6 | "autoload": { 7 | "psr-4": { 8 | "Psr7Examples\\": "src/" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "60fbf2a503aa6b4e02c4207ac8e66d2a", 8 | "packages": [ 9 | { 10 | "name": "phly/http", 11 | "version": "0.11.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/phly/http.git", 15 | "reference": "196f9d0dc11723bbabace8451218c420f90e330f" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/phly/http/zipball/196f9d0dc11723bbabace8451218c420f90e330f", 20 | "reference": "196f9d0dc11723bbabace8451218c420f90e330f", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.4.8", 25 | "psr/http-message": "^0.9" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "3.7.*", 29 | "squizlabs/php_codesniffer": "1.5.*" 30 | }, 31 | "type": "library", 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "1.0-dev", 35 | "dev-develop": "1.1-dev" 36 | } 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Phly\\Http\\": "src/" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "BSD-2-Clause" 46 | ], 47 | "authors": [ 48 | { 49 | "name": "Matthew Weier O'Phinney", 50 | "email": "matthew@weierophinney.net", 51 | "homepage": "http://mwop.net" 52 | } 53 | ], 54 | "description": "PSR HTTP Message implementations", 55 | "homepage": "https://github.com/phly/http", 56 | "keywords": [ 57 | "http" 58 | ], 59 | "time": "2015-02-17 14:28:44" 60 | }, 61 | { 62 | "name": "psr/http-message", 63 | "version": "0.9.2", 64 | "source": { 65 | "type": "git", 66 | "url": "https://github.com/php-fig/http-message.git", 67 | "reference": "7c361ae6b0dcd84d8fc4f89636ee0cc9fcb02035" 68 | }, 69 | "dist": { 70 | "type": "zip", 71 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/7c361ae6b0dcd84d8fc4f89636ee0cc9fcb02035", 72 | "reference": "7c361ae6b0dcd84d8fc4f89636ee0cc9fcb02035", 73 | "shasum": "" 74 | }, 75 | "type": "library", 76 | "extra": { 77 | "branch-alias": { 78 | "dev-master": "1.0.x-dev" 79 | } 80 | }, 81 | "autoload": { 82 | "psr-4": { 83 | "Psr\\Http\\Message\\": "src" 84 | } 85 | }, 86 | "notification-url": "https://packagist.org/downloads/", 87 | "license": [ 88 | "MIT" 89 | ], 90 | "authors": [ 91 | { 92 | "name": "PHP-FIG", 93 | "homepage": "http://www.php-fig.org/" 94 | } 95 | ], 96 | "description": "Common interface for HTTP messages", 97 | "keywords": [ 98 | "http", 99 | "http-message", 100 | "psr", 101 | "psr-7", 102 | "request", 103 | "response" 104 | ], 105 | "time": "2015-03-18 22:07:07" 106 | } 107 | ], 108 | "packages-dev": [], 109 | "aliases": [], 110 | "minimum-stability": "stable", 111 | "stability-flags": [], 112 | "prefer-stable": false, 113 | "prefer-lowest": false, 114 | "platform": [], 115 | "platform-dev": [] 116 | } 117 | -------------------------------------------------------------------------------- /public/copy-stream.php: -------------------------------------------------------------------------------- 1 | withHeader('Content-Type', 'image/jpeg') 25 | ->withHeader('Content-Length', (string) filesize($image)) 26 | ->withBody(new Stream($image)); 27 | return $response; 28 | }, $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); 29 | 30 | $server->listen(); 31 | -------------------------------------------------------------------------------- /public/cuervo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phly/psr7examples/c15a49d3820d8e23373820a9b872a287fe5066c2/public/cuervo.jpg -------------------------------------------------------------------------------- /public/generator.php: -------------------------------------------------------------------------------- 1 | \n"); 21 | } 22 | }; 23 | 24 | $output = new IteratorStream($generator(10)); 25 | 26 | return (new Response()) 27 | ->withHeader('Content-Type', 'text/html') 28 | ->withBody($output); 29 | }, $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); 30 | 31 | $server->listen(); 32 | -------------------------------------------------------------------------------- /public/iterator.php: -------------------------------------------------------------------------------- 1 | \n", 19 | "Bar!
\n", 20 | "Baz!
\n", 21 | ])); 22 | return (new Response()) 23 | ->withHeader('Content-Type', 'text/html') 24 | ->withBody($output); 25 | }, $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); 26 | 27 | $server->listen(); 28 | -------------------------------------------------------------------------------- /public/php-output.php: -------------------------------------------------------------------------------- 1 | \n", $request->getUri()); 25 | return ''; 26 | }); 27 | return (new Response()) 28 | ->withHeader('Content-Type', 'text/html') 29 | ->withBody($output); 30 | }, $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); 31 | 32 | $server->listen(); 33 | -------------------------------------------------------------------------------- /src/CallbackStream.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function __toString() 42 | { 43 | return $this->output(); 44 | } 45 | 46 | /** 47 | * Execute the callback with output buffering. 48 | * 49 | * @return null|string Null on second and subsequent calls. 50 | */ 51 | public function output() 52 | { 53 | if ($this->called) { 54 | return; 55 | } 56 | 57 | $this->called = true; 58 | 59 | ob_start(); 60 | call_user_func($this->callback); 61 | return ob_get_clean(); 62 | } 63 | 64 | /** 65 | * @return void 66 | */ 67 | public function close() 68 | { 69 | } 70 | 71 | /** 72 | * @return null|callable 73 | */ 74 | public function detach() 75 | { 76 | $callback = $this->callback; 77 | $this->callback = null; 78 | return $callback; 79 | } 80 | 81 | /** 82 | * @return int|null Returns the size in bytes if known, or null if unknown. 83 | */ 84 | public function getSize() 85 | { 86 | } 87 | 88 | /** 89 | * @return int|bool Position of the file pointer or false on error. 90 | */ 91 | public function tell() 92 | { 93 | return 0; 94 | } 95 | 96 | /** 97 | * @return bool 98 | */ 99 | public function eof() 100 | { 101 | return $this->called; 102 | } 103 | 104 | /** 105 | * @return bool 106 | */ 107 | public function isSeekable() 108 | { 109 | return false; 110 | } 111 | 112 | /** 113 | * @link http://www.php.net/manual/en/function.fseek.php 114 | * @param int $offset Stream offset 115 | * @param int $whence Specifies how the cursor position will be calculated 116 | * based on the seek offset. Valid values are identical to the built-in 117 | * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to 118 | * offset bytes SEEK_CUR: Set position to current location plus offset 119 | * SEEK_END: Set position to end-of-stream plus offset. 120 | * @return bool Returns TRUE on success or FALSE on failure. 121 | */ 122 | public function seek($offset, $whence = SEEK_SET) 123 | { 124 | return false; 125 | } 126 | 127 | /** 128 | * @see seek() 129 | * @link http://www.php.net/manual/en/function.fseek.php 130 | * @return bool Returns TRUE on success or FALSE on failure. 131 | */ 132 | public function rewind() 133 | { 134 | return false; 135 | } 136 | 137 | /** 138 | * @return bool 139 | */ 140 | public function isWritable() 141 | { 142 | return false; 143 | } 144 | 145 | /** 146 | * @param string $string The string that is to be written. 147 | * @return int|bool Returns the number of bytes written to the stream on 148 | * success or FALSE on failure. 149 | */ 150 | public function write($string) 151 | { 152 | return false; 153 | } 154 | 155 | /** 156 | * @return bool 157 | */ 158 | public function isReadable() 159 | { 160 | return true; 161 | } 162 | 163 | /** 164 | * @param int $length Read up to $length bytes from the object and return 165 | * them. Fewer than $length bytes may be returned if underlying stream 166 | * call returns fewer bytes. 167 | * @return string|false Returns the data read from the stream, false if 168 | * unable to read or if an error occurs. 169 | */ 170 | public function read($length) 171 | { 172 | return $this->output(); 173 | } 174 | 175 | /** 176 | * @return string 177 | */ 178 | public function getContents() 179 | { 180 | return $this->output(); 181 | } 182 | 183 | /** 184 | * @link http://php.net/manual/en/function.stream-get-meta-data.php 185 | * @param string $key Specific metadata to retrieve. 186 | * @return array|mixed|null Returns an associative array if no key is 187 | * provided. Returns a specific key value if a key is provided and the 188 | * value is found, or null if the key is not found. 189 | */ 190 | public function getMetadata($key = null) 191 | { 192 | if ($key === null) { 193 | return array(); 194 | } 195 | return null; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/IteratorStream.php: -------------------------------------------------------------------------------- 1 | getIterator(); 48 | } 49 | $this->iterator = $iterator; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function __toString() 56 | { 57 | $this->iterator->rewind(); 58 | 59 | return $this->getContents(); 60 | } 61 | 62 | /** 63 | * No-op. 64 | * 65 | * @return void 66 | */ 67 | public function close() 68 | { 69 | } 70 | 71 | /** 72 | * @return null|Traversable 73 | */ 74 | public function detach() 75 | { 76 | $iterator = $this->iterator; 77 | $this->iterator = null; 78 | return $iterator; 79 | } 80 | 81 | /** 82 | * @return int|null Returns the size of the iterator, or null if unknown. 83 | */ 84 | public function getSize() 85 | { 86 | if ($this->iterator instanceof Countable) { 87 | return count($this->iterator); 88 | } 89 | 90 | return null; 91 | } 92 | 93 | /** 94 | * @return int|bool Position of the iterator or false on error. 95 | */ 96 | public function tell() 97 | { 98 | return $this->position; 99 | } 100 | 101 | /** 102 | * @return bool 103 | */ 104 | public function eof() 105 | { 106 | if ($this->iterator instanceof Countable) { 107 | return ($this->position === count($this->iterator)); 108 | } 109 | 110 | return (! $this->iterator->valid()); 111 | } 112 | 113 | /** 114 | * @return bool 115 | */ 116 | public function isSeekable() 117 | { 118 | return true; 119 | } 120 | 121 | /** 122 | * @param int $offset Stream offset 123 | * @param int $whence Ignored. 124 | * @return bool Returns TRUE on success or FALSE on failure. 125 | */ 126 | public function seek($offset, $whence = SEEK_SET) 127 | { 128 | if (! is_int($offset) && ! is_numeric($offset)) { 129 | return false; 130 | } 131 | $offset = (int) $offset; 132 | 133 | if ($offset < 0) { 134 | return false; 135 | } 136 | 137 | $key = $this->iterator->key(); 138 | if (! is_int($key) && ! is_numeric($key)) { 139 | $key = 0; 140 | $this->iterator->rewind(); 141 | } 142 | 143 | if ($key >= $offset) { 144 | $key = 0; 145 | $this->iterator->rewind(); 146 | } 147 | 148 | while ($this->iterator->valid() && $key < $offset) { 149 | $this->iterator->next(); 150 | ++$key; 151 | } 152 | 153 | $this->position = $key; 154 | return true; 155 | } 156 | 157 | /** 158 | * @see seek() 159 | * @return bool Returns TRUE on success or FALSE on failure. 160 | */ 161 | public function rewind() 162 | { 163 | $this->iterator->rewind(); 164 | $this->position = 0; 165 | return true; 166 | } 167 | 168 | /** 169 | * @return bool Always returns false 170 | */ 171 | public function isWritable() 172 | { 173 | return false; 174 | } 175 | 176 | /** 177 | * Non-writable 178 | * 179 | * @param string $string The string that is to be written. 180 | * @return int|bool Always returns false 181 | */ 182 | public function write($string) 183 | { 184 | return false; 185 | } 186 | 187 | /** 188 | * @return bool Always returns true 189 | */ 190 | public function isReadable() 191 | { 192 | return true; 193 | } 194 | 195 | /** 196 | * @param int $length Read up to $length items from the object and return 197 | * them. Fewer than $length items may be returned if underlying iterator 198 | * has fewer items. 199 | * @return string|false Returns the data read from the iterator, false if 200 | * unable to read or if an error occurs. 201 | */ 202 | public function read($length) 203 | { 204 | $index = 0; 205 | $contents = ''; 206 | 207 | while ($this->iterator->valid() && $index < $length) { 208 | $contents .= $this->iterator->current(); 209 | $this->iterator->next(); 210 | ++$this->position; 211 | ++$index; 212 | } 213 | 214 | return $contents; 215 | } 216 | 217 | /** 218 | * @return string 219 | */ 220 | public function getContents() 221 | { 222 | $contents = ''; 223 | while ($this->iterator->valid()) { 224 | $contents .= $this->iterator->current(); 225 | $this->iterator->next(); 226 | ++$this->position; 227 | } 228 | return $contents; 229 | } 230 | 231 | /** 232 | * @param string $key Specific metadata to retrieve. 233 | * @return array|null Returns an empty array if no key is provided, and 234 | * null otherwise. 235 | */ 236 | public function getMetadata($key = null) 237 | { 238 | return ($key === null) ? array() : null; 239 | } 240 | } --------------------------------------------------------------------------------