Creating Standalone PHP App Binaries using Static PHP CLI

PHP Bin Image

TL;DR: This article explores the innovative process of transforming PHP projects into standalone executable binaries using Static PHP CLI (SPC). It provides a step-by-step guide on converting a Laravel Zero application into a self-contained binary, highlighting the benefits of independence from PHP installations and the challenges of handling compilation complexities and managing binary sizes. This approach presents a new frontier in PHP application deployment, offering enhanced portability and broader distribution possibilities.

Some weeks back I made a post on X concerning Static PHP CLI, a tool that facilitates building a static binary of PHP with extensions included. The post seems interesting to some people and i thought I should expand more on my thoughts and findings on it since I have been tinkering with it.

Since PHP is an interpreted language, an interpreter is required to run the PHP codes wherever they are deployed to. Unlike compiled languages where code written can be compile and ran directly by the machines they are deployed to. This slight difference already introduced more complexities to how deployments are done for a language like PHP. These complexities are mostly around ensuring the target machine has requisite dependencies, configuration and the interpreter to run the code. This limitation also, sometimes, by extension, limit where PHP apps can be used. Even though, PHP apps can be packaged as a PHAR (PHp ARchive), which can combine the app code and its dependencies as one, it doesn't solve the problem of the interpreter required to run it.

In other words, the farthest we’ve been able to go as far as having a PHP project as a single unit is a PHAR file, which itself depends on having PHP installed wherever it will be used. The further process of making a PHAR file into a single executable binary have some more complex parts, and this is where the Static PHP CLI (SPC) project comes to play. It automates the process of compiling PHP SAPIs (CLI, FPM and MicroSFX) as static binaries along with the extensions, while presenting you with a CLI to customise this process. It also allows you to combine resulting static binaries with your project to produce a single binary with all dependencies and configuration baked in, presenting a new process of shipping PHP apps.

Before I delve into most of my thoughts, let’s see how we can make use of this project in practice for a full-blown application. We will use a CLI app based on Laravel Zero - a framework for building CLI apps in PHP that is based on Laravel. I choose to go with a project like this as it is common for PHP projects to have dependencies, some structure, etc. So, I think using a single PHP script will be oversimplified and might inspire the doubt that you cannot use this process in a standard PHP project.

A CLI application is also chosen since, most benefit of this method of shipping a PHP project comes to light this way. A high-level process of this example will look more or less like this:

Compilation Process

Creating a project using Laravel Zero

First, let's create a Laravel Zero application called "quote" using this command and then enter the root:

composer create-project --prefer-dist laravel-zero/laravel-zero quote && cd quote

Once the project is created you need to rename it like this, to make sure subsequent PHAR files built from it are named as expected:

php application app:rename quote

Next, let's test the application using the inspire command like this, which displays a beautiful quote:

php quote inspire

Now that the app is working as expected, we will build it as a PHAR - consisting of the code and Composer dependencies. This is already a feature provided by Laravel Zero and you can run this to build the PHAR:

php quote app:build

This would generate a file named quote inside the builds directory. This file is a PHAR file even though it is without the extension. We will need this file later in the process, but for now, we can verify that the generated PHAR file works like this:

./builds/quote inspire

Even though it seems like it's a self-executing binary, it isn't because it relies on the presence of PHP to run, which is obviously is present at this stage.

Now that we have the application itself prepared to be compiled as a single binary, we need to prepare SPC in the next steps and then do the final compilation of the app into a single binary.

Installing Static PHP CLI (SPC)

There are a couple of ways you can use to setup the SPC project. You can either clone the project and set it up or use pre-compiled self-executing SPC binary (dog-fooding much!). I prefer the later since it's much faster and easier to use that way.

We can use the GitHub CLI to easily download a release. You can also use cURL or any other tool/way you want. I will stick to downloading the binary at the root of the project to keep things simple. You can find the appropriate release for your OS here on the project's release page.

gh release --repo crazywhalecc/static-php-cli download -p spc-<YOUR OS>-aarch64.tar.gz

Next, we can extract, and then set permissions for the binary:

tar -xzf spc-macos-aarch64.tar.gz && chmod a+x spc

With this, you have an spc binary at the root of the project, ready to use and independent of your PHP installation. You can test the binary to see everything is fine:


Preparing SPC

