Alexander-Schranz
Alexander Schranz
Core Developer – Sulu GmbH
Core developer and support king. So dedicated to his work that we couldn't find a hobby to mention.
@alex_s_

Run Sulu via FrankenPHP with custom PHP extensions

FrankenPHP is a new experimental application server created by Kévin Dunglas, a well-known Symfony core team member famous for projects like API Platform and Mercure Hub. In this blog post we will look at how to use FrankenPHP to run our Sulu Demo application. (Note: it does require additional PHP extensions).

What is FrankenPHP?

FrankenPHP is a PHP Application Server written in Golang. It is very similar to how Roadrunner works, another PHP Application Server written in Golang. FrankenPHP servers try to keep most of the application (in Symfony, the Kernel) in the memory. This allows the application servers to respond a lot faster since they don't need to boot the PHP application and wait for the container and kernel to be initialized.

The difference to Roadrunner is that FrankenPHP is not running in the PHP CLI context. Where Roadrunner uses PSR-7 request/response object, FrankenPHP creates its own SAPI to communicate and uses the normal $_SERVER variables to provide the PHP information.  

FrankenPHP is still experimental. It was released on 14 October 2022 at the AFUP PHP Forum as an easy-to-Dockerize Application Server for PHP. From what we can read on the FrankenPHP Website, it is the first PHP Server which also supports 103 Early Hints. This is a new HTTP response object that supports sending several responses to the client to preload resources (like CSS) which drastically improves website loading times. FrankenPHP is built on top of Caddy Webserver which also provides automatic HTTPS support.

How to use FrankenPHP with Sulu?

The first requirement for using FrankenPHP is to use PHP 8.2 - which is not yet officially released - but if you are using "brew" you can install it via:

brew install php@8.2

For this demonstration we are using the sulu-demo as our test application. We are installing it via:

git clone git@github.com:sulu/sulu-demo.git
cd sulu-demo

We need to change the composer.json to PHP 8.2 by replacing PHP and extra platform to 8.2.0. After that, we need to install the Composer dependencies via the ignore platform reqs flag (because not all dependencies of Sulu support PHP 8.2 yet):

composer update --ignore-platform-reqs="php"

After this, we need to make the sulu-demo able to run via the symfony/runtime package. As Sulu is a DualKernel Setup there is an additional step required for us. For this, we will create a new src/DualKernel.php which moves the logic of our index.php into this new runtime supported Kernel class:

<?php

namespace App;

use Sulu\Component\HttpKernel\SuluKernel;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\TerminableInterface;

class DualKernel implements HttpKernelInterface, TerminableInterface
{
    private Kernel $adminKernel;

    private Kernel $websiteKernel;

    public function __construct($context)
    {
        Request::enableHttpMethodParameterOverride();

        $this->adminKernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], SuluKernel::CONTEXT_ADMIN);

        $this->websiteKernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], SuluKernel::CONTEXT_WEBSITE);

        // Comment this line if you want to use the "varnish" http
        // caching strategy. See http://sulu.readthedocs.org/en/latest/cookbook/caching-with-varnish.html
        // if ('dev' !== $context['APP_ENV']) {
        //    $this->websiteKernel = $this->websiteKernel->getHttpCache();
        // }
    }

    public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response
    {
        if (preg_match('/^\/admin(\/|$)/', $request->getPathInfo())) {
            return $this->adminKernel->handle($request, $type, $catch);
        }

        return $this->websiteKernel->handle($request, $type, $catch);
    }

    public function terminate(Request $request, Response $response)
    {
        if (preg_match('/^\/admin(\/|$)/', $request->getPathInfo())) {
            return $this->adminKernel->terminate($request, $response);
        }

        return $this->websiteKernel->terminate($request, $response);
    }
}
src/DualKernel.php

Next we will edit the public/index.php file to use this new DualKernel class:

<?php

use App\DualKernel;

require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';

return function (array $context) {
    return new DualKernel($context);
};
src/index.php

Now we will install the new FrankenPHP runtime by running:

composer require runtime/frankenphp-symfony --ignore-platform-req="php"

To use the new runtime we will also configure it in the "composer.json" extra section:

{
    // ...
    "extra": {
        "symfony": {
            "allow-contrib": true
        },
        "runtime": {
            "class": "Runtime\\FrankenPhpSymfony\\Runtime"
        }
    }
}
composer.json

Update 2022-10-30: 

Thx to withinboredom Pull Request and the latest changes in the FrankenPHP docker image we not longer need to use a fork of FrankenPHP. To create our own docker image with additional php extensions, we only need to create a the following Dockerfile and build it.

FROM dunglas/frankenphp

# add additional extensions here:
RUN install-php-extensions \
    opcache \
    pdo_mysql \
    gd \
    intl \
    zip
Building our "custom-franken-php" docker image

To build our custom image we need to run:

docker build -t custom-franken-php . 

This can take some time so grab a coffee or a new drink until it is finished.

After successfully compiling our custom FrankenPHP image, we are now able to start our application. For this we go back into the root directory of our sulu-demo and run the following command to start FrankenPHP:

docker run -e FRANKENPHP_CONFIG="worker ./public/index.php" -v $PWD:/app -p 80:80 -p 443:443 custom-franken-php

The demo application is now available under https://localhost. You maybe need to accept unsafe SSL certification to see the page in your browser.

If it is not allowed to connect to your database you'll need to edit the .env.local file to the following to connect via the host IP address:

APP_ENV=dev
REDIS_DSN=redis://host.docker.internal:6378/1
DATABASE_URL=mysql://root:ChangeMe@host.docker.internal:3306/su_demo?serverVersion=8.0
ELASTICSEARCH_HOST=host.docker.internal:9200
REDIS_HOST=host.docker.internal:6379
.env.local

This way you should be able to test any Symfony application with the new FrankenPHP application server, even if the application has a Multi-Kernel setup or requires additional PHP extensions.