Skip to content

Commit 6c05d52

Browse files
authored
Add a generic PSR-17 runtime (#43)
* typo * Add a generic PSR-17 * Fixed tests * cs
0 parents  commit 6c05d52

18 files changed

+533
-0
lines changed

.github/FUNDING.yml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# These are supported funding model platforms
2+
3+
github: [nyholm]

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor/
2+
composer.lock

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2021 Tobias Nyholm
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# PSR-7 and PSR-15 Runtime
2+
3+
A runtime with support for any PSR-17 compatible implementation.
4+
5+
## Installation
6+
7+
Install this runtime plus a package that [provide psr/http-factory-implementation](https://packagist.org/providers/psr/http-factory-implementation).
8+
9+
```
10+
composer require runtime/psr-17 slim/psr7
11+
```
12+
13+
Also update your composer.json with some extra config:
14+
15+
```json
16+
{
17+
"require": {
18+
"...": "..."
19+
},
20+
"extra": {
21+
"runtime": {
22+
"psr17_server_request_factory": "Slim\\Psr7\\Factory\\ServerRequestFactory"
23+
"psr17_uri_factory": "Slim\\Psr7\\Factory\\UriFactory"
24+
"psr17_uploaded_file_factory": "Slim\\Psr7\\Factory\\UploadedFileFactory"
25+
"psr17_stream_factory": "Slim\\Psr7\\Factory\\StreamFactory"
26+
}
27+
}
28+
}
29+
```
30+
31+
## Usage
32+
33+
This runtime is discovered automatically. You can force your application to use
34+
this runtime by defining the environment variable `APP_RUNTIME`.
35+
36+
```
37+
APP_RUNTIME=Runtime\Psr17\Runtime
38+
```
39+
40+
### PSR-7
41+
42+
```php
43+
// public/index.php
44+
45+
use Psr\Http\Message\ServerRequestInterface;
46+
use Any\Psr7;
47+
48+
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
49+
50+
return function (ServerRequestInterface $request) {
51+
return new Psr7\Response(200, [], 'PSR-7');
52+
};
53+
```
54+
55+
### PSR-15
56+
57+
```php
58+
// public/index.php
59+
60+
use Psr\Http\Server\RequestHandlerInterface;
61+
use Psr\Http\Message\ServerRequestInterface;
62+
use Psr\Http\Message\ResponseInterface;
63+
use Any\Psr7;
64+
65+
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
66+
67+
class Application implements RequestHandlerInterface {
68+
// ...
69+
public function handle(ServerRequestInterface $request): ResponseInterface
70+
{
71+
return new Psr7\Response(200, [], 'PSR-15');
72+
}
73+
}
74+
75+
return function (array $context) {
76+
return new Application($context['APP_ENV'] ?? 'dev');
77+
};
78+
```

composer.json

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "runtime/psr-17",
3+
"type": "library",
4+
"description": "PSR runtime with support for any PSR-17 implementation",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Tobias Nyholm",
9+
"email": "[email protected]"
10+
}
11+
],
12+
"require": {
13+
"nyholm/psr7-server": "^1.0",
14+
"psr/http-factory-implementation": "^1.0",
15+
"psr/http-server-handler": "^1.0",
16+
"symfony/runtime": "^5.3"
17+
},
18+
"require-dev": {
19+
"nyholm/psr7": "^1.4",
20+
"symfony/phpunit-bridge": "^5.2"
21+
},
22+
"autoload": {
23+
"psr-4": {
24+
"Runtime\\Psr17\\": "src/",
25+
"Symfony\\Runtime\\Psr\\Http\\": "runtime/"
26+
}
27+
},
28+
"autoload-dev": {
29+
"psr-4": {
30+
"Runtime\\Psr17\\Tests\\": "tests/"
31+
}
32+
},
33+
"minimum-stability": "dev",
34+
"prefer-stable": true
35+
}

