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 :authorityHTTP_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>&copy; 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