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.

Migrate your PHP codebase to async

Bring your existing code along. session_start(), header(), $_GET, $_POST, echo — all overridden via uopz to work inside the coroutine runtime, so the migration ladder starts with "drop your app in and run php app.php" rather than "rewrite for an event loop."

From several services to one process

Typical PHP stack today

  • Nginx / Apache (front-end)
  • PHP-FPM (cold start every request)
  • Redis (sessions, cache, pub/sub)
  • Socket.io / Ratchet (WebSocket)
  • Supervisor / cron (background jobs)
  • SSE proxy or browser polling

6 services, 6 failure points, 6 sets of config.

Same app on ZealPHP

php app.php
  • HTTP + WebSocket + SSE built in
  • Coroutine-safe sessions (no Redis)
  • Shared memory across workers (Store, Counter)
  • Task workers (no cron / supervisor)
  • Persistent connections, no cold starts
  • WordPress via the CGI bridge — showcase

Not every stack fits. Depends on app — see "When migration won't help" below.

The migration ladder — go at your own pace

Each rung is functional on its own. Stop at the rung that gives you enough upside without forcing changes you're not ready for. Most real migrations stay between rungs 1 and 3 for months before reaching 4.

0
Drop in your entire app, unchanged
App::superglobals(true); $app->setFallback(fn() => App::includeFile('index.php'));

Most existing PHP apps — WordPress, Drupal, custom legacy code — run unchanged on OpenSwoole through the CGI worker bridge. No code edits required to start serving requests faster.

Wins: Persistent process, no per-request boot. Sub-millisecond TTFB on cached routes.

Trade-off: Coroutines, WebSocket, SSE — you're still bound by the global-state model.

1
Write LAMP-style PHP in public/
public/about.php → /about · public/users/list.php → /users/list

File-based routing. $_GET, session_start(), echo — everything you know works. No new mental model.

Wins: Add new endpoints without leaving the LAMP idiom your team already uses.

Trade-off: Nothing — this is purely additive.

2
Add REST APIs in api/
api/users/get.php → GET /api/users · api/users/post.php → POST /api/users

Drop a PHP file, get a REST endpoint. ZealAPI auto-routes by filename and HTTP method. Zero config, zero framework boilerplate.

Wins: Replace your "PHP file behind nginx" API layer with structured endpoints in 5 lines each.

Trade-off: Still synchronous — handlers run sequentially. Fine for I/O-light endpoints.

3
Use framework routes for new features
$app->route('/ws/chat', ...); $response->sse(...); yield $html;

WebSocket, SSE streaming, coroutines — available when you're ready, not forced upfront. Mix file-based pages with programmatic routes in the same app.

Wins: Real-time features without spinning up a separate Node/Go service. Stream AI responses, push live updates, run background coroutines.

Trade-off: Still allows blocking calls inside individual handlers — coroutine isolation is opt-in at rung 4.

4
Full coroutine mode
App::superglobals(false); // thousands of concurrent requests per worker

Replace $_GET/$_SESSION globals with G::instance(). Each coroutine gets its own context; one worker handles thousands of concurrent requests without blocking.

Wins: Peak throughput. 117k req/s on 4 workers — Express on the same box does 20k.

Trade-off: You must avoid blocking I/O outside coroutine-hooked extensions, and any code that mutates global state needs a per-coroutine equivalent.

How the compatibility bridge works

PHP-FPM gives you fresh superglobals ($_GET, $_SESSION), fresh header(), fresh session_start() on every request. OpenSwoole is one long-running process — those functions would normally collide across requests. ZealPHP fixes that via three mechanisms:

  • uopz function overrides. At server boot, header(), setcookie(), http_response_code(), and the session_*() family are replaced with implementations that read/write a per-request G::instance() object. Your header('Location: /foo') routes to the right OpenSwoole response without you knowing.
  • Stream-wrapper redirection. php://input is rewired to return the current request body, not stdin. Legacy code that does file_get_contents('php://input') in a JSON API handler works unchanged.
  • CGI worker bridge. When App::superglobals(true) + setFallback() are in use, requests that don't match a framework route are forwarded to a CGI-style child process via proc_open — full process isolation, just like mod_php. That's how WordPress runs.

Net effect — at rung 0 and 1, your code can't tell it's running on OpenSwoole. At rungs 3 and 4, you opt into the coroutine model where it pays off.

Apache+mod_php parity reference

What ZealPHP emulates so legacy apps run unchanged. Most of this is invisible — these rows exist to answer "does X work?" without a code-dive.