Now that we have SPC, we need to prepare it to get it ready for use. The preparation process covers the downloading of some packages and libraries it needs to to compile a final PHP executable with extensions. The other part of the this process is to compile the PHP binary itself together with the extensions.

Using the spc binary you setup in the previous section, we need to download all the source code required for compilation like this:


./spc download --with-php=8.2 --for-extensions="$EXTENSIONS"

The first part of this script defines the extensions we want to download packages for while the other part actually downloads the packages. The extensions you select are dependent on what your application needs to run properly. For Laravel Zero, the ones I have up there should suffice. Instead of the --for-extensions option, we can use the --all flag, which downloads all packages instead.

Sometimes, I notice that, download fails when an extension requires download to be made from a Chinese source. I mostly use VPN to bypass this on my local machine making me conclude that, its probably a location or ISP based issue. In the CI, this doesn't seem to happen and download should work fine.

Now that the packages are downloaded, we are ready to build the static binaries. In this aspect, there are three options - PHP CLI, PHP FPM and MicroSFX. (Needs more clarity) PHP distributions ships with two SAPIs, the CLI SAPI for command line applications and FPM (used with Apache, Nginx, etc.). In PHP distributions they are separate and you can instruct SPC to build any or both of these binaries depending on what you want. The last one is the MicroSFX SAPI which works behind the scene as the glue between a PHP application code and the PHP interpreter.

The MicroSFX SAPI is likened to be a self-extracting archive, such that when launched(or executed), it unpacks it's own content. Instead of unpacking a content in this case, it executes a PHP file or PHAR attached (combined) with it. It's able to do this by handing the attached file over to the PHP interpreter that is embedded inside of it. You can read more on the MicroSFX SAPI here

For our app to work, we only need to build the CLI and the MicroSFX SAPIs since our Laravel Zero app will only run in the command line. We can build the SAPIs like this:

./spc build --build-micro --build-cli "$EXTENSIONS"

In addition to the obvious flags specifying the SAPIs, we also passed the extensions that will be compiled together with the SAPIs. Depending on the extensions you select and the specs of the computer you are running, this process could take while. The brighter part is that you only have to do this once, as long as you don't need extra extensions as this would require another compilation.

At this point you should have php and micro.sfx executables present in the buildroot/bin directory of the current project.

Combining micro.sfx and PHAR file

Now that the major part, the compilation of the SAPIs, is done, we can now compile the application itself into a single binary. SPC allows you to compile a single file script as well as a PHAR into a single binary. Since we already built the application into a PHAR under builds directory, the next step is for us to "combine" the PHAR file and the micro.sfx file as one. We can do this using the micro:combine command of the spc binary like this:

./spc micro:combine builds/quote -O bin/quote

This generates a new file inside the bin directory. A new file that shelters both the application and MicroSFX which has the interpreter. You will notice the size of the final binary is significantly larger than the initial PHAR file under builds/quote as a result of this combination.

You can then test this binary to be sure everything works fine:

./bin/quote inspire

Verifying the binary

The best way to verify this is, of course, in an environment where PHP isn't present. You can uninstall/break (😎) your PHP installation or run the executable on another (Virtual) Machine. However you choose, you should see the binary working as expected.

With this, we are able to get a PHAR file, that depends on PHP being installed in the target environment, to a single binary that doesn't need PHP in the target environment. Next, I try to share some of my thoughts around this exploration. These thoughts covers benefits and downsides alike.

What are some benefits of this method?

There are cases where you are unable to guarantee that PHP is installed where your PHP code is going to be running, so you have to make sure PHP is installed before shipping your code there. Implications of having to install PHP might differ based on needs and constraints. Some environments might not be able to use your software because they can't (or won't) install PHP. Also, depending on how and where you want to install PHP the issues surrounding the installation might be more. Shipping as a binary cuts out installation processes, the dependencies you need to install or configurations you need on the target where the PHP app will run.

Furthermore, you might not be able guarantee that PHP is not broken in the environment your software will be installed. There are cases where, after a Brew upgrade, you will discover that PHP is broken and at this point EVERYTHING that depends on the PHP executable is, broken as well. In a case where some tools are critical for day to day, this might be unacceptable. For example, when PHP is broken, Laravel Valet is broken and cannot rescue the situation because it also depends on PHP. Having a self-executing binary avoids all these and guarantee that the app is not at the mercy of the target's PHP installation.

