diff --git a/src/Io/FiberHandler.php b/src/Io/FiberHandler.php index 5788430..ab39467 100644 --- a/src/Io/FiberHandler.php +++ b/src/Io/FiberHandler.php @@ -46,8 +46,15 @@ public function __invoke(ServerRequestInterface $request, callable $next) // if the next request handler suspends the fiber, we only reach this point after resuming the fiber, so the code below will have assigned a Deferred /** @var ?Deferred $deferred */ if ($deferred !== null) { - \assert($response instanceof ResponseInterface); - $deferred->resolve($response); + \assert($response instanceof ResponseInterface || $response instanceof PromiseInterface); + + // if the next request handler returns a promise after resuming the fiber, the deferred must follow it + if ($response instanceof PromiseInterface) { + /** @var PromiseInterface $response */ + $response->then([$deferred, 'resolve'], [$deferred, 'reject']); + } else { + $deferred->resolve($response); + } } return $response; diff --git a/tests/Io/FiberHandlerTest.php b/tests/Io/FiberHandlerTest.php index cb354f7..0692769 100644 --- a/tests/Io/FiberHandlerTest.php +++ b/tests/Io/FiberHandlerTest.php @@ -176,4 +176,107 @@ public function testInvokeWithHandlerReturningResponseAfterAwaitingPendingPromis $this->assertSame($response, $ret); } + + public function testInvokeWithHandlerReturningPromiseAfterAwaitingPendingPromiseReturnsPromiseResolvingWithSameResponse(): void + { + // work around lack of actual fibers in PHP < 8.1 + if (\method_exists(\Fiber::class, 'mockSuspend')) { + \Fiber::mockSuspend(); + } + + $handler = new FiberHandler(); + + $request = new ServerRequest('GET', 'http://example.com/'); + $response = new Response(); + + $deferred = new Deferred(); + + $promise = $handler($request, function () use ($deferred, $response) { + // going the extra mile if using reactphp/async < 4 on PHP 8.1+ + if (PHP_VERSION_ID >= 80100 && !function_exists('React\Async\async')) { + $fiber = \Fiber::getCurrent(); + assert($fiber instanceof \Fiber); + $deferred->promise()->then(function () use ($fiber): void { + $fiber->resume(); + }); + \Fiber::suspend(); + } else { + await($deferred->promise()); + } + + return resolve($response); + }); + + /** @var PromiseInterface $promise */ + $this->assertInstanceOf(PromiseInterface::class, $promise); + + $ret = null; + $promise->then(function ($value) use (&$ret) { + $ret = $value; + }); + + $this->assertNull($ret); + + $deferred->resolve(null); + + // work around lack of actual fibers in PHP < 8.1 + if (\method_exists(\Fiber::class, 'mockResume')) { + \Fiber::mockResume(); + } + + $this->assertSame($response, $ret); + } + + public function testInvokeWithHandlerReturningPendingPromiseAfterAwaitingPendingPromiseReturnsPromiseResolvingWithSameResponseOnceFulfilled(): void + { + // work around lack of actual fibers in PHP < 8.1 + if (\method_exists(\Fiber::class, 'mockSuspend')) { + \Fiber::mockSuspend(); + } + + $handler = new FiberHandler(); + + $request = new ServerRequest('GET', 'http://example.com/'); + $response = new Response(); + + $deferred = new Deferred(); + $pending = new Deferred(); + + $promise = $handler($request, function () use ($deferred, $pending) { + // going the extra mile if using reactphp/async < 4 on PHP 8.1+ + if (PHP_VERSION_ID >= 80100 && !function_exists('React\Async\async')) { + $fiber = \Fiber::getCurrent(); + assert($fiber instanceof \Fiber); + $deferred->promise()->then(function () use ($fiber): void { + $fiber->resume(); + }); + \Fiber::suspend(); + } else { + await($deferred->promise()); + } + + return $pending->promise(); + }); + + /** @var PromiseInterface $promise */ + $this->assertInstanceOf(PromiseInterface::class, $promise); + + $ret = null; + $promise->then(function ($value) use (&$ret) { + $ret = $value; + }); + + $deferred->resolve(null); + + // work around lack of actual fibers in PHP < 8.1 + if (\method_exists(\Fiber::class, 'mockResume')) { + \Fiber::mockResume(); + } + + $this->assertNull($ret); + + $pending->resolve($response); + + $this->assertSame($response, $ret); + } }