Function overrides (via uopz)

Apache+mod_php functionZealPHP behavior
header(), header_remove(), headers_list(), headers_sent()Per-request via G->response_headers_list. Supports header("HTTP/1.1 404 Not Found") status-line form and the optional $http_response_code param.
setcookie(), setrawcookie()Per-request via G->response_cookies_list / response_rawcookies_list. setrawcookie preserves the raw value (no urlencoding).
http_response_code()Per-request via G->status.
flush(), ob_flush(), ob_end_flush()Switch the response into streaming mode — buffer pushed to OpenSwoole's $response->write(), flips G->_streaming = true.
apache_request_headers(), getallheaders()Return canonical (hyphen-capitalized) request headers from the OpenSwoole request.
apache_response_headers()Returns currently-set outbound headers.
apache_setenv(), apache_getenv(), apache_note()Per-request scratch tables in G->apache_env / apache_notes.
virtual()Returns false — internal subrequests aren't supported in this model.
set_time_limit()No-op success. OpenSwoole owns the worker/coroutine timeout.
ignore_user_abort(), connection_status(), connection_aborted()Per-request; reads $response->isWritable() for connection state.
is_uploaded_file(), move_uploaded_file()Whitelist of $_FILES['*']['tmp_name'] — same security guarantees as mod_php.
session_*() (18 functions)Coroutine-safe session lifecycle via CoSessionManager; files in /var/lib/php/sessions.
set_error_handler(), set_exception_handler(), register_shutdown_function(), error_reporting()Per-coroutine via G stacks. A native dispatcher installed at boot delegates to the active coroutine's handler stack — isolated despite PHP's process-global semantics. See Responses.

public/ routing (DocumentRoot behavior)

Apache directiveZealPHP
DirectoryIndex index.php index.html index.htmSame fallback order via App::$directory_index. HTML/HTM served via $response->sendFile() with ETag + Range.
DirectorySlash On/foo → 301 /foo/ when foo is a directory.
AcceptPathInfo On/script.php/extra exposes PATH_INFO=/extra; rewrites REQUEST_URI.
<FilesMatch "^\.>" denyDotfile URLs return 403 (.well-known/ allow-listed per RFC 8615).
RewriteRule . /index.php [L]App::setFallback(fn() => App::includeFile(...)). Body, status, headers, Generator return all preserved.
ErrorDocument 404 /custom.phpApp::setErrorHandler(404, $cb). Catch-all variant: setErrorHandler($cb). Handlers fire for every 4xx/5xx site in the framework.
FileETag / conditional GET$response->sendFile() emits weak ETag + Last-Modified; honors If-None-Match and If-Modified-Since → 304.

Deeper detail (boot-order tricks, recursion guards, per-coroutine isolation mechanism, source-line references): docs/apache-parity.md and docs/error-handling.md.

When migration is a good fit

Good fit

  • ✓ You're already on PHP and the team knows it
  • ✓ You want WebSocket / SSE / streaming without a separate Node service
  • ✓ You have I/O-bound endpoints (DB, HTTP fetches) — coroutines fan them out
  • ✓ You hit PHP-FPM bottlenecks (request rate, cold start latency, FPM pool tuning)
  • ✓ You want long-lived sessions or pub/sub without Redis
  • ✓ You want to keep session_start() + header() + echo — not rewrite for an event loop

Probably wrong fit

  • ✗ Workload is purely CPU-bound — coroutines don't help, just buy more cores
  • ✗ App relies on extensions OpenSwoole's runtime hooks don't cover (rare, but exists)
  • ✗ You'd accept a full rewrite anyway — Go/Rust/Elixir give bigger ceilings if you can pay the cost
  • ✗ Hard requirement for shared-nothing per-request memory (PHP-FPM's strongest guarantee)
  • ✗ Production team can't accept alpha (v0.2.x) stability — wait for v1.0

Convert your existing config

Paste your Apache .htaccess or nginx config — AI converts it to a working app.php in real-time. The same engine that bridges the migration ladder above.

Apache / nginx config
ZealPHP app.php
// Output will appear here...
Rate limit: 5 conversions per 10 minutes · Powered by gpt-5.4-mini · Source · More on legacy apps →
Start the migration → Legacy apps (WordPress) → Why ZealPHP →

Performance: 117K req/s text · 106K JSON · 50K templated at rung 4 (full coroutine mode).
WordPress + custom CMS migrations: see the showcase repo.