Running Legacy PHP Apps
ZealPHP runs unmodified WordPress — admin dashboard, login, posts, plugins — out of the box. No patches, no forks, no compatibility layers. If it runs on Apache, it runs on ZealPHP.
Zero WordPress modifications required. Login, sessions, cookies, redirects, file uploads, REST API, pretty permalinks — everything works through ZealPHP's CGI worker with true global scope isolation. The same app.php works for Drupal, Laravel, or any traditional PHP application.
How It Works
Three framework features enable legacy app compatibility:
| Feature | What it does | Apache equivalent |
|---|---|---|
App::superglobals(true) |
$_GET, $_POST, $_SERVER, $_SESSION, $_COOKIE work as expected |
mod_php (default behavior) |
App::$ignore_php_ext = false |
Allows .php extensions in URLs (/wp-login.php, /admin/edit.php) |
AddHandler php-script .php |
App::includeFile() |
Runs each PHP file in a separate process at true global scope via the CGI worker | mod_prefork MPM + CGI |
<?php
require 'vendor/autoload.php';
use ZealPHP\App;
App::superglobals(true);
App::$ignore_php_ext = false;
$app = App::init('0.0.0.0', 8080);
$app->run(['task_worker_num' => 0]);
// PHP files in public/ are served automatically with process isolation
Porting from Apache .htaccess
ZealPHP replaces .htaccess entirely. Here are real-world conversions:
WordPress .htaccess
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
App::superglobals(true);
App::$ignore_php_ext = false;
$app = App::init('0.0.0.0', 8080);
$app->setFallback(function() {
$g = G::instance();
$g->server['PHP_SELF'] = '/index.php';
$g->server['SCRIPT_NAME'] = '/index.php';
$g->server['SCRIPT_FILENAME'] =
App::$cwd . '/public/index.php';
App::includeFile(
App::$cwd . '/public/index.php'
);
});
$app->run(['task_worker_num' => 0]);
Redirect rules
RewriteRule ^old-page$ /new [R=301,L]
RewriteRule ^blog/(.*)$ /articles/$1 [R=302,L]
$app->route('/old-page', function() {
header('Location: /new');
return 301;
});
$app->patternRoute('/blog/.*', function() {
$path = preg_replace(
'#^/blog/#', '/articles/',
$_SERVER['REQUEST_URI']
);
header('Location: ' . $path);
return 302;
});
Quick reference
| Apache .htaccess | ZealPHP equivalent |
|---|---|
RewriteEngine On | Not needed — ZealPHP routes natively |
RewriteRule . /index.php [L] | $app->setFallback(function() { ... }) |
RewriteRule ^path$ /dest [R=301,L] | $app->route('/path', function() { header('Location: /dest'); return 301; }) |
DirectoryIndex index.php | Built-in — implicit routes serve index.php for directories |
Options -Indexes | Not needed — ZealPHP never lists directories |
<FilesMatch "\.php$"> | Not needed — ZealPHP IS the PHP runtime |
Porting from nginx
server {
listen 80;
root /var/www/html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php-fpm.sock;
}
location ~* \.(css|js|png)$ {
expires 30d;
}
}
App::superglobals(true);
App::$ignore_php_ext = false;
$app = App::init('0.0.0.0', 8080);
// try_files → fallback
$app->setFallback(function() {
$g = G::instance();
$g->server['PHP_SELF'] = '/index.php';
$g->server['SCRIPT_NAME'] = '/index.php';
$g->server['SCRIPT_FILENAME'] =
App::$cwd . '/public/index.php';
App::includeFile(
App::$cwd . '/public/index.php'
);
});
// Static files (css, js, png) served
// automatically by OpenSwoole
// Cache headers: add custom middleware
$app->run(['task_worker_num' => 0]);
| nginx directive | ZealPHP equivalent |
|---|---|
try_files $uri $uri/ /index.php | $app->setFallback(fn() => App::includeFile(...)) |
location ~ \.php$ { fastcgi_pass ...; } | Not needed — ZealPHP serves PHP directly |
location ~* \.(css|js)$ { expires 30d; } | OpenSwoole enable_static_handler + middleware for headers |
proxy_pass http://backend; | Use native $app->route() or reverse proxy in front |
AI Config Converter
Paste your .htaccess or nginx config — get a working app.php streamed in real-time. Powered by gpt-5.4-mini with the full ZealPHP API reference.
# Pipe any config — get app.php on stdout
cat .htaccess | uv run examples/agents/config_converter.py
# Interactive mode
uv run examples/agents/config_converter.py
WordPress Example
A complete app.php that runs WordPress on ZealPHP:
<?php
require 'vendor/autoload.php';
use ZealPHP\App;
use ZealPHP\G;
App::superglobals(true);
App::$ignore_php_ext = false;
$app = App::init('0.0.0.0', 9501);
// Redirect /wp-admin to /wp-admin/index.php
$app->route('/wp-admin', function() {
$qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '';
header('Location: /wp-admin/index.php' . $qs);
return 301;
});
// Fallback: unmatched URLs → WordPress front controller
// Replaces Apache's: RewriteRule . /index.php [L]
$app->setFallback(function() {
$g = G::instance();
$g->server['PHP_SELF'] = '/index.php';
$g->server['SCRIPT_NAME'] = '/index.php';
$g->server['SCRIPT_FILENAME'] = App::$cwd . '/public/index.php';
App::includeFile(App::$cwd . '/public/index.php');
});
$app->run(['task_worker_num' => 0]);
Setup Steps
See the full working example: github.com/sibidharan/zealphp-wordpress
- Create a ZealPHP project:
composer create-project sibidharan/zealphp-project my-wordpress - Download WordPress into
public/:cd my-wordpress/public && wp core download - Configure
public/wp-config.phpwith your database settings - Write
app.phpas shown above - Start:
php app.php(orphp app.php start -p 9501 -dto daemonize) - Visit
http://localhost:9501/wp-admin/install.phpto complete installation
CGI Worker Architecture
App::includeFile() runs each PHP file in a separate process via proc_open. This gives every request a clean PHP interpreter with true global scope — exactly like Apache's prefork MPM.
OpenSwoole Worker (long-lived) CGI Worker (per-request)
┌─────────────────────────┐ ┌──────────────────────────┐
│ │ proc_open │ php cgi_worker.php │
│ Route matched │ ──────────► │ │
│ App::includeFile() │ │ TRUE global scope: │
│ │ stdin │ ├─ $_SERVER, $_GET, etc. │
│ Serializes context: │ ──────────► │ ├─ $_COOKIE, $_FILES │
│ ├─ $_SERVER, $_GET │ (POST body)│ │ │
│ ├─ $_POST, $_COOKIE │ │ ├─ uopz captures: │
│ └─ Request body │ │ │ header(), setcookie() │
│ │ stdout │ │ http_response_code() │
│ Reads response: │ ◄────────── │ │ │
│ ├─ Body from stdout │ │ ├─ include file.php │
│ ├─ Metadata from stderr│ stderr │ │ ← app runs at global │
│ │ (status, headers, │ ◄────────── │ │ scope │
│ │ cookies as JSON) │ │ │ │
│ └─ Applies to response │ │ └─ Process exits (clean)│
└─────────────────────────┘ └──────────────────────────┘
What the CGI worker handles
| Feature | How |
|---|---|
| All HTTP methods | $_SERVER['REQUEST_METHOD'] passed via context; request body piped to stdin (php://input) |
header() / header_remove() | Captured via uopz_set_return — sent back as JSON metadata |
setcookie() / setrawcookie() | Captured — applied to response by parent worker |
http_response_code() / headers_list() | Captured — status and headers returned in metadata |
exit() / die() | register_shutdown_function flushes output and metadata |
| SSE streaming | Detects text/event-stream; streams via flush() like Apache |
| Static files | Served directly by OpenSwoole — never reaches PHP |
| File uploads / Sessions | $_FILES via context; PHP native sessions work in CGI process |
CLI Management
php app.php # Start with defaults
php app.php start -p 9501 # Start on port 9501
php app.php start -p 9501 -d # Start daemonized
php app.php stop # Stop the server (reads PID file)
php app.php status # Check if server is running
php app.php start -w 8 # Start with 8 workers
php app.php --help # Show all options
Limitations
Performance: Each PHP file request spawns a new process. Static files bypass this (served by OpenSwoole). For high-traffic production, convert hot paths to native ZealPHP routes.
Streaming: SSE works in CGI mode via flush(). WebSocket requires native ZealPHP routes (App::ws()).
Hybrid approach: Mix native routes (coroutine mode, high performance) with legacy PHP file serving (CGI mode) in the same app. Explicit $app->route() handlers run directly in the worker.