Skip to content

Commit 5a1bd6e

Browse files
[Runtime] Automatically enable FrankenPHP runtime when its worker mode is detected
1 parent cb08480 commit 5a1bd6e

8 files changed

+230
-1
lines changed

src/Symfony/Component/Runtime/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Add `FrankenPhpRuntime` and `FrankenPhpRunner`
8+
* Add automatic detection of FrankenPHP worker mode to enable the correct runtime
9+
410
6.4
511
---
612

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Runtime;
13+
14+
use Symfony\Component\HttpKernel\HttpKernelInterface;
15+
use Symfony\Component\Runtime\Runner\FrankenPhpRunner;
16+
17+
/**
18+
* A runtime for FrankenPHP.
19+
*
20+
* @author Kévin Dunglas <kevin@dunglas.dev>
21+
*/
22+
class FrankenPhpRuntime extends SymfonyRuntime
23+
{
24+
/**
25+
* @param array{
26+
* frankenphp_loop_max?: int,
27+
* } $options
28+
*/
29+
public function __construct(array $options = [])
30+
{
31+
$options['frankenphp_loop_max'] = (int) ($options['frankenphp_loop_max'] ?? $_SERVER['FRANKENPHP_LOOP_MAX'] ?? $_ENV['FRANKENPHP_LOOP_MAX'] ?? 500);
32+
33+
parent::__construct($options);
34+
}
35+
36+
public function getRunner(?object $application): RunnerInterface
37+
{
38+
if ($application instanceof HttpKernelInterface && ($_SERVER['FRANKENPHP_WORKER'] ?? false)) {
39+
return new FrankenPhpRunner($application, $this->options['frankenphp_loop_max']);
40+
}
41+
42+
return parent::getRunner($application);
43+
}
44+
}

src/Symfony/Component/Runtime/Internal/ComposerPlugin.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Composer\Plugin\PluginInterface;
1919
use Composer\Script\ScriptEvents;
2020
use Symfony\Component\Filesystem\Filesystem;
21+
use Symfony\Component\Runtime\FrankenPhpRuntime;
2122
use Symfony\Component\Runtime\SymfonyRuntime;
2223

