Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.

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.php — stream AI tokens
$app->route('/ai/chat', function($response) {
    $response->sse(function($emit) {
        $tokens = call_ai_api($prompt);
        foreach ($tokens as $token) {
            $emit($token, 'token');
        }
    });
});
Browser output

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.

ZealPHP AI Chat Demo Checking...
Hi! I'm running on ZealPHP's SSE streaming. Ask me anything — watch the tokens stream in real-time.
View source code → The full backend powering this chat
# examples/agents/chat_agent.py
from agents import Agent, Runner, function_tool, SQLiteSession

@function_tool
def get_zealphp_reference(query: str) -> str:
    """Look up ZealPHP docs — routing, streaming, store, etc."""
    return match_sections(reference, query)

agent = Agent(
    name="ZealPHP Assistant",
    model="gpt-4.1-mini",
    instructions="You are a ZealPHP expert. Output raw HTML.",
    tools=[get_zealphp_reference],
)

# Persistent conversation threads via SQLiteSession
session = SQLiteSession(db_path=DB_PATH, session_id=thread_id)

# Stream tokens as SSE events to stdout
result = Runner.run_streamed(agent, input=message, session=session)
async for event in result.stream_events():
    if event.data.type == "response.output_text.delta":
        print(f"data: {json.dumps({'token': event.data.delta})}")
// route/chat.php
$app->route('/api/chat', ['methods' => ['POST']],
  function($request, $response) {
    $g = G::instance();
    $input = json_decode(
        $g->zealphp_request->parent->getContent(), true
    );

    // SSE stream — proxy Python agent's stdout
    $response->sse(function($emit) use ($input) {
        $cmd = 'uv run chat_agent.py '
             . base64_encode(json_encode($input));
        $process = proc_open($cmd, [
            0 => ['pipe','r'],
            1 => ['pipe','w'],
            2 => ['pipe','w'],
        ], $pipes);

        while (!feof($pipes[1])) {
            $line = fgets($pipes[1]);
            if (str_starts_with($line, 'data: '))
                $emit(substr($line, 6), 'token');
        }
        proc_close($process);
    });
});

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.php
$app->route('/ai/stream', function($response) {
    $response->sse(function($emit) {
        foreach (stream_from_upstream() as $token) {
            $emit($token, 'token');
        }
    });
});
FPM equivalent
// 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.

Return anything — framework picks the right wire shape
$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>';
})());
Same return contract for legacy public/*.php files
// 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.

Cross-worker state on a single node — built in
// 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);
Cross-node — same API, Redis/Valkey backend
// 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);
Fresh machine? One line installs PHP 8.3 + OpenSwoole + uopz + composer:
$ curl -fsSL https://php.zeal.ninja/install.sh | sudo bash

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.

1$ composer create-project sibidharan/zealphp-project:^0.2.40 my-app
2$ cd my-app && php app.php
Server running at http://localhost:8080
Includes CLAUDE.md for AI-assisted development. Restart with php app.php after editing routes.
1$ git clone https://github.com/sibidharan/zealphp.git
2$ cd zealphp && composer install && php app.php
This very site, running locally at http://localhost:8080
The framework repo IS the OSS website — every page is a live, working example of a feature.
1$ git clone https://github.com/sibidharan/zealphp-wordpress.git
2$ cd zealphp-wordpress && composer install
3$ php app.php
WordPress at http://localhost:9501 — admin, login, REST API all working
Zero WordPress modifications. CGI worker provides Apache mod_php compatibility. See Legacy Apps.
Requires PHP 8.3+ OpenSwoole 22.1+ uopz composer Install help →

OpenSwoole 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
  • uopz overrides so session_start(), header(), setcookie(), $_GET/$_POST/$_SESSION, echo all 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 + Counter backends (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://input so legacy file_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.

route()

Routing

Flask-style routes with reflection-based injection. Zero config, zero boilerplate.

Explore →
📦
auto-serialize

Responses

Return int → status, array → JSON, Generator → stream. Framework does the right thing.

Explore →
🔀
go() + Channel

Coroutines

Fan out to multiple AI models in parallel. Merge responses. go() + Channel, zero callback hell.

Explore →
📡
yield · SSE

Streaming

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-15

Middleware

CORS, ETag/304, gzip. PSR-15 compatible — drop in any middleware package.

Explore →
🗄️
drop-in

Sessions

Coroutine-safe sessions. Your existing session_start() code just works via uopz.

Explore →
🗃️
pluggable backend

Store

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.1

HTTP

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-based

REST API

Drop a PHP file in api/. It becomes a route. File-based REST — the simplest API pattern.

Explore →
🏗️
WordPress

Legacy 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()).

ReturnResultExample
intHTTP status codereturn 404; return 201;
array / objectAuto-serialized as JSONreturn ['users' => $list];
stringHTML bodyreturn '<h1>Hello</h1>';
GeneratorSSR streaming (each yield sent immediately)yield '<head>'; yield $body;
void + echoBuffered output via ob_get_clean()echo "Hello"; echo " World";
ResponseInterfacePSR-7 response used directlyreturn new Response(...);

Full contract reference →