Summary
Hey there folks 😁.
Thanks for clicking on this article, shall we dive back into technical stuff? Are you up for it? In any case, I hope you'll enjoy it.
So, how are you doing? I'm in great shape!
You know Caddy, it's a modern web server, written in Go (yes yes, that compiled language that generates static binaries...).
Well today, we're going to tinker with its PHP wrapper, FrankenPHP.
By the way, just a little info, the fact that FrankenPHP uses Caddy gives us access to nice things like "Early hints", you don't know what that is? Go ahead, do some research.
Are you ready? Let's go!
Well then, first of all, this paragraph might not be so true in the near future, since we'll probably all be switching to Apple containers.
I haven't looked into it yet, but I imagine it's promising. For now, I don't really have time to move my stack (there's a lot of stuff, let me tell you) to Apple container.
Let's get back to the present and talk about Docker on macOS.
This is a problem that doesn't really date from yesterday. I haven't done any benchmarks on this subject again since then, simply because my solution has been working perfectly for years.
But by default, when you mount a volume that is actually a folder on your Mac, you're going to have pretty atrocious slowdowns...
By the way, my solution to this is to create a "Sparse Bundle" with a case-sensitive file system on macOS (like Linux respects case sensitivity), and put all my projects in it...
And so for that, I need to run PHP natively on macOS and not in a Docker...
If you want a concrete example of what I'm telling you, let's talk about the Symfony cache...
Symfony is a framework that's a real pleasure to develop with, but for that, we have the profiler...
And so, for all that to work well and give us the best possible development experience, Symfony writes and reads a lot in the var/cache folder.
What about putting this directory in a ramdisk or an unmounted volume?
Well, yes it's a solution... but you know, I'm a big llama...
Let me explain, I really like my little PHPStorm, even if it's heavy.
And to easily see how things work, you have to admit that having the vendors directly on the Mac is convenient.
There would be the solution of putting the vendors on the Mac and in an unmounted Docker image, or even things like rsync (which I tested at the time), but all that becomes a Rube Goldberg machine.
Simply multiple versions of PHP, installed with Homebrew, works very well, and then for extensions, pecl does the job very well.
Once you have this setup, just do:
And it works perfectly, well, it worked perfectly, because now, there's FrankenPHP.
FrankenPHP is a modern PHP application server, built on top of Caddy FrankenPHP: the modern PHP app server, that famous web server written in Go that I was telling you about just before.
But FrankenPHP isn't just a simple wrapper for Caddy. It allows you to embed the PHP interpreter directly into the web server binary.
Unlike PHP-FPM which completely restarts your application with each request (autoloader, container construction, etc.), FrankenPHP's worker mode allows your application to stay in memory and reuse elements that can be reused between different HTTP requests.
According to an analysis performed by Sylius, using FrankenPHP's worker mode reduces response times by 80%, while dividing by 6 the number of machines needed to serve the same number of users.
FrankenPHP uses Go's goroutines (as seen on the project's official page), which allows it to handle concurrency very efficiently. Your application can also be served as is, even if it's not compatible with worker mode.
The server starts with a simple command, and you only need one binary to make everything work, without an external service (there are other modes, but in production, it's quite handy and fits well with Docker philosophy).
The coolest part? FrankenPHP can watch your files and automatically restart workers in the background when a file changes.
Mercure allows you to do real-time in your applications, it's well known in the Symfony ecosystem... (Turbo and all that)
Mercure uses Server-Sent Events (SSE) to push real-time events from the server to clients. No need for a particular JavaScript library, the browser already knows how to do it!
And, it's directly integrated into FrankenPHP.
In short, we're looking at a next-generation web server for PHP.
By the way, on Dockerhub you can see ready-to-use FrankenPHP images.
If you use Windows Subsystem for Linux or simply Linux, I think you should completely use these Docker images for your dev projects.
And yes, Kévin Dunglas makes FrankenPHP available on Homebrew.
Simply because during my tests, when I wanted to use it, it came with PHP 8.4, and in my case I needed an older version of PHP.
The project could probably work on PHP 8.4 but I didn't want to tempt fate, I just wanted to be able to work (and it allowed me to better understand FrankenPHP).
We already talked about this earlier, FrankenPHP's worker mode requires "thread-safe" PHP (ZTS) because FrankenPHP uses Go's goroutines to handle multiple requests in parallel in the same process.
Unlike PHP-FPM which creates separate processes (each with its own isolated memory), FrankenPHP starts multiple workers as threads that persist between requests. These threads share certain memory areas, which absolutely requires PHP to be compiled in "thread-safe" mode.
In Thread-Safe mode, PHP can be a tiny bit slower, which is compensated by the stack with FrankenPHP.
Also, it's possible that some extensions don't work properly with PHP ZTS.
You can recompile your PHP or directly use this Homebrew tap.
As we already discussed, if you want the latest version of PHP, then simply use Homebrew, otherwise...
Otherwise, regardless of the compilation mode, start by pulling out your best "git clone" to get the FrankenPHP code locally:
Well then... of course after that you have to do a legendary "cd" to go into the folder you just created (incredible, isn't it? 😝)
Here in our binary we have PHP directly, but be careful XDEBUG is not compatible with this mode.
If you look in the source code, you'll see a little file "build-static.sh", it's well named isn't it?
This file (build-static.sh) will allow you to create a binary file that contains everything you need to run PHP and you can even choose the extensions you need, however be careful, with this method, you completely recompile PHP, so it's very long.
Before using it we give it environment variables, the PHP version, or the extensions you want...
If you want to see the complete list of environment variables, it's available here.
Yes, yes, you're not dreaming, I'm getting called out because PHP version 8.3 is deprecated.
Once the compilation is finished you'll have a nice binary that contains everything you need to run your Caddy server and your PHP application.
In this mode, we use PHP available on our system (the ZTS installed previously), and we can also use xdebug then 😁.
Well, first we need a "Thread safe" PHP on our system:
Here, it's version 8.3 that I'm installing but you can do it with 8.2 or even 8.4 (if you're in the future 8.5...).
Now, my method is not the one recommended by Kévin Dunglas (the FrankenPHP developer), having already compiled a few Go applications in the past, I just used a good old basic "go build"...
Well, that's my brute force method, but in the official docs, they recommend using xcaddy.
With xcaddy it gives something like this (copy-pasted from the docs).
The advantage with xcaddy is being able to add modules, but I haven't tested it, I just did a quick "go build" myself 😝.
Once you have your wonderful binary available (static or dynamic), you can place it somewhere like "/usr/local/bin/frankenphp" or "/usr/bin/frankenphp".
Then to launch frankenPHP, you need to give it some environment variables and its Caddyfile (its config, which you must create).
As for the Caddyfile, I'll let you search for it, that will give you some work (come on now! 😝).
Our little tour of FrankenPHP is now complete.
The idea of going through the hassle of compiling by hand is to understand how it works, and to understand the value of using it.
Did you know there was a Thread Safe mode for PHP? Personally, I've been using FPM for so long that well... no, I didn't know.
See you next time for another article, in the meantime, take care 🤩.