Skip to content

Commit e98d4d8

Browse files
brettmcNyholm
andauthored
[react] runtime improvements (#118)
* allow configuring react host react config can now be controlled by REACT_xxx env/server vars, or from runtime config. This allows listening on interfaces other than 127.0.0.1, for example 0.0.0.0 when running under docker * refactor react setup making react runtime more testable, and adding some tests * simplify react by moving loop creation into factory * tidy * tidy * cs fixer * removing redundant setup code * moving run() from factory to runner * linting * handle container signals > docker and k8s send SIGTERM and/or SIGKILL when terminating a container, so exit gracefully * Remove first $loop->run() * Fixed tests Co-authored-by: Nyholm <[email protected]>
1 parent 113aa49 commit e98d4d8

File tree

8 files changed

+168
-25
lines changed

8 files changed

+168
-25
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,10 @@ return function (array $context) {
4242
return new Application($context['APP_ENV'] ?? 'dev');
4343
};
4444
```
45+
46+
## Options
47+
48+
| Option | Description | Default |
49+
| --- |--------------------------------------------------------------------------------------------|-------------|
50+
| `host` | The host where the server should bind to (precedes `REACT_HOST` environment variable) | `127.0.0.1` |
51+
| `port` | The port where the server should be listening (precedes `REACT_PORT` environment variable) | `8080` |

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
],
1212
"require": {
1313
"psr/http-server-handler": "^1.0",
14-
"react/http": "^1.2",
14+
"react/http": "^1.6",
1515
"symfony/runtime": "^5.3 || ^6.0"
1616
},
1717
"require-dev": {

src/Runner.php

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,23 @@
22

33
namespace Runtime\React;
44

5-
use Psr\Http\Message\ServerRequestInterface;
5+
use Psr\Http\Server\RequestHandlerInterface;
66
use Symfony\Component\Runtime\RunnerInterface;
77

88
class Runner implements RunnerInterface
99
{
10-
private $application;
11-
private $port;
10+
private RequestHandlerInterface $application;
11+
private ServerFactory $serverFactory;
1212

13-
public function __construct($application, $port)
13+
public function __construct(ServerFactory $serverFactory, RequestHandlerInterface $application)
1414
{
15+
$this->serverFactory = $serverFactory;
1516
$this->application = $application;
16-
$this->port = $port;
1717
}
1818

1919
public function run(): int
2020
{
21-
$application = $this->application;
22-
$loop = \React\EventLoop\Factory::create();
23-
24-
$server = new \React\Http\Server($loop, function (ServerRequestInterface $request) use ($application) {
25-
return $application->handle($request);
26-
});
27-
28-
$socket = new \React\Socket\Server($this->port, $loop);
29-
$server->listen($socket);
30-
21+
$loop = $this->serverFactory->createServer($this->application);
3122
$loop->run();
3223

3324
return 0;

src/Runtime.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,10 @@
1313
*/
1414
class Runtime extends GenericRuntime
1515
{
16-
private $port;
17-
18-
public function __construct(array $options)
19-
{
20-
$this->port = $options['port'] ?? 8080;
21-
parent::__construct($options);
22-
}
23-
2416
public function getRunner(?object $application): RunnerInterface
2517
{
2618
if ($application instanceof RequestHandlerInterface) {
27-
return new Runner($application, $this->port);
19+
return new Runner(new ServerFactory($this->options), $application);
2820
}
2921

3022
return parent::getRunner($application);

src/ServerFactory.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace Runtime\React;
4+
5+
use Psr\Http\Message\ServerRequestInterface;
6+
use Psr\Http\Server\RequestHandlerInterface;
7+
use React\EventLoop\Loop;
8+
use React\EventLoop\LoopInterface;
9+
use React\Http\HttpServer;
10+
use React\Socket\SocketServer;
11+
12+
class ServerFactory
13+
{
14+
private const DEFAULT_OPTIONS = [
15+
'host' => '127.0.0.1',
16+
'port' => 8080,
17+
];
18+
private array $options;
19+
20+
public static function getDefaultOptions(): array
21+
{
22+
return self::DEFAULT_OPTIONS;
23+
}
24+
25+
public function __construct(array $options = [])
26+
{
27+
$options['host'] = $options['host'] ?? $_SERVER['REACT_HOST'] ?? $_ENV['REACT_HOST'] ?? self::DEFAULT_OPTIONS['host'];
28+
$options['port'] = $options['port'] ?? $_SERVER['REACT_PORT'] ?? $_ENV['REACT_PORT'] ?? self::DEFAULT_OPTIONS['port'];
29+
30+
$this->options = array_replace_recursive(self::DEFAULT_OPTIONS, $options);
31+
}
32+
33+
public function createServer(RequestHandlerInterface $requestHandler): LoopInterface
34+
{
35+
$loop = Loop::get();
36+
$loop->addSignal(SIGTERM, function (int $signal) {
37+
exit(128 + $signal);
38+
});
39+
$loop->addSignal(SIGKILL, function (int $signal) {
40+
exit(128 + $signal);
41+
});
42+
$server = new HttpServer($loop, function (ServerRequestInterface $request) use ($requestHandler) {
43+
return $requestHandler->handle($request);
44+
});
45+
46+
$socket = new SocketServer(sprintf('%s:%s', $this->options['host'], $this->options['port']), [], $loop);
47+
$server->listen($socket);
48+
49+
return $loop;
50+
}
51+
52+
public function getOptions(): array
53+
{
54+
return $this->options;
55+
}
56+
}

tests/RunnerTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Runtime\React\Tests;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Psr\Http\Server\RequestHandlerInterface;
7+
use React\EventLoop\Loop;
8+
use React\EventLoop\LoopInterface;
9+
use React\Http\HttpServer;
10+
use Runtime\React\Runner;
11+
use Runtime\React\ServerFactory;
12+
13+
class RunnerTest extends TestCase
14+
{
15+
public function testRun(): void
16+
{
17+
$handler = function () {};
18+
$loop = $this->createMock(LoopInterface::class);
19+
Loop::set($loop);
20+
$server = new HttpServer($handler); //final, cannot be mocked
21+
$factory = $this->createMock(ServerFactory::class);
22+
$application = $this->createMock(RequestHandlerInterface::class);
23+
24+
$factory->expects(self::once())->method('createServer')->willReturn($loop);
25+
26+
$runner = new Runner($factory, $application);
27+
28+
self::assertSame(0, $runner->run());
29+
}
30+
}

tests/RuntimeTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Runtime\React\Tests;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Psr\Http\Server\RequestHandlerInterface;
7+
use Runtime\React\Runner;
8+
use Runtime\React\Runtime;
9+
10+
class RuntimeTest extends TestCase
11+
{
12+
public function testGetRunnerCreatesARunnerForRequestHandlers(): void
13+
{
14+
$options = [];
15+
$runtime = new Runtime($options);
16+
17+
$application = $this->createMock(RequestHandlerInterface::class);
18+
$runner = $runtime->getRunner($application);
19+
20+
self::assertInstanceOf(Runner::class, $runner);
21+
}
22+
}

tests/ServerFactoryTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Runtime\React\Tests;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Psr\Http\Server\RequestHandlerInterface;
7+
use React\EventLoop\Loop;
8+
use React\EventLoop\LoopInterface;
9+
use Runtime\React\ServerFactory;
10+
11+
class ServerFactoryTest extends TestCase
12+
{
13+
private RequestHandlerInterface $handler;
14+
private LoopInterface $loop;
15+
16+
public function setUp(): void
17+
{
18+
parent::setUp();
19+
$this->handler = $this->createMock(RequestHandlerInterface::class);
20+
$this->loop = $this->createMock(LoopInterface::class);
21+
$this->loop->expects($this->never())->method('run');
22+
Loop::set($this->loop);
23+
}
24+
25+
public function testCreateServerWithDefaultOptions(): void
26+
{
27+
$factory = new ServerFactory();
28+
$loop = $factory->createServer($this->handler);
29+
30+
self::assertInstanceOf(LoopInterface::class, $loop);
31+
self::assertSame(ServerFactory::getDefaultOptions(), $factory->getOptions());
32+
}
33+
34+
public function testCreateServerWithOptions(): void
35+
{
36+
$options = [
37+
'host' => '0.0.0.0',
38+
'port' => '9999',
39+
];
40+
$factory = new ServerFactory($options);
41+
$factory->createServer($this->handler);
42+
43+
self::assertSame($options, $factory->getOptions());
44+
}
45+
}

0 commit comments

Comments
 (0)