├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── ConfigProvider.php ├── Emitter ├── EmitterInterface.php ├── EmitterStack.php ├── SapiEmitter.php ├── SapiEmitterTrait.php └── SapiStreamEmitter.php ├── Exception ├── EmitterException.php ├── ExceptionInterface.php └── InvalidEmitterException.php └── RequestHandlerRunner.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 1.1.1 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 1.1.0 - 2019-02-19 28 | 29 | ### Added 30 | 31 | - [#10](https://github.com/zendframework/zend-httphandlerrunner/pull/10) adds support for zend-diactoros v2 releases. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - Nothing. 48 | 49 | ## 1.0.2 - 2019-02-19 50 | 51 | ### Added 52 | 53 | - [#9](https://github.com/zendframework/zend-httphandlerrunner/pull/9) adds support for PHP 7.3. 54 | 55 | ### Changed 56 | 57 | - Nothing. 58 | 59 | ### Deprecated 60 | 61 | - Nothing. 62 | 63 | ### Removed 64 | 65 | - Nothing. 66 | 67 | ### Fixed 68 | 69 | - Nothing. 70 | 71 | ## 1.0.1 - 2018-02-21 72 | 73 | ### Added 74 | 75 | - Nothing. 76 | 77 | ### Changed 78 | 79 | - [#2](https://github.com/zendframework/zend-httphandlerrunner/pull/2) modifies 80 | how the request and error response factories are composed with the 81 | `RequestHandlerRunner` class. In both cases, they are now encapsulated in a 82 | closure which also defines a return type hint, ensuring that if the factories 83 | produce an invalid return type, a PHP `TypeError` will be raised. 84 | 85 | ### Deprecated 86 | 87 | - Nothing. 88 | 89 | ### Removed 90 | 91 | - Nothing. 92 | 93 | ### Fixed 94 | 95 | - Nothing. 96 | 97 | ## 1.0.0 - 2018-02-05 98 | 99 | Initial stable release. 100 | 101 | The `Zend\HttpRequestHandler\Emitter` subcomponent was originally released as 102 | part of two packages: 103 | 104 | - `EmitterInterface` and the two SAPI emitter implementations were released 105 | previously as part of the [zend-diactoros](https://docs.zendframework.com/zend-daictoros) 106 | package. 107 | 108 | - `EmitterStack` was previously released as part of the 109 | [zend-expressive](https://docs.zendframework.com/zend-expressive/) package. 110 | 111 | These features are mostly verbatim from that package, with minor API changes. 112 | 113 | The `RequestHandlerRunner` was originally developed as part of version 3 114 | development of zend-expressive, but extracted here for general use with 115 | [PSR-15](https://www.php-fig.org/psr/psr-15) applications. 116 | 117 | ### Added 118 | 119 | - Everything. 120 | 121 | ### Changed 122 | 123 | - Nothing. 124 | 125 | ### Deprecated 126 | 127 | - Nothing. 128 | 129 | ### Removed 130 | 131 | - Nothing. 132 | 133 | ### Fixed 134 | 135 | - Nothing. 136 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-httphandlerrunner 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-httphandlerrunner](https://github.com/laminas/laminas-httphandlerrunner). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-httphandlerrunner.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-httphandlerrunner) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-httphandlerrunner/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-httphandlerrunner?branch=master) 9 | 10 | This library provides utilities for: 11 | 12 | - Emitting [PSR-7](https://www.php-fig.org/psr/psr-7) responses. 13 | - Running [PSR-15](https://www.php-fig.org/psr/psr-15) server request handlers, 14 | which involves marshaling a PSR-7 `ServerRequestInterface`, handling 15 | exceptions due to request creation, and emitting the response returned by the 16 | composed request handler. 17 | 18 | The `RequestHandlerRunner` will be used in the bootstrap of your application to 19 | fire off the `RequestHandlerInterface` representing your application. 20 | 21 | ## Installation 22 | 23 | Run the following to install this library: 24 | 25 | ```bash 26 | $ composer require zendframework/zend-httphandlerrunner 27 | ``` 28 | 29 | ## Documentation 30 | 31 | Documentation is [in the doc tree](docs/book/), and can be compiled using [mkdocs](http://www.mkdocs.org): 32 | 33 | ```bash 34 | $ mkdocs build 35 | ``` 36 | 37 | You may also [browse the documentation online](https://docs.zendframework.com/zend-httphandlerrunner/). 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-httphandlerrunner", 3 | "description": "Execute PSR-15 RequestHandlerInterface instances and emit responses they generate.", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "components", 7 | "expressive", 8 | "zf", 9 | "zendframework", 10 | "psr-7", 11 | "psr-15" 12 | ], 13 | "support": { 14 | "docs": "https://docs.zendframework.com/zend-httphandlerrunner/", 15 | "issues": "https://github.com/zendframework/zend-httphandlerrunner/issues", 16 | "source": "https://github.com/zendframework/zend-httphandlerrunner", 17 | "rss": "https://github.com/zendframework/zend-httphandlerrunner/releases.atom", 18 | "slack": "https://zendframework-slack.herokuapp.com", 19 | "forum": "https://discourse.zendframework.com/c/questions/expressive" 20 | }, 21 | "require": { 22 | "php": "^7.1", 23 | "psr/http-message": "^1.0", 24 | "psr/http-message-implementation": "^1.0", 25 | "psr/http-server-handler": "^1.0" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^7.0.2", 29 | "zendframework/zend-coding-standard": "~1.0.0", 30 | "zendframework/zend-diactoros": "^1.7 || ^2.1.1" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Zend\\HttpHandlerRunner\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "ZendTest\\HttpHandlerRunner\\": "test/" 40 | }, 41 | "files": [ 42 | "test/TestAsset/SapiResponse.php" 43 | ] 44 | }, 45 | "config": { 46 | "sort-packages": true 47 | }, 48 | "extra": { 49 | "branch-alias": { 50 | "dev-master": "1.1.x-dev", 51 | "dev-develop": "1.2.x-dev" 52 | }, 53 | "zf": { 54 | "config-provider": "Zend\\HttpHandlerRunner\\ConfigProvider" 55 | } 56 | }, 57 | "scripts": { 58 | "check": [ 59 | "@cs-check", 60 | "@test" 61 | ], 62 | "cs-check": "phpcs", 63 | "cs-fix": "phpcbf", 64 | "test": "phpunit --colors=always", 65 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencies(), 16 | ]; 17 | } 18 | 19 | public function getDependencies() : array 20 | { 21 | return [ 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Emitter/EmitterInterface.php: -------------------------------------------------------------------------------- 1 | emit($response)) { 40 | return true; 41 | } 42 | } 43 | 44 | return false; 45 | } 46 | 47 | /** 48 | * Set an emitter on the stack by index. 49 | * 50 | * @param mixed $index 51 | * @param EmitterInterface $emitter 52 | * @return void 53 | * @throws InvalidArgumentException if not an EmitterInterface instance 54 | */ 55 | public function offsetSet($index, $emitter) 56 | { 57 | $this->validateEmitter($emitter); 58 | parent::offsetSet($index, $emitter); 59 | } 60 | 61 | /** 62 | * Push an emitter to the stack. 63 | * 64 | * @param EmitterInterface $emitter 65 | * @return void 66 | * @throws InvalidArgumentException if not an EmitterInterface instance 67 | */ 68 | public function push($emitter) 69 | { 70 | $this->validateEmitter($emitter); 71 | parent::push($emitter); 72 | } 73 | 74 | /** 75 | * Unshift an emitter to the stack. 76 | * 77 | * @param EmitterInterface $emitter 78 | * @return void 79 | * @throws InvalidArgumentException if not an EmitterInterface instance 80 | */ 81 | public function unshift($emitter) 82 | { 83 | $this->validateEmitter($emitter); 84 | parent::unshift($emitter); 85 | } 86 | 87 | /** 88 | * Validate that an emitter implements EmitterInterface. 89 | * 90 | * @param mixed $emitter 91 | * @throws Exception\InvalidEmitterException for non-emitter instances 92 | */ 93 | private function validateEmitter($emitter) : void 94 | { 95 | if (! $emitter instanceof EmitterInterface) { 96 | throw Exception\InvalidEmitterException::forEmitter($emitter); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Emitter/SapiEmitter.php: -------------------------------------------------------------------------------- 1 | assertNoPreviousOutput(); 27 | 28 | $this->emitHeaders($response); 29 | $this->emitStatusLine($response); 30 | $this->emitBody($response); 31 | 32 | return true; 33 | } 34 | 35 | /** 36 | * Emit the message body. 37 | */ 38 | private function emitBody(ResponseInterface $response) : void 39 | { 40 | echo $response->getBody(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Emitter/SapiEmitterTrait.php: -------------------------------------------------------------------------------- 1 | 0 && ob_get_length() > 0) { 39 | throw EmitterException::forOutputSent(); 40 | } 41 | } 42 | 43 | /** 44 | * Emit the status line. 45 | * 46 | * Emits the status line using the protocol version and status code from 47 | * the response; if a reason phrase is available, it, too, is emitted. 48 | * 49 | * It is important to mention that this method should be called after 50 | * `emitHeaders()` in order to prevent PHP from changing the status code of 51 | * the emitted response. 52 | * 53 | * @see \Zend\HttpHandlerRunner\Emitter\SapiEmitterTrait::emitHeaders() 54 | */ 55 | private function emitStatusLine(ResponseInterface $response) : void 56 | { 57 | $reasonPhrase = $response->getReasonPhrase(); 58 | $statusCode = $response->getStatusCode(); 59 | 60 | header(sprintf( 61 | 'HTTP/%s %d%s', 62 | $response->getProtocolVersion(), 63 | $statusCode, 64 | ($reasonPhrase ? ' ' . $reasonPhrase : '') 65 | ), true, $statusCode); 66 | } 67 | 68 | /** 69 | * Emit response headers. 70 | * 71 | * Loops through each header, emitting each; if the header value 72 | * is an array with multiple values, ensures that each is sent 73 | * in such a way as to create aggregate headers (instead of replace 74 | * the previous). 75 | */ 76 | private function emitHeaders(ResponseInterface $response) : void 77 | { 78 | $statusCode = $response->getStatusCode(); 79 | 80 | foreach ($response->getHeaders() as $header => $values) { 81 | $name = $this->filterHeader($header); 82 | $first = $name === 'Set-Cookie' ? false : true; 83 | foreach ($values as $value) { 84 | header(sprintf( 85 | '%s: %s', 86 | $name, 87 | $value 88 | ), $first, $statusCode); 89 | $first = false; 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Filter a header name to wordcase 96 | */ 97 | private function filterHeader(string $header) : string 98 | { 99 | return ucwords($header, '-'); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Emitter/SapiStreamEmitter.php: -------------------------------------------------------------------------------- 1 | maxBufferLength = $maxBufferLength; 30 | } 31 | 32 | /** 33 | * Emits a response for a PHP SAPI environment. 34 | * 35 | * Emits the status line and headers via the header() function, and the 36 | * body content via the output buffer. 37 | */ 38 | public function emit(ResponseInterface $response) : bool 39 | { 40 | $this->assertNoPreviousOutput(); 41 | $this->emitHeaders($response); 42 | $this->emitStatusLine($response); 43 | 44 | $range = $this->parseContentRange($response->getHeaderLine('Content-Range')); 45 | 46 | if (null === $range || 'bytes' !== $range[0]) { 47 | $this->emitBody($response); 48 | return true; 49 | } 50 | 51 | $this->emitBodyRange($range, $response); 52 | return true; 53 | } 54 | 55 | /** 56 | * Emit the message body. 57 | */ 58 | private function emitBody(ResponseInterface $response) : void 59 | { 60 | $body = $response->getBody(); 61 | 62 | if ($body->isSeekable()) { 63 | $body->rewind(); 64 | } 65 | 66 | if (! $body->isReadable()) { 67 | echo $body; 68 | return; 69 | } 70 | 71 | while (! $body->eof()) { 72 | echo $body->read($this->maxBufferLength); 73 | } 74 | } 75 | 76 | /** 77 | * Emit a range of the message body. 78 | */ 79 | private function emitBodyRange(array $range, ResponseInterface $response) : void 80 | { 81 | list($unit, $first, $last, $length) = $range; 82 | 83 | $body = $response->getBody(); 84 | 85 | $length = $last - $first + 1; 86 | 87 | if ($body->isSeekable()) { 88 | $body->seek($first); 89 | 90 | $first = 0; 91 | } 92 | 93 | if (! $body->isReadable()) { 94 | echo substr($body->getContents(), $first, $length); 95 | return; 96 | } 97 | 98 | $remaining = $length; 99 | 100 | while ($remaining >= $this->maxBufferLength && ! $body->eof()) { 101 | $contents = $body->read($this->maxBufferLength); 102 | $remaining -= strlen($contents); 103 | 104 | echo $contents; 105 | } 106 | 107 | if ($remaining > 0 && ! $body->eof()) { 108 | echo $body->read($remaining); 109 | } 110 | } 111 | 112 | /** 113 | * Parse content-range header 114 | * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16 115 | * 116 | * @return null|array [unit, first, last, length]; returns null if no 117 | * content range or an invalid content range is provided 118 | */ 119 | private function parseContentRange(string $header) : ?array 120 | { 121 | if (! preg_match('/(?P[\w]+)\s+(?P\d+)-(?P\d+)\/(?P\d+|\*)/', $header, $matches)) { 122 | return null; 123 | } 124 | 125 | return [ 126 | $matches['unit'], 127 | (int) $matches['first'], 128 | (int) $matches['last'], 129 | $matches['length'] === '*' ? '*' : (int) $matches['length'], 130 | ]; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Exception/EmitterException.php: -------------------------------------------------------------------------------- 1 | handler = $handler; 69 | $this->emitter = $emitter; 70 | 71 | // Factories are cast as Closures to ensure return type safety. 72 | $this->serverRequestFactory = function () use ($serverRequestFactory) : ServerRequestInterface { 73 | return $serverRequestFactory(); 74 | }; 75 | 76 | $this->serverRequestErrorResponseGenerator = 77 | function (Throwable $exception) use ($serverRequestErrorResponseGenerator) : ResponseInterface { 78 | return $serverRequestErrorResponseGenerator($exception); 79 | }; 80 | } 81 | 82 | /** 83 | * Run the application 84 | */ 85 | public function run() : void 86 | { 87 | try { 88 | $request = ($this->serverRequestFactory)(); 89 | } catch (Throwable $e) { 90 | // Error in generating the request 91 | $this->emitMarshalServerRequestException($e); 92 | return; 93 | } 94 | 95 | $response = $this->handler->handle($request); 96 | 97 | $this->emitter->emit($response); 98 | } 99 | 100 | private function emitMarshalServerRequestException(Throwable $exception) : void 101 | { 102 | $response = ($this->serverRequestErrorResponseGenerator)($exception); 103 | $this->emitter->emit($response); 104 | } 105 | } 106 | --------------------------------------------------------------------------------