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.
API Index — Namespaces, Packages, Reports, Indices

Dispatcher
in package

CGI execution machinery extracted from App.php (Phase 2 structural refactor).

Holds the heavy dispatch implementations for ZealPHP's legacy/CGI execution paths: building the RFC 3875 CGI env, minting the host->subprocess session handoff, the FastCGI (fcgi) dispatch, the proc/shebang subprocess dispatch, the RFC 3875 interpreter-response parser, and the native worker-pool dispatch.

Configuration + the public API surface (cgiMode, registerCgiBackend, resolveCgiBackend, the $cgi_* static properties, exec/rawExec) remain on App; this class reads them via App::$cgi_* and App::resolveCgiBackend(). App keeps thin public delegating shims for buildCgiEnv()/parseCgiResponse() so existing callers/tests keep working unchanged.

No logic changes — bodies moved verbatim from App.php.

Table of Contents

Methods

buildCgiEnv()  : array<string, string>
Build the OS-level environment array passed to the CGI subprocess.
cgiFcgi()  : mixed
Dispatch a legacy include to a FastCGI backend (e.g. php-fpm) via the FCGI binary protocol. Used when App::cgiMode() === 'fcgi'.
cgiFork()  : mixed
Fork-per-request CGI dispatch — App::cgiMode('fork') (Apache MPM prefork, EXPERIMENTAL). Routes .php through a {@see ForkPool}: the fork-master forks a FRESH child per request that runs the file at true global scope (the #167 wp-admin fix) and dies — fresh-process correctness (no "Cannot redeclare class") at fork cost (~1 ms), not proc_open cold-start.
cgiPool()  : mixed
cgiMode('pool') dispatch — native FCGI-style worker pool.
cgiSubprocess()  : mixed
Run a PHP file in a separate process at true global scope (CGI-style).
parseCgiResponse()  : array{status: int|null, headers: list, body: string}
Parse a raw CGI/1.1 interpreter response (RFC 3875) into status, headers and body — pure, side-effect-free string handling.
applyCgiHeaders()  : void
Apply CGI-captured response headers to the live response, preserving MULTIPLE same-name headers (multi Set-Cookie, Link, Vary, …).
applyCgiResponseFrame()  : mixed
Apply a pool/fork response frame (status + headers + cookies) to the response and return the body per the universal return contract — shared by cgiPool and cgiFork.
buildCgiRequestFrame()  : array<string, mixed>
Build the IPC request frame from the current request context — shared by cgiPool and cgiFork. The raw body feeds php://input (CgiInputStream); multipart/form-data is skipped (it rides $_FILES, and the full blob could exceed IPC's 64 MB cap).
cgiInterpreterResponse()  : mixed
Consume a standard RFC 3875 CGI response from a non-PHP interpreter subprocess: read stdout (header block + blank line + body), apply the parsed headers / Status: pseudo-header to the response, and return the body through the universal contract. stderr is drained for error logging.
emitCgiSessionCookieFromMeta()  : void
Emit the PHPSESSID Set-Cookie AFTER a CGI subprocess that STARTED a session (#108 / #355). The subprocess can't emit its own session cookie (PHP's session module uses the C-internal php_setcookie(), which the CLI SAPI discards), so it reports the active session id back in the metadata frame (session_id); the host emits the cookie here.
mintCgiSession()  : string|null
Mint and propagate a session id on behalf of a CGI subprocess request (issue #108).

Methods

buildCgiEnv()

Build the OS-level environment array passed to the CGI subprocess.

public static buildCgiEnv(array<string, mixed> $server, string $ctx) : array<string, string>

Extracted as a public static method so unit tests can assert the exact env without spawning a process (reflection is not needed). Apache parity reference: util_script.c ap_add_common_vars() + ap_add_cgi_vars().

Parameters
$server : array<string, mixed>

$g->server (OpenSwoole-populated)

$ctx : string

JSON-encoded ZEALPHP_REQUEST_CONTEXT

Return values
array<string, string>

cgiFcgi()

Dispatch a legacy include to a FastCGI backend (e.g. php-fpm) via the FCGI binary protocol. Used when App::cgiMode() === 'fcgi'.

public static cgiFcgi(string $path[, string|null $address = null ][, array<string, string> $extraParams = [] ]) : mixed

Builds CGI env via App::buildCgiEnv(), adds SCRIPT_FILENAME / SCRIPT_NAME, reads the request body, calls \ZealPHP\CGI\FastCgiClient::request(), maps the response (status, headers, body, stderr) back through the universal return contract. The FastCgiClient socket layer is OpenSwoole-native (Coroutine\Client) — the call never blocks the worker.

On connection failure or protocol error, logs via elog() and returns 502.

Parameters
$path : string
$address : string|null = null

Per-backend FastCGI address override (host:port or unix:/path). null → falls back to App::$fcgi_address.

$extraParams : array<string, string> = []

Extra FCGI params merged after buildCgiEnv() (Apache SetEnvIf / nginx fastcgi_param parity).

cgiFork()

Fork-per-request CGI dispatch — App::cgiMode('fork') (Apache MPM prefork, EXPERIMENTAL). Routes .php through a {@see ForkPool}: the fork-master forks a FRESH child per request that runs the file at true global scope (the #167 wp-admin fix) and dies — fresh-process correctness (no "Cannot redeclare class") at fork cost (~1 ms), not proc_open cold-start.

public static cgiFork(string $path) : mixed

Shares the request-frame shape and response-apply contract with cgiPool.

Parameters
$path : string

cgiPool()

cgiMode('pool') dispatch — native FCGI-style worker pool.

public static cgiPool(string $path) : mixed

Lazily creates a per-OpenSwoole-worker singleton WorkerPool with $cgi_pool_size pre-spawned PHP subprocesses, then dispatches the current request frame to an idle subprocess via Coroutine\Channel. The subprocess executes the file with mod_php-style isolation (clean global scope per request — same as cgiMode('proc')), then writes the response frame back through the IPC pipe; the parent coroutine yields the entire time (HOOK_ALL hooks pipe reads), letting the OpenSwoole worker serve thousands of other coroutines in parallel while pool subprocesses execute legacy PHP.

Response-shape handling mirrors cgiFork() exactly — both consume the same IPC frame format (status, headers, cookies, rawcookies, body, return_value) and apply captures to $g->zealphp_response via the same patterns, so the host response builder treats pool dispatch identically to proc/fork.

Auto-respawn on subprocess death (FPM-equivalent recovery semantics — proc_get_status(['running' => false]) triggers respawn(idx)) and recycle after $cgi_pool_max_requests (FPM pm.max_requests parity).

Parameters
$path : string

cgiSubprocess()

Run a PHP file in a separate process at true global scope (CGI-style).

public static cgiSubprocess(string $path[, string|null $interpreter = null ]) : mixed

Required for legacy apps like WordPress that depend on bare variable assignments and global keyword declarations being seen by every file.

The subprocess (src/cgi_worker.php) serialises status, headers, cookies AND the include's return value to stderr as a single JSON line; this method consumes that channel and returns the same shape executeFile() would have, so the universal return contract applies in both modes.

Streaming responses (Generator returns, text/event-stream content type) are consumed inside the subprocess and streamed back through stdout; this method threads them through to the OpenSwoole response and returns null (the caller signals _streaming and ResponseMiddleware skips its buffering).

Parameters
$path : string
$interpreter : string|null = null

Full path to the interpreter binary. null (default) → PHP + cgi_worker.php (existing behaviour, uopz captures). Non-null → proc_open([$interpreter, $scriptPath]) with buildCgiEnv() env (RFC 3875; CGI/1.1 Status: header parsing still works for any interpreter).

parseCgiResponse()

Parse a raw CGI/1.1 interpreter response (RFC 3875) into status, headers and body — pure, side-effect-free string handling.

public static parseCgiResponse(string $raw) : array{status: int|null, headers: list, body: string}

The header block is split from the body on the FIRST blank line. Per RFC 3875 the separator is CRLF CRLF; a bare LF LF is tolerated as a fallback (the CRLF CRLF form is searched first regardless of position). Header lines are parsed Name: value (first colon splits; colons in the value are preserved). The Status: NNN Reason pseudo-header (§6.3.3) is extracted into the returned status (only when NNN is a valid 100–599 code) and is NOT echoed back as a header. Lines without a colon are ignored.

When $raw has no blank-line separator at all, the whole input is treated as the body and status/headers come back empty — the caller decides whether that's a malformed-header error (cgiInterpreterResponse() already does, before calling this) or a bodies-only response.

Parameters
$raw : string
Return values
array{status: int|null, headers: list, body: string}

applyCgiHeaders()

Apply CGI-captured response headers to the live response, preserving MULTIPLE same-name headers (multi Set-Cookie, Link, Vary, …).

private static applyCgiHeaders(mixed $resp, iterable<string|int, mixed> $pairs) : void

#260 — the proc/pool/fork workers capture headers as an ordered list of [name, value] pairs (the uopz header() override already honours each header(..., $replace) call), but applying every captured pair with the wrapper's default $replace = true collapsed each same-name header to the LAST on the wire. Here the FIRST occurrence of a name replaces any framework default; every subsequent same-name pair is appended, so the worker's exact multi-header set reaches the client.

The CGI/1.1 "Status:" pseudo-header (RFC 3875 §6.3.3) is consumed as the response code and never forwarded (mod_cgi parity).

Parameters
$resp : mixed

a Response-like object exposing header(string, string, bool) — the live $g->zealphp_response (typed mixed) or a test double; null/non-object emits nothing

$pairs : iterable<string|int, mixed>

each element a [string $name, string $value] pair

applyCgiResponseFrame()

Apply a pool/fork response frame (status + headers + cookies) to the response and return the body per the universal return contract — shared by cgiPool and cgiFork.

private static applyCgiResponseFrame(array<string|int, mixed> $resp) : mixed
Parameters
$resp : array<string|int, mixed>

buildCgiRequestFrame()

Build the IPC request frame from the current request context — shared by cgiPool and cgiFork. The raw body feeds php://input (CgiInputStream); multipart/form-data is skipped (it rides $_FILES, and the full blob could exceed IPC's 64 MB cap).

private static buildCgiRequestFrame(RequestContext $g, string $path) : array<string, mixed>
Parameters
$g : RequestContext
$path : string
Return values
array<string, mixed>

cgiInterpreterResponse()

Consume a standard RFC 3875 CGI response from a non-PHP interpreter subprocess: read stdout (header block + blank line + body), apply the parsed headers / Status: pseudo-header to the response, and return the body through the universal contract. stderr is drained for error logging.

private static cgiInterpreterResponse(resource $process, array<int, resource> $pipes, string $path) : mixed

Unlike cgiSubprocess()'s PHP/cgi_worker path, there is NO stderr metadata channel — a Python/Perl script just writes CGI to stdout. text/event-stream responses stream chunk-by-chunk; everything else returns one buffered body.

Apache parity: ap_scan_script_header_err_brigade_ex() (header scan + Status:), log_script_err() (stderr drain), CGIScriptTimeout (read deadline).

Parameters
$process : resource
$pipes : array<int, resource>
$path : string

emitCgiSessionCookieFromMeta()

Emit the PHPSESSID Set-Cookie AFTER a CGI subprocess that STARTED a session (#108 / #355). The subprocess can't emit its own session cookie (PHP's session module uses the C-internal php_setcookie(), which the CLI SAPI discards), so it reports the active session id back in the metadata frame (session_id); the host emits the cookie here.

private static emitCgiSessionCookieFromMeta(RequestContext $g, mixed $response, array<string|int, mixed> $meta) : void

Lazy + mod_php-faithful: a script that never calls session_start() reports no session id, so NOTHING is emitted (no unsolicited cookie). The cookie is also skipped when the client already sent that exact id (no redundant Set-Cookie on a returning visitor). This is the post-run counterpart to mintCgiSession()'s pre-run client-id forwarding — together they give #108 first-visit cookie delivery WITHOUT #355's eager mint.

Parameters
$g : RequestContext
$response : mixed

the response wrapper carrying ->cookie()/->isWritable()

$meta : array<string|int, mixed>

the decoded subprocess metadata frame

mintCgiSession()

Mint and propagate a session id on behalf of a CGI subprocess request (issue #108).

private static mintCgiSession(RequestContext $g) : string|null

In cgiOwnsSessions() mode the host's SessionManager is bypassed, so NOTHING on the host side emits a Set-Cookie for first-time visitors. The subprocess can't emit one either: PHP's session module sends the session cookie through its internal php_setcookie() C function, NOT the userspace setcookie() — uopz can't intercept that path, and CLI SAPI silently discards the buffered SAPI headers. Result: every first visit gets a freshly generated SID inside the subprocess and zero Set-Cookie on the wire, so the next request has no PHPSESSID to send and the cycle repeats forever ("Value: EMPTY" loop).

This helper closes that gap. When called from a CGI dispatcher:

  • If the request already has a PHPSESSID cookie, return it (no-op).
  • Otherwise mint a fresh id via session_create_id(), stash it in $g->cookie[session_name] so the subprocess sees it in $_COOKIE, AND emit Set-Cookie on the outbound response so the client uses the same id on the next request.

session_create_id() is safe to call without an active session in PHP 7.1+ — it just returns a fresh random string in the configured session.sid_bits_per_character format.

Returns null when the host is NOT in cgiOwnsSessions() mode — host SessionManager already owns cookie emission there.

Parameters
$g : RequestContext
Return values
string|null
On this page