2324
/**
@@ -96,6 +97,7 @@ public function updateAutoloadFile(): void
9697
$code = strtr(file_get_contents($autoloadTemplate), [
9798
'%project_dir%' => $projectDir,
9899
'%runtime_class%' => var_export($runtimeClass, true),
100+
'%frankenphp_runtime_class%' => var_export(FrankenPhpRuntime::class, true),
99101
'%runtime_options%' => '['.substr(var_export($extra, true), 7, -1)." 'project_dir' => {$projectDir},\n]",
100102
]);
101103

src/Symfony/Component/Runtime/Internal/autoload_runtime.template

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ if (!is_object($app)) {
1212
throw new TypeError(sprintf('Invalid return value: callable object expected, "%s" returned from "%s".', get_debug_type($app), $_SERVER['SCRIPT_FILENAME']));
1313
}
1414

15-
$runtime = $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? %runtime_class%;
15+
if (null === ($runtime = $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? null)) {
16+
$runtime = ($_SERVER['FRANKENPHP_WORKER'] ?? $_ENV['FRANKENPHP_WORKER']) ? %frankenphp_runtime_class% : %runtime_class%;
17+
}
18+
1619
$runtime = new $runtime(($_SERVER['APP_RUNTIME_OPTIONS'] ?? $_ENV['APP_RUNTIME_OPTIONS'] ?? []) + %runtime_options%);
1720

1821
[$app, $args] = $runtime
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Runtime\Runner;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\HttpKernelInterface;
16+
use Symfony\Component\HttpKernel\TerminableInterface;
17+
use Symfony\Component\Runtime\RunnerInterface;
18+
19+
/**
20+
* A runner for FrankenPHP.
21+
*
22+
* @author Kévin Dunglas <kevin@dunglas.dev>
23+
*/
24+
class FrankenPhpRunner implements RunnerInterface
25+
{
26+
public function __construct(
27+
private HttpKernelInterface $kernel,
28+
private int $loopMax,
29+
) {
30+
}
31+
32+
public function run(): int
33+
{
34+
// Prevent worker script termination when a client connection is interrupted
35+
ignore_user_abort(true);
36+
37+
$xdebugConnectToClient = function_exists('xdebug_connect_to_client');
38+
39+
$server = array_filter($_SERVER, static fn (string $key) => !str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY);
40+
$server['APP_RUNTIME_MODE'] = 'web=1&worker=1';
41+
42+
$handler = function () use ($server, &$sfRequest, &$sfResponse, $xdebugConnectToClient): void {
43+
// Connect to the Xdebug client if it's available
44+
if ($xdebugConnectToClient) {
45+
xdebug_connect_to_client();
46+
}
47+
48+
// Merge the environment variables coming from DotEnv with the ones tied to the current request
49+
$_SERVER += $server;
50+
51+
$sfRequest = Request::createFromGlobals();
52+
$sfResponse = $this->kernel->handle($sfRequest);
53+
54+
$sfResponse->send();
55+
};
56+
57+
$loops = 0;
58+
do {
59+
$ret = \frankenphp_handle_request($handler);
60+
61+
if ($this->kernel instanceof TerminableInterface && $sfRequest && $sfResponse) {
62+
$this->kernel->terminate($sfRequest, $sfResponse);
63+
}
64+
65+
gc_collect_cycles();
66+
} while ($ret && (-1 === $this->loopMax || ++$loops < $this->loopMax));
67+
68+
return 0;
69+
}
70+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Runtime\Tests;
13+
14+
require_once __DIR__.'/frankenphp-function-mock.php';
15+
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpFoundation\Response;
19+
use Symfony\Component\HttpKernel\HttpKernelInterface;
20+
use Symfony\Component\HttpKernel\TerminableInterface;
21+
use Symfony\Component\Runtime\Runner\FrankenPhpRunner;
22+
23+
interface TestAppInterface extends HttpKernelInterface, TerminableInterface
24+
{
25+
}
26+
27+
/**
28+
* @author Kévin Dunglas <kevin@dunglas.fr>
29+
*/
30+
class FrankenPhpRunnerTest extends TestCase
31+
{
32+
public function testRun()
33+
{
34+
$application = $this->createMock(TestAppInterface::class);
35+
$application
36+
->expects($this->once())
37+
->method('handle')
38+
->willReturnCallback(function (Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response {
39+
$this->assertSame('bar', $request->server->get('FOO'));
40+
41+
return new Response();
42+
});
43+
$application->expects($this->once())->method('terminate');
44+
45+
$_SERVER['FOO'] = 'bar';
46+
47+
$runner = new FrankenPhpRunner($application, 500);
48+
$this->assertSame(0, $runner->run());
49+
}
50+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Runtime\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpKernel\HttpKernelInterface;
16+
use Symfony\Component\Runtime\FrankenPhpRuntime;
17+
use Symfony\Component\Runtime\Runner\FrankenPhpRunner;
18+
19+
/**
20+
* @author Kévin Dunglas <kevin@dunglas.dev>
21+
*/
22+
class FrankenPhpRuntimeTest extends TestCase
23+
{
24+
public function testGetRunner()
25+
{
26+
$application = $this->createStub(HttpKernelInterface::class);
27+
28+
$runtime = new FrankenPhpRuntime();
29+
$this->assertNotInstanceOf(FrankenPhpRunner::class, $runtime->getRunner(null));
30+
$this->assertNotInstanceOf(FrankenPhpRunner::class, $runtime->getRunner($application));
31+
32+
$_SERVER['FRANKENPHP_WORKER'] = 1;
33+
$this->assertInstanceOf(FrankenPhpRunner::class, $runtime->getRunner($application));
34+
}
35+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
if (!function_exists('frankenphp_handle_request')) {
13+
function frankenphp_handle_request(callable $callable): bool
14+
{
15+
$callable();
16+
17+
return false;
18+
}
19+
}

0 commit comments

Comments
 (0)