diff --git a/src/Browser.php b/src/Browser.php index ad9187a6..c085e054 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -3,6 +3,8 @@ namespace React\Http; use Psr\Http\Message\ResponseInterface; +use React\Http\Browser\MiddlewareInterface; +use React\Http\Browser\MiddlewareRunner; use RingCentral\Psr7\Uri; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; @@ -27,6 +29,9 @@ class Browser 'User-Agent' => 'ReactPHP/1' ); + /** @var MiddlewareInterface[] */ + private $middlewares = array(); + /** * The `Browser` is responsible for sending HTTP requests to your HTTP server * and keeps track of pending incoming HTTP responses. @@ -851,8 +856,21 @@ private function requestMayBeStreaming($method, $url, array $headers = array(), } } - return $this->transaction->send( - new Request($method, $url, $headers, $body, $this->protocolVersion) - ); + $request = new Request($method, $url, $headers, $body, $this->protocolVersion); + + $middlewareRunner = new MiddlewareRunner($this->middlewares, $this->transaction); + return $middlewareRunner($request); + } + + /** + * @param MiddlewareInterface $middleware + * @return Browser + */ + public function withMiddleware(MiddlewareInterface $middleware) + { + $browser = clone $this; + $browser->middlewares[] = $middleware; + + return $browser; } } diff --git a/src/Browser/MiddlewareInterface.php b/src/Browser/MiddlewareInterface.php new file mode 100644 index 00000000..80f2ee49 --- /dev/null +++ b/src/Browser/MiddlewareInterface.php @@ -0,0 +1,17 @@ +|ResponseInterface + */ + public function __invoke(RequestInterface $request, callable $next); +} diff --git a/src/Browser/MiddlewareRunner.php b/src/Browser/MiddlewareRunner.php new file mode 100644 index 00000000..ef7fad34 --- /dev/null +++ b/src/Browser/MiddlewareRunner.php @@ -0,0 +1,67 @@ +middleware = \array_values($middleware); + $this->transaction = $transaction; + } + + /** + * @param RequestInterface $request + * @return ResponseInterface|PromiseInterface + * @throws \Exception + */ + public function __invoke(RequestInterface $request) + { + return $this->call($request, 0); + } + + /** @internal */ + public function call(RequestInterface $request, $position) + { + if (count($this->middleware) === 0) { + return $this->transaction->send($request); + } + + // final request handler will be invoked with transaction send callable + if (!isset($this->middleware[$position + 1])) { + $handler = $this->middleware[$position]; + $that = $this; + return $handler($request, function () use ($that, $request) { + return $that->transaction->send($request); + }); + } + + $that = $this; + $next = function (RequestInterface $request) use ($that, $position) { + return $that->call($request, $position + 1); + }; + + // invoke middleware request handler with next handler + $handler = $this->middleware[$position]; + return $handler($request, $next); + } +} diff --git a/tests/Browser/MiddlewareRunnerTest.php b/tests/Browser/MiddlewareRunnerTest.php new file mode 100644 index 00000000..f74cf2f2 --- /dev/null +++ b/tests/Browser/MiddlewareRunnerTest.php @@ -0,0 +1,52 @@ +getMockBuilder('\React\Http\Io\Transaction') + ->disableOriginalConstructor() + ->getMock(); + $response = new Response(); + $transaction->expects(static::once())->method('send')->with($request)->willReturn($response); + + $middlewares = array(); + $middlewareStack = new MiddlewareRunner($middlewares, $transaction); + + $actualResponse = $middlewareStack($request); + $this->assertSame($response, $actualResponse); + } + + public function testWithMiddleware() + { + /** @var Transaction&MockObject $transaction */ + $transaction = $this->getMockBuilder('\React\Http\Io\Transaction') + ->disableOriginalConstructor() + ->getMock(); + $transaction->expects(static::never())->method('send'); + + $response = new Response(); + $middlewareStack = new MiddlewareRunner(array( + new BrowserMiddlewareStub($response) + ), $transaction); + + $request = new ServerRequest('GET', 'http://example.com/'); + + $actualResponse = $middlewareStack($request); + $this->assertSame($response, $actualResponse); + } +} diff --git a/tests/BrowserMiddlewareStub.php b/tests/BrowserMiddlewareStub.php new file mode 100644 index 00000000..2d9e4ac5 --- /dev/null +++ b/tests/BrowserMiddlewareStub.php @@ -0,0 +1,29 @@ +response = $response; + } + + /** + * @param RequestInterface $request + * @param callable $next + * @return PromiseInterface|ResponseInterface + */ + public function __invoke(RequestInterface $request, callable $next) + { + return $this->response; + } +}