Client Side PHP

October 6, 2019

I'm sure you've come across plenty of sites that use javascript on the front end, but what about using PHP as a client side language? How, you ask? Since PHP is C based, it's actually possible to compile a version that's able to run inside your web browser using web assembly.

Is it a good idea? Let's find out!

Compiling PHP to Web Assembly

Compiling PHP for browsers isn't actually as hard you'd think. Using Oraoto's compilation scripts it's pretty easy to get a PHP wasm build that works just like PHP would on a server or in your CLI.

Well, mostly. Currently it's a bit limited as it only allows you eval code, and then receive lines printed by PHP. Even so, it's possible to build some pretty cool stuff.

Building a Laravel Collection Playground

I love Laravel's collections. They make working with arrays of data awesome, but sometimes it takes a bit of tinkering and back and forth between your code and the docs to figure out how implement your logic.

Building something to muck around with collections in the browser, jsfiddle style, seemed like a good chance to check out PHP in browser & build something cool.

You can check out the repository on Github and try it out for yourself here.

Playground

How it works

I built a small PHP package that receives the input json & collection code from Vue. The code is compiled into a phar (php executable) along with the code of Laravel's collection component.

It simply converts the json into a collection, and then uses eval to execute the collection code provided. The result, or any errors (caught Exceptions or Throwables) are encoded back into json and printed. Back in the javascript, we hook the PHP's stdout and display the execution result back to the user.

It works surprisingly well, and thanks to the PWA support you can even use it without internet.

What if we could run Laravel on the client side?!

Cool, we've built something that runs PHP code, but collections are pretty simple. What if we could run an entire web application, built in Laravel completely client side? That's true serverless 😉

The TodoMVC project is often used to test frameworks, so I thought it would be a good candidate to try and get running.

The first hurdle we have to solve is making requests to Laravel, since there's no web server running we can't just use a web request.

Time to get hacky! We can actually run the framework with a mocked PSR7 request, similar to how you would run the framework in an integration test. Since we can only interact with PHP by executing code, I wrapped the framework request cycle in a function:

function run(string $requestAsJson, string $requestId)
{
    $requestData = json_decode($requestAsJson, true);

    require __DIR__ . '/bootstrap/autoload.php';

    $app = require_once __DIR__ . '/bootstrap/app.php';

    $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

    $_SERVER['CONTENT_TYPE'] = 'application/json';

    $request = \Illuminate\Http\Request::create(
        $requestData['uri'],
        $requestData['method'],
        $requestData['params'] ?? [],
        $requestData['cookies'] ?? [],
        [],
        $_SERVER,
        $requestData['body'] ?? null
    );


    /** @var \Illuminate\Http\Response $response */
    $response = $kernel->handle($request);

    echo json_encode([
        'id' => $requestId,
        'status' => $response->getStatusCode(),
        'content' => $response->getContent(),
        'headers' => $response->headers->all(),
    ]);

    $kernel->terminate($request, $response);
}

After a quick test in the console to make sure it's working, we can bundle Laravel and all its dependencies into a phar (emscipten's virtual file system doesn't handle large numbers of files well) and build that into our web assembly binary.

Since we instructed laravel to use SQlite, our application state can persist between (faked) requests. It will be reset each reload however, as the storage is only temporary.

Testing it out in the browser's console, we can add a new todo item and the make a request to the index method:

function request(data) {
    const reqId = Math.random().toString(36).substring(7);
    const code = `$phar = 'phar://app.phar';require $phar . '/index.php';run('${JSON.stringify(data)}', '${reqId}');echo PHP_EOL;`;
    const ret = phpModule.ccall('pib_eval', 'number', ['string'], [code]);
}

// Add a todo item
request({
  uri: '/api',
  method: 'POST',
  body: JSON.stringify({
      title: 'do washing'
  })
})

// Get the todo list
request({
  uri: '/api',
  method: 'GET'
})

{
    "status": 200,
    "content": "[{\"id\":1,\"title\":\"do washing\",\"order\":null,\"completed\":false,\"created_at\":\"2019-10-06 07:25:29\",\"updated_at\":\"2019-10-06 07:25:29\",\"url\":\"\\\/1\"}]",
    "headers": {
        "cache-control": [
            "no-cache, private"
        ],
        "content-type": [
            "application\/json"
        ],
        "date": [
            "Sun, 06 Oct 2019 07:25:53 GMT"
        ],
        "x-ratelimit-limit": [
            "60"
        ],
        "x-ratelimit-remaining": [
            59
        ],
    }
}

Conclusion

While it's fun to muck around with PHP in the browser, it's a long way from being usable. Here's a non exhaustive list of the drawbacks:

  • It's about 5x slower than PHP normally is
  • It uses >1GB of memory on startup, which causes lower-end devices to chug
  • It only works on recent desktop versions of Chrome, Firefox and Safari
  • It takes ages to compile a wasm build, and it's required for every code change
  • You can't do anything javascript can't do. This means no web requests, which limits the potential a ton
  • It requires you to download the entire PHP build + code (unless you are caching it client side) which comes in at around 4mb
  • It just decides not to work sometimes, and debugging is hard. You can't see much apart from PHP's exit code

Obviously, it's not suitable for real world use, but maybe in the future we could be writing PHP code for the client side.

Feel free to leave a comment below if you have any thoughts or suggestions!