phpunit.xml.dist

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
4+
backupGlobals="false"
5+
colors="true"
6+
bootstrap="vendor/autoload.php"
7+
failOnRisky="true"
8+
failOnWarning="true"
9+
>
10+
<php>
11+
<ini name="error_reporting" value="-1"/>
12+
</php>
13+
<testsuites>
14+
<testsuite name="Test Suite">
15+
<directory>./tests</directory>
16+
<directory suffix=".phpt">./tests/phpt</directory>
17+
</testsuite>
18+
</testsuites>
19+
</phpunit>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Symfony\Runtime\Psr\Http\Message;
4+
5+
use Runtime\Psr17\Runtime;
6+
7+
class ResponseInterfaceRuntime extends Runtime
8+
{
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Symfony\Runtime\Psr\Http\Message;
4+
5+
use Runtime\Psr17\Runtime;
6+
7+
class ServerRequestInterfaceRuntime extends Runtime
8+
{
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Symfony\Runtime\Psr\Http\Server;
4+
5+
use Runtime\Psr17\Runtime;
6+
7+
class RequestHandlerInterfaceRuntime extends Runtime
8+
{
9+
}

src/Emitter.php

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace Runtime\Psr17;
4+
5+
use Psr\Http\Message\ResponseInterface;
6+
use Psr\Http\Message\ServerRequestInterface;
7+
use Psr\Http\Server\RequestHandlerInterface;
8+
use Symfony\Component\Runtime\RunnerInterface;
9+
10+
/**
11+
* @author Tobias Nyholm <[email protected]>
12+
*/
13+
class Emitter implements RunnerInterface
14+
{
15+
private $requestHandler;
16+
private $response;
17+
private $request;
18+
19+
private function __construct()
20+
{
21+
}
22+
23+
public static function createForResponse(ResponseInterface $response): self
24+
{
25+
$self = new self();
26+
$self->response = $response;
27+
28+
return $self;
29+
}
30+
31+
public static function createForRequestHandler(RequestHandlerInterface $handler, ServerRequestInterface $request): self
32+
{
33+
$self = new self();
34+
$self->requestHandler = $handler;
35+
$self->request = $request;
36+
37+
return $self;
38+
}
39+
40+
public function run(): int
41+
{
42+
if (null === $this->response) {
43+
$this->response = $this->requestHandler->handle($this->request);
44+
}
45+
46+
$this->emit($this->response);
47+
48+
return 0;
49+
}
50+
51+
/**
52+
* Emits a response for a PHP SAPI environment.
53+
*
54+
* Emits the status line and headers via the header() function, and the
55+
* body content via the output buffer.
56+
*
57+
* @Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
58+
*/
59+
public function emit(ResponseInterface $response): void
60+
{
61+
if (headers_sent()) {
62+
throw EmitterException::forHeadersSent();
63+
}
64+
65+
if (ob_get_level() > 0 && ob_get_length() > 0) {
66+
throw EmitterException::forOutputSent();
67+
}
68+
69+
$this->emitHeaders($response);
70+
$this->emitStatusLine($response);
71+
echo $response->getBody();
72+
}
73+
74+
/**
75+
* Emit the status line.
76+
*
77+
* Emits the status line using the protocol version and status code from
78+
* the response; if a reason phrase is available, it, too, is emitted.
79+
*
80+
* It is important to mention that this method should be called after
81+
* `emitHeaders()` in order to prevent PHP from changing the status code of
82+
* the emitted response.
83+
*
84+
* @Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
85+
*/
86+
private function emitStatusLine(ResponseInterface $response): void
87+
{
88+
$reasonPhrase = $response->getReasonPhrase();
89+
$statusCode = $response->getStatusCode();
90+
91+
header(sprintf(
92+
'HTTP/%s %d%s',
93+
$response->getProtocolVersion(),
94+
$statusCode,
95+
($reasonPhrase ? ' '.$reasonPhrase : '')
96+
), true, $statusCode);
97+
}
98+
99+
/**
100+
* Emit response headers.
101+
*
102+
* Loops through each header, emitting each; if the header value
103+
* is an array with multiple values, ensures that each is sent
104+
* in such a way as to create aggregate headers (instead of replace
105+
* the previous).
106+
*
107+
* @Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
108+
*/
109+
private function emitHeaders(ResponseInterface $response): void
110+
{
111+
$statusCode = $response->getStatusCode();
112+
113+
foreach ($response->getHeaders() as $header => $values) {
114+
$name = ucwords($header, '-');
115+
$first = 'Set-Cookie' === $name ? false : true;
116+
foreach ($values as $value) {
117+
header(sprintf('%s: %s', $name, $value), $first, $statusCode);
118+
$first = false;
119+
}
120+
}
121+
}
122+
}

src/EmitterException.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/**
4+
* Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Runtime\Psr17;
10+
11+
class EmitterException extends \RuntimeException
12+
{
13+
public static function forHeadersSent(): self
14+
{
15+
return new self('Unable to emit response; headers already sent');
16+
}
17+
18+
public static function forOutputSent(): self
19+
{
20+
return new self('Output has been emitted previously; cannot emit response');
21+
}
22+
}

0 commit comments

Comments
 (0)