Skip to content

Commit 81ade34

Browse files
dunglasalexander-schranzNyholm
authored
feat: add a runtime for FrankenPHP with Symfony (#124)
Add a runtime for the worker mode of [FrankenPHP](https://frankenphp.dev). To be released this afternoon at ForumPHP! ![frankenphp](https://user-images.githubusercontent.com/57224/195830342-61650267-68a6-4f05-9c34-f8f099a6c9d4.png) Co-authored-by: Alexander Schranz <alexander@sulu.io> Co-authored-by: Nyholm <tobias.nyholm@gmail.com>
0 parents  commit 81ade34

File tree

12 files changed

+281
-0
lines changed

12 files changed

+281
-0
lines changed

.github/FUNDING.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# These are supported funding model platforms
2+
3+
github: [nyholm]

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor/
2+
composer.lock

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Change Log
2+
3+
## 0.1.0
4+
5+
First version

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2021 Tobias Nyholm
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# FrankenPHP Runtime for Symfony
2+
3+
A runtime for [FrankenPHP](https://frankenphp.dev/).
4+
5+
If you are new to the Symfony Runtime component, read more in the [main readme](https://github.com/php-runtime/runtime).
6+
7+
## Installation
8+
9+
```
10+
composer require runtime/frankenphp-symfony
11+
```
12+
13+
## Usage
14+
15+
Define the environment variable `APP_RUNTIME` for your application.
16+
17+
```
18+
// .env
19+
APP_RUNTIME=Runtime\FrankenPhpSymfony\Runtime
20+
```
21+
22+
```
23+
// .rr.yaml
24+
server:
25+
...
26+
env:
27+
APP_RUNTIME: Runtime\FrankenPhpSymfony\Runtime
28+
```
29+
30+
```php
31+
// public/index.php
32+
33+
use App\Kernel;
34+
35+
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
36+
37+
return function (array $context) {
38+
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
39+
};
40+
```

composer.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "runtime/frankenphp-symfony",
3+
"description": "FrankenPHP runtime for Symfony",
4+
"license": "MIT",
5+
"type": "library",
6+
"authors": [
7+
{
8+
"name": "Kévin Dunglas",
9+
"email": "kevin@dunglas.dev"
10+
}
11+
],
12+
"require": {
13+
"php": ">=8.2.0",
14+
"symfony/dependency-injection": "^5.4 || ^6.0",
15+
"symfony/http-kernel": "^5.4 || ^6.0",
16+
"symfony/runtime": "^5.4 || ^6.0"
17+
},
18+
"require-dev": {
19+
"phpunit/phpunit": "^9.5"
20+
},
21+
"minimum-stability": "dev",
22+
"prefer-stable": true,
23+
"autoload": {
24+
"psr-4": {
25+
"Runtime\\FrankenPhpSymfony\\": "src/"
26+
}
27+
},
28+
"autoload-dev": {
29+
"psr-4": {
30+
"Runtime\\FrankenPhpSymfony\\Tests\\": "tests/"
31+
}
32+
},
33+
"config": {
34+
"allow-plugins": {
35+
"symfony/runtime": true
36+
}
37+
}
38+
}

phpunit.xml.dist

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
4+
backupGlobals="false"
5+
colors="true"
6+
bootstrap="vendor/autoload.php"
7+
failOnRisky="true"
8+
failOnWarning="true"
9+
>
10+
<php>
11+
<ini name="error_reporting" value="-1"/>
12+
</php>
13+
<testsuites>
14+
<testsuite name="Test Suite">
15+
<directory>./tests</directory>
16+
</testsuite>
17+
</testsuites>
18+
</phpunit>

src/Runner.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Runtime\FrankenPhpSymfony;
6+
7+
use Symfony\Component\HttpFoundation\Request;
8+
use Symfony\Component\HttpKernel\HttpKernelInterface;
9+
use Symfony\Component\HttpKernel\TerminableInterface;
10+
use Symfony\Component\Runtime\RunnerInterface;
11+
12+
/**
13+
* A runner for FrankenPHP.
14+
*
15+
* @author Kévin Dunglas <kevin@dunglas.dev>
16+
*/
17+
class Runner implements RunnerInterface
18+
{
19+
private HttpKernelInterface $kernel;
20+
21+
public function __construct(HttpKernelInterface $kernel)
22+
{
23+
$this->kernel = $kernel;
24+
}
25+
26+
public function run(): int
27+
{
28+
$server = array_filter($_SERVER, static fn (string $key) => !str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY);
29+
do {
30+
$ret = \frankenphp_handle_request(function () use ($server, &$sfRequest, &$sfResponse): void {
31+
// Merge the environment variables coming from DotEnv with the ones tight to the current request
32+
$_SERVER += $server;
33+
34+
$sfRequest = Request::createFromGlobals();
35+
$sfResponse = $this->kernel->handle($sfRequest);
36+
37+
$sfResponse->send();
38+
});
39+
40+
if ($this->kernel instanceof TerminableInterface && $sfRequest && $sfResponse) {
41+
$this->kernel->terminate($sfRequest, $sfResponse);
42+
}
43+
} while ($ret);
44+
45+
return 0;
46+
}
47+
}

src/Runtime.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Runtime\FrankenPhpSymfony;
6+
7+
use Symfony\Component\HttpKernel\HttpKernelInterface;
8+
use Symfony\Component\Runtime\RunnerInterface;
9+
use Symfony\Component\Runtime\SymfonyRuntime;
10+
11+
/**
12+
* A runtime for FrankenPHP.
13+
*
14+
* @author Kévin Dunglas <kevin@dunglas.dev>
15+
*/
16+
class Runtime extends SymfonyRuntime
17+
{
18+
public function getRunner(?object $application): RunnerInterface
19+
{
20+
if ($application instanceof HttpKernelInterface && ($_SERVER['FRANKENPHP_WORKER'] ?? false)) {
21+
return new Runner($application);
22+
}
23+
24+
return parent::getRunner($application);
25+
}
26+
}

tests/RunnerTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Runtime\FrankenPhpSymfony\Tests;
6+
7+
require_once __DIR__.'/function-mock.php';
8+
9+
use PHPUnit\Framework\TestCase;
10+
use Runtime\FrankenPhpSymfony\Runner;
11+
use Symfony\Component\HttpFoundation\Request;
12+
use Symfony\Component\HttpFoundation\Response;
13+
use Symfony\Component\HttpKernel\HttpKernelInterface;
14+
use Symfony\Component\HttpKernel\TerminableInterface;
15+
16+
interface TestAppInterface extends HttpKernelInterface, TerminableInterface
17+
{
18+
}
19+
20+
/**
21+
* @author Kévin Dunglas <kevin@dunglas.fr>
22+
*/
23+
class RunnerTest extends TestCase
24+
{
25+
public function testRun(): void
26+
{
27+
$application = $this->createMock(TestAppInterface::class);
28+
$application
29+
->expects($this->once())
30+
->method('handle')
31+
->willReturnCallback(function (Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response {
32+
$this->assertSame('bar', $request->server->get('FOO'));
33+
34+
return new Response();
35+
});
36+
$application->expects($this->once())->method('terminate');
37+
38+
$_SERVER['FOO'] = 'bar';
39+
40+
$runner = new Runner($application);
41+
$this->assertSame(0, $runner->run());
42+
}
43+
}

tests/RuntimeTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Runtime\FrankenPhpSymfony\Tests;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Runtime\FrankenPhpSymfony\Runner;
9+
use Runtime\FrankenPhpSymfony\Runtime;
10+
use Symfony\Component\HttpKernel\HttpKernelInterface;
11+
12+
/**
13+
* @author Kévin Dunglas <kevin@dunglas.dev>
14+
*/
15+
final class RuntimeTest extends TestCase
16+
{
17+
public function testGetRunner(): void
18+
{
19+
$application = $this->createStub(HttpKernelInterface::class);
20+
21+
$runtime = new Runtime();
22+
$this->assertNotInstanceOf(Runner::class, $runtime->getRunner(null));
23+
$this->assertNotInstanceOf(Runner::class, $runtime->getRunner($application));
24+
25+
$_SERVER['FRANKENPHP_WORKER'] = 1;
26+
$this->assertInstanceOf(Runner::class, $runtime->getRunner($application));
27+
}
28+
}

tests/function-mock.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
if (!function_exists('frankenphp_handle_request')) {
4+
function frankenphp_handle_request(callable $callable): bool
5+
{
6+
$callable();
7+
8+
return false;
9+
}
10+
}

0 commit comments

Comments
 (0)