From 892564228fa2030f78ca4ed439b620bc5af5a675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 8 Jun 2026 00:04:04 +0200 Subject: [PATCH 1/7] Document Playground as a generic PHP runtime --- packages/docs/site/docs/main/guides/index.md | 4 + .../site/docs/main/guides/php-frameworks.md | 149 ++++++++++++++++++ packages/docs/site/sidebars.js | 1 + .../PhpCodeSnippetLiveExample.examples.ts | 45 ++++++ 4 files changed, 199 insertions(+) create mode 100644 packages/docs/site/docs/main/guides/php-frameworks.md diff --git a/packages/docs/site/docs/main/guides/index.md b/packages/docs/site/docs/main/guides/index.md index 4555e909976..c92760a1e3b 100644 --- a/packages/docs/site/docs/main/guides/index.md +++ b/packages/docs/site/docs/main/guides/index.md @@ -13,6 +13,10 @@ In this section we present a selection of guides that will help you to both work Embed editable, runnable PHP and WordPress examples in any web page with the `` web component. The guide covers custom Blueprints, expected output, pure-PHP snippets, runtime sharing, and the standalone PHP Playground. +## [Run PHP frameworks in Playground](/guides/php-frameworks) + +Use Playground as a generic browser-based PHP runtime. This guide shows how to skip the WordPress download, load a bundled Symfony app with a Blueprint, and run it from a ``. + ## [WordPress Playground for Everyone](/guides/playground-for-everyone) Think Playground is only for developers? Think again. This guide shows how WordPress Playground helps beginners, site owners, and everyday users experiment safely — no technical expertise required. diff --git a/packages/docs/site/docs/main/guides/php-frameworks.md b/packages/docs/site/docs/main/guides/php-frameworks.md new file mode 100644 index 00000000000..111a49f8a61 --- /dev/null +++ b/packages/docs/site/docs/main/guides/php-frameworks.md @@ -0,0 +1,149 @@ +--- +title: Run PHP frameworks in Playground +slug: /guides/php-frameworks +description: Use WordPress Playground as a browser-based PHP runtime for frameworks and apps that are not WordPress. +sidebar_class_name: navbar-build-item +--- + +import { PhpCodeSnippetExample } from '@site/src/components/PhpCodeSnippetLiveExample'; + +# Run PHP frameworks in Playground + +WordPress Playground is also a browser-based PHP runtime. WordPress is the +most common app it boots, but a Blueprint can skip the WordPress download, +write any PHP files into the virtual filesystem, and run a framework such as +Symfony. + +This guide shows the shape of that setup. Use it when you want a shareable demo, +a docs example, or a quick compatibility check for a PHP app that does not need +a server, database, Node.js, Sass, or a local Composer install. + +## What changes when you skip WordPress + +Set `preferredVersions.wp` to `false` in a Blueprint, or `wp="none"` on a +``. Playground still downloads PHP, mounts a writable filesystem, +runs Blueprint steps, and supports networking when `features.networking` is +`true`. It just does not download or boot WordPress. + +That makes Playground useful for generic PHP examples: + +- PHP libraries that need a real filesystem. +- Framework demos that can run behind `public/index.php`. +- Documentation snippets that should execute in the browser. +- Reproducible bug reports for PHP code that is not WordPress-specific. + +## Try a Symfony app + +The example below uses a Blueprint to download and unzip a bundled Symfony app, +then a `` boots the Symfony kernel and renders the dashboard route. +The snippet prints the Symfony response status, the page title, and whether a +WordPress install exists. + + + +Here is the complete embed: + +```html + + + + + + + + +``` + +The same app is also available as a full Playground page: + +[Open the Symfony Package Radar demo](https://playground.wordpress.net/?blueprint-url=https%3A%2F%2Fraw.githubusercontent.com%2FWordPress%2Fblueprints%2Ftrunk%2Fblueprints%2Fsymfony-package-radar%2Fblueprint.json) + +## Package the app as a ZIP + +For framework demos, prefer a ZIP that already contains `vendor/`. That keeps +the Playground startup path short and avoids asking every visitor to wait for +Composer, Git, and package registry downloads. + +A small Blueprint can then install the app with one step: + +```json +{ + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "landingPage": "/symfony-package-radar/public/index.php", + "preferredVersions": { + "php": "8.4", + "wp": false + }, + "features": { + "networking": true + }, + "steps": [ + { + "step": "unzip", + "zipFile": { + "resource": "bundled", + "path": "./symfony-package-radar.zip" + }, + "extractToPath": "/wordpress" + } + ] +} +``` + +Use `bundled` resources when the ZIP ships next to `blueprint.json`, or use a +`url` resource when the ZIP is hosted separately. See [Blueprint bundles](/blueprints/bundles) +for packaging details. + +## Keep the demo browser-friendly + +A Playground-hosted framework demo works best when it: + +- Does not require a long-running background process. +- Stores generated files under the virtual filesystem. +- Avoids native extensions that are not compiled into PHP.wasm. +- Avoids frontend build steps at runtime. +- Keeps network calls optional or resilient, because browsers may require CORS + proxying for third-party services. + +Those constraints still leave plenty of room for real framework behavior: +controllers, routing, dependency injection, templates, forms, HTTP clients, and +plain PHP libraries all work when their PHP dependencies are available. diff --git a/packages/docs/site/sidebars.js b/packages/docs/site/sidebars.js index 821d62fa8cf..0552b70ab1f 100644 --- a/packages/docs/site/sidebars.js +++ b/packages/docs/site/sidebars.js @@ -46,6 +46,7 @@ const sidebars = { }, items: [ 'main/guides/php-code-snippets', + 'main/guides/php-frameworks', 'main/guides/agent-skill-wp-playground', 'main/guides/wordpress-native-ios-app', 'main/guides/for-plugin-developers', diff --git a/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts b/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts index c6a37a6d1d9..361d693d965 100644 --- a/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts +++ b/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts @@ -187,6 +187,51 @@ echo get_bloginfo( 'version' ); +`, + symfonyBlueprint: String.raw` + + + + `, illustration: String.raw` - - + + ``` + + The same app is also available as a full Playground page: -[Open the Symfony Package Radar demo](https://playground.wordpress.net/?blueprint-url=https%3A%2F%2Fraw.githubusercontent.com%2FWordPress%2Fblueprints%2Ftrunk%2Fblueprints%2Fsymfony-package-radar%2Fblueprint.json) +[Open the Symfony Package Radar demo](https://playground.wordpress.net/?blueprint-url=https%3A%2F%2Fwordpress.github.io%2Fblueprints%2Fblueprints%2Fsymfony-package-radar%2Fblueprint.json) ## Package the app as a ZIP @@ -103,7 +107,8 @@ For framework demos, prefer a ZIP that already contains `vendor/`. That keeps the Playground startup path short and avoids asking every visitor to wait for Composer, Git, and package registry downloads. -A small Blueprint can then install the app with one step: +For snippets or CLI runs, a small Blueprint can install the app into `/app` with +one step: ```json { @@ -123,7 +128,7 @@ A small Blueprint can then install the app with one step: "resource": "bundled", "path": "./symfony-package-radar.zip" }, - "extractToPath": "/wordpress" + "extractToPath": "/app" } ] } @@ -133,6 +138,10 @@ Use `bundled` resources when the ZIP ships next to `blueprint.json`, or use a `url` resource when the ZIP is hosted separately. See [Blueprint bundles](/blueprints/bundles) for packaging details. +For a full-page Playground website, use a Blueprint like the gallery demo. It +adds a tiny router at the Playground document root so the Symfony `public/` +directory can respond to browser requests. + ## Keep the demo browser-friendly A Playground-hosted framework demo works best when it: diff --git a/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts b/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts index 361d693d965..4da7dc74b01 100644 --- a/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts +++ b/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts @@ -198,9 +198,9 @@ echo get_bloginfo( 'version' ); "step": "unzip", "zipFile": { "resource": "url", - "url": "https://raw.githubusercontent.com/WordPress/blueprints/trunk/blueprints/symfony-package-radar/symfony-package-radar.zip" + "url": "https://wordpress.github.io/blueprints/blueprints/symfony-package-radar/symfony-package-radar.zip" }, - "extractToPath": "/wordpress" + "extractToPath": "/app" } ] } @@ -209,7 +209,7 @@ echo get_bloginfo( 'version' ); @@ -105,7 +119,9 @@ The same app is also available as a full Playground page: For framework demos, prefer a ZIP that already contains `vendor/`. That keeps the Playground startup path short and avoids asking every visitor to wait for -Composer, Git, and package registry downloads. +Composer, Git, and package registry downloads. The Symfony demo uses that path to +bundle both Symfony and a Composer-installed copy of the WordPress HTML API; it +still does not include a WordPress install. For snippets or CLI runs, a small Blueprint can install the app into `/app` with one step: diff --git a/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts b/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts index 4da7dc74b01..6bf7550f7f4 100644 --- a/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts +++ b/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts @@ -218,10 +218,23 @@ $kernel = new Kernel('prod', false); $request = Request::create('/'); $response = $kernel->handle($request); -preg_match('/