Another common thing is differences in PHP versions or having to be as backward compatible as much as possible knowing well that you are not sure of what version might be running on the target. A PHP app might benefit from a new feature in a new PHP version, but doesn't mean users are bound to migrate, even for logical reasons. When you compile the app as a binary, this isn't a problem as your users can keep whichever PHP version they want while you are able use new language features in your own software. This is particularly useful where PHP apps are distributed as CLI tools.

Another benefit I see, which is win for the community is that more people will be able to use tools from the PHP community since the barrier of setting PHP up is removed and the app can now function just like any binary they are used to.

Even more, we will be able to use PHP, combined with extensions like Swoole/Swow or Libraries like ReactPHP and Amphp to build system tools that might otherwise be built with compiled languages like Rust or Golang for the very reason of performance and portability as a single binary.

Another benefits of this method is that, we can now distribute PHP tools outside of Composer. Some PHP-based tools are distributed via composer to ensure the target platform have the dependencies required for the tool. Now that everything can ship as a single binary, we don't have to distribute through Composer or limit it to Composer who also requires PHP on the target platform. Maybe in the future we can see PHP tools distributed through channels like APT and Homebrew.

Some downsides

This also doesn't come with some downsides and limitations. Some of this can be desirable or not depending the people concerned.

One that comes to light quickly is that the process can be tedious since this isn't something that is part of the "standard" way you would use to ship PHP apps. The process involves downloading dependencies and then compilation occurs. How fast the compilation of the initial static binaries will be depends on:

  • System specs (Cores, mostly)
  • The number of extensions that will be compiled with it
  • Some extension can also take longer to compile than another
  • The SAPIs you are compiling for - since the goal is to build a binary embedding your application code, the MicroSFX and the PHP CLI is often needed
  • The amount of dependencies that will be downloaded based on the extensions

Generally, the SPC package has made this as simple as possible to automate/abstract a lot of this part. It even caches the downloaded the dependencies, so it won't be downloaded again. also, if the extensions aren't changing much, the already compiled static PHP binaries can be used for compiling the final application binary.

Size is another thing when considering this method of shipping your application. Depending on the extension(s) being compiled with the static PHP binary, the size of the final binary can be huge. If size is a constraint for you, this might not be good. Compiling recommended extensions together with another project went up as high as 80Mb. The size can be reduced further by getting rid of development dependencies and only compiling what you need in the application PHAR. Since shipping without some extensions might not be good for some applications, there isn't much choice as to what can be done to reduce the size of the generated static PHP binary.

Another difficulty you want to watch out for is building for other platforms. Compiling binaries in languages like Rust and Golang for other platform in the development platform is already built into their tooling. With that you can target the platform you want to build for when developing on a different platform. This isn't possible in this case, since you need to be on the same platform as your target to compile the static PHP binary for it. A work around is to rely on a platform like GitHub Actions to build for different platforms.

Just incase it isn't obvious, you won't be able to modify the source code once the binary is built since PHP apps are not usually deployed this way. So, this is another thing to consider when choosing to go with this method as you can't just SSH to the server and make changes 😎. I also think that, for some type of applications this shouldn't be a problem.

One last thing to mention here, is that you will still need to ship your PHP code as it is if you need FPM - for applications that are served this way. The aspect of a static binary that will be of benefit here will only go as far as having a PHP FPM binary where all extensions are statically compiled together. The advantage here is that extensions won't need to be loaded dynamically, so the app will boot up faster.

What does adoption of this method currently look like?

  • Swoole CLI, a binary distribution of PHP with Swoole extension compiled together
  • Hyperf, a Laravel-like framework based on Swoole and Swow has a tool, Box, that can compile a Swow based Hyperf application as a single binary
  • Webman, a async PHP framework, based on Workerman, already have a version of this method of compilation as well
  • Laravel Herd, a new development environment for Laravel uses this method to ship multiple PHP binaries with extensions already compiled in them
  • FrankenPHP , a new PHP SAPI, also use SPC to embed PHP application code and asset as a single binary

Converting PHP projects into single binaries with SPC opens up exciting possibilities for PHP developers. While there are challenges to overcome, such as compilation intricacies and binary size considerations, the benefits, including improved compatibility and easier distribution, make this approach a compelling option. As adoption grows within the PHP community, we can anticipate further refinements and enhancements, making this method an increasingly valuable tool for PHP developers.

Do you want to share your thoughts on this article? You can leave a comment on X(Twitter) or LinkedIn.