PHP & WordPress
bext supports PHP applications in two modes:
1. FastCGI proxy — Forward requests to existing PHP-FPM pools (WordPress, Laravel, Symfony, any PHP app) 2. 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
1. bext parses your nginx config and finds fastcgi_pass directives
2. WordPress sites are auto-detected by checking for wp-config.php / wp-content
3. PHP requests (.php files, try_files fallbacks) are forwarded to PHP-FPM via the FastCGI protocol
4. Static assets (CSS, JS, images) are served directly from disk — never routed through PHP
5. 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
- $_SERVER variables fully populated (REQUEST_URI, HTTP_HOST, SCRIPT_FILENAME, etc.)
- HTTP/2 with correct :authority → HTTP_HOST mapping
- 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 <<{$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 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
1. Request comes in → bext matches route to a file in app/pages/ or app/api/
2. [param] segments are extracted from the URL
3. 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 4. For pages: output is wrapped in the layout, ISR-cached with any specified tags 5. For API routes: output is JSON-encoded, never cached
- Object with