(.*?)<\/h1>/', $response->getContent(), $match); +$processor = WP_HTML_Processor::create_fragment($response->getContent()); +$pageTitle = 'unknown'; +if ($processor->next_tag('H1')) { + $pageTitle = ''; + while ($processor->next_token()) { + if ('H1' === $processor->get_tag() && $processor->is_tag_closer()) { + break; + } + if ('#text' === $processor->get_token_type()) { + $pageTitle .= $processor->get_modifiable_text(); + } + } + $pageTitle = trim($pageTitle); +} echo 'HTTP ' . $response->getStatusCode() . PHP_EOL; -echo 'Symfony page: ' . html_entity_decode($match[1] ?? 'unknown') . PHP_EOL; +echo 'Symfony page: ' . $pageTitle . PHP_EOL; echo 'WordPress installed: '; echo file_exists('/wordpress/wp-load.php') ? 'yes' : 'no'; @@ -229,7 +242,7 @@ $kernel->terminate($request, $response); `, From 82543b6f9569a1578ea9b60380f6659a25ae7895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 8 Jun 2026 12:50:38 +0200 Subject: [PATCH 4/7] [Docs] Cache-bust Symfony demo ZIP --- packages/docs/site/docs/main/guides/php-frameworks.md | 2 +- .../site/src/components/PhpCodeSnippetLiveExample.examples.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docs/site/docs/main/guides/php-frameworks.md b/packages/docs/site/docs/main/guides/php-frameworks.md index 57223085eb3..5af15d7e9e9 100644 --- a/packages/docs/site/docs/main/guides/php-frameworks.md +++ b/packages/docs/site/docs/main/guides/php-frameworks.md @@ -59,7 +59,7 @@ Here is the complete embed: "step": "unzip", "zipFile": { "resource": "url", - "url": "https://wordpress.github.io/blueprints/blueprints/symfony-package-radar/symfony-package-radar.zip" + "url": "https://wordpress.github.io/blueprints/blueprints/symfony-package-radar/symfony-package-radar.zip?v=html-api-2026-06-08" }, "extractToPath": "/app" } diff --git a/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts b/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts index 6bf7550f7f4..e7fada6f9e6 100644 --- a/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts +++ b/packages/docs/site/src/components/PhpCodeSnippetLiveExample.examples.ts @@ -198,7 +198,7 @@ echo get_bloginfo( 'version' ); "step": "unzip", "zipFile": { "resource": "url", - "url": "https://wordpress.github.io/blueprints/blueprints/symfony-package-radar/symfony-package-radar.zip" + "url": "https://wordpress.github.io/blueprints/blueprints/symfony-package-radar/symfony-package-radar.zip?v=html-api-2026-06-08" }, "extractToPath": "/app" } From 44cf9bac35df230eec757b6e63f6ba99c78d52ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 8 Jun 2026 13:00:04 +0200 Subject: [PATCH 5/7] Show selected PHP snippet text and shorten Symfony sample --- .../site/docs/main/guides/php-frameworks.md | 32 ++++++++++-------- .../PhpCodeSnippetLiveExample.examples.ts | 33 +++++++++++-------- .../components/PhpCodeSnippetLiveExample.tsx | 4 +-- .../website/public/php-code-snippet.js | 2 +- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/packages/docs/site/docs/main/guides/php-frameworks.md b/packages/docs/site/docs/main/guides/php-frameworks.md index 5af15d7e9e9..c18b6e3ea8d 100644 --- a/packages/docs/site/docs/main/guides/php-frameworks.md +++ b/packages/docs/site/docs/main/guides/php-frameworks.md @@ -79,27 +79,33 @@ $kernel = new Kernel( 'prod', false ); $request = Request::create( '/' ); $response = $kernel->handle( $request ); -$processor = WP_HTML_Processor::create_fragment( $response->getContent() ); -$page_title = 'unknown'; -if ( $processor->next_tag( 'H1' ) ) { - $page_title = ''; +$page_title = get_first_h1_text( $response->getContent() ); + +echo 'HTTP ' . $response->getStatusCode() . PHP_EOL; +echo 'Symfony page: ' . $page_title . PHP_EOL; +echo 'WordPress installed: '; +echo file_exists( '/wordpress/wp-load.php' ) ? 'yes' : 'no'; + +$kernel->terminate( $request, $response ); + +function get_first_h1_text( string $html ): string { + $processor = WP_HTML_Processor::create_fragment( $html ); + if ( ! $processor->next_tag( 'H1' ) ) { + return 'unknown'; + } + + $text = ''; while ( $processor->next_token() ) { if ( 'H1' === $processor->get_tag() && $processor->is_tag_closer() ) { break; } if ( '#text' === $processor->get_token_type() ) { - $page_title .= $processor->get_modifiable_text(); + $text .= $processor->get_modifiable_text(); } } - $page_title = trim( $page_title ); -} -echo 'HTTP ' . $response->getStatusCode() . PHP_EOL; -echo 'Symfony page: ' . $page_title . PHP_EOL; -echo 'WordPress installed: '; -echo file_exists( '/wordpress/wp-load.php' ) ? 'yes' : 'no'; - -$kernel->terminate( $request, $response ); + return trim( $text ); +}