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.phpthrough 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: listapplyCgiHeaders()
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(typedmixed) 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 emitSet-Cookieon 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