Skip to content

Commit c8a4cdf

Browse files
authored
Merge pull request #128 from clue-labs/fiber-compatibility
Add fiber compatibility mode for PHP < 8.1
2 parents cfe0174 + 528aaaf commit c8a4cdf

File tree

6 files changed

+132
-43
lines changed

6 files changed

+132
-43
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
- uses: shivammathur/setup-php@v2
5050
with:
5151
php-version: ${{ matrix.php }}
52-
- run: composer install --no-dev
52+
- run: composer remove --dev phpunit/phpunit
5353
- run: php examples/index.php &
5454
- run: bash tests/await.sh
5555
- run: bash tests/acceptance.sh
@@ -71,7 +71,7 @@ jobs:
7171
- uses: shivammathur/setup-php@v2
7272
with:
7373
php-version: ${{ matrix.php }}
74-
- run: composer install --no-dev
74+
- run: composer remove --dev phpunit/phpunit
7575
- run: docker run -d -v "$PWD":/home/framework-x php:${{ matrix.php }}-fpm
7676
- run: docker run -d -p 80:80 --link $(docker ps -qn1):php -v "$PWD":/home/framework-x -v "$PWD"/examples/nginx/nginx.conf:/etc/nginx/conf.d/default.conf nginx:stable-alpine
7777
- run: bash tests/await.sh http://localhost
@@ -94,7 +94,7 @@ jobs:
9494
- uses: shivammathur/setup-php@v2
9595
with:
9696
php-version: ${{ matrix.php }}
97-
- run: composer install --no-dev
97+
- run: composer remove --dev phpunit/phpunit
9898
- run: docker run -d -p 80:80 -v "$PWD":/home/framework-x php:${{ matrix.php }}-apache sh -c "rmdir /var/www/html;ln -s /home/framework-x/examples/apache /var/www/html;ln -s /etc/apache2/mods-available/rewrite.load /etc/apache2/mods-enabled; apache2-foreground"
9999
- run: bash tests/await.sh http://localhost
100100
- run: bash tests/acceptance.sh http://localhost
@@ -116,7 +116,7 @@ jobs:
116116
- uses: shivammathur/setup-php@v2
117117
with:
118118
php-version: ${{ matrix.php }}
119-
- run: composer install --no-dev
119+
- run: composer remove --dev phpunit/phpunit
120120
- run: php -S localhost:8080 examples/index.php &
121121
- run: bash tests/await.sh
122122
- run: bash tests/acceptance.sh

docs/async/fibers.md

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,6 @@ return value.
4343

4444
## Requirements
4545

46-
> ⚠️ **Feature preview**
47-
>
48-
> This is a feature preview, i.e. it might not have made it into the current beta.
49-
> Give feedback to help us prioritize.
50-
> We also welcome [contributors](../getting-started/community.md) to help out!
51-
5246
At the moment, fibers are available as a development version by installing
5347
[react/async](https://github.com/reactphp/async) from a development branch
5448
like this:
@@ -59,24 +53,9 @@ $ composer require react/async:^4@dev
5953

6054
Installing this package version requires PHP 8.1+ (2021-11-25) as fibers are a
6155
core ingredient of PHP 8.1+. We understand that adoption of this very new PHP
62-
version is going to take some time, so we acknowledge that this is probably one
63-
of the largest limitations of using fibers at the moment.
64-
65-
But don't worry, we're committed to providing long-term support (LTS) options
66-
and providing a smooth upgrade path. As such, we also provide limited support
67-
for older PHP versions using a compatible API without taking advantage of newer
68-
language features. By installing the v3 development version of this package, the
69-
same `await()` syntax also works on PHP 7.1+ to some degree if you only have
70-
limited concurrency. You can install either supported development version like
71-
this:
72-
73-
```bash
74-
$ composer require react/async:"^4@dev || ^3@dev"
75-
```
76-
77-
This way, you have a much smoother upgrade path, as you can already start using
78-
the future API for testing and development purposes and upgrade your PHP version
79-
for production use at a later time.
56+
version is going to take some time, so we also provide a limited
57+
[compatibility mode](#compatibility-mode) that also works on PHP 7.1+ to ease
58+
upgrading.
8059

8160
> ℹ️ **Coroutines and Promises work anywhere**
8261
>
@@ -145,6 +124,56 @@ Coroutines allow consuming async APIs in a way that resembles a synchronous
145124
code flow using the `yield` keyword. You can also directly use promises as a
146125
core building block used in all our async APIs for maximum performance.
147126

127+
### Compatibility mode
128+
129+
Fibers are a core ingredient of PHP 8.1+, but the same syntax also works on
130+
older PHP versions to some degree if you only have limited concurrency.
131+
132+
For production usage, we highly recommend using PHP 8.1+. At the moment, fibers
133+
are available as a development version by installing
134+
[react/async](https://github.com/reactphp/async) from a development branch
135+
like this:
136+
137+
```bash
138+
$ composer require react/async:^4@dev
139+
```
140+
141+
Installing this package version requires PHP 8.1+ (2021-11-25) as fibers are a
142+
core ingredient of PHP 8.1+. We understand that adoption of this very new PHP
143+
version is going to take some time, so we acknowledge that this is probably one
144+
of the largest limitations of using fibers at the moment.
145+
146+
But don't worry, we're committed to providing long-term support (LTS) options
147+
and providing a smooth upgrade path. As such, we also provide limited support
148+
for older PHP versions using a compatible API without taking advantage of newer
149+
language features. By installing the v3 development version of this package, the
150+
same `await()` syntax also works on PHP 7.1+ to some degree if you only have
151+
limited concurrency. You can install either supported development version like
152+
this:
153+
154+
```bash
155+
$ composer require react/async:"^4@dev || ^3@dev"
156+
```
157+
158+
This way, you have a much smoother upgrade path, as you can already start using
159+
the future API for testing and development purposes and upgrade your PHP version
160+
for production use at a later time.
161+
162+
> ⚠️ **Production usage**
163+
>
164+
> For production usage, we highly recommend using PHP 8.1+. If you're using the
165+
> `await()` function in compatibility mode, it may stop the loop from running and
166+
> may print a warning like this:
167+
>
168+
> ```
169+
> Warning: Loop restarted. Upgrade to react/async v4 recommended […]
170+
> ```
171+
>
172+
> Internally, the compatibility mode will cause recursive loop executions when
173+
> dealing with concurrent requests. This should work fine for development
174+
> purposes and fast controllers with low concurrency, but may cause issues in
175+
> production with high concurrency.
176+
148177
### How do fibers work?
149178
150179
Fibers are a means of creating code blocks that can be paused and resumed, but

examples/index.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,19 @@
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-
});
27+
$app->get('/sleep/fiber', function () {
28+
React\Async\await(React\Promise\Timer\sleep(0.1));
29+
return React\Http\Message\Response::plaintext("OK\n");
3130
});
3231
$app->get('/sleep/coroutine', function () {
3332
yield React\Promise\Timer\sleep(0.1);
3433
return React\Http\Message\Response::plaintext("OK\n");
3534
});
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));
35+
$app->get('/sleep/promise', function () {
36+
return React\Promise\Timer\sleep(0.1)->then(function () {
3937
return React\Http\Message\Response::plaintext("OK\n");
4038
});
41-
}
39+
});
4240

