How to create a PHP Playground for your documentation
Since I gave the talk “True Serverless or how to run PHP in the browser to document an open source project” and released the API Platform Playground, many asked to build their own. I’ll show here how to use php and WASM to build your own playground.
Let’s build this PHP playground in 43 lines of code.
# PHP + WASM
There’s some history about PHP In Browser by oraoto and PHP Wasm by seanmorris, I mainly continued working on this at PHP Wasm where the build got improved with Docker steroids.
We use Emscripten to build PHP and target Webassembly so that you can run php index.php
in a web page.
# First step: get the PHP WASM build
# Get my last image (contains libxml and sqlite)
docker pull soyuka/php-wasm:8.2.10
# Create a container
docker create --name=php-wasm soyuka/php-wasm:8.2.10
# Our build goes to `php-wasm`
mkdir -p public/ dist/
# Copy the build of PHP Webassembly
docker cp php-wasm:/build/php-web.mjs ./dist
docker cp php-wasm:/build/php-web.wasm ./public
We use the Emscripten module (mjs
) build. If you want to change this you need to head to php-wasm README.
# Second step: preload data
To run PHP we need PHP code. Let’s prepare a directory with an index.php
file:
<?php
# src/index.php
phpinfo();
Then we want to integrate this to our php-web.mjs
build. We use three different volumes, one for source code (src
), one for our output (dist
) and our public
directory:
docker run -v $(pwd)/src:/src -v $(pwd)/public:/public -v $(pwd)/dist:/dist php-wasm python3 /emsdk/upstream/emscripten/tools/file_packager.py /public/php-web.data --use-preload-cache --lz4 --preload "/src" --js-output=/dist/php-web.data.js --no-node --exclude '*/.*' --export-name=createPhpModule
Because the way emscripten JavaScript shell works we need to copy the new php-web.data.js
inside the php-web.mjs
we built on first step:
sed '/--pre-js/r dist/php-web.data.js' dist/php-web.mjs > public/php-web.mjs
# Third step: run PHP
Create a public/index.html
file where you run:
<!-- public/index.html -->
<iframe id="output" width="100%" height="50%" frameBorder="0"></iframe>
<script type="module">
import phpBinary from "./php-web.mjs";
async function main() {
const output = document.getElementById('output')
const buffer = [];
const {ccall, FS} = await phpBinary({
print(data) {
buffer.push(data);
}
})
console.log(ccall("phpw_exec", "string", ["string"], ["phpversion();"]));
window.FS = FS;
window.run = () => {
// Note that `/src` is the path we used when preloading!
ccall("phpw", null, ["string"], ["/src/index.php"]);
output.contentWindow.document.body.innerHTML = buffer.join('');
buffer.length = 0;
}
run();
}
main();
</script>
I usually run a Caddy file-server:
caddy file-server --listen localhost:8080 --root public
# Last step: add some JavaScript
Let’s add a Monaco (VSCode) editor
<div id="monaco" style="min-height: 100px"></div>
<script type="module">
import * as monaco from 'https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/+esm';
const editor = monaco.editor.create(document.getElementById('monaco'));
const model = editor.getModel();
model.setLanguage('php');
model.setValue(window.FS.readFile('/src/index.php', {encoding: 'utf8'}).toString());
model.onDidChangeContent((v) => {
window.FS.unlink('/src/index.php')
window.FS.writeFile('/src/index.php', model.getValue());
window.run();
});
</script>
Note that FS
is documented at https://emscripten.org/docs/api_reference/Filesystem-API.html and everything is synchronous.
You can see this live at: https://soyuka.github.io/php-wasm/
On the API Platform playground we use React, the logic remains the same.
If you like the work consider sponsoring.