Skip to content

Commit

Permalink
Merge pull request #8 from friends-of-reactphp/fibers
Browse files Browse the repository at this point in the history
Use fibers instead of monkey patching
  • Loading branch information
WyriHaximus authored Aug 14, 2022
2 parents cce5223 + e2bdaa7 commit 078431f
Show file tree
Hide file tree
Showing 11 changed files with 68 additions and 323 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ jobs:
strategy:
matrix:
php:
- 7.2
- 7.1
- 7.0
- 8.1
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
Expand Down
44 changes: 8 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![CI status](https://github.com/friends-of-reactphp/http-middleware-psr15-adapter/workflows/CI/badge.svg)](https://github.com/friends-of-reactphp/http-middleware-psr15-adapter/actions)

Wraps PSR-15 middleware into coroutines using [`RecoilPHP`](https://github.com/recoilphp) making them usable within `react/http` as middleware.
Wraps PSR-15 middleware using `async` and `await` from `react/async` utilizing fibers making them usable within `react/http` as middleware.

# Install

Expand All @@ -18,27 +18,13 @@ The following usage example uses [`middlewares/redirect`](https://github.com/mid
and using the callback to call several methods on the redirect middleware to change it's behavior:

```php
$loop = Factory::create();
$server = new Server([
$server = new Server(
/** Other middleware */
new PSR15Middleware(
$loop, // The react/event-loop (required)
Redirect::class, // String class name of the middleware (required)
[ // Any constructor arguments (optional)
['/old-url' => '/new-url']
],
function ($redirectMiddleware) {
// This callback is optional, but when used it must return the
// instance passed into it, or a clone of it.
return $redirectMiddleware
->permanent(false)
->query(false)
->method(['GET', 'POST'])
;
}
(new Redirect(['/old-url' => '/new-url']))->permanent(false)->query(false)->method(['GET', 'POST'])
),
/** Other middleware */
]);
);
```

# Grouped Usage
Expand All @@ -51,24 +37,10 @@ $loop = Factory::create();
$server = new Server([
/** Other middleware */
(new GroupedPSR15Middleware($loop))->withMiddleware(
Redirect::class,
[
['/old-url' => '/new-url']
],
function ($redirectMiddleware) {
return $redirectMiddleware
->permanent(false)
->query(false)
->method(['GET', 'POST'])
;
}
)->withMiddleware(Expires::class),
(new Redirect(['/old-url' => '/new-url']))->permanent(false)->query(false)->method(['GET', 'POST'])
)->withMiddleware(
new Expires()
),
/** Other middleware */
]);
```

# Warning

This adapter rewrites the code of the PSR-15 middleware during the constructor phase, wrapping all `$delegate->process($request)`
calls into a yield `(yield $delegate->process($request))`. This should work for most middleware but cannot be guaranteed for all.
In case you run into issues please open an issue with the middleware in question you're having problems with.
13 changes: 5 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@
"keywords": ["HTTP", "HTTPS", "ReactPHP", "middleware", "PSR15"],
"license": "MIT",
"require": {
"php": "^7.0",
"recoil/react": "^1.0",
"recoil/recoil": "^1.0",
"nikic/php-parser": "^4.0 || ^3.1",
"psr/http-server-middleware": "^1.0"
"php": "^8.1",
"psr/http-server-middleware": "^1.0",
"react/async": "^4"
},
"require-dev": {
"phpunit/phpunit": "^6.0",
"clue/block-react": "^1.2",
"react/http": "^0.8.0"
"phpunit/phpunit": "^9.5",
"react/http": "^1.5"
},
"autoload": {
"psr-4": {
Expand Down
22 changes: 22 additions & 0 deletions src/AwaitRequestHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace FriendsOfReact\Http\Middleware\Psr15Adapter;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function React\Async\await;
use function React\Promise\resolve;

/**
* @internal
*/
final class AwaitRequestHandler implements RequestHandlerInterface
{
public function __construct(private \Closure $next) {}

public function handle(ServerRequestInterface $request): ResponseInterface
{
return await(resolve(($this->next)($request)));
}
}
76 changes: 12 additions & 64 deletions src/GroupedPSR15Middleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,86 +5,34 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface as PSR15MiddlewareInterface;
use React\EventLoop\LoopInterface;
use React\Promise;
use Recoil\React\ReactKernel;
use Throwable;
use function React\Async\async;

final class GroupedPSR15Middleware
{
/**
* @var ReactKernel
* @var array<PSR15MiddlewareInterface>
*/
private $kernel;
private array $middleware = [];

/**
* @var PSR15MiddlewareInterface[]
*/
private $middleware = [];

public function __construct(LoopInterface $loop)
public function withMiddleware(PSR15MiddlewareInterface $middleware): self
{
$this->kernel = ReactKernel::create($loop);
}

public function withMiddleware(string $middleware, array $arguments = [], callable $func = null)
{
if ($func === null) {
$func = function ($middleware) {
return $middleware;
};
}

$clone = clone $this;
$clone->middleware[] = $func(YieldingMiddlewareFactory::construct($middleware, $arguments));

$clone->middleware[] = $middleware;
return $clone;
}

public function __invoke(ServerRequestInterface $request, callable $next): Promise\PromiseInterface
{
return async(function (ServerRequestInterface $request, callable $next): ResponseInterface {
$middleware = array_reverse($this->middleware);
$requestHandler = new AwaitRequestHandler($next);

$stack = $this->createStack($next);

return new Promise\Promise(function ($resolve, $reject) use ($request, $next, $stack) {
$this->kernel->execute(function () use ($resolve, $reject, $request, $next, $stack) {
try {
$response = $stack($request, $next);
if ($response instanceof ResponseInterface) {
$response = Promise\resolve($response);
}
$response = (yield $response);
$resolve($response);
} catch (Throwable $throwable) {
$reject($throwable);
}
});
});
}

private function createStack($next)
{
$stack = function (ServerRequestInterface $request) use ($next) {
$response = $next($request);
if ($response instanceof ResponseInterface) {
$response = Promise\resolve($response);
foreach ($middleware as $mw) {
$requestHandler = new PassThroughRequestHandler(static fn (ServerRequestInterface $request): ResponseInterface => $mw->process($request, $requestHandler));
}
return (yield $response);
};

$middleware = $this->middleware;
$middleware = array_reverse($middleware);
foreach ($middleware as $mw) {
$mwh = $mw;
$stack = function (ServerRequestInterface $request) use ($stack, $mwh) {
$response = $mwh->process($request, new PassThroughRequestHandler($stack));
if ($response instanceof ResponseInterface) {
$response = Promise\resolve($response);
}
return (yield $response);
};
}

return $stack;
return $requestHandler->handle($request);
})($request, $next);
}
}
47 changes: 7 additions & 40 deletions src/PSR15Middleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,17 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface as PSR15MiddlewareInterface;
use React\EventLoop\LoopInterface;
use React\Promise;
use Recoil\React\ReactKernel;
use Throwable;
use React\Promise\PromiseInterface;
use function React\Async\async;

final class PSR15Middleware
{
/**
* @var ReactKernel
*/
private $kernel;
public function __construct(private PSR15MiddlewareInterface $middleware) {}

/**
* @var PSR15MiddlewareInterface
*/
private $middleware;

public function __construct(LoopInterface $loop, string $middleware, array $arguments = [], callable $func = null)
{
if ($func === null) {
$func = function ($middleware) {
return $middleware;
};
}

$this->kernel = ReactKernel::create($loop);
$this->middleware = $func(YieldingMiddlewareFactory::construct($middleware, $arguments));
}

public function __invoke(ServerRequestInterface $request, callable $next): Promise\PromiseInterface
public function __invoke(ServerRequestInterface $request, callable $next): PromiseInterface
{
return new Promise\Promise(function ($resolve, $reject) use ($request, $next) {
$this->kernel->execute(function () use ($resolve, $reject, $request, $next) {
try {
$response = $this->middleware->process($request, new RecoilWrappedRequestHandler($next));
if ($response instanceof ResponseInterface) {
$response = Promise\resolve($response);
}
$response = (yield $response);
$resolve($response);
} catch (Throwable $throwable) {
$reject($throwable);
}
});
});
return async(
fn (): ResponseInterface => $this->middleware->process($request, new AwaitRequestHandler($next))
)($request, $next);
}
}
22 changes: 6 additions & 16 deletions src/PassThroughRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,19 @@

namespace FriendsOfReact\Http\Middleware\Psr15Adapter;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* @internal
*/
final class PassThroughRequestHandler
final class PassThroughRequestHandler implements RequestHandlerInterface
{
/**
* @var callable
*/
private $next;
public function __construct(private \Closure $next) {}

/**
* @param callable $next
*/
public function __construct(callable $next)
public function handle(ServerRequestInterface $request): ResponseInterface
{
$this->next = $next;
}

public function handle(ServerRequestInterface $request)
{
$next = $this->next;
return $next($request);
return ($this->next)($request);
}
}
31 changes: 0 additions & 31 deletions src/RecoilWrappedRequestHandler.php

This file was deleted.

Loading

0 comments on commit 078431f

Please sign in to comment.