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.

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-legacy actually does — and what it requires (ext-zealphp)
  • Why coroutine-legacy is flagged experimental right now — the real reasons
  • The dividing line: Composer/PSR-4 apps work; pure require_once apps belong in legacy-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. You can pass the App::MODE_* constants (preferred) or their string equivalents. 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(App::MODE_COROUTINE)
or 'coroutine'
the default
Off — use $g->get / $g->session ✅ Full coroutine concurrency New apps. Fastest, cleanest, real in-request parallelism.
App::mode(App::MODE_MIXED)
or 'mixed'
✅ Real $_GET/$_SESSION Sequential (one request at a time per worker) Symfony / Laravel bridges — the stable PHP-FPM drop-in.
App::mode(App::MODE_LEGACY_CGI)
or 'legacy-cgi'
✅ Real, fully isolated per process Sequential, process-per-file (warm pool, ~1–3 ms) Unmodified WordPress / Drupal — maximum compatibility.
App::mode(App::MODE_COROUTINE_LEGACY)
or '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.

The Superpower: Side-by-Side

The whole point of coroutine-legacy is that you get to write classic PHP syntax while keeping the performance and concurrency of coroutines. Let's look at what that means in practice:

Coroutine Mode
// In pure Coroutine Mode, you must use $g
$g = \ZealPHP\RequestContext::instance();

$id = $g->get['id'] ?? null;

if (!isset($g->session['user'])) {
    $g->session['user'] = 'guest';
}

echo "Hello, " . $g->session['user'];
Coroutine-Legacy Mode
// In Coroutine-Legacy Mode, classic PHP just works!
session_start();

$id = $_GET['id'] ?? null;

if (!isset($_SESSION['user'])) {
    $_SESSION['user'] = 'guest';
}

echo "Hello, " . $_SESSION['user'];

In Coroutine-Legacy mode, ext-zealphp isolates $_GET, $_SESSION, and other global state per-coroutine. You don't have to rewrite your app to use RequestContext.

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(App::MODE_COROUTINE_LEGACY) auto-enables the whole isolation stack:

app.php — coroutine-legacy is one call (ext-zealphp required)
use ZealPHP\App;

// Requires ext-zealphp: pie install zealphp/ext
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 appClass loadingRecommendation
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.
  • App::MODE_COROUTINE (default) for new apps; App::MODE_MIXED for stable PHP-FPM-style Composer legacy; App::MODE_LEGACY_CGI for unmodified WordPress/Drupal.
  • App::MODE_COROUTINE_LEGACY is 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_once apps (classic WordPress) belong in App::MODE_LEGACY_CGI for now.
  • When unsure, pick App::MODE_MIXED — the stable apples-to-apples PHP-FPM swap.