Skip to content

Commit e450527

Browse files
committed
Add FiberStub to achieve 100% code coverage for PHP < 8.1
1 parent b9c6b63 commit e450527

5 files changed

Lines changed: 181 additions & 12 deletions

File tree

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
"autoload-dev": {
3131
"psr-4": {
3232
"FrameworkX\\Tests\\": "tests/"
33-
}
33+
},
34+
"files": [
35+
"tests/FiberStub.php"
36+
]
3437
}
3538
}

phpstan.neon.dist

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,3 @@ parameters:
1010
ignoreErrors:
1111
# ignore generic usage like `PromiseInterface<ResponseInterface>` until fixed upstream
1212
- '/^PHPDoc tag @return contains generic type React\\Promise\\PromiseInterface<Psr\\Http\\Message\\ResponseInterface> but interface React\\Promise\\PromiseInterface is not generic\.$/'
13-
# ignore unknown `Fiber` class (PHP 8.1+)
14-
- '/^Instantiated class Fiber not found\.$/'
15-
- '/^Call to method (start|isTerminated|getReturn)\(\) on an unknown class Fiber\.$/'

src/Io/FiberHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class FiberHandler
3535
* be turned into a valid error response before returning.
3636
* @throws void
3737
*/
38-
public function __invoke(ServerRequestInterface $request, callable $next): mixed
38+
public function __invoke(ServerRequestInterface $request, callable $next)
3939
{
4040
$deferred = null;
4141
$fiber = new \Fiber(function () use ($request, $next, &$deferred) {

tests/FiberStub.php

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
if (!class_exists(Fiber::class)) {
4+
class Fiber
5+
{
6+
/** @var callable */
7+
private $callback;
8+
9+
/** @var mixed */
10+
private $return;
11+
12+
/** @var bool */
13+
private $started = false;
14+
15+
/** @var bool */
16+
private $terminated = false;
17+
18+
/** @var bool */
19+
private static $halt = false;
20+
21+
/** @var ?Fiber */
22+
private static $suspended = null;
23+
24+
public function __construct(callable $callback)
25+
{
26+
$this->callback = $callback;
27+
}
28+
29+
/**
30+
* @param mixed ...$args
31+
* @return mixed
32+
* @throws \Throwable
33+
*/
34+
public function start(...$args)
35+
{
36+
if ($this->started) {
37+
throw new \FiberError();
38+
}
39+
$this->started = true;
40+
41+
if (self::$halt) {
42+
assert(self::$suspended === null);
43+
self::$suspended = $this;
44+
return null;
45+
}
46+
47+
try {
48+
return $this->return = ($this->callback)(...$args);
49+
} finally {
50+
$this->terminated = true;
51+
}
52+
}
53+
54+
/**
55+
* @param mixed $value
56+
* @return mixed
57+
* @throws \BadMethodCallException
58+
*/
59+
public function resume($value = null)
60+
{
61+
throw new \BadMethodCallException();
62+
}
63+
64+
/**
65+
* @param Throwable $exception
66+
* @return mixed
67+
* @throws \BadMethodCallException
68+
*/
69+
public function throw(Throwable $exception)
70+
{
71+
throw new \BadMethodCallException();
72+
}
73+
74+
/**
75+
* @return mixed
76+
* @throws FiberError
77+
*/
78+
public function getReturn()
79+
{
80+
if (!$this->terminated) {
81+
throw new \FiberError();
82+
}
83+
84+
return $this->return;
85+
}
86+
87+
public function isStarted(): bool
88+
{
89+
return $this->started;
90+
}
91+
92+
public function isSuspended(): bool
93+
{
94+
return false;
95+
}
96+
97+
public function isRunning(): bool
98+
{
99+
return $this->started && !$this->terminated;
100+
}
101+
102+
public function isTerminated(): bool
103+
{
104+
return $this->terminated;
105+
}
106+
107+
/**
108+
* @param mixed $value
109+
* @return mixed
110+
* @throws \Throwable
111+
*/
112+
public static function suspend($value = null)
113+
{
114+
throw new \BadMethodCallException();
115+
}
116+
117+
public static function getCurrent(): ?Fiber
118+
{
119+
return null;
120+
}
121+
122+
/**
123+
* @internal
124+
*/
125+
public static function mockSuspend(): void
126+
{
127+
assert(self::$halt === false);
128+
self::$halt = true;
129+
}
130+
131+
/**
132+
* @internal
133+
* @throws void
134+
*/
135+
public static function mockResume(): void
136+
{
137+
assert(self::$halt === true);
138+
assert(self::$suspended instanceof self);
139+
140+
$fiber = self::$suspended;
141+
assert($fiber->started);
142+
assert(!$fiber->terminated);
143+
144+
self::$halt = false;
145+
self::$suspended = null;
146+
147+
/** @throws void */
148+
$fiber->return = ($fiber->callback)();
149+
$fiber->terminated = true;
150+
}
151+
}
152+
153+
final class FiberError extends Error {
154+
155+
}
156+
}

tests/Io/FiberHandlerTest.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,6 @@
1414

1515
class FiberHandlerTest extends TestCase
1616
{
17-
public function setUp(): void
18-
{
19-
if (PHP_VERSION_ID < 80100 || !function_exists('React\Async\async')) {
20-
$this->markTestSkipped('Requires PHP 8.1+ with react/async 4+');
21-
}
22-
}
23-
2417
public function testInvokeWithHandlerReturningResponseReturnsSameResponse(): void
2518
{
2619
$handler = new FiberHandler();
@@ -132,6 +125,11 @@ public function testInvokeWithHandlerReturningResponseAfterAwaitingResolvedPromi
132125

133126
public function testInvokeWithHandlerReturningResponseAfterAwaitingPendingPromiseReturnsPromiseResolvingWithSameResponse(): void
134127
{
128+
// work around lack of actual fibers in PHP < 8.1
129+
if (\method_exists(\Fiber::class, 'mockSuspend')) {
130+
\Fiber::mockSuspend();
131+
}
132+
135133
$handler = new FiberHandler();
136134

137135
$request = new ServerRequest('GET', 'http://example.com/');
@@ -140,6 +138,16 @@ public function testInvokeWithHandlerReturningResponseAfterAwaitingPendingPromis
140138
$deferred = new Deferred();
141139

142140
$promise = $handler($request, function () use ($deferred) {
141+
// going the extra mile if using reactphp/async < 4 on PHP 8.1+
142+
if (PHP_VERSION_ID >= 80100 && !function_exists('React\Async\async')) {
143+
$fiber = \Fiber::getCurrent();
144+
assert($fiber instanceof \Fiber);
145+
$deferred->promise()->then(function () use ($fiber): void {
146+
$fiber->resume();
147+
});
148+
\Fiber::suspend();
149+
}
150+
143151
return await($deferred->promise());
144152
});
145153

@@ -155,6 +163,11 @@ public function testInvokeWithHandlerReturningResponseAfterAwaitingPendingPromis
155163

156164
$deferred->resolve($response);
157165

166+
// work around lack of actual fibers in PHP < 8.1
167+
if (\method_exists(\Fiber::class, 'mockResume')) {
168+
\Fiber::mockResume();
169+
}
170+
158171
$this->assertSame($response, $ret);
159172
}
160173
}

0 commit comments

Comments
 (0)