Skip to main content

The Web APIs powering the Drupal CMS trial experience

Published on

This blog expands on my DrupalCon Barcelona talk, which I managed to squeeze into a twenty-minute session slot. You can download a copy of my slides. Unfortunately, I could not dedicate enough time to the project and stepped down as the trial track lead. The Drupal CMS trial is no longer based on my WebAssembly work, and an ongoing process is being conducted to provide an official demo. But, I think there are some exciting things in the future with Drupal and WebAssembly.

The Drupal CMS trial experience has been a challenging and exciting project to build. It has allowed me to work with Web APIs that I've never tried before and experience what a truly fantastic platform the modern web provides. The Drupal CMS triage leverages native and open features available on the web for a unique trial experience. The trial experience runs PHP in the browser with WebAssembly (Wasm) and various Web APIs to interact with the Wasm runtime. 

WebAssembly... PHP in the browser?

WebAssembly, known as Wasm, is part of the open web platform and allows the execution of compiled code in the browser. It defines a portable binary-code format (like assembly language.) Compiled languages like Rust, Go, and C/C++ can have Wasm as a compilation target. The Emscripten project allows the compiling of C or C++ projects for Wasm. Since the PHP interpreter is written in C, it can be compiled into Wasm for execution in the browser. This is what allows us to run PHP scripts in the browser!

All browser JavaScript engines support WebAssembly; you can also run WebAssembly in Node or Deno! There are also standalone Wasm runtimes. WebAssembly is an open standard and managed by a W3C Community Group and W3C Working Group.

I recommend reading the FAQ: https://webassembly.org/docs/faq/.

Emscripten compiles C to WebAssembly

Let's look at a typical server stack when running a Drupal site. Users have their browsers that access the server. The server runs an operating system, a web server, PHP, and a database. The web server receives the request, and a Common Gateway Interface (CGI) like PHP-FPM handles the request from the web server and executes the Drupal codebase. The server then returns server-side rendered HTML from Drupal.

Drupal running in a normal stack

Now, let's imagine that stack with Drupal running in WebAssembly. The browser becomes the web server. We have PHP and SQLite available within WebAssembly. The rest of this blog will detail how the browser acts as the web server and CGI for serving Drupal.

Drupal running in WebAssembly

WebAssembly removes the need for third-party services to try Drupal.

WebAssembly & JavaScript

Emscripten provides the required JavaScript to interact with PHP in WebAssembly using the ccall function. We can then build code around the Emscription JavaScript for easily calling these functions and building applications.

Here is an example invoking Emscripten ccall directly

import PhpBinary from './php-web.mjs';
return new PhpBinary()
.then(({ccall, FS}) => {
 ccall(
   'phpw_run',
   'string',
   ['string'],
   [`<?php echo "Hello, world!";`]
 );
})

Here is an example of printing “Hello world” using php-wasm's provided JavaScript code:

const { PhpWeb } = await import('PhpWeb.mjs');
const php = new PhpWeb;

const exitCode = await php.run(
'<?php echo "Hello, world!";'
);

Emscripten provides more than compiling to Wasm and JavaScript to interact with Wasm. Emscripten provides a filesystem API through JavaScript, which stores files in memory or IndexedDB. This enables the browser to act as the web server that “hosts” the files. Then, we can use a service worker to act like the CGI in sending requests to PHP and return server-side rendered content.

IndexedDB as the filesystem

IndexedDB is a low-level API for browser storage intended for structured data, including files and blobs. As stated before, Emscripten provides a filesystem API through JavaScript that uses IndexedDB. These files are accessible by the Wasm runtime and are available in PHP. When using the Drupal CMS Wasm trial, it downloaded a Zip archive and extracted it into the IndexedDB filesystem using PHP code. The Drupal codebase is then stored on the browser inside an IndexedDB store. 

Drupal codebase in the IndexedDB store

The trial also used IndexedDB to store cookies for user sessions (see https://github.com/seanmorris/php-wasm/issues/57.)

Web Workers API

The trial experience used a dedicated worker and a service worker to run Drupal in the browser.

Service worker for serving PHP requests, and a dedicated worker for executing PHP scripts

Service worker

A service worker runs in the background and can intercept outgoing fetch requests. If you have worked with Progressive Web Applications, you may be familiar with them. The service worker checks each browser request and sees if PHP should handle it, such as if it wasn't for an "external" URL or a static file stored in IndexedDB. The service worker would invoke PHP in WebAssembly to run Drupal's index.php for the request. It then processes the response from PHP and returns it to the browser. The service worker was like CGI to return data from the filesystem or server-side rendered content in PHP. It acted like PHP-FPM. 

One caveat is that the PHP CGI build doesn’t allow running arbitrary code, only scripts, on the filesystem. So, a different way was needed to run arbitrary scripts (like unpacking the Zip archive containing a Drupal codebase.)

Dedicated worker

The dedicated worker allowed executing PHP code directly without blocking the main thread. Initially, when executing PHP scripts, the browser would appear “locked up” and not repaint UI changes. That's because it was utilizing the main thread. Dedicated workers allow performing work in the background while still updating the main window. This was used to unarchive Drupal CMS or perform behind-the-scenes actions.

Web Components (custom elements)

The trial leveraged a web component for its dynamic user interface, instead of using a third-party library like React, Vue, or Svelte. The trial-manager web component was used to start a trial or perform actions on an existing trial (like exporting or deleting it.) Web Components classes allow encapsulating logic and reacting to attribute changes. The component interacted with the dedicated worker to download and extract Drupal CMS

Broadcast channels

Only the main window can (easily) send messages to the service worker, and the service worker is vital to running Drupal in the browser. The Broadcast Channel API created a message bus to send events between the various components - the web component, the dedicated worker, and the service worker. When the service worker is ready, it dispatches a message so the trial manager web component can start by downloading the Drupal archive. The web component ca then tell the service worker to refresh and recognize new files after the setup.

What is next?

I need to update my repository at https://github.com/mglaman/wasm-drupal.It's a little outdated as I focused on work inside of the Drupal CMS repository. But I'd like to continue making it so anyone could stand up a Drupal + WebAssembly experiment.