Skip to content

Commit 2d57b8c

Browse files
authored
Merge pull request #215 from WyriHaximus/middleware-runner
Middleware Runner
2 parents 6646135 + 1c82840 commit 2d57b8c

File tree

6 files changed

+288
-0
lines changed

6 files changed

+288
-0
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht
1111
* [Server](#server)
1212
* [Request](#request)
1313
* [Response](#response)
14+
* [Middleware](#middleware)
1415
* [Install](#install)
1516
* [Tests](#tests)
1617
* [License](#license)
@@ -651,6 +652,31 @@ As such, HTTP/1.1 response messages will automatically include a
651652
`Connection: close` header, irrespective of what header values are
652653
passed explicitly.
653654

655+
### Middleware
656+
657+
Middleware can be added to the server using [`MiddlewareRunner`](src/MiddlewareRunner.php)
658+
instead of the `callable`. A middleware is expected to adhere the following rules:
659+
660+
* It is a `callable`.
661+
* It accepts `ServerRequestInterface` as first argument and optional `callable` as second argument.
662+
* It returns a `ResponseInterface` (or any promise which can be consumed by [`Promise\resolve`](http://reactphp.org/promise/#resolve) resolving to a `ResponseInterface`)
663+
* It calls `$next($request)` to continue processing the next middleware function or returns explicitly to abort the chain
664+
665+
The following example adds a middleware that adds the current time to the request as a
666+
header (`Request-Time`) and middleware that always returns a 200 code without a body:
667+
668+
```php
669+
$server = new Server(new MiddlewareRunner([
670+
function (ServerRequestInterface $request, callable $next) {
671+
$request = $request->withHeader('Request-Time', time());
672+
return $next($request);
673+
},
674+
function (ServerRequestInterface $request, callable $next) {
675+
return new Response(200);
676+
},
677+
]));
678+
```
679+
654680
## Install
655681

656682
The recommended way to install this library is [through Composer](http://getcomposer.org).

src/MiddlewareRunner.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace React\Http;
4+
5+
use Psr\Http\Message\ResponseInterface;
6+
use Psr\Http\Message\ServerRequestInterface;
7+
use React\Promise;
8+
use React\Promise\PromiseInterface;
9+
10+
final class MiddlewareRunner
11+
{
12+
/**
13+
* @var callable[]
14+
*/
15+
private $middleware = array();
16+
17+
/**
18+
* @param callable[] $middleware
19+
*/
20+
public function __construct(array $middleware)
21+
{
22+
$this->middleware = $middleware;
23+
}
24+
25+
/**
26+
* @param ServerRequestInterface $request
27+
* @return PromiseInterface<ResponseInterface>
28+
*/
29+
public function __invoke(ServerRequestInterface $request)
30+
{
31+
if (count($this->middleware) === 0) {
32+
return Promise\reject(new \RuntimeException('No middleware to run'));
33+
}
34+
35+
$middlewareCollection = $this->middleware;
36+
$middleware = array_shift($middlewareCollection);
37+
38+
$cancel = null;
39+
return new Promise\Promise(function ($resolve, $reject) use ($middleware, $request, $middlewareCollection, &$cancel) {
40+
$cancel = $middleware(
41+
$request,
42+
new MiddlewareRunner(
43+
$middlewareCollection
44+
)
45+
);
46+
$resolve($cancel);
47+
}, function () use (&$cancel) {
48+
if ($cancel instanceof Promise\CancellablePromiseInterface) {
49+
$cancel->cancel();
50+
}
51+
});
52+
}
53+
}

tests/FunctionalServerTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace React\Tests\Http;
44

5+
use React\Http\MiddlewareRunner;
56
use React\Socket\Server as Socket;
67
use React\EventLoop\Factory;
78
use React\Http\Server;
@@ -45,6 +46,59 @@ public function testPlainHttpOnRandomPort()
4546
$socket->close();
4647
}
4748

49+
public function testPlainHttpOnRandomPortWithMiddlewareRunner()
50+
{
51+
$loop = Factory::create();
52+
$connector = new Connector($loop);
53+
54+
$server = new Server(new MiddlewareRunner(array(function (RequestInterface $request) {
55+
return new Response(200, array(), (string)$request->getUri());
56+
})));
57+
58+
$socket = new Socket(0, $loop);
59+
$server->listen($socket);
60+
61+
$result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) {
62+
$conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n");
63+
64+
return Stream\buffer($conn);
65+
});
66+
67+
$response = Block\await($result, $loop, 1.0);
68+
69+
$this->assertContains("HTTP/1.0 200 OK", $response);
70+
$this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response);
71+
72+
$socket->close();
73+
}
74+
75+
public function testPlainHttpOnRandomPortWithEmptyMiddlewareRunner()
76+
{
77+
$loop = Factory::create();
78+
$connector = new Connector($loop);
79+
80+
$server = new Server(new MiddlewareRunner(array(
81+
function () {
82+
return new Response(404);
83+
},
84+
)));
85+
86+
$socket = new Socket(0, $loop);
87+
$server->listen($socket);
88+
89+
$result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) {
90+
$conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n");
91+
92+
return Stream\buffer($conn);
93+
});
94+
95+
$response = Block\await($result, $loop, 1.0);
96+
97+
$this->assertContains("HTTP/1.0 404 Not Found", $response);
98+
99+
$socket->close();
100+
}
101+
48102
public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri()
49103
{
50104
$loop = Factory::create();

tests/Middleware/ProcessStack.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace React\Tests\Http\Middleware;
4+
5+
use Psr\Http\Message\ServerRequestInterface;
6+
use React\Promise;
7+
8+
final class ProcessStack
9+
{
10+
/**
11+
* @var int
12+
*/
13+
private $callCount = 0;
14+
15+
public function __invoke(ServerRequestInterface $request, $stack)
16+
{
17+
$this->callCount++;
18+
return Promise\resolve($stack($request));
19+
}
20+
21+
/**
22+
* @return int
23+
*/
24+
public function getCallCount()
25+
{
26+
return $this->callCount;
27+
}
28+
}

tests/MiddlewareRunnerTest.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace React\Tests\Http;
4+
5+
use Psr\Http\Message\ResponseInterface;
6+
use React\EventLoop\Factory;
7+
use React\Http\MiddlewareRunner;
8+
use React\Http\ServerRequest;
9+
use React\Tests\Http\Middleware\ProcessStack;
10+
use RingCentral\Psr7\Response;
11+
use Clue\React\Block;
12+
13+
final class MiddlewareRunnerTest extends TestCase
14+
{
15+
public function testDefaultResponse()
16+
{
17+
$this->setExpectedException('\RuntimeException');
18+
$request = new ServerRequest('GET', 'https://example.com/');
19+
$middlewares = array();
20+
$middlewareStack = new MiddlewareRunner($middlewares);
21+
22+
Block\await($middlewareStack($request), Factory::create());
23+
}
24+
25+
public function provideProcessStackMiddlewares()
26+
{
27+
$processStackA = new ProcessStack();
28+
$processStackB = new ProcessStack();
29+
$processStackC = new ProcessStack();
30+
$processStackD = new ProcessStack();
31+
$responseMiddleware = function () {
32+
return new Response(200);
33+
};
34+
return array(
35+
array(
36+
array(
37+
$processStackA,
38+
$responseMiddleware,
39+
),
40+
1,
41+
),
42+
array(
43+
array(
44+
$processStackB,
45+
$processStackB,
46+
$responseMiddleware,
47+
),
48+
2,
49+
),
50+
array(
51+
array(
52+
$processStackC,
53+
$processStackC,
54+
$processStackC,
55+
$responseMiddleware,
56+
),
57+
3,
58+
),
59+
array(
60+
array(
61+
$processStackD,
62+
$processStackD,
63+
$processStackD,
64+
$processStackD,
65+
$responseMiddleware,
66+
),
67+
4,
68+
),
69+
);
70+
}
71+
72+
/**
73+
* @dataProvider provideProcessStackMiddlewares
74+
*/
75+
public function testProcessStack(array $middlewares, $expectedCallCount)
76+
{
77+
$request = new ServerRequest('GET', 'https://example.com/');
78+
$middlewareStack = new MiddlewareRunner($middlewares);
79+
80+
/** @var ResponseInterface $result */
81+
$result = Block\await($middlewareStack($request), Factory::create());
82+
$this->assertSame(200, $result->getStatusCode());
83+
foreach ($middlewares as $middleware) {
84+
if (!($middleware instanceof ProcessStack)) {
85+
continue;
86+
}
87+
88+
$this->assertSame($expectedCallCount, $middleware->getCallCount());
89+
}
90+
}
91+
}

tests/ServerTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace React\Tests\Http;
44

5+
use React\Http\MiddlewareRunner;
56
use React\Http\Server;
67
use Psr\Http\Message\ServerRequestInterface;
78
use React\Http\Response;
@@ -99,6 +100,41 @@ public function testRequestEvent()
99100
$this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']);
100101
}
101102

103+
public function testRequestEventWithMiddlewareRunner()
104+
{
105+
$i = 0;
106+
$requestAssertion = null;
107+
$server = new Server(new MiddlewareRunner(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) {
108+
$i++;
109+
$requestAssertion = $request;
110+
111+
return \React\Promise\resolve(new Response());
112+
})));
113+
114+
$this->connection
115+
->expects($this->any())
116+
->method('getRemoteAddress')
117+
->willReturn('127.0.0.1:8080');
118+
119+
$server->listen($this->socket);
120+
$this->socket->emit('connection', array($this->connection));
121+
122+
$data = $this->createGetRequest();
123+
$this->connection->emit('data', array($data));
124+
125+
$serverParams = $requestAssertion->getServerParams();
126+
127+
$this->assertSame(1, $i);
128+
$this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
129+
$this->assertSame('GET', $requestAssertion->getMethod());
130+
$this->assertSame('/', $requestAssertion->getRequestTarget());
131+
$this->assertSame('/', $requestAssertion->getUri()->getPath());
132+
$this->assertSame(array(), $requestAssertion->getQueryParams());
133+
$this->assertSame('http://example.com/', (string)$requestAssertion->getUri());
134+
$this->assertSame('example.com', $requestAssertion->getHeaderLine('Host'));
135+
$this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']);
136+
}
137+
102138
public function testRequestGetWithHostAndCustomPort()
103139
{
104140
$requestAssertion = null;

0 commit comments

Comments
 (0)