├── .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 | }
--------------------------------------------------------------------------------