PHP & WordPress
bext supports PHP applications in two modes:
- FastCGI proxy — Forward requests to existing PHP-FPM pools (WordPress, Laravel, Symfony, any PHP app)
- Embedded PHP runtime — Run PHP directly inside bext with the micro-framework (zero external dependencies)
WordPress & PHP-FPM (nginx-compat mode)
When migrating from nginx, bext automatically detects WordPress sites and routes PHP requests through FastCGI to your existing PHP-FPM sockets. No configuration needed.
How it works
- bext parses your nginx config and finds
fastcgi_passdirectives - WordPress sites are auto-detected by checking for
wp-config.php/wp-content - PHP requests (
.phpfiles,try_filesfallbacks) are forwarded to PHP-FPM via the FastCGI protocol - Static assets (CSS, JS, images) are served directly from disk — never routed through PHP
- Connections to PHP-FPM are pooled for performance (8 idle connections per socket)
Supported features
- WordPress admin (
/wp-admin/), login (/wp-login.php), cron (/wp-cron.php) - Pretty permalinks via
try_files $uri /index.php $_SERVERvariables fully populated (REQUEST_URI,HTTP_HOST,SCRIPT_FILENAME, etc.)- HTTP/2 with correct
:authority→HTTP_HOSTmapping - Multiple PHP versions per server (PHP 7.4, 8.0, 8.1, 8.2, 8.3, 8.4)
- Per-site PHP-FPM pools with isolated Unix sockets
- SSL certificate expiry tracking for all domains
Cloud management
After migration, sync your sites to the bext cloud dashboard:
bext login # authenticate with bext cloud
bext nginx sync-cloud # sync all sites + metadata
The cloud dashboard shows each WordPress site with:
- PHP version and FPM socket path
- SSL certificate expiry dates
- Domain count and verification status
- Agency/client ownership
Framework detection
| Framework | Detection | Action |
|---|---|---|
| WordPress | wp-config.php or wp-content/ in document root |
FastCGI → PHP-FPM |
| Laravel | artisan file in parent directory |
FastCGI → PHP-FPM |
| Symfony | bin/console in parent directory |
FastCGI → PHP-FPM |
| Generic PHP | Any fastcgi_pass directive |
FastCGI → PHP-FPM |
Embedded PHP Micro-Framework
The bext/framework package is a lightweight (~200 lines) PHP micro-framework with file-based routing, designed to run inside bext's embedded PHP runtime.
Installation
composer require bext/framework
Requirements: PHP >= 8.2
Project Structure
my-php-app/
├── worker.php # Entry point
├── app/
│ ├── pages/ # File-based HTML routes (ISR cached)
│ │ ├── index.php # GET /
│ │ ├── about.php # GET /about
│ │ └── products/
│ │ ├── index.php # GET /products
│ │ └── [id].php # GET /products/:id
│ ├── api/ # File-based API routes (JSON, never cached)
│ │ ├── health.php # GET /api/health
│ │ └── products/
│ │ └── [id].php # GET/POST/PUT/DELETE /api/products/:id
│ └── layouts/
│ └── default.php # Wraps all page routes
├── public/ # Static assets (CSS, JS, images)
└── bext.config.toml
Entry Point
<?php
// worker.php
require __DIR__ . '/vendor/bext/bext.php';
Bext\App::run(__DIR__ . '/app');
Configuration
# bext.config.toml
[php]
enabled = true
document_root = "./public"
workers = 4
worker_script = "./worker.php"
extensions = [".php"]
index = "index.php"
Page Routes
Files in app/pages/ serve HTML responses. GET requests are ISR-cached by bext.
Simple Closure
<?php
// app/pages/index.php
return function (array $params, array $query) {
return "<h1>Welcome</h1><p>Hello from bext!</p>";
};
Object with Metadata
Return an object with render() for cache tag support:
<?php
// app/pages/products/[id].php
return new class {
public string $title = 'Product Detail';
public string $cache_tag = 'products:detail';
public function render(array $params, array $query): string
{
$id = $params['id'];
$product = db_get_product($id);
return <<<HTML
<h1>{$product['name']}</h1>
<p>{$product['description']}</p>
<p class="price">\${$product['price']}</p>
HTML;
}
};
The $cache_tag property tells bext's ISR cache which tag this page belongs to, enabling targeted invalidation with Bext\App::invalidate("products:detail").
Static HTML
For simple pages, just return a string:
<?php
// app/pages/about.php
return '<h1>About Us</h1><p>We build fast things.</p>';
Dynamic Segments
Use [param] in filenames for dynamic route segments:
app/pages/products/[id].php → /products/:id
app/pages/blog/[slug].php → /blog/:slug
app/api/users/[userId]/posts.php → /api/users/:userId/posts
The parameter values are passed in the $params array:
return function (array $params, array $query) {
$id = $params['id']; // from URL segment
$page = $query['page']; // from ?page=2
// ...
};
API Routes
Files in app/api/ serve JSON responses. They are never ISR-cached and support HTTP method dispatch.
Object with Method Handlers
<?php
// app/api/products/[id].php
return new class {
public function GET(array $params, array $query): array
{
return db_get_product($params['id']);
}
public function POST(array $params, array $body): array
{
$product = db_create_product($body);
return ['created' => true, 'id' => $product['id'], 'status' => 201];
}
public function PUT(array $params, array $body): array
{
db_update_product($params['id'], $body);
return ['updated' => true];
}
public function DELETE(array $params): array
{
db_delete_product($params['id']);
return ['deleted' => true];
}
};
The status key in the return array sets the HTTP status code (default: 200).
Closure with Method Dispatch
<?php
return function (string $method, array $params, array $query, ?array $body) {
return match ($method) {
'GET' => ['items' => get_all_items()],
'POST' => ['created' => create_item($body)],
default => ['error' => 'Method not allowed', 'status' => 405],
};
};
Layouts
Files in app/layouts/ wrap page route output:
<?php
// app/layouts/default.php
return function (string $html, array $meta): string {
$title = $meta['title'] ?? 'My App';
return <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{$title}</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
<a href="/about">About</a>
</nav>
<main>{$html}</main>
<footer>© 2026 My App</footer>
</body>
</html>
HTML;
};
The $meta array contains properties from the page route object ($title, $cache_tag, etc.).
Platform Services
Static methods for bext services:
// Invalidate ISR cache
Bext\App::invalidate("products");
// Publish real-time event
Bext\App::publish("orders", ["id" => 123, "status" => "shipped"]);
// Register a scheduled task
Bext\App::schedule("cleanup", "0 2 * * *", "cache::gc");
How Routing Works
- Request comes in → bext matches route to a file in
app/pages/orapp/api/ [param]segments are extracted from the URL- The file is
required and its return value is evaluated:- Closure → called with
($params, $query)for pages,($method, $params, $query, $body)for API - Object with
render()→render($params, $query)called, metadata extracted - Object with HTTP method → the matching method is called
- String → returned directly
- Closure → called with
- For pages: output is wrapped in the layout, ISR-cached with any specified tags
- For API routes: output is JSON-encoded, never cached
This page covers the embedded PHP micro-framework and FastCGI proxy mode. For hosting a full WordPress install with purge-on-change, the admin-bar pill, and multisite support, see WordPress integration.
Files in app/api/ are never ISR-cached — only app/pages/ responses are cached. If you need to cache an API response, move it into app/pages/ and return a JSON string instead.
Related
- WordPress integration — full WordPress + WooCommerce behind bext's edge cache
- Masquerade mode — how bext parses nginx configs to drive FastCGI dispatch
- Caching — ISR TTL, stale-while-revalidate, and cache tag invalidation
- Laravel adapter — PHP service provider for bext SDK services
- Symfony adapter — Symfony bundle with autowired bext services
Links
- php.net — PHP language reference