Skip to content

Commit aca973e

Browse files
committed
Automatically start new fiber for each request on PHP 8.1+
1 parent 58db825 commit aca973e

8 files changed

Lines changed: 539 additions & 36 deletions

File tree

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"react/promise": "^2.7"
1818
},
1919
"require-dev": {
20-
"phpunit/phpunit": "^9.5 || ^7.5"
20+
"phpunit/phpunit": "^9.5 || ^7.5",
21+
"react/async": "^4@dev || ^3@dev"
2122
},
2223
"autoload": {
2324
"psr-4": {

examples/index.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@
2424
);
2525
});
2626

27+
$app->get('/sleep/promise', function () {
28+
return React\Promise\Timer\sleep(0.1)->then(function () {
29+
return React\Http\Message\Response::plaintext("OK\n");
30+
});
31+
});
32+
$app->get('/sleep/coroutine', function () {
33+
yield React\Promise\Timer\sleep(0.1);
34+
return React\Http\Message\Response::plaintext("OK\n");
35+
});
36+
if (PHP_VERSION_ID >= 80100 && function_exists('React\Async\async')) { // requires PHP 8.1+ with react/async 4+
37+
$app->get('/sleep/fiber', function () {
38+
React\Async\await(React\Promise\Timer\sleep(0.1));
39+
return React\Http\Message\Response::plaintext("OK\n");
40+
});
41+
}
42+
2743
$app->get('/uri[/{path:.*}]', function (ServerRequestInterface $request) {
2844
return React\Http\Message\Response::plaintext(
2945
(string) $request->getUri() . "\n"

src/App.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,19 @@ public function __construct(...$middleware)
5252
}
5353
}
5454

55-
// new MiddlewareHandler([$accessLogHandler, $errorHandler, ...$middleware, $routeHandler])
55+
// new MiddlewareHandler([$fiberHandler, $accessLogHandler, $errorHandler, ...$middleware, $routeHandler])
5656
\array_unshift($middleware, $errorHandler);
5757

5858
// only log for built-in webserver and PHP development webserver by default, others have their own access log
5959
if (\PHP_SAPI === 'cli' || \PHP_SAPI === 'cli-server') {
6060
\array_unshift($middleware, new AccessLogHandler());
6161
}
6262

63+
// automatically start new fiber for each request on PHP 8.1+
64+
if (\PHP_VERSION_ID >= 80100) {
65+
\array_unshift($middleware, new FiberHandler()); // @codeCoverageIgnore
66+
}
67+
6368
$this->router = new RouteHandler($container);
6469
$middleware[] = $this->router;
6570
$this->handler = new MiddlewareHandler($middleware);

src/FiberHandler.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace FrameworkX;
4+
5+
use Psr\Http\Message\ResponseInterface;
6+
use Psr\Http\Message\ServerRequestInterface;
7+
use React\Promise\Deferred;
8+
use React\Promise\Promise;
9+
use React\Promise\PromiseInterface;
10+
11+
/**
12+
* [Internal] Fibers middleware handler to ensure each request is processed in a separate `Fiber`
13+
*
14+
* The `Fiber` class has been added in PHP 8.1+, so this middleware is only used
15+
* on PHP 8.1+. On supported PHP versions, this middleware is automatically
16+
* added to the list of middleware handlers, so there's no need to reference
17+
* this class in application code.
18+
*
19+
* @internal
20+
* @link https://framework-x.org/docs/async/fibers/
21+
*/
22+
class FiberHandler
23+
{
24+
/**
25+
* @return PromiseInterface<ResponseInterface,void>
26+
* Returns a promise that is fulfilled with a `ResponseInterface` on
27+
* success. This method never throws or resolves a rejected promise.
28+
* If the request can not be routed or the handler fails, it will be
29+
* turned into a valid error response before returning.
30+
* @throws void
31+
*/
32+
public function __invoke(ServerRequestInterface $request, callable $next): PromiseInterface
33+
{
34+
return new Promise(function ($resolve) use ($next, $request) {
35+
$fiber = new \Fiber(function () use ($resolve, $next, $request) {
36+
$response = $next($request);
37+
if ($response instanceof \Generator) {
38+
$response = $this->coroutine($response);
39+
}
40+
41+
$resolve($response);
42+
});
43+
$fiber->start();
44+
});
45+
}
46+
47+
private function coroutine(\Generator $generator): PromiseInterface
48+
{
49+
$next = null;
50+
$deferred = new Deferred();
51+
$next = function () use ($generator, &$next, $deferred) {
52+
if (!$generator->valid()) {
53+
$deferred->resolve($generator->getReturn());
54+
return;
55+
}
56+
57+
$promise = $generator->current();
58+
$promise->then(function ($value) use ($generator, $next) {
59+
$generator->send($value);
60+
$next();
61+
}, function ($reason) use ($generator, $next) {
62+
$generator->throw($reason);
63+
$next();
64+
});
65+
};
66+
67+
$next();
68+
69+
return $deferred->promise();
70+
}
71+
}

tests/AppMiddlewareTest.php

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

33
namespace FrameworkX\Tests;
44

5+
use FrameworkX\AccessLogHandler;
56
use FrameworkX\App;
7+
use FrameworkX\FiberHandler;
68
use FrameworkX\RouteHandler;
79
use PHPUnit\Framework\TestCase;
810
use Psr\Http\Message\ResponseInterface;
@@ -169,7 +171,7 @@ public function testMapMethodWithMiddlewareAddsGivenMethodsOnRouter()
169171

170172
public function testMiddlewareCallsNextReturnsResponseFromRouter()
171173
{
172-
$app = $this->createAppWithoutLogger();
174+
$app = $this->createAppWithoutFibersOrLogger();
173175

174176
$middleware = function (ServerRequestInterface $request, callable $next) {
175177
return $next($request);
@@ -203,7 +205,7 @@ public function testMiddlewareCallsNextReturnsResponseFromRouter()
203205

204206
public function testMiddlewareCallsNextWithModifiedRequestReturnsResponseFromRouter()
205207
{
206-
$app = $this->createAppWithoutLogger();
208+
$app = $this->createAppWithoutFibersOrLogger();
207209

208210
$middleware = function (ServerRequestInterface $request, callable $next) {
209211
return $next($request->withAttribute('name', 'Alice'));
@@ -237,7 +239,7 @@ public function testMiddlewareCallsNextWithModifiedRequestReturnsResponseFromRou
237239

238240
public function testMiddlewareCallsNextReturnsResponseModifiedInMiddlewareFromRouter()
239241
{
240-
$app = $this->createAppWithoutLogger();
242+
$app = $this->createAppWithoutFibersOrLogger();
241243

242244
$middleware = function (ServerRequestInterface $request, callable $next) {
243245
$response = $next($request);
@@ -364,7 +366,7 @@ public function testMiddlewareCallsNextReturnsCoroutineResponseModifiedInMiddlew
364366

365367
public function testMiddlewareCallsNextWhichThrowsExceptionReturnsInternalServerErrorResponse()
366368
{
367-
$app = $this->createAppWithoutLogger();
369+
$app = $this->createAppWithoutFibersOrLogger();
368370

369371
$middleware = function (ServerRequestInterface $request, callable $next) {
370372
return $next($request);
@@ -395,7 +397,7 @@ public function testMiddlewareCallsNextWhichThrowsExceptionReturnsInternalServer
395397

396398
public function testMiddlewareWhichThrowsExceptionReturnsInternalServerErrorResponse()
397399
{
398-
$app = $this->createAppWithoutLogger();
400+
$app = $this->createAppWithoutFibersOrLogger();
399401

400402
$line = __LINE__ + 2;
401403
$middleware = function (ServerRequestInterface $request, callable $next) {
@@ -424,7 +426,7 @@ public function testMiddlewareWhichThrowsExceptionReturnsInternalServerErrorResp
424426

425427
public function testGlobalMiddlewareCallsNextReturnsResponseFromController()
426428
{
427-
$app = $this->createAppWithoutLogger(function (ServerRequestInterface $request, callable $next) {
429+
$app = $this->createAppWithoutFibersOrLogger(function (ServerRequestInterface $request, callable $next) {
428430
return $next($request);
429431
});
430432

@@ -461,7 +463,7 @@ public function __invoke(ServerRequestInterface $request, callable $next)
461463
}
462464
};
463465

464-
$app = $this->createAppWithoutLogger($middleware);
466+
$app = $this->createAppWithoutFibersOrLogger($middleware);
465467

466468
$app->get('/', function () {
467469
return new Response(
@@ -496,7 +498,7 @@ public function __invoke(ServerRequestInterface $request, callable $next)
496498
}
497499
};
498500

499-
$app = $this->createAppWithoutLogger(get_class($middleware));
501+
$app = $this->createAppWithoutFibersOrLogger(get_class($middleware));
500502

501503
$app->get('/', function () {
502504
return new Response(
@@ -532,7 +534,7 @@ public function __invoke(ServerRequestInterface $request, callable $next)
532534
}
533535
};
534536

535-
$app = $this->createAppWithoutLogger(get_class($middleware));
537+
$app = $this->createAppWithoutFibersOrLogger(get_class($middleware));
536538

537539
$app->get('/', get_class($middleware), function (ServerRequestInterface $request) {
538540
return new Response(
@@ -560,7 +562,7 @@ public function __invoke(ServerRequestInterface $request, callable $next)
560562

561563
public function testGlobalMiddlewareCallsNextWithModifiedRequestWillBeUsedForRouting()
562564
{
563-
$app = $this->createAppWithoutLogger(function (ServerRequestInterface $request, callable $next) {
565+
$app = $this->createAppWithoutFibersOrLogger(function (ServerRequestInterface $request, callable $next) {
564566
return $next($request->withUri($request->getUri()->withPath('/users')));
565567
});
566568

@@ -590,7 +592,7 @@ public function testGlobalMiddlewareCallsNextWithModifiedRequestWillBeUsedForRou
590592

591593
public function testGlobalMiddlewareCallsNextReturnsModifiedResponseWhenModifyingResponseFromRouter()
592594
{
593-
$app = $this->createAppWithoutLogger(function (ServerRequestInterface $request, callable $next) {
595+
$app = $this->createAppWithoutFibersOrLogger(function (ServerRequestInterface $request, callable $next) {
594596
$response = $next($request);
595597
assert($response instanceof ResponseInterface);
596598

@@ -621,7 +623,7 @@ public function testGlobalMiddlewareCallsNextReturnsModifiedResponseWhenModifyin
621623

622624
public function testGlobalMiddlewareReturnsResponseWithoutCallingNextReturnsResponseWithoutCallingRouter()
623625
{
624-
$app = $this->createAppWithoutLogger(function () {
626+
$app = $this->createAppWithoutFibersOrLogger(function () {
625627
return new Response(
626628
200,
627629
[
@@ -788,8 +790,46 @@ private function createAppWithoutLogger(...$middleware): App
788790
$ref->setAccessible(true);
789791
$handlers = $ref->getValue($middleware);
790792

791-
unset($handlers[0]);
792-
$ref->setValue($middleware, array_values($handlers));
793+
if (PHP_VERSION_ID >= 80100) {
794+
$first = array_shift($handlers);
795+
$this->assertInstanceOf(FiberHandler::class, $first);
796+
797+
$next = array_shift($handlers);
798+
$this->assertInstanceOf(AccessLogHandler::class, $next);
799+
800+
array_unshift($handlers, $next, $first);
801+
}
802+
803+
$first = array_shift($handlers);
804+
$this->assertInstanceOf(AccessLogHandler::class, $first);
805+
806+
$ref->setValue($middleware, $handlers);
807+
808+
return $app;
809+
}
810+
811+
/** @param callable|class-string ...$middleware */
812+
private function createAppWithoutFibersOrLogger(...$middleware): App
813+
{
814+
$app = new App(...$middleware);
815+
816+
$ref = new \ReflectionProperty($app, 'handler');
817+
$ref->setAccessible(true);
818+
$middleware = $ref->getValue($app);
819+
820+
$ref = new \ReflectionProperty($middleware, 'handlers');
821+
$ref->setAccessible(true);
822+
$handlers = $ref->getValue($middleware);
823+
824+
if (PHP_VERSION_ID >= 80100) {
825+
$first = array_shift($handlers);
826+
$this->assertInstanceOf(FiberHandler::class, $first);
827+
}
828+
829+
$first = array_shift($handlers);
830+
$this->assertInstanceOf(AccessLogHandler::class, $first);
831+
832+
$ref->setValue($middleware, $handlers);
793833

794834
return $app;
795835
}

0 commit comments

Comments
 (0)