Lifecycle Modes & Legacy Apps
The four runtime modes, when each one fits, and an honest look at why coroutine-legacy is still experimental.
You will learn
- The four modes
App::mode()selects — and the one knob that distinguishes them - A decision guide: which mode for new apps, Symfony/Laravel, and unmodified WordPress
- What
coroutine-legacyactually does — and what it requires (ext-zealphp) - Why
coroutine-legacyis flagged experimental right now — the real reasons - The dividing line: Composer/PSR-4 apps work; pure
require_onceapps belong inlegacy-cgi
One call picks the runtime: App::mode()
Everything about how a request is handled — whether $_GET/$_SESSION are real,
whether requests run concurrently, whether each .php file gets its own process — is decided
by a single call near the top of app.php. The trade-off behind every mode is the same one you met
in Lesson 3: process-wide superglobals are not safe across
concurrent coroutines. Each mode resolves that tension differently.
| Mode | Superglobals | Concurrency | Best for |
|---|---|---|---|
App::mode('coroutine')the default |
Off — use $g->get / $g->session |
✅ Full coroutine concurrency | New apps. Fastest, cleanest, real in-request parallelism. |
App::mode('mixed') |
✅ Real $_GET/$_SESSION |
Sequential (one request at a time per worker) | Symfony / Laravel bridges — the stable PHP-FPM drop-in. |
App::mode('legacy-cgi') |
✅ Real, fully isolated per process | Sequential, process-per-file (warm pool, ~1–3 ms) | Unmodified WordPress / Drupal — maximum compatibility. |
App::mode('coroutine-legacy')experimental |
✅ Real, isolated per coroutine | ✅ Full coroutine concurrency | Composer/PSR-4 legacy apps that want real superglobals and concurrency. |
The first three are stable and well-trodden. mixed is the closest apples-to-apples PHP-FPM
replacement; legacy-cgi is the conservative choice for apps that assume “this script runs
alone.” The fourth, coroutine-legacy, is the ambitious one — and the rest of this
lesson is an honest look at it.
What coroutine-legacy is trying to do
It is a compatibility runtime: it lets traditional request-style PHP — the PHP-FPM
“fresh state per request” mental model — run under OpenSwoole coroutine concurrency, with
every request-state primitive isolated per coroutine. Real $_GET, $_POST,
$_SESSION, $GLOBALS, function-local static $x, and
require_once state all behave as if each request had its own process — while dozens of
requests run concurrently on one worker.
It pulls this off with ext-zealphp, a small C extension that hooks OpenSwoole’s
scheduler and snapshots/restores per-coroutine state across every yield. App::mode('coroutine-legacy')
auto-enables the whole isolation stack:
use ZealPHP\App;
// Requires ext-zealphp: pie install sibidharan/ext-zealphp
App::mode(App::MODE_COROUTINE_LEGACY);
// equivalent to: superglobals(true) + isolation(coroutine) + silentRedeclare(true)
// + includeIsolation(true) + coroutineGlobalsIsolation(true)
// + coroutineStaticsIsolation(true)
$app = App::init('0.0.0.0', 8080);
$app->run();
The dividing line: how is your app’s class graph loaded?
The single best predictor of whether coroutine-legacy will be smooth is how your app loads
its classes — because that determines whether the class graph can be warmed before concurrency
arrives.
| Your app | Class loading | Recommendation |
|---|---|---|
| Symfony, Laravel, Slim, any modern Composer app | PSR-4 autoload (each class required once per worker) |
✅ coroutine-legacy works — warm with App::preloadClassmap() (run composer dump-autoload --optimize). Validated across a 12-app sweep. |
| Unmodified WordPress, Drupal 7, phpBB | Pure require_once bootstrap, no autoloader |
⚠️ Use legacy-cgi instead. It is process-isolated, has no coroutine race at all, and is the correct race-free home for these apps today. |
| Brand-new ZealPHP app | n/a — you write $g->get code |
✅ Plain coroutine mode. You don’t need any of this. |
You want to run an unmodified WordPress install (pure require_once bootstrap, no autoloader) on ZealPHP today. Which mode?
Key Takeaways
- One call —
App::mode()— selects the runtime; the axis behind every mode is the superglobals-vs-concurrency trade-off. coroutine(default) for new apps;mixedfor stable PHP-FPM-style Composer legacy;legacy-cgifor unmodified WordPress/Drupal.coroutine-legacyis the experimental compatibility runtime: real superglobals + coroutine concurrency, via per-coroutine isolation in ext-zealphp (required).- It's experimental because it needs a C extension, the “just works” promise is conditional on warming the class graph, there are open WordPress-teardown issues, and opcache can fight class re-declaration.
- The dividing line is class loading: PSR-4/Composer apps can be warmed and work; pure
require_onceapps (classic WordPress) belong inlegacy-cgifor now. - When unsure, pick
mixed— the stable apples-to-apples PHP-FPM swap.