ZealPHP
The PHP Runtime for AI Web Apps
PHP is the HTTP server now — not a CGI worker behind one.
WebSocket, SSE, streaming, coroutines, shared memory, task workers —
first‑class because the server never shuts down between requests.
Bring your existing PHP code. New features go async without a separate Node or Go service.
Alpha · v0.2.40 · built on OpenSwoole
Why? covers the problem PHP-FPM can't solve, where ZealPHP fits vs Laravel Octane / FrankenPHP / RoadRunner, and when it's the wrong choice.
$app->route('/ai/chat', function($response) {
$response->sse(function($emit) {
$tokens = call_ai_api($prompt);
foreach ($tokens as $token) {
$emit($token, 'token');
}
});
});
With 4 HTTP workers, full PSR-15 stack — 117k req/s text · 106k JSON · 50k templated, 0 failures across 150k requests.
Reproduce in 60s: scripts/bench_vs_express.sh.
Full concurrency sweep, latency percentiles, methodology, and caveats —
/performance.
The PHP we love. The execution model we needed.
For 25 years, the HTTP server was C. PHP was the worker that died. Apache + mod_php, nginx + PHP-FPM — the HTTP server is always a C binary that bridges to a PHP process via FastCGI. PHP runs the request, then exits the request context. PHP is the language, never the server. That model gave us shared-nothing isolation, cheap workers, and ~71% of the web (per W3Techs). It also gave us "PHP can't do WebSockets" and a separate Node service for every streaming feature.
ZealPHP is what happens when PHP becomes the HTTP server.
Always-on, coroutine-native, owns the event loop, holds the connections. WebSocket, SSE,
timers, and shared memory are first-class because the server never shuts down. The on-ramp
is real too — session_start(), header(), $_GET,
echo all route through uopz
overrides into per-request state, so existing PHP code runs unchanged.
Try it — live AI chat, streaming on this server
Wired for the OpenAI Agents SDK — an agent with tool use, streamed token-by-token over ZealPHP SSE. The live demo runs in demo-fallback mode when no API key is configured (check /api/chat/status); the production deploy flips OPENAI_API_KEY and the same code path streams real model tokens.
What being the HTTP server actually buys you
SSE, WebSocket, shared memory, timers — not bolt-ons. First-class because the server is alive between requests.
SSE / token streaming as a first-class response primitive
The server holds the connection. $response->sse() is the framework primitive — no framing, no transports library, no separate sidecar.
$app->route('/ai/stream', function($response) {
$response->sse(function($emit) {
foreach (stream_from_upstream() as $token) {
$emit($token, 'token');
}
});
});
// In an FPM world, the same endpoint pins a worker for
// the lifetime of the stream — the request-per-process
// model has no concept of "yield while waiting on I/O."
// Most teams solve this by running a separate Node or
// Go service just for streaming endpoints.
// ZealPHP does it in the same process as the rest of
// your app, in a coroutine instead of a pinned worker.
Routing & auto-serialization with the LAMP idiom intact
Reflection-based parameter injection. Return whatever shape fits — int becomes a status, array becomes JSON, generator becomes a stream. No DI container, no $request-first convention. The universal return contract is one table.
$app->route('/users/{id}', function($id) {
return ['user' => User::find($id)]; // auto JSON, 200
});
$app->route('/missing', fn() => 404); // int → status
$app->route('/page', fn() => (function() {
yield '<html><body>'; // generator → SSR stream
yield '<div>...</div>';
yield '</body></html>';
})());
// public/users.php — Apache-style file routing,
// same return contract as $app->route() handlers.
<?php
require_once __DIR__ . '/../vendor/autoload.php';
return ['users' => User::all()]; // → JSON, 200
// public/old-handler.php — buffered echo still works
// for unmodified legacy code.
<?php session_start();
echo "<h1>Hello, {$_SESSION['user']}</h1>";
Workers + coroutines + shared state in one PHP application server
OpenSwoole's master/manager/worker model: N worker processes, each running thousands of coroutines on a reactor loop. OpenSwoole is the runtime; ZealPHP is the framework layer on top. Cross-worker state ships built-in for one machine; cross-node uses the same API with a Redis backend.
// N workers × thousands of coroutines per worker.
// Coroutines yield on I/O automatically (HOOK_ALL).
ZEALPHP_WORKERS=16 php app.php
// Store: cross-worker shared state, in-process.
// No Redis needed for one node — OpenSwoole\Table.
Store::set('cache', $key, $data);
$data = Store::get('cache', $key);
// Multi-node deploy? Same code, one-line backend flip.
// Federated WebSocket rooms + pub/sub need Redis;
// in-memory Table can't reach across machines.
Store::defaultBackend(Store::BACKEND_REDIS);
Store::publish('chat:room', $payload);
App::subscribe('chat:room', $handler);
// Or run TIERED: L1 = local Table, L2 = Redis,
// HMAC-signed cross-node L1 invalidations.
Store::defaultBackend(Store::BACKEND_TIERED);
Ubuntu/Debian/WSL2 · macOS · auto-detects unsupported distros and prints manual steps · inspect first
Quick Start
PHP installed? From zero to running server in 60 seconds.
http://localhost:8080php app.php after editing routes.http://localhost:8080http://localhost:9501 — admin, login, REST API all workingOpenSwoole is the engine. ZealPHP is the harness.
OpenSwoole gives PHP a real long-lived HTTP/WebSocket server with native coroutines, Atomic, and Table. It's the C-extension that makes everything else possible. But raw OpenSwoole leaves the framework problem unsolved — routing, middleware, sessions, legacy-PHP compatibility, return-shape resolution, CLI tooling. Every team that builds on raw OpenSwoole re-invents the same harness.
OpenSwoole gives you
- HTTP server + WebSocket server primitives
- Coroutines + Channel + WaitGroup
Atomic+Table(shared memory)- Coroutine\Http\Client + DNS + sleep hooks
HOOK_ALL— PHP I/O yields the reactor- Process\Pool + master/manager/worker lifecycle
ZealPHP adds on top
- Routing (
route()+nsRoute+patternRoute) with reflection-based parameter injection - PSR-15 middleware stack — 18 built-ins covering common Apache/nginx behaviors
uopzoverrides sosession_start(),header(),setcookie(),$_GET/$_POST/$_SESSION,echoall just work- Coroutine-safe sessions (per-request
RequestContext, no process-wide superglobal races) - Templating (
App::render/renderStream/fragment) with streaming-Generator output - Universal return contract (int = status, array = JSON, Generator = SSE/SSR stream)
- ZealAPI — file-based REST (drop
api/users/get.php→ auto-route) - CGI worker bridge for unmodified WordPress / Drupal compatibility
- Pluggable
Store+Counterbackends (Table → Redis/Valkey → Tiered with HMAC-signed cross-node L1 invalidation) - Cross-host pub/sub (
App::subscribe) + Streams (App::subscribeReliable) + WSRouter for cross-server WebSocket routing + first-class WS rooms - Stream wrapper for
php://inputso legacyfile_get_contents('php://input')just works in long-running workers - CLI tooling:
php app.php start/stop/restart/status/logs+ daemonization + per-port PID files
When raw OpenSwoole is the right choice: you're building a custom binary-protocol server (your own ASR / database / message broker), you need to avoid uopz entirely (compliance), you're building your own framework. For everything else — HTTP, WebSocket, SSE, REST, web apps with sessions — the harness saves you weeks per project.
AI-friendly by being boring
ZealPHP doesn't require a heavy frontend stack to build interactive AI apps. Routes can return HTML. SSE can stream tokens. WebSockets can push live events. Task workers can run long jobs. Templates stay close to backend state. That smaller surface area — HTML as the interface contract, fewer files between handler and DOM — makes the app easier to understand, test, and modify, for humans and for AI coding assistants.
It's an architectural note, not a product claim: less architecture is often more accuracy. Build small explicit server flows; let the browser stay browser-shaped. Worked example: the live AI chat above is ~40 lines of PHP + a Python agent, no SPA framework involved.
Everything you need
Every feature is a live running example — click any card to explore.
Routing
Flask-style routes with reflection-based injection. Zero config, zero boilerplate.
Explore → auto-serializeResponses
Return int → status, array → JSON, Generator → stream. Framework does the right thing.
Explore → go() + ChannelCoroutines
Fan out to multiple AI models in parallel. Merge responses. go() + Channel, zero callback hell.
Explore → yield · SSEStreaming
Stream AI tokens as they generate. yield is your streaming primitive. SSR, SSE, stream() built-in.
Explore → App::ws()WebSocket
Real-time agent-to-user comms. Multi-user AI sessions, live collaboration, binary frames.
Explore → PSR-15Middleware
CORS, ETag/304, gzip. PSR-15 compatible — drop in any middleware package.
Explore → drop-inSessions
Coroutine-safe sessions. Your existing session_start() code just works via uopz.
Explore → pluggable backendStore
Cross-worker shared state on one node via OpenSwoole\Table; flip to Redis/Valkey for cross-node + persistence. One API.
Explore → tick() · after()Timers
Schedule recurring AI tasks. Polling, cleanup, model warmup, health checks.
Explore → HTTP/1.1HTTP
Full HTTP/1.1 compliance. HEAD, OPTIONS, Range, redirects, CORS, ETag, gzip — all built-in.
Explore → renderStream()Components
SSR streaming components. Compose views with yield from. renderStream() for progressive HTML.
Explore → file-basedREST API
Drop a PHP file in api/. It becomes a route. File-based REST — the simplest API pattern.
Explore → WordPressLegacy Apps
WordPress compatibility showcase via the CGI bridge (admin, login, REST API working) — with documented limits and trade-offs.
Explore →Bring your PHP code along
Many traditional PHP patterns run unchanged in compatibility mode. session_start(),
header(), $_GET, $_POST, echo, setcookie()
— all routed through uopz overrides into per-request state, so existing files
can sit beside coroutine-native routes in the same app. Compatibility is a migration on-ramp,
not a guarantee that every PHP application is safe to drop in without an audit.
Today: nginx + PHP-FPM + Redis + Socket.io + cron + …
Each tier is mature in isolation, but the per-feature wiring (sessions, real-time, background jobs) lives across several services and config files.
On ZealPHP: php app.php
HTTP, WebSocket, SSE, sessions, task workers, shared memory, timers — one PHP application server. Front it with nginx / Caddy / Traefik in production for TLS + load-balancing across instances, exactly as you would an FPM pool.
The migration ladder has 5 rungs (0 → 4). Rung 0 is “drop your existing app in a fallback handler.” Rung 4 is full coroutine mode (measured throughput on /performance). Most teams stay on rungs 1–3 indefinitely; the upgrade path is opt-in, not forced. Real-world fit depends on extension coverage, blocking-I/O usage, and how much of the app reaches for global state — honest fit guide.
Return anything, get the right response
ZealPHP inspects your return type and does the right thing — no boilerplate. One contract for every entry point (route handler, public file, API closure, fallback, error handler, App::render() / renderToString() / renderStream() / include()).
| Return | Result | Example |
|---|---|---|
int | HTTP status code | return 404; return 201; |
array / object | Auto-serialized as JSON | return ['users' => $list]; |
string | HTML body | return '<h1>Hello</h1>'; |
Generator | SSR streaming (each yield sent immediately) | yield '<head>'; yield $body; |
void + echo | Buffered output via ob_get_clean() | echo "Hello"; echo " World"; |
ResponseInterface | PSR-7 response used directly | return new Response(...); |