Skip to content

Commit 1be5a66

Browse files
[Runtime] Automatically use FrankenPHP runner when its worker mode is detected
1 parent cb08480 commit 1be5a66

File tree

7 files changed

+189
-1
lines changed

7 files changed

+189
-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 `FrankenPhpRunner`
8+
* Add automatic detection of FrankenPHP worker mode in `SymfonyRuntime`
9+
410
6.4
511
---
612

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

Lines changed: 1 addition & 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
/**
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
$server = array_filter($_SERVER, static fn (string $key) => !str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY);
38+
$server['APP_RUNTIME_MODE'] = 'web=1&worker=1';
39+
40+
$handler = function () use ($server, &$sfRequest, &$sfResponse): void {
41+
// Connect to the Xdebug client if it's available
42+
if (\extension_loaded('xdebug') && \function_exists('xdebug_connect_to_client')) {
43+
xdebug_connect_to_client();
44+
}
45+
46+
// Merge the environment variables coming from DotEnv with the ones tied to the current request
47+
$_SERVER += $server;
48+
49+
$sfRequest = Request::createFromGlobals();
50+
$sfResponse = $this->kernel->handle($sfRequest);
51+
52+
$sfResponse->send();
53+
};
54+
55+
$loops = 0;
56+
do {
57+
$ret = \frankenphp_handle_request($handler);
58+
59+
if ($this->kernel instanceof TerminableInterface && $sfRequest && $sfResponse) {
60+
$this->kernel->terminate($sfRequest, $sfResponse);
61+
}
62+
63+
gc_collect_cycles();
64+
} while ($ret && (-1 === $this->loopMax || ++$loops < $this->loopMax));
65+
66+
return 0;
67+
}
68+
}

src/Symfony/Component/Runtime/SymfonyRuntime.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Component\HttpKernel\HttpKernelInterface;
2424
use Symfony\Component\Runtime\Internal\MissingDotenv;
2525
use Symfony\Component\Runtime\Internal\SymfonyErrorHandler;
26+
use Symfony\Component\Runtime\Runner\FrankenPhpRunner;
2627
use Symfony\Component\Runtime\Runner\Symfony\ConsoleApplicationRunner;
2728
use Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner;
2829
use Symfony\Component\Runtime\Runner\Symfony\ResponseRunner;
@@ -64,6 +65,7 @@ class_exists(MissingDotenv::class, false) || class_exists(Dotenv::class) || clas
6465
* - int|string|null as handled by GenericRuntime.
6566
*
6667
* @author Nicolas Grekas <p@tchwork.com>
68+
* @author Kévin Dunglas <kevin@dunglas.dev>
6769
*/
6870
class SymfonyRuntime extends GenericRuntime
6971
{
@@ -73,7 +75,7 @@ class SymfonyRuntime extends GenericRuntime
7375
private readonly Command $command;
7476

7577
/**
76-
* @param array {
78+
* @param array{
7779
* debug?: ?bool,
7880
* env?: ?string,
7981
* disable_dotenv?: ?bool,
@@ -88,6 +90,7 @@ class SymfonyRuntime extends GenericRuntime
8890
* debug_var_name?: string,
8991
* dotenv_overload?: ?bool,
9092
* dotenv_extra_paths?: ?string[],
93+
* frankenphp_loop_max?: int,
9194
* } $options
9295
*/
9396
public function __construct(array $options = [])
@@ -142,13 +145,18 @@ public function __construct(array $options = [])
142145
}
143146

144147
$options['error_handler'] ??= SymfonyErrorHandler::class;
148+
$options['frankenphp_loop_max'] = (int) ($options['frankenphp_loop_max'] ?? $_SERVER['FRANKENPHP_LOOP_MAX'] ?? $_ENV['FRANKENPHP_LOOP_MAX'] ?? 500);
145149

146150
parent::__construct($options);
147151
}
148152

149153
public function getRunner(?object $application): RunnerInterface
150154
{
151155
if ($application instanceof HttpKernelInterface) {
156+
if ($_SERVER['FRANKENPHP_WORKER'] ?? false) {
157+
return new FrankenPhpRunner($application, $this->options['frankenphp_loop_max']);
158+
}
159+
152160
return new HttpKernelRunner($application, Request::createFromGlobals(), $this->options['debug'] ?? false);
153161
}
154162

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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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\Runner\FrankenPhpRunner;
17+
use Symfony\Component\Runtime\SymfonyRuntime;
18+
19+
/**
20+
* @author Kévin Dunglas <kevin@dunglas.dev>
21+
* @author Alexandre Daubois <alex.daubois@gmail.com>
22+
*/
23+
class SymfonyRuntimeTest extends TestCase
24+
{
25+
public function testGetRunner()
26+
{
27+
$application = $this->createStub(HttpKernelInterface::class);
28+
29+
$runtime = new SymfonyRuntime();
30+
$this->assertNotInstanceOf(FrankenPhpRunner::class, $runtime->getRunner(null));
31+
$this->assertNotInstanceOf(FrankenPhpRunner::class, $runtime->getRunner($application));
32+
33+
$_SERVER['FRANKENPHP_WORKER'] = 1;
34+
$this->assertInstanceOf(FrankenPhpRunner::class, $runtime->getRunner($application));
35+
}
36+
}
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)