4341
$app->get('/uri[/{path:.*}]', function (ServerRequestInterface $request) {
4442
return React\Http\Message\Response::plaintext(

src/App.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,6 @@ public function run()
180180
} else {
181181
$this->runOnce(); // @codeCoverageIgnore
182182
}
183-
184-
Loop::run();
185183
}
186184

187185
private function runLoop()
@@ -208,6 +206,13 @@ private function runLoop()
208206

209207
\fwrite(STDERR, (string)$orig);
210208
});
209+
210+
do {
211+
Loop::run();
212+
213+
// Fiber compatibility mode for PHP < 8.1: Restart loop as long as socket is available
214+
$this->sapi->log('Warning: Loop restarted. Upgrade to react/async v4 recommended for production use.');
215+
} while ($socket->getAddress() !== null);
211216
}
212217

213218
private function runOnce()
@@ -223,6 +228,8 @@ private function runOnce()
223228
$this->sapi->sendResponse($response);
224229
});
225230
}
231+
232+
Loop::run();
226233
}
227234

228235
/**

tests/AppTest.php

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ public function testRunWillReportListeningAddressAndRunLoopWithSocketServer()
171171
fclose($socket);
172172
});
173173

174-
$this->expectOutputRegex('/' . preg_quote('Listening on http://127.0.0.1:8080' . PHP_EOL, '/') . '$/');
174+
$this->expectOutputRegex('/' . preg_quote('Listening on http://127.0.0.1:8080' . PHP_EOL, '/') . '.*/');
175175
$app->run();
176176
}
177177

@@ -193,7 +193,7 @@ public function testRunWillReportListeningAddressFromEnvironmentAndRunLoopWithSo
193193
fclose($socket);
194194
});
195195

196-
$this->expectOutputRegex('/' . preg_quote('Listening on http://' . $addr . PHP_EOL, '/') . '$/');
196+
$this->expectOutputRegex('/' . preg_quote('Listening on http://' . $addr . PHP_EOL, '/') . '.*/');
197197
$app->run();
198198
}
199199

@@ -211,7 +211,7 @@ public function testRunWillReportListeningAddressFromEnvironmentWithRandomPortAn
211211
fclose($socket);
212212
});
213213

214-
$this->expectOutputRegex('/' . preg_quote('Listening on http://127.0.0.1:', '/') . '\d+' . PHP_EOL . '$/');
214+
$this->expectOutputRegex('/' . preg_quote('Listening on http://127.0.0.1:', '/') . '\d+' . PHP_EOL . '.*/');
215215
$app->run();
216216
}
217217

