It is time to bring opposites together, to stir up these stagnant waters, and to restore an industry that is dynamic from within, with multiple connections that were previously impossible or unimaginable.
In the previous parts of this series, we explored how PureScript acts as a quiet rewrite of the Web, offering a territory of mathematical precision above JavaScript, and how its universal polymorphism allows it to target wildly different runtimes like Node.js, Erlang's BEAM, Chez Scheme, etc.
But hey, what about the elephant in the room? Yes, what is running the majority of the web, today, in the shadow? You know. The eternal one, the big one, the one whose death everyone talks about, but who never truly dies.
PHP.
While the modern JavaScript ecosystem (among others) races toward edge computing and containerized microservices, around 70% of the web is still powered by PHP. From legacy WordPress sites to affordable shared hosting (cPanel, OVH, GoDaddy, basic LAMP stacks), a massive portion of the internet lives in environments where escaping the PHP runtime is either financially unviable, technically impossible, or simply unnecessary. And I haven't even mentioned powerful and mature ecosystems like Laravel, Symfony, etc. They have proven their worth time and again over the years.
And yet...
For a long time, the PHP ecosystem has been left without a true, strictly typed functional and safe alternative, like TypeScript for Javascript. Even though recent PHP versions introduced fantastic features (short closures, match expressions, Fibers), the frustration that comes with its historical limitations and typing remains very real for some of its users.
Oh yes, there is Hack, and there are phenomenal static analyzers like PHPStan... but Hack eventually required a custom runtime, and PHPStan (as powerful as it is) is ultimately trying to bolt safety onto a fundamentally permissive and imperative language through comments.
What we are talking about here is a true paradigm shift in safety and ergonomics, keeping the great portability of PHP, as it is.
Very well!
This is why I built phpurs: a brand new compiler backend that transpiles PureScript directly to modern PHP 8.4+ syntax.
It is no longer a proof of concept. It is passing the official PureScript test suite, and it is ready for production.
Here is how we bring the elegance of purely functional programming to the big web, while keeping deployment as simple as a legacy FTP upload, or anything alike.
The easy deployment paradox
In modern DevOps, deploying an app often means configuring Docker containers, CI/CD pipelines, Kubernetes, or serverless edge functions. It is powerful, but it is expensive and sometimes... a bit too complex for the real need. Let's be clear: I have nothing against it, I use it every day. But it's not always necessary. A website can run just fine on a very low cost server, possibly a shared one, without needing such an arsenal of tools.
For many freelancers and agencies, the reality is different indeed: clients often have a simple, small and cheap shared hosting plan. They want a robust application, but they don't want to pay for a managed AWS infrastructure.
So... phpurs bridges this gap. It allows you to write your entire business logic, API, and background processes in 100% pure, strictly typed PureScript. And you get all the guarantees of ADTs, pattern matching, the Aff monad, HKTs... and so much more...
Then, you compile it, and you get plain .php files. No overhead. The compiler has pruned what the runtime won't need, and optimized everything (e.g. Tail-Call Optimization, Dead-Code Elimination, Uncurrying...).
You just drag and drop these files via FTP to your client's cheap HostGator, OVH server, or whatever... and it works. No container, no daemon, no reverse proxy. Just native execution.
One of the biggest strengths of PHP is its portability. We leverage it.
Architecture: standing on the shoulders of giants
In fact, phpurs doesn't reinvent the entiiiire wheel. It stands on the incredible work of the PureScript community (e.g. Arista's purescript-backend-optimizer).
The compilation pipeline is beautifully decoupled:
- At first,
purscompiles your code tocorefn.json. - The
backend-optimizerreads this CoreFn and performs aggressive dead code elimination (DCE), inlining, uncurrying, etc. - At the end,
phpurstakes this highly optimized intermediary AST and prints it as modern PHP syntax. And of course, it will add additional and sophisticated optimizations for PHP's sake.
Concurrency: Aff meets PHP Fibers
One of the biggest historical flaws of PHP was its synchronous, blocking nature. How do you implement PureScript's asynchronous Aff monad in such an environment?
Well, PHP 8.1 introduced Fibers, and the Revolt event loop followed.
Support for Aff has been natively added to phpurs. The behavior of the event loop in the compiled PHP code perfectly mirrors its Node.js counterpart: asynchronous operations are handled transparently without blocking the OS thread. The main process will automatically wait for all pending Aff tasks to complete before exiting, for example.
You can seamlessly run your code with a JS runtime, or a PHP one. You can even switch from one to another, when you're coding.
There is no need for complex daemonized C or Go extensions like Swoole or FrankenPHP. By relying exclusively on native PHP Fibers and asynchronous libraries (like Amphp), phpurs ensures that your concurrent PureScript code can run natively anywhere PHP runs. It just works, and handles multiple requests, like a good ol' Express server in Node.
Moving complexity from runtime to comptime
In standard PHP or JS development, safety often comes at the cost of defensive programming at runtime: checking boolean flags, instantiating heavy wrapper classes, or placing if (!$form->isValid()) throw new Exception(...) checks throughout your domain logic.
Like TypeScript, PureScript flips this paradigm. But it goes further, with a powerful Hindley-Milner type system. Once the PureScript compiler validates your logic, the underlying compiled PHP is freed from defensive checks, leaving a trivial, lightning-fast execution path.
For example, consider a form processing pipeline where you must guarantee that data is validated before saving it to a database. Using Phantom Types, we can tag the state of our data (Unchecked vs Checked).
-- 1. Phantom Types
-- They exist ONLY for the compiler, zero runtime cost.
data Unchecked
data Checked
newtype FormData status = FormData { email :: String, age :: Int }
-- 2. Construction: Creates an `Unchecked` form
createForm :: String -> Int -> FormData Unchecked
createForm email age = FormData { email, age }
-- 3. Smart Constructor.
-- Only validation transforms `Unchecked` into `Checked`
validate :: FormData Unchecked -> Either String (FormData Checked)
validate (FormData d) =
if d.age >= 18 && contains (Pattern "@") d.email
then Right (FormData d)
else Left "Invalid form data"
-- 4. Database operation ONLY accepts a `FormData Checked`!
saveToDb :: FormData Checked -> Effect Unit
saveToDb (FormData d) = ...
It is mathematically impossible to call saveToDb with unvalidated data (FormData Unchecked). If you try, the compiler halts with a type error before any code is ever generated.
In PHP, we could translate what's above into something like that:
// 1. We must define wrapper classes and mutable state flags
class FormData {
public function __construct(
public readonly string $email,
public readonly int $age,
public bool $isValidated = false // ⚠️ Runtime state to track!
) {}
}
// 2. Construction: creates an "unchecked" form
function createForm(string $email, int $age): FormData {
// We instantiate a real class object in memory
return new FormData($email, $age);
}
// 3. Validation mutates the state or throws Exceptions
function validate (FormData $data): void {
if ($data->age < 18 || !str_contains($data->email, '@')) {
throw new Exception("Invalid form data"); // ⚠️ Runtime defensive throw
}
$data->isValidated = true; // ⚠️ Runtime mutation
}
// 4. Database operation MUST defensively check the state
function saveToDb (FormData $data): void {
// ⚠️ Defensive check at runtime: did the developer call validate() before?
if (!$data->isValidated) {
throw new Exception("Security error: Data not validated!");
}
// ...
}
Of course, advanced PHP developers might say they could use strict Value Objects (creating a separate ValidatedFormData class) or advanced PHPStan docblocks to avoid the boolean flag. And they'd be completely right. I did it too, for years.
But Value Objects add memory allocations and mapping overhead at runtime, while PHPStan only enforces safety through comments, not rock-solid compiler guarantees. More generally speaking, existing solutions are often smart compromises.
PureScript gives you the best of both worlds: strict, mathematically proven safety at compile-time, that compiles down to raw, zero-overhead unboxed objects at runtime. I simplified it for the example, but now, look at the compiled PHP code phpurs outputs for the entire pipeline:
// 1. Creating a `FormData Unchecked` simply returns a raw PHP object.
// The `FormData` newtype wrapper is completely unboxed!
$createForm = function ($email = null, $age = null) {
return (object)["email" => $email, "age" => $age];
};
// 2. Validation checks the raw data and wraps it in `Right`.
// No state classes or newtypes are instantiated!
$validate = function ($formData = null) {
if ($formData->age >= 18 && str_contains($formData->email, "@")) {
return new Data1("Right", $formData);
}
return new Data1("Left", "Invalid form data");
};
// 3. Database save has ZERO defensive checks (no `$data->isValidated`)
// PureScript guaranteed 100% safety at compile-time.
$saveToDb = function ($formData = null) {
...
};
Zero defensive runtime overhead. No boolean flags, no try/catch blocks, no dynamic state assertions inside $saveToDb.
Zero object allocations: the Phantom Types (Unchecked/Checked) and FormData newtype wrapper don't instantiate any PHP classes. At runtime, $formData is just a plain, raw PHP object seamlessly passed from $createForm to $saveToDb.
Complexity lives strictly in the compiler. What runs on your host is clean and fast.
What about performance?
It would be a legitimate question. For example, PHP is often criticized for its raw execution speed compared to V8 (Node.js).
In raw micro-benchmarks, PHP 8 is indeed about 2 to 3 times slower than V8 for heavy computational tasks. However, in the vast majority of real-world web applications, the main bottleneck is I/O (database, network, filesystem).
The difference in performance is entirely negligible in practice. You lose almost nothing in speed, but you gain the ability to deploy your code on any server in the world for pennies.
I recently battle-tested phpurs against the test suite of a real-world project (still a WIP, but the code is mature). The results are clear: in practice, it is only x1.15 slower. Of course, the value of this factor will depend on your very specific project(s). But remember: PHP is not the preferred backend for performance, it is for portability.
The next frontier
I am already using it in production for several client projects with strictly PHP-only environments, and the runtime stability is rock solid (and super fast). 🟢
Pure dev pleasure, with pragmatic client constraints. Getting the best of both worlds is a very, very pleasant feeling.
Community contributions are highly welcome! If you want to help add missing PHP FFIs to core and major PureScript libraries, your PRs are more than welcome.
Let's bring the elegance of PureScript to the 70% of the web that still runs on PHP! 🐘
Cheers 👋






Top comments (0)