Exakat on FrankenPHPExakat on FrankenPHP

I learnt about Franken PHP by listening to Kevin Dunglas at the AFUP conferences. This is a PHP application server: it loads the code and runs the application from memory.

At first sight, it is made for Webapplication. Load the code once, and then run it upon request. But it also has a lot of appeal to Exakat, which is a long running CLI script. Exakat runs several distinct tasks, may it be analysis, export operations or report building. There is a notion of ‘task’, which delegate work one another and allow it to run in parallel. In particular, the numerous analysis tasks may take advantage of running simultaneously, and reuse some cache (code, data) between operations.

So, with a bit of time on my hands, I started the trip to make Exakat run on FrankenPHP. There will be learning along the way, and possibly, several new features and speed gain. That’s exciting!

Expectations

Before diving head first in the coding and devops, let’s list what I expected by moving to FrankenPHP, or, by extension, any application server.

  • Make the number of parallel tasks easy to configure
  • Speed boost compared to individual tasks, by reusing code and cache
  • Serve the reports directly from an Exakat interface, during the running of the audit: that one is probably a feature for later.

Also, let’s plan for the potential failures, or the various problems that can show up :

  • Running the graph database, which requires Java, with FrankenPHP
  • Rewriting the shell commands to accept the Web protocol
  • Changing the internal commands to query with HTTP
  • Running into unforeseen issues (I always keep entry on such list)

With some clear ideas written down, it is time to make a bit of a experimentation with that shiny tool.

Exakat version first

Let’s first run the simplest exakat command, using FrankenPHP.

From the documentation, I am going to use the ‘workers‘. There are little details on what that means in the context of FrankenPHP, but I can guess it: workers are dedicated to taking care of one task. On a web application, that would be an HTTP query. For a command line tool, it is a shell command, and later, a subtask. So, I need to add Exakat to the application server, and query it with HTTP.

To start with, I run the first Franken Docker command:

docker run \
    -v $PWD:/app \
    -p 80:80 -p 443:443 \
    dunglas/frankenphp

$PWD is the current directory, and it is called app in the Docker container. FrankenPHP searches for the script to run in the folder /app/public. So, I created a public folder in the current installation of Exakat, and add the following code :

<?php

print '<pre>'.shell_exec('cd ../;php exakat').'</pre>';

?>

It is a bit rough, but it should work fine. Then, I can run the docker command, and check with the browser.

The pre tags were an ugly hack, to convert terminal presentation to web format easily. It shows that the output of exakat needs some work to be web compatible. This is is not a surprise, though it has to be added to the original planned issues list.

Exakat doctor

After that first command, I’ll move to the doctor command. I’m already running into the problem of converting HTTP requests into shell commands. PHP shell_exec() will do the job for now, but I make a note to update that later.

In the mean time, the initial script is upgraded with this:

<?php

$command = match($_GET['command'] ?? '') {
    'version' => 'version',
    'doctor' => 'doctor',
    default => ''
};

if (empty($command)) { 
    print "Provide a command for exakat: version or doctor."; 
    die();
}

print '<pre>'.shell_exec('cd ../;php exakat '.$command).'</pre>';

?>

It is now possible to submit two different commands, with an URL like https://localhost/index.php?command=doctor. Indeed, I get this:

By reading the diagnostic of the doctor, two issues need my attention: Java is missing, and memory_limit is set to 128M, which is way too low.

For the former problem, I need to update php.ini. For the first problem, we need to install Java.

frankenphp documentation has a section about Docker. I can create a dedicated Dockerfile, and add some extra installation commands: this is all I need. The image [dunglas/frankenphp](https://github.com/dunglas/frankenphp/blob/main/docs/docker.md)is built on top of ‘Debian 12.2’, for which I can install Java 11 from Debian 11.

This gives the following Dockerfile:

FROM dunglas/frankenphp

COPY . /app/public

RUN \
    sed -i 's/Suites: bookworm bookworm-updates/Suites: bookworm bookworm-updates bullseye/g' /etc/apt/sources.list.d/debian.sources && \
    apt-get update && \
    apt-get -y install openjdk-11-jre git && \
    echo '' > /usr/local/etc/php/php.ini  && \
    echo 'memory_limit = -1' >> /usr/local/etc/php/php.ini && \
    echo 'max_execution_time = 0' >> /usr/local/etc/php/php.ini

memory_limit is been raised to infinity (aka, -1). And the max_execution_time to 0, to avoid running into limits. I also added a few extra lines to handle the ‘project’ command in the current index.php file, along with some parameters: they are now passed by GET, and have to be translated to command line options. The translation is straightforward.

The result is build and then used instead of the default image.

docker build -f Dockerfile -t exakat/frankenphp:test . 

To run an project, I shall start the FrankenPHP application server, and call the following URL:

http://localhost:80/index.php?command=project&p=xxx&v=1

php exakat.phar project -p xxx -v

The URL is now equivalent to the command line just above. Nice.

I used curl --insecure 'https://localhost:443/index.php?command=project&p=xxx&v=1'to direct the call to frankenPHP and wait for the results. Doing the call from the browser was a lot of clicks to dismiss the warnings.

The first sign that the application server was taking charge was the CPU load that spiked. I let it run patiently, and finally, I got the result of the audit in the expected folder. Ta da!

Skipping the intermediate shell

So far, Exakat is running on FrankenPHP. Yet, I haven’t really used any specific feature of it. In fact, I even added one extra layer by calling a web server that called a shell that runs Exakat. Let’s get rid of this.

Instead of relying on a shell_exec() to relay the incoming argument, I refactored Exakat and its commandline reader. I adapted the system that parses the command line arguments: now, it also reads the parameters from the classic web variables, provided by PHP: $_GET, $_POST… That makes Exakat aware of the web server, and that opens the door to new features. In the future.

As a result, I can simply include() the exakat script in the public index.php, to access the same features.

<?php

include '../exakat';

?>

And this works just fine.

The path so far

At the end of the day, several milestones were successfully cleared in the quest to run Exakat on FrankenPHP.

  • Exakat runs on FrankenPHP, with little modifications. PHP version is recent (8.3), the needed extensions are all available. It just needs a bit more resources, which are controlled with php.ini, as usual.
  • The final Docker image is an extended version of the original one from Kevin: this was expected, as Exakat needs Java. The extension was straight forward.
  • I ran into a couple of panic errors. It took some trial and error to figure out the culprit: it was mostly my own making. In particular, it seems that messing with the php.ini was a sensitive subject. I wish FrankenPHP was a bit less overreacting with a panic crash, but overall, I kept progressing.
  • By using exakat in different settings that its usual ones (Alpine docker, Exakat in web environment, …) I spotted some edge cases that needed some reinforcements. Even if I stopped at this point, this afternoon of experimentation was constructive and useful for Exakat.
  • I will also add to the forseeable issues that some classic behaviors of Exakat started to show their impact with FrankenPHP: a lot of the code is made for single use (one audit), while FrankenPHP push toward clean-after-use code. Another interesting challenge on the horizon!

So far, this was a very interesting afternoon of experimentation, and a great welcome on the shoulders of FrankenPHP. I still have to make use some of the features and refactor the current parallelisation system with specific calls. If that second part interest you, drop us a ping on mastodon @dseguy@phpc.social or Twitter exakat and I’ll make it an following post.