@@ -838,6 +838,34 @@ public function testHandleRequestWithMatchingRouteReturnsPendingPromiseWhenHandl
838838
$this->assertFalse($resolved);
839839
}
840840

841+
public function testHandleRequestWithMatchingRouteReturnsResponseWhenHandlerReturnsResponseAfterAwaitingPromiseResolvingWithResponse()
842+
{
843+
$app = $this->createAppWithoutLogger();
844+
845+
$app->get('/users', function () {
846+
return await(resolve(new Response(
847+
200,
848+
[
849+
'Content-Type' => 'text/html'
850+
],
851+
"OK\n"
852+
)));
853+
});
854+
855+
$request = new ServerRequest('GET', 'http://localhost/users');
856+
857+
// $response = $app->handleRequest($request);
858+
$ref = new ReflectionMethod($app, 'handleRequest');
859+
$ref->setAccessible(true);
860+
$response = $ref->invoke($app, $request);
861+
862+
/** @var ResponseInterface $response */
863+
$this->assertInstanceOf(ResponseInterface::class, $response);
864+
$this->assertEquals(200, $response->getStatusCode());
865+
$this->assertEquals('text/html', $response->getHeaderLine('Content-Type'));
866+
$this->assertEquals("OK\n", (string) $response->getBody());
867+
}
868+
841869
public function testHandleRequestWithMatchingRouteReturnsPromiseResolvingWithResponseWhenHandlerReturnsResponseAfterAwaitingPromiseResolvingWithResponse()
842870
{
843871
if (PHP_VERSION_ID < 80100 || !function_exists('React\Async\async')) {
@@ -1111,6 +1139,33 @@ public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWit
11111139
$this->assertStringContainsString("<p>Expected request handler to return <code>Psr\Http\Message\ResponseInterface</code> but got uncaught <code>RuntimeException</code> with message <code>Foo</code> in <code title=\"See " . __FILE__ . " line $line\">AppTest.php:$line</code>.</p>\n", (string) $response->getBody());
11121140
}
11131141

1142+
public function testHandleRequestWithMatchingRouteReturnsInternalServerErrorResponseWhenHandlerThrowsAfterAwaitingPromiseRejectingWithException()
1143+
{
1144+
$app = $this->createAppWithoutLogger();
1145+
1146+
$line = __LINE__ + 2;
1147+
$app->get('/users', function () {
1148+
return await(reject(new \RuntimeException('Foo')));
1149+
});
1150+
1151+
$request = new ServerRequest('GET', 'http://localhost/users');
1152+
1153+
// $response = $app->handleRequest($request);
1154+
$ref = new ReflectionMethod($app, 'handleRequest');
1155+
$ref->setAccessible(true);
1156+
$response = $ref->invoke($app, $request);
1157+
1158+
/** @var ResponseInterface $response */
1159+
$this->assertInstanceOf(ResponseInterface::class, $response);
1160+
$this->assertEquals(500, $response->getStatusCode());
1161+
$this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type'));
1162+
$this->assertStringMatchesFormat("<!DOCTYPE html>\n<html>%a</html>\n", (string) $response->getBody());
1163+
1164+
$this->assertStringContainsString("<title>Error 500: Internal Server Error</title>\n", (string) $response->getBody());
1165+
$this->assertStringContainsString("<p>The requested page failed to load, please try again later.</p>\n", (string) $response->getBody());
1166+
$this->assertStringContainsString("<p>Expected request handler to return <code>Psr\Http\Message\ResponseInterface</code> but got uncaught <code>RuntimeException</code> with message <code>Foo</code> in <code title=\"See " . __FILE__ . " line $line\">AppTest.php:$line</code>.</p>\n", (string) $response->getBody());
1167+
}
1168+
11141169
public function testHandleRequestWithMatchingRouteReturnsPromiseWhichFulfillsWithInternalServerErrorResponseWhenHandlerThrowsAfterAwaitingPromiseRejectingWithException()
11151170
{
11161171
if (PHP_VERSION_ID < 80100 || !function_exists('React\Async\async')) {

tests/acceptance.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ out=$(curl -v $base/ 2>&1 -X POST); match "HTTP/.* 405"
2626
out=$(curl -v $base/error 2>&1); match "HTTP/.* 500" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]" && match "<code>Unable to load error</code>"
2727
out=$(curl -v $base/error/null 2>&1); match "HTTP/.* 500" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
2828

29-
out=$(curl -v $base/sleep/promise 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
29+
out=$(curl -v $base/sleep/fiber 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
3030
out=$(curl -v $base/sleep/coroutine 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
31-
out=$(curl -v $base/sleep/fiber 2>&1); skipif "HTTP/.* 404" && match "HTTP/.* 200" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]" # skip PHP < 8.1
31+
out=$(curl -v $base/sleep/promise 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
3232

3333
out=$(curl -v $base/uri 2>&1); match "HTTP/.* 200" && match "$base/uri"
3434
out=$(curl -v $base/uri/ 2>&1); match "HTTP/.* 200" && match "$base/uri/"

0 commit comments

Comments
 (0)