diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 544964d..1b83b36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index be9ea92..a0640b9 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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. diff --git a/composer.json b/composer.json index 5b9f626..b1eabd1 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/src/AwaitRequestHandler.php b/src/AwaitRequestHandler.php new file mode 100644 index 0000000..f610884 --- /dev/null +++ b/src/AwaitRequestHandler.php @@ -0,0 +1,22 @@ +next)($request))); + } +} diff --git a/src/GroupedPSR15Middleware.php b/src/GroupedPSR15Middleware.php index 3a9e4c0..a63d927 100644 --- a/src/GroupedPSR15Middleware.php +++ b/src/GroupedPSR15Middleware.php @@ -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 */ - 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); } } diff --git a/src/PSR15Middleware.php b/src/PSR15Middleware.php index 371bf7e..2a896c7 100644 --- a/src/PSR15Middleware.php +++ b/src/PSR15Middleware.php @@ -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); } } diff --git a/src/PassThroughRequestHandler.php b/src/PassThroughRequestHandler.php index 9a2e1c0..bc1c74b 100644 --- a/src/PassThroughRequestHandler.php +++ b/src/PassThroughRequestHandler.php @@ -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); } } diff --git a/src/RecoilWrappedRequestHandler.php b/src/RecoilWrappedRequestHandler.php deleted file mode 100644 index 12a8c40..0000000 --- a/src/RecoilWrappedRequestHandler.php +++ /dev/null @@ -1,31 +0,0 @@ -next = $next; - } - - public function handle(ServerRequestInterface $request) - { - $next = $this->next; - return (yield resolve($next($request))); - } -} diff --git a/src/YieldingMiddlewareFactory.php b/src/YieldingMiddlewareFactory.php deleted file mode 100644 index d69c6a7..0000000 --- a/src/YieldingMiddlewareFactory.php +++ /dev/null @@ -1,118 +0,0 @@ -findFile($middleware)); - break; - } - } - - if (!isset($file)) { - throw new \Exception('Could not find composer loader'); - } - - $dir = realpath(dirname($file)); - $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); - $stmts = $parser->parse(file_get_contents($file)); - $stmts = self::iterateStmts($stmts, $dir, $file); - $prettyPrinter = new Standard(); - $code = $prettyPrinter->prettyPrint($stmts); - - $namespace = explode('\\', $middleware); - $className = array_pop($namespace); - $newClassName = str_replace('.', '_', uniqid($className . '_', true)); - $FQCN = implode('\\', $namespace) . '\\' . $newClassName; - $code = str_replace('class ' . $className, 'class ' . $newClassName, $code); - eval($code); - return new $FQCN(...$arguments); - } - - private static function iterateStmts(array $stmts, string $dir, string $file): array - { - foreach ($stmts as &$stmt) { - if ($stmt instanceof Class_) { - $stmt->implements = []; - } - - if ($stmt instanceof ClassMethod && (string)$stmt->name === 'process') { - $stmt->returnType = null; - $stmt->params[1]->type = null; - } - - if (isset($stmt->stmts)) { - $stmt->stmts = static::iterateStmts($stmt->stmts, $dir, $file); - } - - $stmt = static::checkStmt($stmt, $dir, $file); - } - return $stmts; - } - - private static function checkStmt($stmt, string $dir, string $file) - { - if (isset($stmt->stmts)) { - $stmt->stmts = static::iterateStmts($stmt->stmts, $dir, $file); - } - - if (isset($stmt->expr)) { - $stmt->expr = static::checkStmt($stmt->expr, $dir, $file); - } - - if (isset($stmt->args)) { - $stmt->args = static::iterateStmts($stmt->args, $dir, $file); - } - - if ($stmt instanceof MethodCall) { - if ($stmt->var instanceof Variable && $stmt->var->name == 'handler' && $stmt->name == 'handle') { - return new Yield_($stmt); - } - $stmt->var = static::checkStmt($stmt->var, $dir, $file); - return $stmt; - } - - if ($stmt instanceof Dir) { - return new String_($dir); - } - - if ($stmt instanceof File) { - return new String_($file); - } - - if ($stmt instanceof Concat) { - $stmt->left = self::checkStmt($stmt->left, $dir, $file); - $stmt->right = self::checkStmt($stmt->right, $dir, $file); - return $stmt; - } - - if ($stmt instanceof Arg) { - $stmt->value = self::checkStmt($stmt->value, $dir, $file); - return $stmt; - } - - return $stmt; - } -} diff --git a/tests/PSR15MiddlewareTest.php b/tests/PSR15MiddlewareTest.php index 01c6b81..4b0c32c 100644 --- a/tests/PSR15MiddlewareTest.php +++ b/tests/PSR15MiddlewareTest.php @@ -2,28 +2,27 @@ namespace FriendsOfReact\Tests\Http\Middleware\Psr15Adapter; -use function Clue\React\Block\await; use FriendsOfReact\Http\Middleware\Psr15Adapter\PSR15Middleware; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; -use React\EventLoop\Factory; -use React\Http\Io\ServerRequest; -use function React\Promise\resolve; +use React\Http\Message\ServerRequest; use RingCentral\Psr7\Response; +use function React\Async\async; +use function React\Async\await; +use function React\Promise\resolve; final class PSR15MiddlewareTest extends TestCase { public function testBasic() { - $loop = Factory::create(); - $middleware = new PSR15Middleware($loop, PSR15TestMiddleware::class); + $middleware = new PSR15Middleware(new PSR15TestMiddleware()); $request = new ServerRequest('GET', 'https://example.com/'); $next = function () { return resolve(new Response()); }; /** @var ResponseInterface $response */ - $response = await($middleware($request, $next), $loop, 10); + $response = await(async(static fn (): mixed => await($middleware($request, $next)))()); self::assertSame(200, $response->getStatusCode()); self::assertSame('passed', $response->getHeaderLine('X-Test')); diff --git a/tests/PSR15TestMiddleware.php b/tests/PSR15TestMiddleware.php index 1896a61..f2d3504 100644 --- a/tests/PSR15TestMiddleware.php +++ b/tests/PSR15TestMiddleware.php @@ -15,6 +15,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $response = $handler->handle($request); $response = $response->withHeader('X-Test', 'passed'); $response = $response->withBody(stream_for('__DIR__:' . __DIR__ . ';__FILE__:' . __FILE__)); + return $response; } }