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

App
in package

ZealPHP framework core — the single process-wide singleton that owns the OpenSwoole server lifecycle, route table, PSR-15 middleware stack, and all per-request lifecycle configuration.

Typical boot sequence:

App::superglobals(false);          // choose lifecycle mode
$app = App::init('0.0.0.0', 8080); // create singleton + install overrides
$app->route('/hello', fn() => 'Hello!');
$app->addMiddleware(new CorsMiddleware());
$app->run();                        // start the OpenSwoole event loop

Configuration is expressed as static fluent setters (e.g. App::superglobals(), App::documentRoot()) that MUST be called before App::run() — OpenSwoole freezes the server settings at $server->start(). See the "Architecture" section of CLAUDE.md for the full lifecycle-mode matrix.

Table of Contents

Constants

DEFAULT_MAX_COROUTINE  : mixed = 10000
Default per-worker coroutine ceiling applied in run() when the operator sets none (via ZEALPHP_MAX_COROUTINE or $app->run(['max_coroutine' => N])).
DEFAULT_MAX_REQUEST  : mixed = 100000
Default per-worker request-recycle cap (OpenSwoole max_request): the worker exits cleanly and respawns after this many requests, bounding memory growth from leaks. 0 disables recycling (OpenSwoole native semantics) — see resolveMaxRequest() / the ZEALPHP_MAX_REQUEST env. (#449)
ISOLATION_CGI_FCGI  : mixed = 'cgi-fcgi'
ISOLATION_CGI_POOL  : mixed = 'cgi-pool'
ISOLATION_CGI_PROC  : mixed = 'cgi-proc'
ISOLATION_COROUTINE  : mixed = 'coroutine'
Isolation strategy constants — canonical user-facing surface for App::isolation().
ISOLATION_NONE  : mixed = 'none'
KNOWN_METHODS  : array<int, string> = [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTI...
Methods ZealPHP recognises. A request whose method is outside this set gets 501 Not Implemented (Apache: M_INVALIDHTTP_NOT_IMPLEMENTED, server/protocol.c:1253). Standard RFC 9110 methods plus the common WebDAV verbs Apache registers in ap_method_registry_init(). A recognised method that has no matching route still flows through to 404/405/fallback.
MODE_COROUTINE  : mixed = 'coroutine'
MODE_COROUTINE_LEGACY  : mixed = 'coroutine-legacy'
MODE_LEGACY_CGI  : mixed = 'legacy-cgi'
High-level mode presets — canonical user-facing surface for App::mode().
MODE_MIXED  : mixed = 'mixed'
LIFECYCLE_MODE_NAMES  : mixed = ['coroutine', 'coroutine-legacy', 'legacy-cgi',...
Lifecycle modes a user might mistakenly pass as a route backend. Rejected with a message pointing at the separate-process model.
REASON_PHRASES  : mixed = [ // 1xx Informational 100 => 'Continue', 101 =...
IANA-registered HTTP status reason phrases (RFC 9110 §15).
ROUTE_BACKEND_MODES  : mixed = ['pool', 'proc', 'fork', 'fcgi']
The four CGI dispatch strategies a per-route backend: may name — the CGI-ISOLATION family. Excludes the COROUTINE-SCHEDULER family (coroutine/coroutine-legacy/legacy-cgi/mixed), which is a process-wide lifecycle decision frozen at $server->start() and cannot be chosen per route. See normalizeBackendConfig()'s guard.
WHEN_MEMO_MAX  : mixed = 4096
Upper bound on the App::when per-path memo (memory-exhaustion guard).

Properties

$access_log_format  : string
Apache LogFormat "...". Format string used by access_log() to render each request line. Tokens (Apache mod_log_config subset):
$admin_checker  : callable|null
Callback consulted by ZealAPI::isAdmin(). Signature: fn(): bool.
$allow_encoded_slashes  : bool
Apache AllowEncodedSlashes — when false (default, matching Apache), a request whose RAW (pre-decode) path contains an encoded slash (%2F/%2f) is refused with 404 before route matching. Apache's unescape_url() forbids AP_SLASHES by default; we mirror that. Set true to permit encoded slashes (they are then decoded to / like any other octet).
$api_null_not_found  : bool
#347 — the 404 {"error":"method_not_found"} envelope for a ZealAPI handler that returns null with NO output, NO explicit status and NO streaming. **Mode-aware (corrected rule):** it applies ONLY to **per-method** dispatch ($get/$post/…) — a method handler that ran and produced nothing. A **filename-match** handler ($list, serving all methods) returning null is an intentional **empty 200** (native-PHP parity — an empty-set / infinite-scroll tail), never a 404. A method with no handler at all already 405s before this point. Default ON.
$api_warn_collisions  : bool
Log warnings when a ZealAPI filename collides with an HTTP method keyword (get.php defining $get) or when a filename-matched handler shadows per-method handlers in the same file. Default ON so new apps surface mistakes; set to false (or 'api_warn_collisions' => false in the run() config) for legacy codebases that knowingly use method names as filenames.
$auth_checker  : callable|null
Auth-hook callbacks consulted by ZealAPI::isAuthenticated(), ::isAdmin(), and ::getUsername() so the framework's built-in file-based API layer can delegate auth questions to whatever auth system the app uses (Symfony Security, Auth0, the SelfMadeNinja stack, a custom $_SESSION['user'] check, etc.) without subclassing or monkey-patching ZealAPI itself.
$block_dotfiles  : bool
Block any path containing a dotfile component (.git, .env, .htaccess, etc.). Apache convention.
$boot_env  : array<string, string>
Process environment captured at boot (real getenv()), before the per-coroutine putenv/getenv overrides are installed in Mode 4. The overridden \ZealPHP\zeal_getenv falls back to this for variables not set request-scoped via \ZealPHP\zeal_putenv.
$canonical_name  : string|null
Apache ServerName www.example.com:443. The canonical host the server advertises in absolute redirects (and other absolute URL builders) when $use_canonical_name is true. Include scheme-port if relevant; the raw value is returned as-is by App::canonicalHost().
$cgi_backend_aliases  : array<string, array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}>
Named CGI backend aliases for the per-route backend: option, registered via App::cgiBackendAlias(). Maps an alias name to a normalised backend config (the same shape resolveCgiBackend() returns, minus exec_paths).
$cgi_backends  : array<string, array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array, exec_paths?: array}>
Per-extension CGI backend registry. Apache AddHandler/ProxyPassMatch + nginx fastcgi_pass-per-location parity.
$cgi_fork_instance  : ForkPool|null
Per-worker ForkPool singleton for cgiMode('fork') — the fork-master subprocess. Lazy-spawned on first dispatch in this OpenSwoole worker (same per-worker ownership rationale as $cgi_pool_instance).
$cgi_fork_max_concurrent  : int
Live-child concurrency cap for cgiMode('fork') — the fork-master refuses to fork past this many simultaneous children (fork-bomb guard + backpressure).
$cgi_fork_max_concurrent_set  : bool
$cgi_mode  : string
How a process-isolated legacy include is dispatched, when processIsolation() is on:
$cgi_mode_set  : bool
"Explicitly set by a fluent setter" flags for the env-overridable CGI knobs. App::resolveCgiEnv() applies a ZEALPHP_CGI_* value only when the matching flag is false — so explicit code config always wins over the environment, which in turn wins over the hardcoded default.
$cgi_pool_env_allowlist  : array<int, string>
Optional strict env allowlist for cgiMode('pool') subprocesses (WorkerPool::filterSubprocessEnv). Empty (default) = pass the parent environment through to the subprocess (legacy-app compatibility) MINUS the request-controlled HTTP_PROXY (httpoxy). Set via App::cgiPoolEnvAllowlist([...]) to a list of exact names / PREFIX* globs to restrict what secrets the long-lived subprocess inherits; ZEALPHP_POOL_MAX_REQUESTS is always passed. Per-request CGI vars travel over the IPC frame, not this env, so a strict allowlist doesn't lose them.
$cgi_pool_instance  : WorkerPool|null
Per-worker WorkerPool singleton for cgiMode('pool'). Lazy-spawned on first dispatch in this OpenSwoole worker. Held here (not on a Store) because each OpenSwoole worker owns its own subprocess pool — proc resources don't share across workers.
$cgi_pool_max_requests  : int
Per-subprocess recycle threshold for cgiMode('pool'). After this many requests, the subprocess exits cleanly and the pool spawns a fresh replacement — FPM pm.max_requests parity, bounds memory leak from long-running plugin code. Set to 1 to recycle every request (true fresh-process semantics; same isolation as cgiMode('proc') but with the pool managing spawn-cost amortisation).
$cgi_pool_max_requests_set  : bool
True once cgiPoolMaxRequests() set the recycle count explicitly. The mode() presets consult this so a mode('legacy-cgi') default (recycle=1) never clobbers an explicit user choice, regardless of call order.
$cgi_pool_size  : int
Subprocess count for cgiMode('pool') — the native FCGI-style worker pool. Each OpenSwoole worker process spawns this many persistent PHP subprocesses on first dispatch (lazy). FPM pm.max_children parity: sets the per-worker concurrency cap. Default 4 — balances spawn cost with concurrency for typical web workloads.
$cgi_pool_size_set  : bool
$cgi_script_aliases  : array<string, array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array}>
ScriptAlias-style CGI path registry (Apache ScriptAlias parity). Maps a normalised URL prefix (leading slash, no trailing slash) to a backend config. Any file served under a registered prefix is treated as executable regardless of its extension.
$cgi_session_auto_start  : bool
legacy-cgi only: eagerly mint a session id + Set-Cookie on a FIRST-time visitor (no incoming PHPSESSID) BEFORE the CGI subprocess runs (#108).
$cgi_subprocess_autoload  : bool
Whether cgi_worker.php (proc-mode subprocess entry) loads Composer's vendor/autoload.php on startup. Default false — restores the pre- v0.2.20 behaviour where the subprocess runs at true global scope with NO ZealPHP framework loaded, suitable for unmodified WordPress / Drupal.
$cgi_timeout  : int
Maximum seconds to wait for a CGI subprocess (proc mode) to produce its metadata line on stderr. After this deadline the child receives SIGTERM; if it does not exit within 5 s it receives SIGKILL. Matches Apache's CGIScriptTimeout directive. Default 60 s.
$cgi_timeout_set  : bool
$coproc_implicit_request_handler  : bool
Enable the legacy CGI request handler for public/*.php paths.
$coroutine_cwd_isolation  : bool
Per-coroutine WORKING-DIRECTORY isolation (#323). chdir() is a process-level syscall, so under coroutine concurrency one request's chdir() (or the framework's own executeFile() chdir-to-script-dir) changes the CWD of every concurrently-running peer — racy relative includes / fopen across the whole worker. When ON (and ext-zealphp 0.3.35+ is loaded), the scheduler hooks save each coroutine's cwd on yield (re-parking the worker baseline so peers start clean) and restore it on resume — chdir() becomes per-coroutine, like PHP-FPM's per-process CWD. Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_CWD_ISOLATION_DISABLE=1); off by default elsewhere (coroutines that never chdir cost one getcwd+strcmp per yield when on, zero when off).
$coroutine_globals_isolation  : bool
Per-coroutine $GLOBALS isolation via ext-zealphp's zealphp_coroutine_globals(). When enabled, each coroutine gets its own snapshot of EG(symbol_table) swapped in on yield/resume — so $GLOBALS['app_state'] and global $foo; writes never race across concurrent coroutines.
$coroutine_isolated_superglobals  : bool
True when ext-zealphp's per-coroutine superglobal isolation is active.
$coroutine_libxml_isolation  : bool
Per-coroutine libxml_use_internal_errors() FLAG isolation — the libxml error-buffering flag is process-global (measured 128/250 leaks).
$coroutine_locale_isolation  : bool
Per-coroutine LOCALE isolation — setlocale() is process-global (string casing, number/date formatting), so one request's locale change leaks into every concurrently-running peer mid-request. When ON (ext-zealphp 0.3.38+), the scheduler hooks save each coroutine's locale on yield (re-parking the worker baseline captured at enable time — a boot-time setlocale() before App::run() IS the baseline) and restore it on resume. Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_LOCALE_ISOLATION_DISABLE=1); off by default elsewhere.
$coroutine_mbenc_isolation  : bool
Per-coroutine mb_internal_encoding() isolation — the mbstring current internal encoding is process-global; legacy code sets it before string work (measured 173/250 leaks at 49-way concurrency). ext-zealphp 0.3.45+; auto-refuses when mbstring is absent. Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_MBENC_ISOLATION_DISABLE=1); off by default elsewhere.
$coroutine_statics_isolation  : bool
Stage 5 — per-coroutine FUNCTION-local static $x isolation via ext-zealphp's zealphp_coroutine_statics(). This is the LAST request-state primitive that previously leaked across coroutines (everything else — superglobals, class statics, $GLOBALS, constants, ini_set, putenv — is already isolated). When enabled, the on_yield hook snapshots every instantiated function/method's live static table per coroutine and restores THIS coroutine's values on resume — the same snapshot/restore model already proven for class statics. Cooperative scheduling makes it correct: a coroutine writes its statics after its own restore and reads them before its next yield, so values never bleed.
$coroutine_tz_isolation  : bool
Per-coroutine date_default_timezone_set() isolation — the default timezone is process-global; WordPress-class apps set it per request (core boot reads the site option), so one request's timezone leaks into every concurrently-running peer (measured 179/250 at 49-way concurrency). Same stage shape as locale/umask (ext-zealphp 0.3.45+, via the engine's own getter/setter pair). Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_TZ_ISOLATION_DISABLE=1); off by default elsewhere.
$coroutine_umask_isolation  : bool
Per-coroutine UMASK isolation — umask() is process-global file-mode state; one request's umask(0077) changes every peer's file creation mid-request. Same stage shape as locale/CWD (ext-zealphp 0.3.38+; the umask read+re-park is a single syscall). Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_UMASK_ISOLATION_DISABLE=1); off by default elsewhere.
$cwd  : string
Absolute working directory the framework boots in. Resolved at boot via realpath(__DIR__ . '/..') and exposed read-only for handlers that need to build paths relative to the project root (e.g.
$default_charset  : string
Apache AddDefaultCharset. Stored here for consumers (e.g. a future CharsetMiddleware) that want a server-wide default charset to append to text-ish Content-Type headers.
$default_mimetype  : string
Apache DefaultType / PHP default_mimetype. The Content-Type applied by CharsetMiddleware to a response that doesn't set one itself (mod_php sends text/html by default). Set to '' to leave untyped responses untouched.
$default_php_self  : string|null
Override value for $_SERVER['PHP_SELF'] and friends. null means "use the request URI verbatim", which is the normal Apache/nginx convention. Apps that need a stable PHP_SELF (legacy WordPress plugins, etc.) can pin it here.
$define_isolation  : bool
Per-request define() isolation. When true, constants defined during a request are tracked and removed at request end. Boot-time constants (PHP_VERSION, extension defines, autoloaded class constants) survive.
$dev_reload  : bool|null
Dev route hot-reload toggle. When true, each worker polls route/*.php mtimes and calls reloadRoutes() on change (no process restart). null resolves to the ZEALPHP_DEV env var at run(). OFF in production (the route table stays master-loaded + COW-shared). Set via App::devReload().
$directory_index  : array<int, string>
Apache DirectoryIndex — file names tried in order when a directory is requested.
$directory_slash  : bool
Apache DirectorySlash equivalent — redirect /foo/foo/ when foo is a directory.
$display_errors  : bool|null
Whether framework error pages render the captured exception + stack trace inline. Secure-by-default: null (the default) resolves at runtime to the ZEALPHP_DEV env var — OFF in production, so 5xx pages show a generic message and never leak traces/secrets (#412). Call App::displayErrors(true) for the inline-trace development view; an explicit setter call always wins over the env resolution.
$document_root  : string
Apache DocumentRoot equivalent. Relative values (the default) are resolved against App::$cwd; absolute values are used as-is. Drives App::include() path resolution and the implicit /{file}/{dir/uri} routes.
$enable_coroutine_override  : bool|null
OpenSwoole enable_coroutine server-setting override. null means "follow !$superglobals" (true → coroutine-per-request, false → one synchronous request at a time per worker). Set via App::enableCoroutine(bool). Combining true with $superglobals=true is unsafe — process-wide $_GET/$_POST/$_SESSION will race across concurrent coroutines; the helper warns at run() time.
$fcgi_address  : string
FastCGI backend address used when App::cgiMode() === 'fcgi'.
$fcgi_address_set  : bool
$file_etag  : bool
Apache FileETag. When false, ETagMiddleware emits no ETag header and never returns 304 (equivalent to FileETag None). Default true.
$function_isolation  : bool
Per-request function/class/include isolation via ext-zealphp's zealphp_process_state_snapshot() / zealphp_process_state_clean().
$global_scope_include  : bool|null
Stage 8 — true-global-scope request include (coroutine-legacy). When on (and ext-zealphp exposes zealphp_require_global()), App::include() runs the target file at TRUE global scope so a bare file-scope $x = ... — and every transitive require_once — binds to $GLOBALS instead of the executeFile() method frame. This is what lets unmodified require_once- bootstrap apps (WordPress's $menu/$submenu/$_wp_submenu_nopriv, built bare at file scope in wp-admin/includes/menu.php) render in **coroutine-legacy** in-process mode, where the request entry runs inside a PHP method and those vars would otherwise be method-local.
$hook_all_override  : bool|int|null
OpenSwoole\Runtime::enableCoroutine($flags) override. Same shape as App::hookAll() input: null → follow !$superglobals (HOOK_ALL when coroutine mode, 0 in superglobals mode); trueHOOK_ALL; false0; int → explicit bitmask. PDO is intentionally NOT hooked in OpenSwoole 22.1 / 26.2 regardless of this flag.
$hook_exec  : bool|null
Toggle the uopz override of the exec family (backtick / shell_exec / exec / system / passthru) so they yield via OpenSwoole's coroutine scheduler instead of blocking the worker.
$hook_exit  : bool|null
Toggle the ext-zealphp interception of exit()/die() so a userland exit inside a coroutine throws ZealPHP\HaltException (which extends \Error, so the ubiquitous try { … exit; } catch (\Exception) legacy-router idiom cannot swallow the normal exit and turn it into a 500 — issue ext#47; FreshRSS/DokuWiki/CodeIgniter). The framework's halt-aware sites flush the buffered output as the body. null (default) resolves to "on when the coroutine scheduler is active" (enableCoroutine effective) at App::run(); a non-null value forces it. Requires ext-zealphp 0.3.48+ (zealphp_exit_hook); a no-op otherwise. Env opt-out: ZEALPHP_EXIT_HOOK_DISABLE=1.
$hostname_lookups  : bool
Apache HostnameLookups On|Off. When true, the framework populates $g->server['REMOTE_HOST'] via gethostbyaddr($g->server['REMOTE_ADDR']) on each request. **WARNING**: this performs a blocking reverse-DNS lookup per request (mitigated by OpenSwoole's coroutine hook converting it to a non-blocking async resolve, but still a measurable per-request cost). Off by default — Apache's own default since 1.3.
$ignore_php_ext  : bool
When true (default), URLs ending in .php get a 403. The framework encourages extensionless URLs as the canonical public surface (matches Apache RewriteRule \.php$ - [F] parity). Set false to allow direct *.php routing — useful when porting an existing app that links to /foo.php from external sources.
$include_isolation  : bool
Per-request require_once cache reset. Clears EG(included_files) so files loaded via require_once on request N re-execute on request N+1.
$initial_error_reporting  : int
Initial error_reporting level captured at boot — referenced by the per-coroutine override.
$keep_globals  : bool
Keep user-defined $GLOBALS across requests within the same worker.
$limit_request_field_size  : int
Apache LimitRequestFieldSize — maximum byte length of a single request header line. **NOT enforced by ZealPHP.** OpenSwoole's C-layer HTTP parser owns all wire-level framing; ZealPHP only sees the already-parsed $request->header array. The http_header_buffer_size option was explicitly NOT passed to OpenSwoole (its option validator rejects it at boot — see App::run() ~line 3748). Changing this value has no effect on the actual per-header byte limit, which is governed by OpenSwoole's global header-buffer size (~8 KiB default). This property is retained for documentation and future compatibility only.
$limit_request_fields  : int
Apache LimitRequestFields — maximum number of request header fields a single request may carry. Enforced at the PHP application layer: requests carrying more than this many headers are rejected with 400 before route dispatch. Set to 0 to disable the check (unlimited). Default 100 matches Apache's compiled-in default.
$limit_request_line  : int
Apache LimitRequestLine — maximum byte length of the HTTP request line (method + URI + protocol). **NOT enforced by ZealPHP.** OpenSwoole's C parser reads the request line before any PHP code runs; there is no per-request-line cap that ZealPHP can apply after the fact. OpenSwoole's global http_header_buffer_size governs this limit at the wire level.
$middleware_aliases  : array<string, MiddlewareInterface|callable>
Named middleware registry — Traefik's "named & shared" middleware and Laravel's route-middleware aliases. Maps a short name to either a ready MiddlewareInterface instance or a factory callable(...$args) that returns one. Populated by App::middlewareAlias() at boot; resolved to instances once at App::run() (single-coroutine, so the hot path never does a registry lookup or instantiation). Reused across routes — middleware objects MUST be stateless (request state lives in $g/RequestContext, never on the middleware) because one instance is shared by every concurrent coroutine that uses the alias.
$middleware_stack  : StackHandler|null
The PSR-15 middleware stack handler, built during App::run() from the registered middleware list. null before run(). Generally read via the public App::middleware() accessor; this property is public for advanced introspection (e.g. /healthz dumps).
$middleware_wait_stack  : array<int, MiddlewareInterface>
Middleware queued via App::addMiddleware() BEFORE App::run().
$path_info  : bool
Apache PATH_INFO — when /script.php/extra/path, expose /extra/path as PATH_INFO.
$port  : int
$preload_classes  : array<int, class-string>
Extra classes to compile at worker start so they are NEVER cold-autoloaded under request concurrency. APPENDED to the framework's own request-path warmup set (see preloadRequestPathClasses()).
$preload_classmap  : bool
When true, warm EVERY class in Composer's classmap in the MASTER process (before $server->start() forks the workers), so a user app's own controllers/services (autoloaded on demand, deep inside handlers — "the app is just the server") are born LINKED and copy-on-write-forked into every worker, never compiled on the concurrent cold path. This is the structural fix for the present-but-unlinked inheritance race: the whole dependency graph is bound in a single process with NO coroutine scheduler, so nothing can yield and let a worker interleave a cold compile. Same idea as PHP's opcache.preload. Validated: 0 failures across cold bursts with the framework's own onWorkerStart preload disabled (classmap-only).
$preload_dirs  : array<int, string>
Source directory trees to warm at worker start (PSR-4 roots / app source whose symbols a registered autoloader can resolve). Each .php file's declared symbols are extracted via the tokenizer and autoloaded+linked single-coroutine. Append via App::preloadDir().
$process_isolation  : bool|null
Per-include CGI process-isolation override. null means "follow $superglobals" (true → CGI subprocess via cgi_worker.php; false → in-process via executeFile()), which preserves today's default coupling. Set via App::processIsolation(bool) — see that method for the trade-offs. App::run() resolves this into the backing $coproc_implicit_request_handler flag right before the server starts.
$reloading  : bool
True only while App::reloadRoutes() is re-including route/*.php files.
$run_has_started  : bool
Set true at the top of App::run() so the four lifecycle setters (superglobals, processIsolation, enableCoroutine, hookAll) can refuse mutations made AFTER the server has booted.
$sapi_name  : string|null
mod_php-parity SAPI identity for the php_sapi_name() override. Default null returns the real PHP_SAPI ("cli") — no behavior change. Set to a web SAPI string (e.g. 'apache2handler', 'fpm-fcgi') so legacy code branching on php_sapi_name() takes its web path. The PHP_SAPI *constant* is unaffected (uopz cannot redefine it). Configure via App::sapiName() before App::init().
$server  : Server|Server|null
The active OpenSwoole server instance after App::run() constructs it; null before run(). Returned as a WebSocket\Server when any App::ws() route was registered (the framework upgrades from Http\Server automatically), Http\Server for pure HTTP apps.
$server_admin  : string|null
Apache ServerAdmin webmaster@example.com. When set, the framework's default 500/error page mentions this contact. null disables the contact line.
$server_tokens  : string
Apache ServerTokens. Controls how much detail the X-Powered-By response header advertises: 'Full' (default) → ZealPHP + OpenSwoole 'Prod' / 'Major' / 'Minor' / 'Min' / 'OS'ZealPHP 'None' (or '') → header omitted entirely (info-leak hardening) Set via App::serverTokens() before App::init().
$session_data_size  : int
Maximum serialized session size in bytes when using TableSessionHandler.
$session_handler  : string|SessionHandlerInterface|null
Session storage backend. One of: - null (default) — the framework inline **file** path in ALL modes (flock read-merge-write under $session_save_path). #295: deliberately NOT auto-promoted to TableSessionHandler; the unconfigured default is file-backed everywhere. Opt into a concurrent-safe backend explicitly.
$session_lifecycle  : bool
Whether ZealPHP's per-request session lifecycle runs. Default true: the SessionManager / CoSessionManager OnRequest wrapper reads the PHPSESSID cookie, calls zeal_session_start(), optionally emits the Set-Cookie header, and closes the session at request end. Set to false when another framework (e.g. Symfony's NativeSessionStorage via the zealphp-symfony bridge) owns the session lifecycle — ZealPHP then skips the session-specific work but still does request-context setup ($g->openswoole_request, $g->zealphp_response, error-stack reset, etc.).
$session_max_rows  : int
Maximum concurrent sessions in OpenSwoole\Table when using TableSessionHandler. Default 65536 (64K) — accommodates medium-scale deployments without re-tuning. Each row costs `$session_data_size + ~64 bytes` of shared memory (one allocation per OpenSwoole server, NOT per worker). Default config = 64K × 16KB ≈ 1 GB shared memory.
$session_save_path  : string
File-backing directory for session storage. Default /var/lib/php/sessions (matches PHP's default). Used by FileSessionHandler and TableSessionHandler's file backing layer.
$session_strict_mode  : bool
PHP session.use_strict_mode parity (#244). When true (the default — security-first) a CLIENT-SUPPLIED session id (from a PHPSESSID cookie or query param) whose backing store loads an EMPTY session is treated as untrusted: the session managers mint a fresh server-generated id and switch the client to it. This defeats session FIXATION — an attacker who plants a known id into the victim's browser can no longer have it promoted to an authenticated session, because the framework rotates any unrecognised id before the victim ever authenticates under it. A well-formed id that DOES resolve to a non-empty stored session is preserved unchanged.
$session_ttl  : int
Session TTL in seconds. Default 7200 (2 hours — modern-app reasonable; PHP's stock 1440 / 24 min is too short for typical workflows).
$silent_redeclare  : bool
Stage 3 — silent-redeclare opcode hooks. When enabled, ext-zealphp's ZEND_DECLARE_FUNCTION / ZEND_DECLARE_CLASS / _DELAYED opcode handlers check if the target symbol already exists in EG(function_table) / CG(class_table). If it does, the opcode is silently skipped instead of throwing E_COMPILE_ERROR ("Cannot redeclare …"). First declaration wins — matches what FPM gets "for free" by forking a fresh process per request.
$static_handler_locations  : array<int, string>
Static handler URL-prefix whitelist. Empty = serve any path under document_root (Apache default).
$strip_trailing_slash  : bool
Apache RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.+)/$ /$1 [R=301,L].
$superglobals  : bool
Per-request lifecycle mode (the dial that picks $g storage + SessionManager + enable_coroutine + HOOK_ALL default). See the "Lifecycle modes" matrix in CLAUDE.md — short version:
$trace_enabled  : bool
Apache TraceEnable — defaults to OFF for security. When false (default) ResponseMiddleware refuses HTTP TRACE with 405 regardless of any matching route definition. Set to true only if you know you need TRACE.
$trusted_proxies  : array<int, string>
CIDR list of proxy IPs whose X-Forwarded-For / X-Real-IP headers App::clientIp() will trust. Empty (the default) means no proxies trusted — App::clientIp() always returns REMOTE_ADDR. Critical for production deploys behind Traefik/Caddy/nginx; without it rate limiters and access logs see the proxy IP instead of the real client.
$use_canonical_name  : bool
Apache UseCanonicalName On|Off. When true and $canonical_name is set, App::canonicalHost() returns the canonical name; otherwise it returns the request Host header. Default false (Apache's default since 2.0).
$username_provider  : callable|null
Callback consulted by ZealAPI::getUsername(). Signature: fn(): ?string.
$when_middleware  : array<int, MiddlewareInterface|string>}>
Path-scoped middleware registry — App::when($path, $middleware). Each entry scopes a chain to a URL path prefix (or a #...# PCRE), and runs for EVERY request whose normalized path matches — route or api alike, since api endpoints are just /api/... URLs on the same stack. Stored in registration order (first registered = outermost). Raw specs here; resolved to instances at App::run() into $when_middleware_compiled.
$when_middleware_compiled  : array<int, MiddlewareInterface>}>
Boot-compiled App::when chains (alias→instance), read-only at request time so the hot path never does a registry lookup or new.
$when_middleware_memo  : array<string, array<int, MiddlewareInterface>>
Per-normalized-path memo of the flattened matching when chain (the registry is immutable after boot, so this is a write-once-per-path cache; concurrent same-path writes are idempotent). Capped at WHEN_MEMO_MAX entries so an attacker spraying distinct paths can't grow it without bound — past the cap, paths simply recompute the (cheap) prefix scan.
$autoloadSerializerInstalled  : bool
True once the per-worker coroutine autoload serializer is installed.
$bootedAt  : int|null
Unix timestamp the master process booted at (set by run()).
$host  : string
Bind address for the OpenSwoole server (e.g. '0.0.0.0' or '127.0.0.1'). Set in __construct() from App::init().
$processBootWired  : bool
True once the onWorkerStart hook for process-pool is wired.
$processHandlers  : array<string, array{callable: callable, workers: int, coroutine: bool}>
$pubsubBootWired  : bool
True once the onWorkerStart hook for pubsub/streams is wired (one-time guard).
$pubsubRegistry  : array<string, array<int, callable>>
$reliableRegistry  : array<string, array<int, array{group: string, handler: callable, blockMs: int, batchSize: int}>>
$route_baseline  : MiddlewareInterface|callable>, backend_aliases?: array}>}|null
Snapshot of the route/middleware registries taken at App::run() *before* the route/*.php files + implicit routes are loaded — i.e. just the app.php-defined explicit routes/aliases/scopes. App::reloadRoutes() restores this baseline, then re-runs the file-based registration, so a route-file edit can be picked up without restarting the worker. Null until run() snapshots it.
$routes  : array<int, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>
$routes_by_exact_method  : array<string, array<string, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>>
$routes_by_method  : array<string, array<int, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>>
$signalHandlers  : array<int, array<int, array{handler: callable, worker_only: bool}>>
$task_worker_num  : int
Resolved task-worker count after run() reads CLI/env/settings. Zero when task workers are disabled.
$worker_num  : int
Resolved worker counts after run() reads CLI/env/settings.
$workerStartedAt  : float
Unix timestamp (float) of the moment this worker's onWorkerStart callback finished — used by App::stats() to compute per-worker uptime. Zero until the first worker start fires.
$workerStartHooks  : array<int, callable>
$workerStopHooks  : array<int, callable>
$ws_routes  : array<string, array{message: callable, open: callable|null, close: callable|null}>
$access_log_format_compiled  : array<int, array{kind: string, arg?: string}>|null
Parsed format spec cache (token list). Filled lazily by formatAccessLogLine() the first time it sees a given format string. Resets when accessLogFormat() is reassigned via the fluent setter.
$configMap  : array<string, string>
ZealPHP config keys recognized in the run() $settings array.
$error_handlers  : array<int, array{handler: callable, param_map: array, raw: bool}>
Status -> custom error handler registry (key 0 = catch-all).
$fallback_handler  : array<string, mixed>|null
$fatal_guard_inflight  : array<int, Response>
#338 — in-flight raw responses, per worker process. A worker-killing FATAL (E_COMPILE_ERROR / E_ERROR / …) never reaches the normal emit path, so the client connection — held open by the master's reactor — would hang until the CLIENT's timeout (HTTP 000). Apache/mod_php answers 500. The session managers track every request's raw OpenSwoole response here and release it on normal completion; the native shutdown guard below answers whatever is left when a fatal tears the worker down. Coroutine mode can hold several entries at once — one fatal kills them all, so all of them get the 500.
$instance  : self|null
$overridesRegistered  : bool
Guard preventing registerAllOverrides() from installing uopz/zealphp built-in overrides more than once per process.
$resolved_session_handler  : SessionHandlerInterface|null
Memoised resolution of {@see $session_handler} (see resolveActiveSessionHandler).
$session_handler_resolved  : bool
Whether {@see $resolved_session_handler} has been computed yet.

Methods

__wakeup()  : mixed
accessLogFormat()  : string
Apache LogFormat. Resets the compiled-spec cache on set.
addMiddleware()  : void
addProcess()  : void
Register a long-running sidecar process — runs alongside the HTTP/WS server, managed by the OpenSwoole master (same fate-sharing: dies when the server stops, respawned on graceful reload). Different from task workers (which are queue consumers) and worker hooks (which run inside HTTP workers); these are independent processes for background work like log shippers, file watchers, scheduled-job runners, OAuth token refreshers, etc.
adminChecker()  : callable|null
Register a callback that ZealAPI::isAdmin() consults.
adoptRequestContext()  : void
#42 — make the CURRENT (child) coroutine inherit the spawning request's context. Two layers: RequestContext::instance() walks the parent-coroutine chain automatically (so $g is the request's), and — in coroutine-legacy, where $g->server et al. are live aliases of the process superglobals — zealphp_superglobals_adopt() (ext-zealphp 0.3.43+) gives this coroutine its OWN superglobal snapshot lane: its first yield CAPTURES the live view (the spawning request's state) without clearing it, so $_SERVER/$_GET/… survive the child's own yields and the parent is never stolen from. Safe no-op outside a coroutine or without the ext function. Called automatically by App::go() / App::parallel() / App::parallelLimit().
after()  : int
One-shot timer: calls $fn once after $ms milliseconds.
any()  : void
apiNullNotFound()  : bool
#347 — whether a ZealAPI handler returning null with no output, no explicit status and no streaming yields the Apache-parity 404 {"error":"method_not_found"} envelope instead of 200 + empty body. Default true. No-arg call returns the current value.
apiWarnCollisions()  : bool
Whether ZealAPI logs a warning when a filename collides with an HTTP method keyword (e.g. get.php defining $get) or a filename-matched handler shadows per-method handlers. Default true. No-arg call returns the current value.
applySignalHandlersFor()  : void
Internal: wire registered signal handlers into the OpenSwoole process lifecycle. Called from App::run() (master) and via onWorkerStart (workers).
authChecker()  : callable|null
Register a callback that ZealAPI::isAuthenticated() consults.
blockDotfiles()  : bool
Block any request whose path contains a dotfile component (.git, .env, .htaccess, etc.) with 403. Default true — matches Apache's convention of not serving hidden files. No-arg call returns the current value.
buildCgiEnv()  : array<string, string>
Build the OS-level environment array passed to the CGI subprocess.
canonicalHost()  : string
Canonical host for absolute URL building. Returns $canonical_name when useCanonicalName() is on AND $canonical_name is set; otherwise returns the request Host header (falling back to SERVER_NAME, then ''). Used by absolute-redirect builders that need to decide between the configured server name and the client-provided Host.
canonicalName()  : string|null
Apache ServerName. Canonical host advertised in absolute redirects when useCanonicalName() is on. Pass null/'' to clear.
cgiBackendAlias()  : void
Register a named CGI backend alias for the per-route backend: option — the App::middlewareAlias() of the CGI dispatch world. Lets a route say backend: 'wp-fork' instead of repeating an inline config.
cgiForkMaxConcurrent()  : int
Max concurrent cgiMode('fork') children — a per-request fork ceiling, NOT the pre-spawned cgiPoolSize(). Default 16. No-arg returns the current value; with-arg sets and returns it. Env: ZEALPHP_CGI_FORK_MAX_CONCURRENT (applied at boot unless set explicitly).
cgiMode()  : string
Select how a process-isolated legacy include is dispatched: 'pool' (default) — pre-spawned PHP worker pool, mod_php-style isolation, ~1-3 ms warm.
cgiOwnsSessions()  : bool
True when the CGI subprocess is the sole owner of the per-request session lifecycle — superglobals(true) + processIsolation(true).
cgiPoolEnvAllowlist()  : array<int, string>
Strict environment allowlist for cgiMode('pool') subprocesses — exact names and/or PREFIX* globs. A no-arg call returns the current list; a one-arg call sets it. Empty (default) passes the parent env through (legacy-app compatibility) minus the httpoxy HTTP_PROXY var; a non-empty list restricts the subprocess to matching vars only (the pool IPC var is always passed). Set BEFORE App::run().
cgiPoolMaxRequests()  : int
Per-subprocess request count before recycle for cgiMode('pool').
cgiPoolSize()  : int
Worker count for cgiMode('pool') — the native FCGI-style subprocess pool. FPM pm.max_children parity. Default 4. Set BEFORE App::run().
cgiScriptAlias()  : void
Register a ScriptAlias-style executable URL prefix (Apache ScriptAlias parity). Any file served under $urlPrefix is treated as executable, regardless of its extension or whether a per-extension backend exists.
cgiSubprocessAutoload()  : bool
Whether cgi_worker.php (proc-mode subprocess entry) loads Composer's vendor/autoload.php on startup. Default false — restores pre-v0.2.20 behaviour suitable for unmodified WordPress / Drupal / Joomla / plain PHP. Set to true when your public/*.php files explicitly need \ZealPHP\App or framework classes inside the CGI subprocess.
cgiTimeout()  : int
Max seconds to wait for a proc-mode CGI subprocess to emit its metadata line before SIGTERM/SIGKILL. Apache CGIScriptTimeout parity. Default 60.
clearTimer()  : void
Cancel a timer returned by tick() or after().
clientIp()  : string
Resolve the real client IP for the current request, honouring the $trusted_proxies allow-list. Behaviour:
coerceStatusCode()  : int
Coerce a handler's int return value to a valid HTTP status code.
compileMiddlewareChain()  : array<int, MiddlewareInterface>
Resolve a normalized middleware spec (instances + alias strings) to a flat list of MiddlewareInterface instances. Called once per route at App::run() boot time, so the dispatch hot path never does a registry lookup or new.
composeRequestArray()  : array<string, mixed>
Compose $_REQUEST from the GET and POST bags per PHP's default request_order='GP' (#356).
coroutineCwdIsolation()  : bool
Per-coroutine CWD isolation (#323) — see the $coroutine_cwd_isolation docblock. The ext C-level flag is asserted at App::run() boot wiring (alongside the other isolation knobs) so the scheduler hooks are guaranteed installed and the worker baseline is captured pre-fork; setting it here only records the intent.
coroutineGlobalsIsolation()  : bool
Per-coroutine $GLOBALS isolation. See $coroutine_globals_isolation docblock. Requires ext-zealphp 0.3.6+.
coroutineGlobalsMemoryAdvisory()  : void
One-time boot-time advisory for coroutineGlobalsIsolation(true).
coroutineLibxmlIsolation()  : bool
Per-coroutine libxml error-flag isolation — see the $coroutine_libxml_isolation docblock. Asserted at App::run() boot wiring.
coroutineLocaleIsolation()  : bool
Per-coroutine locale isolation — see the $coroutine_locale_isolation docblock. Asserted at App::run() boot wiring (pre-fork, so a boot-time setlocale() becomes the baseline); setting here records the intent.
coroutineMbencIsolation()  : bool
Per-coroutine mb-internal-encoding isolation — see the $coroutine_mbenc_isolation docblock. Asserted at App::run() boot wiring.
coroutineStaticsIsolation()  : bool
Stage 5 per-coroutine function-static isolation. Opt-in — see $coroutine_statics_isolation docblock for the perf tradeoff. The ext C-level flag is asserted at App::run() boot wiring (alongside the other isolation knobs) so the scheduler hooks are guaranteed installed first; setting it here only records the intent.
coroutineTimezoneIsolation()  : bool
Per-coroutine default-timezone isolation — see the $coroutine_tz_isolation docblock. Asserted at App::run() boot wiring.
coroutineUmaskIsolation()  : bool
Per-coroutine umask isolation — see the $coroutine_umask_isolation docblock. Asserted at App::run() boot wiring (pre-fork baseline).
decodeUntilStable()  : string
Percent-decode a path repeatedly until it stops changing.
defaultCharset()  : string
Apache AddDefaultCharset. Server-wide default.
defaultMimeType()  : string
Apache DefaultType / PHP default_mimetype. The Content-Type CharsetMiddleware applies to responses that don't set one. Pass '' to disable. No-arg call returns the current value.
defaultStaticHandlerLocations()  : array<int, string>
The built-in default static_handler_locations — DIRECTORY entries only, every one trailing-slash terminated so OpenSwoole's raw string-prefix match is segment-bounded (a bare /js would steal /json).
defineIsolation()  : bool
Per-request define() isolation. Opt-in — see $define_isolation docblock.
delete()  : void
describeRoutes()  : array{global: list, aliases: list, when: list}>, routes: list, path: string, middleware: list, handler: string}>}
Introspect the routing + middleware topology — the data behind the "middleware visualizer" (think Traefik's dashboard for your own routes).
devReload()  : bool
Enable/disable dev route hot-reload. When on, each worker polls the route/*.php mtimes and calls reloadRoutes() on change — "save file → routes update" with no process restart. A no-arg call returns the resolved value; null (the default) falls back to the ZEALPHP_DEV env var. OFF in production, where the route table stays master-loaded + COW-shared.
directoryIndex()  : array<int, string>
directorySlash()  : bool
Apache DirectorySlash — redirect /foo/foo/ when foo is a directory.
dispatchTaskCallback()  : array{task: array, result: mixed}|false
Dispatch payload for the $server->on('task', …) callback.
display_errors()  : void
displayErrors()  : bool
Whether framework error pages render the captured exception and stack trace inline. Secure-by-default (#412): a one-arg call sets the value explicitly (and wins forever after); a no-arg call returns the resolved value — when never set explicitly, null falls back to the ZEALPHP_DEV env var, so production (env unset) returns false and never leaks traces.
documentRoot()  : string
Apache DocumentRoot equivalent. Relative path → resolved against cwd; absolute path → used as-is. Drives App::include() resolution and the implicit-route file lookups.
emitEffectiveStatus()  : int
Emit the EFFECTIVE response status and return the code that reached the wire. Resolves the raw header("HTTP/x.x <code> <reason>") override (#327): when the request carries RequestContext::$raw_status_code, that code is emitted — with its verbatim reason when one was given, else the IANA phrase — exactly as Apache mod_php forwards an explicit status line. Without an override this is emitStatus() on the PSR status. Callers use the returned code for body-forbidding rules and access logging so they agree with the wire.
emitGeneratorStream()  : Response
Stream a \Generator response chunk-by-chunk to the live OpenSwoole response (SSR streaming), returning an empty placeholder Response once done. Shared by the route dispatcher (dispatchMatched) and ZealAPI's runHandlerWithContract so both stream identically. HEAD sends headers only (no body). Assumes the caller has already discarded its output buffer.
emitStatus()  : void
Set the response status via OpenSwoole's two-arg form so codes its native list doesn't recognise still emit correctly on the wire.
enableCoroutine()  : bool
OpenSwoole's enable_coroutine server setting — whether each inbound HTTP request is auto-wrapped in its own coroutine. When false, requests run synchronously one at a time per worker (a worker handling request N blocks any other inbound request until N completes). When true, requests can yield on hooked I/O and other requests dispatched on the same worker make progress.
exec()  : array{output: string, code: int, signal: int}
Coroutine-safe command execution.
exitHookAdvisory()  : string|null
Boot-time advisory for App::hookExit(true) set without a coroutine scheduler (#454). The ext-zealphp exit() interception is scheduler-bound: with enableCoroutine off (mixed / in-process-sync modes) a userland exit()/die() still throws OpenSwoole\ExitException (extends \Exception) — which a legacy try { … exit; } catch (\Exception) router swallows — NOT ZealPHP\HaltException. Forcing the hook on there is silently inert, so surface it at boot rather than let the developer believe exit() is protected. Returns the advisory string, or null when N/A; the null (auto) default follows the scheduler and never warns. Testable seam.
fatalGuardRelease()  : void
#338 — release a response tracked by fatalGuardTrack().
fatalGuardTrack()  : int
#338 — track an in-flight raw response; returns the release key.
fatalResponseGuard()  : void
#338 — native shutdown callback (registered in registerAllOverrides() BEFORE the register_shutdown_function override installs). On a fatal, answers every in-flight connection with a minimal 500 — mod_php parity — instead of leaving clients to time out, and surfaces the fatal in the PHP error log (debug.log misses engine fatals; that silence is what made ext-zealphp#36 a multi-day hunt).
fcgiAddress()  : string
FastCGI backend address for App::cgiMode('fcgi') dispatch.
fileETag()  : bool
Apache FileETag. false ⇒ ETagMiddleware emits no ETag and never 304s (FileETag None). No-arg call returns the current value.
formatAccessLogLine()  : string
Render one access-log line for the current request using App::$access_log_format.
fragment()  : void
Declare a named region inside a template — the htmx-essay "template fragment" pattern. The same template renders the full page when called via App::render('page', $args), and just the named region when called via App::render('page', ['fragment' => $name] + $args). One file, two responses — no separate partial file required.
functionIsolation()  : bool
Per-request function/class/include isolation. Opt-in — see $function_isolation docblock.
get()  : void
getCurrentFile()  : string
Returns the current executing script name without extenstion
getErrorHandler()  : array{handler: callable, param_map: array, raw: bool}|null
getFallback()  : array<string, mixed>|null
getServer()  : Server|Server|null
Return the underlying OpenSwoole server. Use this when you need to push to a WebSocket client from a context that didn't receive $server as a callback argument — e.g. from an App::subscribe pub/sub handler, an App::tick timer, or a sidecar process registered via App::addProcess.
globalScopeInclude()  : bool
Stage 8 global-scope request include (coroutine-legacy). A no-arg call returns the current setting; a one-arg call sets it. See the $global_scope_include docblock for the contract. Set BEFORE App::run().
go()  : int|false
Request-aware go() — spawns a child coroutine that INHERITS the current request's context ($g + the live superglobals, #42). Use inside handlers instead of raw go() whenever the child reads $g->server / $_SERVER / $_GET etc. Returns the child's coroutine id, or false if creation failed.
group()  : void
Route group — apply a shared URL prefix and/or a shared middleware chain to many routes at once (Traefik chains, Slim/Laravel route groups).
hookAll()  : int
OpenSwoole\Runtime::enableCoroutine($flags) — process-wide PHP I/O hooks that make blocking calls (fopen, fread, curl, mysqli, etc.) yield to the coroutine scheduler instead of blocking the worker. PDO is intentionally NOT hooked in OpenSwoole 22.1 / 26.2 regardless of this flag — Doctrine queries always block.
hookExec()  : bool|null
Toggle the coroutine-safe exec family hook (backtick / shell_exec / exec / system / passthru). Pass null (or no arg) to read the current value; pass a non-null value to set and return it. null = auto = follow coroutine mode (resolves to self::$superglobals === false) at run() time; overriding these built-ins routes them through coroutine-safe equivalents.
hookExit()  : bool|null
Toggle the ext-zealphp exit()/die()ZealPHP\HaltException interception (ext#47). Pass null (or no arg) to read; non-null to set.
hostnameLookups()  : bool
Apache HostnameLookups. Default false — blocking DNS is a perf cost.
ignorePhpExt()  : bool
Whether URLs ending in .php are blocked with 403. Default true (Apache RewriteRule \.php$ - [F] parity). Set false to allow direct *.php routing.
include()  : mixed
Run a public/ file with Apache document-root parity and the framework's universal return contract.
includeCheck()  : bool
Checks if the given file path is safe to serve/execute from the document root. Apache ap_directory_walk / resolve_symlink parity:
includeFile()  : mixed
includeIsolation()  : bool
Per-request require_once cache reset. See $include_isolation docblock.
init()  : App
Initializes the application.
instance()  : App|null
isEnotdir()  : bool
ENOTDIR detection for Apache parity (request.c:1244-1250 — "deny rather than assume not found"). When a path component that should be a directory is actually a regular file (e.g. /home.php/extra), Apache returns 403, not 404, deliberately refusing to leak whether the deeper path exists.
isolation()  : string
The single knob that says HOW a request is isolated — folds the (processIsolation × enableCoroutine × hookAll × cgiMode) cross-product into one intention-revealing value. Pure sugar over the existing fluent setters (they all keep working unchanged); accepts the App::ISOLATION_* constant, an Isolation enum case, or a bare string ("no strong").
keepGlobals()  : bool
Keep $GLOBALS across requests within the worker. See $keep_globals docblock for the full semantics + when to use it.
limitRequestFields()  : int
Apache LimitRequestFields.
limitRequestFieldSize()  : int
Apache LimitRequestFieldSize. Maps to OpenSwoole http_header_buffer_size.
limitRequestLine()  : int
Apache LimitRequestLine. Advisory; OpenSwoole's header buffer covers it.
middleware()  : StackHandler|null
The assembled PSR-15 middleware stack (built at boot from App::$middleware_wait_stack by buildMiddlewareStack()).
middlewareAlias()  : void
Register a named, reusable middleware — the "named & shared" middleware vocabulary from Traefik, the route-middleware alias from Laravel.
mode()  : void
High-level mode preset — sets BOTH axes (superglobals + isolation) in one call. Sugar over the fine-grained setters; accepts an App::MODE_* constant or a bare string ("no strong"). All the individual setters remain available to override afterwards.
normalizeMiddlewareSpec()  : array<int, MiddlewareInterface|string>
Validate + flatten a per-route middleware spec into a list, WITHOUT resolving aliases (resolution is deferred to App::run() so an alias may be registered after the route that references it). A single instance or alias string is wrapped into a one-element list.
normalizeRequestPath()  : string
Normalise a request path the way Apache's ap_normalize_path() does (server/util.c): collapse runs of // to a single / (MergeSlashes, on by default), drop /./ segments, and unwind /segment/../ back over the preceding segment. A .. that would climb above root is dropped (clamped at /), matching Apache's behaviour for the routing path.
normalizeUploadedFiles()  : array<string, mixed>
Transpose OpenSwoole's $request->files into PHP/mod_php-canonical $_FILES (issue #304).
nsPathRoute()  : void
nsPathRoute: Define a route under a namespace but allow the last parameter to capture everything (including slashes).
nsRoute()  : void
nsRoute: Define a route under a specific namespace.
offPubSub()  : int
BC alias for unsubscribe(). See onPubSub docblock.
onProcess()  : void
BC alias for addProcess(). The on*-prefixed name was a misnomer (this method REGISTERS a process — it isn't an event). New code should call App::addProcess(); pairs symmetrically with OpenSwoole's $server->addProcess() API.
onPubSub()  : void
BC alias for subscribe(). The original on*-prefixed name was a misnomer — the act IS subscribing, not an event. New code should call App::subscribe() directly; pairs symmetrically with Store::publish().
onReliableMessage()  : void
BC alias for subscribeReliable(). See onPubSub docblock.
onSignal()  : void
Register a signal handler. Fires in the master process by default; pass $workerOnly=true to fire only inside workers.
onWorkerStart()  : void
Register a callback to run inside every worker's workerStart event.
onWorkerStop()  : void
Register a per-worker shutdown hook. Runs inside the worker process when it exits (max_request recycle, graceful shutdown, or reload), BEFORE the process terminates — the reliable place to flush per-worker state (counters, buffered I/O, coverage dumps). Unlike register_shutdown_function, this fires on OpenSwoole's signal-driven worker stop.
opcacheLegacyAdvisory()  : string
Build the opcache + coroutine-legacy boot advisory string. Split out of opcacheLegacyBootCheck() as a pure (opcache-independent) seam so both dups_fix branches are unit-testable without opcache enabled in the SAPI.
opcacheLegacyBootCheck()  : string|null
options()  : void
parallel()  : array<int, T|null>
Fork-join helper — runs every closure in $tasks in its own coroutine in parallel and returns the results in input order.
parallelLimit()  : array<string|int, mixed>
Bounded fan-out — runs $fn over each item with at most $concurrency in-flight coroutines at a time. Results keyed by the input's original keys.
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.
parseCookieHeader()  : array<string, mixed>
Parse a raw Cookie: header through PHP's cookie treat-data semantics (issue #305) — the same php_default_treat_data routine PHP applies to the query string, but with the cookie value-decoding rule:
parseCss()  : array<string, array<string, string>>
Parses the given CSS file.
patch()  : void
pathInfo()  : bool
Apache PATH_INFO — expose the path suffix after a script name as PATH_INFO in $_SERVER (e.g. /script.php/extra/pathPATH_INFO=/extra/path).
pathWithinRoot()  : bool
Boundary-aware containment test: is $candidate the same path as $root, or a descendant of it?
patternRoute()  : void
patternRoute: Allow full control of the pattern without {param} placeholders.
perRequestStateResetsActive()  : bool
True when the per-request state RESETS — zealphp_reset_request_rtcaches() / zealphp_reset_request_statics() / zealphp_reset_request_class_statics(), run in the session-manager finally block — are SAFE to execute.
post()  : void
poweredByHeader()  : string|null
Resolve the X-Powered-By header value for the current ServerTokens setting, or null when the header should be omitted. Consumed at the response-emission boundary; exposed for introspection/testing.
preloadClasses()  : void
Register classes to compile at worker start so they are never cold- autoloaded under request concurrency (coroutine-legacy mode). Call BEFORE App::run(). Idempotent; duplicates are harmless. See App::$preload_classes for the full rationale and the failure mode it prevents.
preloadClassmap()  : void
Opt into warming the ENTIRE Composer classmap at worker start — the structural fix so a user app's own classes (autoloaded on demand inside handlers) are born LINKED, never compiled on the concurrent cold path.
preloadDir()  : void
Register a source directory to warm at worker start: every class / interface / trait / enum declared under $dir (recursively) is autoloaded+linked single-coroutine before request concurrency. Use this for PSR-4 apps WITHOUT an optimized classmap, or any app whose own autoloader (not Composer's classmap) resolves these symbols. Call BEFORE App::run(). The symbol must still be resolvable by a registered autoloader — a pure require_once legacy app (no autoloader) won't warm this way; run such apps in legacy-cgi mode (no coroutine race) instead.
prependToStreamable()  : Generator
Combine a pre-yield buffered chunk with a Generator so the wire order is "echo first, then stream". Returns a new Generator that yields the buffered chunk before delegating to the original.
processIsolation()  : bool
Per-include CGI process isolation (Apache mod_php-style fresh process per file). When true (the default in superglobals mode), App::include() dispatches each .php file through cgi_worker.php via proc_open() — global state (defined classes, constants, ini_set, output handlers) is contained inside the subprocess. When false, runs in-process via executeFile() — saves the ~30-50ms proc_open + PHP startup + autoloader cost per call, but every include shares the worker's PHP arena.
publish()  : int
Fire-and-forget Redis pub/sub publish.
publishReliable()  : string
Reliable publish via Redis Streams (XADD) — at-least-once delivery via consumer groups. Returns the Redis-generated message id.
put()  : void
rawExec()  : string|null
Raw blocking command execution via proc_open.
reasonPhrase()  : string
Look up an IANA reason phrase for the given status code. Used by emitStatus() to pass an explicit reason to OpenSwoole's two-arg $response->status($code, $reason) — required because the native one-arg form silently rejects codes missing from its internal C list (notably 451, even on ext 26.x), and the request emits HTTP 200 instead.
reassertRotatedSessionId()  : array<string, mixed>
Re-assert a session id the session manager already rotated, over a freshly-parsed request-cookie map (#371, CWE-384 session fixation).
rebindRequestInput()  : void
Re-establish the request-input superglobals ($_GET / $_POST / $_COOKIE / $_SERVER / $_FILES / $_REQUEST) FROM the per-coroutine OpenSwoole request, in the coroutine that is about to read them. Called right before every handler / included-file dispatch in coroutine-legacy mode.
refreshGlobalsBaseline()  : bool
Re-capture the per-coroutine $GLOBALS baseline from the current symbol table.
registerCgiBackend()  : void
Register a per-extension CGI backend. Apache AddHandler/ProxyPassMatch + nginx fastcgi_pass-per-location parity.
reloadRoutes()  : int
**Hot-reload the route table from route/*.php WITHOUT restarting the worker process.** Restores the app.php-defined baseline (explicit routes + the alias / App::when registries), re-includes the route files (picking up edits — opcache is invalidated for them first), re-appends the framework's implicit routes in priority order, and rebuilds the dispatch table. Returns the new route count.
render()  : mixed
Render a template with the provided data.
renderError()  : ResponseInterface
Render the response for an error status. Dispatches a user-registered handler if one exists (status-specific takes precedence over catch-all); otherwise returns the framework's default body (HTML or JSON per Accept).
renderHtmx()  : mixed
htmx-aware render: return a fragment (partial) for an htmx request, the full page otherwise — a thin selector over {@see App::render()} that keeps the universal return contract and streaming intact (it does NOT touch executeFile(); it only chooses what to render).
renderStream()  : Generator
Render a template as a Generator. Streaming templates (return-a-Closure or return-a-Generator) yield directly; echo-style templates yield their buffered output once.
renderToString()  : string
Render a template and return the result as a string. Generators are consumed; Closures are invoked with param injection; arrays/objects are JSON-encoded.
requestCookieMap()  : array<string, mixed>
mod_php-canonical cookie map for a request (issue #305).
requestIsHttps()  : bool
Whether the request arrived over HTTPS. X-Forwarded-Proto is honoured ONLY when the immediate peer (REMOTE_ADDR) is a configured trusted proxy — parity with App::clientIp(). Public so the session layer (zeal_session_start's Secure-cookie auto-detect) shares this one gated source of truth instead of trusting the header from any client.
resetCgiBackends()  : void
Reset the CGI backend + ScriptAlias registries. Test-support helper — lets unit tests start from a clean registry without process recycling.
resolveActiveSessionHandler()  : SessionHandlerInterface|null
Resolve the configured session handler instance — the single source of truth the zeal_session_* overrides and both session managers consult.
resolveCgiBackend()  : array{backend: array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array, exec_paths?: array}, mayExecute: bool}
Resolve the CGI backend config + execution permission for a given path.
resolveCgiEnv()  : void
Resolve the CGI subprocess-pool config from the environment at boot.
resolveDocumentRoot()  : string
Resolve App::$document_root to an absolute path. Relative values are treated as ${App::$cwd}/$document_root; absolute values pass through.
resolveMaxRequest()  : int
Resolve OpenSwoole's max_request from the raw ZEALPHP_MAX_REQUEST env value. #449 — getenv() returns false when unset and the string "0" when explicitly set to disable; the old getenv() ?: 100000 collapsed both to the default ("0" is falsy in ?:), so ZEALPHP_MAX_REQUEST=0 ("set 0 to disable", per docs/deployment.md) silently never reached the server. Test for presence (=== false) instead, so 0 is honoured (OpenSwoole: never recycle the worker).
resolveWhenMiddleware()  : array<int, MiddlewareInterface>
Select the App::when middleware chain for a normalized request path — every matching scope's instances flattened in registration order (outermost first). Memoized per path; the registry is immutable after boot, so this never recomputes for a repeated path. Returns [] (the fast path) when nothing is registered or nothing matches.
route()  : void
Registers a route with the application.
routes()  : array<int, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>
routesByExactMethod()  : array<string, array<string, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>>
routesByMethod()  : array<string, array<int, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>>
run()  : void
runRequestStaticsBeginRefresh()  : bool
Request-BEGIN function-static refresh (coroutine-legacy, #28).
sapiName()  : string|null
mod_php-parity SAPI name reported by the php_sapi_name() override.
serveDirectory()  : mixed
Apache DirectorySlash + DirectoryIndex behavior.
serverAdmin()  : string|null
Apache ServerAdmin. Contact email/identifier embedded in the framework's default error pages. Pass null (or '') to clear.
serverTokens()  : string
Apache ServerTokens. Controls the X-Powered-By header detail.
sessionDataSize()  : int
Max serialized session size in bytes for TableSessionHandler.
sessionHandler()  : string|SessionHandlerInterface|null
Session storage backend selector. See $session_handler docblock.
sessionLifecycle()  : bool
Toggle ZealPHP's per-request session lifecycle. When disabled, the SessionManager / CoSessionManager OnRequest wrapper skips session_start / cookie emission / session write-close — request-context init (openswoole_request, zealphp_response, error-stack reset) still runs unconditionally. Use this when another framework (e.g. Symfony's NativeSessionStorage via the zealphp-symfony bridge) owns sessions and you don't want ZealPHP racing it for the PHPSESSID cookie. The zeal_session_* uopz overrides remain installed and callable from user code either way.
sessionMaxRows()  : int
Max concurrent sessions in TableSessionHandler's OpenSwoole\Table.
sessionSavePath()  : string
File-backing directory for session storage. Default /var/lib/php/sessions.
sessionStrictMode()  : bool
PHP session.use_strict_mode parity (#244). When on (the default), a client-supplied session id that loads an empty session is rotated to a fresh server-generated id, defeating session fixation. Pass false to accept client-supplied ids verbatim (only safe for multi-node setups without shared/sticky session storage — see App::$session_strict_mode).
sessionTtl()  : int
Session TTL in seconds. Default 1440 (PHP's default). See $session_ttl.
setErrorHandler()  : void
Register a custom error page handler — Apache's ErrorDocument equivalent.
setFallback()  : void
Register a fallback handler for unmatched routes (like Apache's RewriteRule . /index.php [L]).
silentRedeclare()  : bool
Stage 3 silent-redeclare. Opt-in — see $silent_redeclare docblock.
staticHandlerLocations()  : array<int, string>
URL-prefix whitelist for static-file serving. Empty array (default) allows any path under document_root. When non-empty, only paths whose prefix matches one of the listed strings are served as static files; others fall through to route matching. No-arg call returns the current list.
stats()  : array<string, mixed>
Aggregated framework health snapshot — backends, pool, workers, memory, uptime, plus per-subsystem counters (X-4). Designed for /healthz middleware exposure + Prometheus exposition (see App::onSchedule v0.3.0 P1.10 plan).
statusForbidsBody()  : bool
Whether a status code MUST be sent without a message body (RFC 7230 §3.3.2 / RFC 9110 §6.4.1): every 1xx informational response, plus 204 No Content and 304 Not Modified. For these, a server must emit neither a body nor a Content-Length / Content-Type header — a non-empty body is a framing violation that some clients treat as the start of the next response. The emit chokepoint uses this to drop any body a handler accidentally produced (#290).
stripTrailingSlash()  : bool
Apache RewriteCond %{REQUEST_FILENAME} !-d; RewriteRule ^(.+)/$ /$1 [R=301,L].
subscribe()  : void
Register a Redis pub/sub handler — runs once for every message App::publish (or any other Redis client) sends on the channel.
subscribeReliable()  : void
Register a Redis Streams consumer-group handler. At-least-once delivery via XREADGROUP. Handler signature: function(string $payload, string $messageId, string $stream, array $fields): bool Return true to XACK (message removed from pending). Return false OR throw to leave pending (retried on consumer recovery).
superglobals()  : void
Toggle the superglobals-mode lifecycle. See App::$superglobals for the full semantics. Must be called BEFORE App::run() — the method calls refuseAfterRun() and throws \RuntimeException if the server is already serving requests.
synthesizeRequestServerVars()  : array<string, bool|float|int|string|null>
Synthesize the mod_php request-surface $_SERVER vars that OpenSwoole's raw $request->server omits or gets wrong (issue #306 + #307). Pure transform of an already-built server array — operates on the upper-cased keys buildServerVars() produces, so it is unit-testable in isolation:
tick()  : int
Recurring timer: calls $fn every $ms milliseconds in this worker.
traceEnabled()  : bool
Apache TraceEnable. Default OFF for security (XST attack vector).
trustedProxies()  : array<int, string>
Trusted proxy CIDRs consulted by App::clientIp().
tryInclude()  : mixed
Like App::include() but returns null instead of 403 when the requested file does not exist under the document root. Use for "try this file, fall through to something else if missing" patterns:
unsubscribe()  : int
Unregister handlers for a channel/pattern. With $handler null, removes every registered handler for that channel. Returns the count removed.
useCanonicalName()  : bool
Apache UseCanonicalName. See $use_canonical_name docblock.
usernameProvider()  : callable|null
Register a callback that ZealAPI::getUsername() consults.
when()  : void
Scope a middleware chain to a URL **path** — the centralized, "think like Traefik" way to apply middleware to a slice of the site, **including the ZealAPI layer**. Because every request (route or api/** file) flows through the same stack and api/admin/x is just the URL /api/admin/x, one mechanism covers everything — there is no separate "api middleware".
ws()  : void
Register a WebSocket endpoint. Returns void — the OpenSwoole WebSocket\Server is owned by the framework lifecycle, not the route registration. To push to a client you have **two** ways to reach the server object:
wsRoutes()  : array<string, array{message: callable, open: callable|null, close: callable|null}>
isExactRoutePath()  : bool
__clone()  : mixed
__construct()  : mixed
Private constructor — use App::init() to obtain the singleton instance.
activateIsolationRuntime()  : void
Activate the ext-zealphp per-coroutine isolation runtime stack (superglobals / define / $GLOBALS / silent-redeclare / function-static / include isolation) and register the matching onWorkerStart hooks.
applyRouteBackend()  : array{backend: array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array, exec_paths?: array}, mayExecute: bool}
Apply the per-request route backend override (set by the matched route's backend: option in ResponseMiddleware::dispatchRoute()) over a resolveCgiBackend() result. A route that names a backend is itself the ExecCGI authorisation for its App::include(), so mayExecute is forced true. No override → the resolved backend passes through unchanged.
assertUrlPrefix()  : void
Validate that an ExecCGI scope value is a URL path prefix, not a filesystem path. exec_paths / cgiScriptAlias prefixes are matched against the request URL (resolveCgiBackend()pathUnderPrefix()), so a filesystem path (e.g. '/var/www/cgi-bin') can never match the incoming URL and silently yields a bare 403 (GitHub #155). Fail fast at registration instead: reject anything that is not absolute (/-rooted) or that resolves to an existing directory on disk. A real URL prefix like '/cgi-bin' is not a directory on a normal host, so correct configs pass untouched.
backendKind()  : string
baseServerVars()  : array<string, string>
The static CGI/SAPI server vars mod_php exposes even OUTSIDE a request (PHP_SELF, SCRIPT_NAME, SCRIPT_FILENAME, REQUEST_URI, DOCUMENT_ROOT).
buildMiddlewareStack()  : void
Reverse + add the queued middleware-wait-stack onto the live PSR-15 StackHandler. Extracted verbatim from App::run() (Phase 3) — runs at the same point, mutating self::$middleware_stack in the same first-registered- outermost order.
buildParamMap()  : array<int, array{name: string, has_default: bool, default: mixed}>
buildServerVars()  : array<string, bool|float|int|string|null>
Build the per-request $_SERVER array from an OpenSwoole request — mod_php parity. Merges $request->server (upper-cased keys), the HTTP_* header vars, and the constant CGI keys mod_php always provides (GATEWAY_INTERFACE, REQUEST_SCHEME, SCRIPT_FILENAME, SERVER_SOFTWARE, …).
cidrContains()  : bool
clearHandlerHeaders()  : void
Clear previously-accumulated response headers from a handler that then failed, keeping only the headers that Apache preserves across an error response (ap_send_error_response: apr_table_clear(r->headers_out) then re-instate headers required by HTTP protocol for specific status codes).
coerceToStream()  : Generator
Coerce an executeFile() result to a Generator. Strings/scalars yield once; Generators yield-from; null yields nothing.
coerceToString()  : string
Coerce an executeFile() result to a string. Generators are consumed and concatenated; arrays/objects are JSON-encoded; null becomes ''.
collapseMappedIp()  : string
Collapse an IPv4-mapped IPv6 address (::ffff:a.b.c.d) to its IPv4 form so an IPv4 CIDR matches a mapped peer (#433). Mirrors IpAccessMiddleware::normalizeIp byte-for-byte. Non-mapped input is returned unchanged.
compileAccessLogFormat()  : array<int, array{kind: string, arg?: string}>
Compile an Apache LogFormat string into a flat token list. Supported directive families (Apache mod_log_config subset): %h %l %u %t %r %s %>s %b %B %D %T %m %U %q %H %v %{NAME}i %{NAME}o %{NAME}e Unknown directives are passed through verbatim (Apache compatibility: mod_log_config logs '-' for unknown but compatibility matters less than surfacing typos to the operator).
compileRouteTable()  : void
Resolve per-route + App::when middleware specs (alias → instance) and (re)build the method-indexed dispatch table. Idempotent — it resets the indexes first, so reloadRoutes() can call it to rebuild from scratch; at boot it runs exactly once.
defaultErrorResponse()  : ResponseInterface
Default error body. Honors Accept: application/json for JSON envelope, otherwise emits HTML. Stack trace included only when App::$display_errors.
executeFile()  : mixed
Run a PHP file with the framework's universal return contract.
fileEntryIsSingle()  : bool
True when $entry is a single PHP file struct (has a scalar/array tmp_name AND error directly on it), rather than an index/name-keyed group of nested file structs.
getFragmentState()  : array{wanted: string, matched: bool, result: mixed}|null
Read and narrow the current fragment-extraction state from $g->memo.
globalScopeIncludeEffective()  : bool
Whether THIS request's App::include() should run at true global scope: the gate is on AND we're in coroutine-legacy (so the per-coroutine globals isolation stack is active). The ext-capability check (function_exists) is done inline at the executeFile() call site. Policy-only helper.
handlerDisplayName()  : string
Human-readable label for a route handler — Class::method for an array callable, otherwise Closure/function.
includeRouteFiles()  : void
Include the route/*.php files (the file-based route definitions).
installCoroutineAutoloadSerializer()  : void
Coroutine-aware autoload serializer — the HAZARD-2 correctness fix for coroutine-legacy mode.
invokeFallbackOrNotFound()  : ResponseInterface
lifecycleBackendMessage()  : string
The error shown when a route backend: (or a cgiBackendAlias) is given a process-wide lifecycle mode instead of a CGI dispatch strategy.
logExecScopeMiss()  : void
Emit a diagnostic when a request matched a registered CGI extension but fell outside its exec_paths scope (ExecCGI off → bare 403). Without this, a misconfigured exec_paths (e.g. a filesystem path that can never match the URL — GitHub #155) surfaces only as an opaque 403. Logs only for files whose extension HAS a registered backend, so unrelated unregistered-extension 403s stay quiet.
middlewareDisplayName()  : string
Human-readable label for a middleware spec entry — the class short-name for an instance, the alias string (verbatim) for an unresolved reference.
normalizeBackendConfig()  : array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}
Normalise + validate one inline backend config array into the canonical shape (mode + the optional interpreter/address/fcgi_params). The single validation point for cgiBackendAlias() and resolveBackendSpec().
normalizeMethods()  : array<int, string>
Normalize a methods array (any shape) into a list of uppercase strings.
normalizeSingleFileEntry()  : array<string, mixed>
Normalise one flat OpenSwoole file struct to PHP canonical shape, adding the PHP 8.1+ full_path key when absent (value = name).
overrideBuiltin()  : void
Install a uopz or ext-zealphp override for a named PHP built-in function, routing calls to the given ZealPHP replacement. Uses zealphp_override() when ext-zealphp is loaded (preferred), falling back to uopz_set_return().
pathUnderPrefix()  : bool
Boundary-safe URL-prefix test. $url is "under" $prefix only when it equals the prefix exactly or begins with $prefix . '/' — so /cgi-bins does NOT match the /cgi-bin scope.
peerInTrustedProxies()  : bool
Match $ip against every entry in App::$trusted_proxies. Wrapper so the CIDR walk lives in one place; callers pass user-controlled input here so the per-entry guard inside cidrContains() is the only validation needed.
phpredisSubscribeWouldBlock()  : bool
H7 detection: would a phpredis SUBSCRIBE/PSUBSCRIBE block the whole worker?
preloadRequestPathClasses()  : void
Warm the per-request framework classes at worker start so the first concurrent request wave never autoloads them under coroutine overlap.
refuseAfterRun()  : void
Throw if a lifecycle setter is being called after App::run() has started. The Session-manager class, OpenSwoole's enable_coroutine flag, and HOOK_ALL are all frozen at boot — mid-game mutation of $superglobals etc. leaves the framework in a partial state that races on $_GET/$_POST/$_SESSION. Boot-only is the contract.
registerAllOverrides()  : void
Install all ZealPHP uopz/ext-zealphp built-in overrides in one shot.
registerImplicitRoutes()  : void
Register the implicit framework routes (api dispatch, .php-ext block, dotfile block, index, CGI extension/ScriptAlias URL parity, and the public file/directory catch-alls).
registerOnRequest()  : void
Register the OnRequest event handler — the per-request entry point that populates RequestContext, runs the middleware stack, fires shutdown functions, and emits the response. Extracted verbatim from App::run() (Phase 3) — runs at the same point; the only captured locals are $server and the resolved session-manager class name.
registerSessionGc()  : void
Schedule the deterministic session garbage collector on a worker-0 timer.
registerTaskHandlers()  : void
Register the task + finish OpenSwoole event handlers when task workers are configured. Extracted verbatim from App::run() (Phase 3) — runs at the same point, gated on the same effective_settings['task_worker_num'].
registerWebSocketHandlers()  : void
Register the WebSocket open/message/close/shutdown event handlers, sharing the per-worker fd → ws-path map across the closures. Extracted verbatim from App::run() (Phase 3); the $wsFdMap is local to these four closures.
registerWorkerStart()  : void
Register the workerStart event handler — re-registers the php:// stream wrapper, fires user onWorkerStart hooks, and wires dev route hot-reload.
registerWorkerStop()  : void
Register the workerStop event handler — fires user onWorkerStop hooks then logs worker-recycle observability. Extracted verbatim from App::run() (Phase 3).
renderAccessLogToken()  : string
Render one compiled access-log token. Kept separate from the tokenizer so the hot path (per-request) only does the table-lookup half; the tokenize path runs once per format-string change.
resolveBackendSpec()  : array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null
Resolve a route backend: spec — a bare mode string, a registered alias name, or an inline config array — into a concrete backend config. null passes through (no backend). Called at route registration; the dispatch hot path never re-resolves.
resolveClosureParams()  : array<int, mixed>
Resolve a Closure's parameters by name from $args, using each parameter's default value when the name is absent. Reflection is cached per file path so repeated calls (e.g. streaming templates yielded in a loop) pay only one reflection cost per worker.
resolveMiddleware()  : MiddlewareInterface
Resolve one spec entry to an instance. An instance passes through; an alias string is looked up in the registry (name or name:arg1,arg2).
resolveTemplatePath()  : string
Resolve a template-file name to an absolute path.
restoreFragmentState()  : void
Restore $g->memo['_fragment'] to its prior state. Called by executeFile() to undo fragment-mode setup for nested renders and on error paths. null means "no fragment mode was active before" — drop the slot entirely so the next App::fragment() call falls into the normal inline-render branch.
routeBackendSpec()  : array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null
Combine the two ways a route can declare a backend — the 'backend' key in $options and the backend: named argument — and resolve to a concrete config. The named argument wins when both are present. Returns null (the fast path: no per-route backend) when neither is set.
routeFileWithTopLevelFunction()  : string|null
The first route/*.php file that declares a top-level function (which cannot be re-included without a redeclaration fatal), or null if every route file is function-free and therefore hot-reloadable. The path is cwd-relative for logging. A $x = function(){} closure has no name and is not matched; a class method carrying a visibility keyword isn't either.
routeMiddlewareSpec()  : array<int, MiddlewareInterface|string>
Combine the two ways a route can declare middleware — the 'middleware' key of the $options array and the middleware: named argument — into a single normalized spec. When both are present the array-option entries run first (outermost). Stored on the route as the raw spec; resolved to instances at App::run().
runTasksSequentially()  : array<int, T>
Sequential fallback for parallel() when no coroutine scheduler is available (reactor worker in mixed / legacy-cgi — #429). Runs each task in input order; the first throw propagates immediately, matching parallel()'s fail-fast-on-first-error contract.
runUserFile()  : mixed
Run a user file (template / public page) in an ISOLATED scope and return its value (#458). The file is included HERE, not in executeFile(), so a page that reuses a common variable name — $g, $result, $output, $args, … — only shadows this helper's throwaway locals and never corrupts executeFile()'s framework state used after the include (which previously fatalled "Attempt to assign property _ob_floor on array" → 500 when a page did $g = …). Locals are $__zeal_-prefixed (and EXTR_SKIP keeps them safe during extract) to stay clear of app/template names.
safeStats()  : array<string, mixed>
serverNameFromHost()  : string
Derive SERVER_NAME from the request Host header. SERVER_NAME is the server host NAME only (CGI/1.1, RFC 3875 §4.1.14) — the port belongs in SERVER_PORT. The Host header carries host[:port], so strip the port.
symbolDefined()  : bool
True if $name is already defined as a class, interface, or trait (autoload disabled). Extracted so the autoload serializer can re-test "is it loaded now?" after a concurrent load.
symbolsInFile()  : array<int, string>
Extract fully-qualified class/interface/trait/enum names declared in a PHP file via the tokenizer — without executing it. Single namespace per file is the common case; multiple namespaces are handled.
transposeFileGroup()  : array<string, array<string|int, mixed>>
Transpose an index-major group of file structs (the OpenSwoole shape for files[] / doc[main]) into PHP's field-major layout. Recurses through nested name groups so ['main' => <struct>, 'thumb' => <struct>] yields ['name'=>['main'=>…,'thumb'=>…], 'tmp_name'=>[…], …].
validateLifecycleCombination()  : void
Validate lifecycle mode combinations at boot.
warmBulkPreloads()  : void
Bulk warming (whole Composer classmap + registered directory trees) — runs in the MASTER process before $server->start(), NOT in a worker coroutine. This is load-bearing: warming hundreds/thousands of arbitrary classes inside the coroutine onWorkerStart is unsafe — a class with load-time I/O (or anything the runtime hooks coroutinize) YIELDS, and the worker then accepts requests MID-WARMUP → cold concurrent compile → the duplicate-CE / unlinked race we are trying to avoid (empirically: a classmap warm in onWorkerStart reintroduced HAZARD-2 TypeErrors). The master has no coroutine scheduler, so every load is blocking + atomic; the warmed (linked) classes are then COW-forked into every worker. Same model as PHP's opcache.preload. No-op unless preloadClassmap()/preloadDir() registered something.
warmClass()  : void
Trigger one symbol's autoload+link, swallowing load errors (a class with an unmet dependency must not abort worker start). class_exists() fires the registered autoloader for classes/interfaces/traits/enums alike.
warmComposerClassmap()  : void
Warm every class Composer's registered loaders know about. Iterates the classmap of each registered Composer\Autoload\ClassLoader and triggers its autoload (single-coroutine at worker start → whole hierarchy linked).
warmDir()  : void
Warm every PHP-declared symbol under a directory tree. Reads each .php file and extracts its namespace + class|interface|trait|enum names via the tokenizer (no file is executed), then triggers each symbol's autoload.
whenScopeMatches()  : bool
Whether an App::when scope matches a normalized path. Prefixes match on segment boundaries (the trailing / stops /admin matching /administrators); '/' matches all; regex scopes use preg_match.
wireProcessHandlers()  : void
Internal: wire registered sidecar processes into the OpenSwoole server via $server->addProcess(). Called from App::run() after the server is constructed but before start().
wirePubSubBoot()  : void
One-time hook into onWorkerStart that builds + starts the RedisPubSub and RedisStreams runners based on what's in the registries. Re-callable; only wires once.

Constants

DEFAULT_MAX_COROUTINE

Default per-worker coroutine ceiling applied in run() when the operator sets none (via ZEALPHP_MAX_COROUTINE or $app->run(['max_coroutine' => N])).

public mixed DEFAULT_MAX_COROUTINE = 10000

OpenSwoole's own default is ~100,000/worker, which is effectively unbounded relative to every downstream resource (the Redis pool of 8, the DB connection budget, per-coroutine memory). With no ceiling a load burst has no front-door shed path — it propagates inward until the first bounded resource fails as a cliff (OOM / pool-acquire-timeout 500s) instead of OpenSwoole rejecting the over-limit coroutine. This default restores backpressure while staying generous: ~40k concurrent in-flight coroutines across 4 workers (10k/worker), ~10x below OpenSwoole's 100k and far above the c=1000 benchmark's ~250/worker — a real bound that won't shed normal load or a typical streaming/WebSocket fan-in.

SCALE IT for very high long-lived-connection counts (each SSE/WS client holds a coroutine): ZEALPHP_MAX_COROUTINE=50000 or $app->run(['max_coroutine' => 50000]), and/or add workers/nodes. Pair with ConcurrencyLimitMiddleware (nginx limit_conn parity) for graceful 503s before the ceiling is hit.

DEFAULT_MAX_REQUEST

Default per-worker request-recycle cap (OpenSwoole max_request): the worker exits cleanly and respawns after this many requests, bounding memory growth from leaks. 0 disables recycling (OpenSwoole native semantics) — see resolveMaxRequest() / the ZEALPHP_MAX_REQUEST env. (#449)

public mixed DEFAULT_MAX_REQUEST = 100000

ISOLATION_CGI_FCGI

public mixed ISOLATION_CGI_FCGI = 'cgi-fcgi'

ISOLATION_CGI_POOL

public mixed ISOLATION_CGI_POOL = 'cgi-pool'

ISOLATION_CGI_PROC

public mixed ISOLATION_CGI_PROC = 'cgi-proc'

ISOLATION_COROUTINE

Isolation strategy constants — canonical user-facing surface for App::isolation().

public mixed ISOLATION_COROUTINE = 'coroutine'

ISOLATION_NONE

public mixed ISOLATION_NONE = 'none'

KNOWN_METHODS

Methods ZealPHP recognises. A request whose method is outside this set gets 501 Not Implemented (Apache: M_INVALIDHTTP_NOT_IMPLEMENTED, server/protocol.c:1253). Standard RFC 9110 methods plus the common WebDAV verbs Apache registers in ap_method_registry_init(). A recognised method that has no matching route still flows through to 404/405/fallback.

public array<int, string> KNOWN_METHODS = [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'PATCH', 'CONNECT', // WebDAV (RFC 4918 / 3253) — registered by Apache's method registry. 'PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'LOCK', 'UNLOCK', 'VERSION-CONTROL', 'REPORT', 'CHECKOUT', 'CHECKIN', 'UNCHECKOUT', 'MKWORKSPACE', 'UPDATE', 'LABEL', 'MERGE', 'BASELINE-CONTROL', 'MKACTIVITY', 'ORDERPATCH', 'ACL', 'SEARCH', ]

MODE_COROUTINE

public mixed MODE_COROUTINE = 'coroutine'

MODE_COROUTINE_LEGACY

public mixed MODE_COROUTINE_LEGACY = 'coroutine-legacy'

MODE_LEGACY_CGI

High-level mode presets — canonical user-facing surface for App::mode().

public mixed MODE_LEGACY_CGI = 'legacy-cgi'

MODE_MIXED

public mixed MODE_MIXED = 'mixed'

LIFECYCLE_MODE_NAMES

Lifecycle modes a user might mistakenly pass as a route backend. Rejected with a message pointing at the separate-process model.

private mixed LIFECYCLE_MODE_NAMES = ['coroutine', 'coroutine-legacy', 'legacy-cgi', 'mixed']

REASON_PHRASES

IANA-registered HTTP status reason phrases (RFC 9110 §15).

private mixed REASON_PHRASES = [ // 1xx Informational 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 103 => 'Early Hints', // 2xx Success 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', 208 => 'Already Reported', 226 => 'IM Used', // 3xx Redirection 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', // 4xx Client Errors 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Content Too Large', 414 => 'URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Range Not Satisfiable', 417 => 'Expectation Failed', 418 => "I'm a teapot", 421 => 'Misdirected Request', 422 => 'Unprocessable Content', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Too Early', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', // 5xx Server Errors 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 510 => 'Not Extended', 511 => 'Network Authentication Required', ]

Source: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml (registry snapshot 2025-09-15). Phrases match the IANA "Description" column verbatim — pinned exhaustively by tests/Unit/IanaStatusConformanceTest.

Documented deviations:

  • 418 'I'm a teapot' — IANA lists "(Unused)"; kept as the RFC 2324 / widely-recognised extension phrase.
  • 306 and 418 are the only reserved/"(Unused)" codes; all other entries are IANA-assigned. 104 (temporary registration) is intentionally omitted.

Universal return contract: handlers may return any 100-599 status — see template/pages/responses.php#status-range (canonical).

ROUTE_BACKEND_MODES

The four CGI dispatch strategies a per-route backend: may name — the CGI-ISOLATION family. Excludes the COROUTINE-SCHEDULER family (coroutine/coroutine-legacy/legacy-cgi/mixed), which is a process-wide lifecycle decision frozen at $server->start() and cannot be chosen per route. See normalizeBackendConfig()'s guard.

private mixed ROUTE_BACKEND_MODES = ['pool', 'proc', 'fork', 'fcgi']

WHEN_MEMO_MAX

Upper bound on the App::when per-path memo (memory-exhaustion guard).

private mixed WHEN_MEMO_MAX = 4096

Properties

$access_log_format

Apache LogFormat "...". Format string used by access_log() to render each request line. Tokens (Apache mod_log_config subset):

public static string $access_log_format = '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"'

%h Remote host/IP (uses App::clientIp() when $trusted_proxies set) %l Remote logname (always - — RFC 1413 ident is dead) %u Remote user (session username if set, else -) %t Time [17/May/2026:07:30:00 +0000] %r First line of request "GET /foo HTTP/1.1" %>s Final response status %b Response body bytes (- when zero, CLF convention) %B Response body bytes (0 when zero) %D Request duration in microseconds %T Request duration in seconds %{NAME}i Value of request header NAME (e.g. %{Referer}i) %{NAME}o Value of response header NAME %{NAME}e Value of $g->server[NAME] (env) %m Request method %U URL path (no query string) %q Query string (prefixed with ? if present) %H Request protocol ("HTTP/1.1") %v Server name (from Host header)

Default is Apache's NCSA combined format (the prior hardcoded ZealPHP output — preserving behaviour for existing log parsers). Switch to the shorter Common Log Format via: App::accessLogFormat('%h %l %u %t "%r" %>s %b');

$admin_checker

Callback consulted by ZealAPI::isAdmin(). Signature: fn(): bool.

public static callable|null $admin_checker = null

Default nullisAdmin() returns false (fail-closed). Set via App::adminChecker().

$allow_encoded_slashes

Apache AllowEncodedSlashes — when false (default, matching Apache), a request whose RAW (pre-decode) path contains an encoded slash (%2F/%2f) is refused with 404 before route matching. Apache's unescape_url() forbids AP_SLASHES by default; we mirror that. Set true to permit encoded slashes (they are then decoded to / like any other octet).

public static bool $allow_encoded_slashes = false

$api_null_not_found

#347 — the 404 {"error":"method_not_found"} envelope for a ZealAPI handler that returns null with NO output, NO explicit status and NO streaming. **Mode-aware (corrected rule):** it applies ONLY to **per-method** dispatch ($get/$post/…) — a method handler that ran and produced nothing. A **filename-match** handler ($list, serving all methods) returning null is an intentional **empty 200** (native-PHP parity — an empty-set / infinite-scroll tail), never a 404. A method with no handler at all already 405s before this point. Default ON.

public static bool $api_null_not_found = true

Escape hatches: return '';, set a status, or this false (disables the per-method 404 for pure-native APIs).

$api_warn_collisions

Log warnings when a ZealAPI filename collides with an HTTP method keyword (get.php defining $get) or when a filename-matched handler shadows per-method handlers in the same file. Default ON so new apps surface mistakes; set to false (or 'api_warn_collisions' => false in the run() config) for legacy codebases that knowingly use method names as filenames.

public static bool $api_warn_collisions = true

$auth_checker

Auth-hook callbacks consulted by ZealAPI::isAuthenticated(), ::isAdmin(), and ::getUsername() so the framework's built-in file-based API layer can delegate auth questions to whatever auth system the app uses (Symfony Security, Auth0, the SelfMadeNinja stack, a custom $_SESSION['user'] check, etc.) without subclassing or monkey-patching ZealAPI itself.

public static callable|null $auth_checker = null

Set via the fluent setters App::authChecker(), App::adminChecker(), App::usernameProvider(). Defaults: nullZealAPI returns the safe fail-closed values (false, false, null). See the issue #13 discussion and /learn/api for usage.

SECURITY (#244): on any privilege change (login / logout / role change) call session_regenerate_id(true) to defeat session fixation. Strict mode (App::$session_strict_mode, default on) blocks an attacker from planting an id; regenerate-on-auth blocks reusing a pre-auth id. The framework can't force this — it doesn't know when your app authenticates.

$block_dotfiles

Block any path containing a dotfile component (.git, .env, .htaccess, etc.). Apache convention.

public static bool $block_dotfiles = true

$boot_env

Process environment captured at boot (real getenv()), before the per-coroutine putenv/getenv overrides are installed in Mode 4. The overridden \ZealPHP\zeal_getenv falls back to this for variables not set request-scoped via \ZealPHP\zeal_putenv.

public static array<string, string> $boot_env = []

$canonical_name

Apache ServerName www.example.com:443. The canonical host the server advertises in absolute redirects (and other absolute URL builders) when $use_canonical_name is true. Include scheme-port if relevant; the raw value is returned as-is by App::canonicalHost().

public static string|null $canonical_name = null

$cgi_backend_aliases

Named CGI backend aliases for the per-route backend: option, registered via App::cgiBackendAlias(). Maps an alias name to a normalised backend config (the same shape resolveCgiBackend() returns, minus exec_paths).

public static array<string, array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}> $cgi_backend_aliases = []

Resolved at route registration so backend: 'wp-fork' becomes a concrete dispatch strategy with zero per-request lookup. Mirrors the $middleware_aliases precedent.

$cgi_backends

Per-extension CGI backend registry. Apache AddHandler/ProxyPassMatch + nginx fastcgi_pass-per-location parity.

public static array<string, array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array, exec_paths?: array}> $cgi_backends = []

Shape: [ '.ext' => ['mode' => 'proc'|'fork'|'fcgi', ...options] ]

Default: empty — unregistered extensions (including .php) fall through to App::$cgi_mode (which defaults to 'proc', preserving existing behaviour). Register additional extensions with App::registerCgiBackend().

$cgi_fork_instance

Per-worker ForkPool singleton for cgiMode('fork') — the fork-master subprocess. Lazy-spawned on first dispatch in this OpenSwoole worker (same per-worker ownership rationale as $cgi_pool_instance).

public static ForkPool|null $cgi_fork_instance = null

$cgi_fork_max_concurrent

Live-child concurrency cap for cgiMode('fork') — the fork-master refuses to fork past this many simultaneous children (fork-bomb guard + backpressure).

public static int $cgi_fork_max_concurrent = 16

Defaults to 16; this is a per-request fork ceiling, NOT the pre-spawned process count cgi_pool_size (which would wrongly throttle to 4 under a coroutine worker handling many concurrent requests).

$cgi_fork_max_concurrent_set

public static bool $cgi_fork_max_concurrent_set = false

$cgi_mode

How a process-isolated legacy include is dispatched, when processIsolation() is on:

public static string $cgi_mode = 'pool'

'pool' (default) — native FCGI-style worker pool. Each OpenSwoole worker spawns $cgi_pool_size persistent PHP subprocesses (FPM pm.max_children parity); each subprocess provides mod_php-style isolation per request (clean global scope — top-level $x = ... is visible via global $x, so unmodified WordPress/Drupal work). Parent dispatches via Coroutine\Channel — thousands of coroutines fan out across the pool without blocking the worker. Subprocess recycle after $cgi_pool_max_requests (FPM pm.max_requests parity).

'proc' (legacy fallback) — proc_open() spawns a FRESH PHP per request (src/cgi_worker.php). Same isolation as 'pool' but pays cold-PHP startup + autoload EVERY request (~tens of ms). Kept as a fallback for environments where the pool can't be used (e.g. uopz unavailable in the subprocess, or audit / compliance requiring zero pre-warm).

'fork' (experimental) — Apache MPM prefork. A long-lived fork-master (src/fork_master.php) forks a FRESH child per request that runs the include at TRUE global scope (~1 ms fork cost), captures the response, then hard-exits. Same fresh-process correctness as 'proc' (no class-redeclare) but at fork cost instead of proc_open cold start. Requires pcntl + posix; bounds live children via App::$cgi_fork_max_concurrent (503 when full).

'fcgi' (deployment mode) — forward to an external FastCGI backend (php-fpm, hhvm, roadrunner) via the FCGI binary protocol over TCP or Unix socket. Target address via App::fcgiAddress(). Use when ZealPHP fronts an existing FPM pool you don't want to retire.

Set via App::cgiMode('pool'|'proc'|'fork'|'fcgi'). Default 'pool'.

$cgi_mode_set

"Explicitly set by a fluent setter" flags for the env-overridable CGI knobs. App::resolveCgiEnv() applies a ZEALPHP_CGI_* value only when the matching flag is false — so explicit code config always wins over the environment, which in turn wins over the hardcoded default.

public static bool $cgi_mode_set = false

$cgi_pool_env_allowlist

Optional strict env allowlist for cgiMode('pool') subprocesses (WorkerPool::filterSubprocessEnv). Empty (default) = pass the parent environment through to the subprocess (legacy-app compatibility) MINUS the request-controlled HTTP_PROXY (httpoxy). Set via App::cgiPoolEnvAllowlist([...]) to a list of exact names / PREFIX* globs to restrict what secrets the long-lived subprocess inherits; ZEALPHP_POOL_MAX_REQUESTS is always passed. Per-request CGI vars travel over the IPC frame, not this env, so a strict allowlist doesn't lose them.

public static array<int, string> $cgi_pool_env_allowlist = []

$cgi_pool_instance

Per-worker WorkerPool singleton for cgiMode('pool'). Lazy-spawned on first dispatch in this OpenSwoole worker. Held here (not on a Store) because each OpenSwoole worker owns its own subprocess pool — proc resources don't share across workers.

public static WorkerPool|null $cgi_pool_instance = null

$cgi_pool_max_requests

Per-subprocess recycle threshold for cgiMode('pool'). After this many requests, the subprocess exits cleanly and the pool spawns a fresh replacement — FPM pm.max_requests parity, bounds memory leak from long-running plugin code. Set to 1 to recycle every request (true fresh-process semantics; same isolation as cgiMode('proc') but with the pool managing spawn-cost amortisation).

public static int $cgi_pool_max_requests = 500

$cgi_pool_max_requests_set

True once cgiPoolMaxRequests() set the recycle count explicitly. The mode() presets consult this so a mode('legacy-cgi') default (recycle=1) never clobbers an explicit user choice, regardless of call order.

public static bool $cgi_pool_max_requests_set = false

$cgi_pool_size

Subprocess count for cgiMode('pool') — the native FCGI-style worker pool. Each OpenSwoole worker process spawns this many persistent PHP subprocesses on first dispatch (lazy). FPM pm.max_children parity: sets the per-worker concurrency cap. Default 4 — balances spawn cost with concurrency for typical web workloads.

public static int $cgi_pool_size = 4

$cgi_pool_size_set

public static bool $cgi_pool_size_set = false

$cgi_script_aliases

ScriptAlias-style CGI path registry (Apache ScriptAlias parity). Maps a normalised URL prefix (leading slash, no trailing slash) to a backend config. Any file served under a registered prefix is treated as executable regardless of its extension.

public static array<string, array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array}> $cgi_script_aliases = []

$cgi_session_auto_start

legacy-cgi only: eagerly mint a session id + Set-Cookie on a FIRST-time visitor (no incoming PHPSESSID) BEFORE the CGI subprocess runs (#108).

public static bool $cgi_session_auto_start = false

Default false for session.auto_start=0 / mod_php parity (#355): a CGI script that never calls session_start() must emit NO Set-Cookie and leave the request-side $_COOKIE untouched. With this off, Dispatcher::mintCgiSession() only forwards an id the client ALREADY sent (returning visitor); it never injects an unsolicited id into $_COOKIE or the response.

Set true only for legacy require_once-bootstrap apps in legacy-cgi that depend on a first-visit session cookie being present before the subprocess emits one (the subprocess can't — PHP's session module sends the cookie via the C-internal php_setcookie(), which the CLI SAPI discards, so without an eager host mint a session-using app would loop with an empty PHPSESSID). The escape hatch for #108; off honours #355.

$cgi_subprocess_autoload

Whether cgi_worker.php (proc-mode subprocess entry) loads Composer's vendor/autoload.php on startup. Default false — restores the pre- v0.2.20 behaviour where the subprocess runs at true global scope with NO ZealPHP framework loaded, suitable for unmodified WordPress / Drupal.

public static bool $cgi_subprocess_autoload = false

Why off by default: the autoloader load costs ~30 ms per subprocess spawn (measured on Ryzen 9 7900X / PHP 8.3). For WordPress's wp_cron() self-call pattern (a non-blocking HTTP POST to /wp-cron.php with a timeout of 0.01 s), that 30 ms upfront cost causes the wp-cron POSTs to queue at the parent faster than workers can drain them — eventually deadlocking the pool. Issue #18 (the v0.2.41 WP-on-proc regression vs v0.2.0).

When to set true: your public/*.php files need to call \ZealPHP\App, the Apache shims, or any framework class. Modern apps built ON ZealPHP, NOT migrated TO it. Most legacy apps (WordPress, Drupal, Joomla, plain PHP) ship their own bootstrap and don't need it.

$cgi_timeout

Maximum seconds to wait for a CGI subprocess (proc mode) to produce its metadata line on stderr. After this deadline the child receives SIGTERM; if it does not exit within 5 s it receives SIGKILL. Matches Apache's CGIScriptTimeout directive. Default 60 s.

public static int $cgi_timeout = 60

$cgi_timeout_set

public static bool $cgi_timeout_set = false

$coproc_implicit_request_handler

Enable the legacy CGI request handler for public/*.php paths.

public static bool $coproc_implicit_request_handler = false

Resolved from $process_isolation at App::run(); you generally don't set this directly. See App::processIsolation() and the Lifecycle-modes matrix in CLAUDE.md.

$coroutine_cwd_isolation

Per-coroutine WORKING-DIRECTORY isolation (#323). chdir() is a process-level syscall, so under coroutine concurrency one request's chdir() (or the framework's own executeFile() chdir-to-script-dir) changes the CWD of every concurrently-running peer — racy relative includes / fopen across the whole worker. When ON (and ext-zealphp 0.3.35+ is loaded), the scheduler hooks save each coroutine's cwd on yield (re-parking the worker baseline so peers start clean) and restore it on resume — chdir() becomes per-coroutine, like PHP-FPM's per-process CWD. Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_CWD_ISOLATION_DISABLE=1); off by default elsewhere (coroutines that never chdir cost one getcwd+strcmp per yield when on, zero when off).

public static bool $coroutine_cwd_isolation = false

$coroutine_globals_isolation

Per-coroutine $GLOBALS isolation via ext-zealphp's zealphp_coroutine_globals(). When enabled, each coroutine gets its own snapshot of EG(symbol_table) swapped in on yield/resume — so $GLOBALS['app_state'] and global $foo; writes never race across concurrent coroutines.

public static bool $coroutine_globals_isolation = false

Closes the last architectural gap in Mode 4/5 — user-defined globals (which were previously process-wide) now isolate alongside super- globals, constants, ini settings, and static properties.

Tradeoff: each coroutine maintains its own $GLOBALS deep-copy at yield boundary — O(N keys) extra memory per active coroutine. No function/class table impact — autoloaders keep working as before.

Requires ext-zealphp 0.3.6+ with zealphp_coroutine_globals function. Set via App::coroutineGlobalsIsolation(true) BEFORE App::run().

$coroutine_isolated_superglobals

True when ext-zealphp's per-coroutine superglobal isolation is active.

public static bool $coroutine_isolated_superglobals = false

Set by App::run() when sg=T + ec=T + ext-zealphp loaded. Tells RequestContext::instance() to use per-coroutine instances even in superglobals mode — ext-zealphp isolates $_GET/$_SESSION per coroutine, so per-coroutine $g is safe and necessary for framework state ($g->zealphp_response, etc.) to be isolated too.

$coroutine_libxml_isolation

Per-coroutine libxml_use_internal_errors() FLAG isolation — the libxml error-buffering flag is process-global (measured 128/250 leaks).

public static bool $coroutine_libxml_isolation = false

ext-zealphp 0.3.45+. Fidelity note: collected errors are preserved within a slice (parse + libxml_get_errors with no yield between — the dominant pattern) but not across an I/O yield (php-src's own disable semantic frees the list on re-park). Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_LIBXML_ISOLATION_DISABLE=1); off by default elsewhere.

$coroutine_locale_isolation

Per-coroutine LOCALE isolation — setlocale() is process-global (string casing, number/date formatting), so one request's locale change leaks into every concurrently-running peer mid-request. When ON (ext-zealphp 0.3.38+), the scheduler hooks save each coroutine's locale on yield (re-parking the worker baseline captured at enable time — a boot-time setlocale() before App::run() IS the baseline) and restore it on resume. Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_LOCALE_ISOLATION_DISABLE=1); off by default elsewhere.

public static bool $coroutine_locale_isolation = false

$coroutine_mbenc_isolation

Per-coroutine mb_internal_encoding() isolation — the mbstring current internal encoding is process-global; legacy code sets it before string work (measured 173/250 leaks at 49-way concurrency). ext-zealphp 0.3.45+; auto-refuses when mbstring is absent. Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_MBENC_ISOLATION_DISABLE=1); off by default elsewhere.

public static bool $coroutine_mbenc_isolation = false

$coroutine_statics_isolation

Stage 5 — per-coroutine FUNCTION-local static $x isolation via ext-zealphp's zealphp_coroutine_statics(). This is the LAST request-state primitive that previously leaked across coroutines (everything else — superglobals, class statics, $GLOBALS, constants, ini_set, putenv — is already isolated). When enabled, the on_yield hook snapshots every instantiated function/method's live static table per coroutine and restores THIS coroutine's values on resume — the same snapshot/restore model already proven for class statics. Cooperative scheduling makes it correct: a coroutine writes its statics after its own restore and reads them before its next yield, so values never bleed.

public static bool $coroutine_statics_isolation = false

Verified: 0 leaks across 240 requests at peak-40 concurrency, both opcache on and off, no crash (tests/Integration/TrustBarIsolationTest keeps fn_static in the hard contract).

DEFAULT ON in coroutine-legacy (v0.3.10): a touched-set registry — populated by a ZEND_BIND_STATIC opcode hook as functions first instantiate their statics — means the per-yield snapshot iterates ONLY the functions that actually use statics, not every declared function. Cost is therefore decoupled from total function count: ~1.9µs/yield at 50 static-using functions, FLAT from 500 to 8000 total functions (the pre-registry full-table walk was ~0.16ms/yield at 1200 functions and scaled with the total — that version halved throughput at scale). Cost now scales only with the (small) number of static-USING functions — the irreducible per-coroutine snapshot. Closures + eval/top-level code are excluded from the registry (their op_arrays have per-instance lifetime; this matches exactly what the snapshot already covered — no regression).

Opt out with env ZEALPHP_FN_STATICS_DISABLE=1 (or App::coroutineStaticsIsolation(false) before App::run()) for raw throughput on apps that don't depend on per-request function statics.

Requires ext-zealphp 0.3.10+ with zealphp_coroutine_statics.

$coroutine_tz_isolation

Per-coroutine date_default_timezone_set() isolation — the default timezone is process-global; WordPress-class apps set it per request (core boot reads the site option), so one request's timezone leaks into every concurrently-running peer (measured 179/250 at 49-way concurrency). Same stage shape as locale/umask (ext-zealphp 0.3.45+, via the engine's own getter/setter pair). Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_TZ_ISOLATION_DISABLE=1); off by default elsewhere.

public static bool $coroutine_tz_isolation = false

$coroutine_umask_isolation

Per-coroutine UMASK isolation — umask() is process-global file-mode state; one request's umask(0077) changes every peer's file creation mid-request. Same stage shape as locale/CWD (ext-zealphp 0.3.38+; the umask read+re-park is a single syscall). Auto-enabled by App::mode('coroutine-legacy') (opt out with env ZEALPHP_UMASK_ISOLATION_DISABLE=1); off by default elsewhere.

public static bool $coroutine_umask_isolation = false

$cwd

Absolute working directory the framework boots in. Resolved at boot via realpath(__DIR__ . '/..') and exposed read-only for handlers that need to build paths relative to the project root (e.g.

public static string $cwd = ''

App::$cwd . '/.cache' for the file-tier cache directory).

Defaults to '' so reads before App::init() (e.g. elog() building a relative path) don't fatal with "typed static accessed before initialization" — str_replace('', ...) is a harmless no-op. init() sets the real working directory.

$default_charset

Apache AddDefaultCharset. Stored here for consumers (e.g. a future CharsetMiddleware) that want a server-wide default charset to append to text-ish Content-Type headers.

public static string $default_charset = 'utf-8'

$default_mimetype

Apache DefaultType / PHP default_mimetype. The Content-Type applied by CharsetMiddleware to a response that doesn't set one itself (mod_php sends text/html by default). Set to '' to leave untyped responses untouched.

public static string $default_mimetype = 'text/html'

$default_php_self

Override value for $_SERVER['PHP_SELF'] and friends. null means "use the request URI verbatim", which is the normal Apache/nginx convention. Apps that need a stable PHP_SELF (legacy WordPress plugins, etc.) can pin it here.

public static string|null $default_php_self = null

$define_isolation

Per-request define() isolation. When true, constants defined during a request are tracked and removed at request end. Boot-time constants (PHP_VERSION, extension defines, autoloaded class constants) survive.

public static bool $define_isolation = false

WARNING: breaks require_once apps — the file won't re-execute to re-define the constant on the next request. Use only with apps that guard defines (defined('X') ?: define('X', ...)) AND use require (not require_once) for the defining file, OR with processIsolation where each request gets a fresh process.

Set via App::defineIsolation(true) BEFORE App::run().

$dev_reload

Dev route hot-reload toggle. When true, each worker polls route/*.php mtimes and calls reloadRoutes() on change (no process restart). null resolves to the ZEALPHP_DEV env var at run(). OFF in production (the route table stays master-loaded + COW-shared). Set via App::devReload().

public static bool|null $dev_reload = null

$directory_index

Apache DirectoryIndex — file names tried in order when a directory is requested.

public static array<int, string> $directory_index = ['index.php', 'index.html', 'index.htm']

$directory_slash

Apache DirectorySlash equivalent — redirect /foo/foo/ when foo is a directory.

public static bool $directory_slash = true

$display_errors

Whether framework error pages render the captured exception + stack trace inline. Secure-by-default: null (the default) resolves at runtime to the ZEALPHP_DEV env var — OFF in production, so 5xx pages show a generic message and never leak traces/secrets (#412). Call App::displayErrors(true) for the inline-trace development view; an explicit setter call always wins over the env resolution.

public static bool|null $display_errors = null

Read through App::displayErrors() (not the raw property) to get the env-resolved value; a raw null/false is treated as "do not leak".

$document_root

Apache DocumentRoot equivalent. Relative values (the default) are resolved against App::$cwd; absolute values are used as-is. Drives App::include() path resolution and the implicit /{file}/{dir/uri} routes.

public static string $document_root = 'public'

$enable_coroutine_override

OpenSwoole enable_coroutine server-setting override. null means "follow !$superglobals" (true → coroutine-per-request, false → one synchronous request at a time per worker). Set via App::enableCoroutine(bool). Combining true with $superglobals=true is unsafe — process-wide $_GET/$_POST/$_SESSION will race across concurrent coroutines; the helper warns at run() time.

public static bool|null $enable_coroutine_override = null

$fcgi_address

FastCGI backend address used when App::cgiMode() === 'fcgi'.

public static string $fcgi_address = '127.0.0.1:9000'

Format: "host:port" for TCP (e.g. "127.0.0.1:9000") or "unix:/path/to/php-fpm.sock" for a Unix-domain socket. Set via App::fcgiAddress(). Default is the standard php-fpm TCP listener.

$fcgi_address_set

public static bool $fcgi_address_set = false

$file_etag

Apache FileETag. When false, ETagMiddleware emits no ETag header and never returns 304 (equivalent to FileETag None). Default true.

public static bool $file_etag = true

Set via App::fileETag() before App::init().

$function_isolation

Per-request function/class/include isolation via ext-zealphp's zealphp_process_state_snapshot() / zealphp_process_state_clean().

public static bool $function_isolation = false

When enabled, the worker snapshots its function table, class table, and included-files cache at boot. At request end, any functions, classes, or require_once entries added during the request are removed — giving fresh-process semantics without subprocess overhead.

ONLY safe in Mode 3 (sync, sequential). In coroutine modes the function table is shared across concurrent coroutines — cleaning it mid-flight would crash other coroutines.

Set via App::functionIsolation(true) BEFORE App::run().

$global_scope_include

Stage 8 — true-global-scope request include (coroutine-legacy). When on (and ext-zealphp exposes zealphp_require_global()), App::include() runs the target file at TRUE global scope so a bare file-scope $x = ... — and every transitive require_once — binds to $GLOBALS instead of the executeFile() method frame. This is what lets unmodified require_once- bootstrap apps (WordPress's $menu/$submenu/$_wp_submenu_nopriv, built bare at file scope in wp-admin/includes/menu.php) render in **coroutine-legacy** in-process mode, where the request entry runs inside a PHP method and those vars would otherwise be method-local.

public static bool|null $global_scope_include = null

Only effective under coroutine-legacy ($silent_redeclare): global-scope includes need the per-coroutine globals isolation stack, else file-scope globals leak across coroutines. Off by default — it changes include scope (the included file does NOT see executeFile()'s injected $g / route params as locals), so enable it only for legacy apps that read request state through superglobals, not via ZealPHP's $g. null follows the ZEALPHP_GLOBAL_INCLUDE env var (default off). Set via App::globalScopeInclude(true) BEFORE App::run(). Canonical reference: docs/architecture/2026-06-02-stage8-global-scope-include.md.

$hook_all_override

OpenSwoole\Runtime::enableCoroutine($flags) override. Same shape as App::hookAll() input: null → follow !$superglobals (HOOK_ALL when coroutine mode, 0 in superglobals mode); trueHOOK_ALL; false0; int → explicit bitmask. PDO is intentionally NOT hooked in OpenSwoole 22.1 / 26.2 regardless of this flag.

public static bool|int|null $hook_all_override = null

$hook_exec

Toggle the uopz override of the exec family (backtick / shell_exec / exec / system / passthru) so they yield via OpenSwoole's coroutine scheduler instead of blocking the worker.

public static bool|null $hook_exec = null

null (default) resolves to "on when coroutine mode" (!$superglobals) at App::run() — matches the production-safe expectation. Set via App::hookExec(bool) for an explicit override.

$hook_exit

Toggle the ext-zealphp interception of exit()/die() so a userland exit inside a coroutine throws ZealPHP\HaltException (which extends \Error, so the ubiquitous try { … exit; } catch (\Exception) legacy-router idiom cannot swallow the normal exit and turn it into a 500 — issue ext#47; FreshRSS/DokuWiki/CodeIgniter). The framework's halt-aware sites flush the buffered output as the body. null (default) resolves to "on when the coroutine scheduler is active" (enableCoroutine effective) at App::run(); a non-null value forces it. Requires ext-zealphp 0.3.48+ (zealphp_exit_hook); a no-op otherwise. Env opt-out: ZEALPHP_EXIT_HOOK_DISABLE=1.

public static bool|null $hook_exit = null

$hostname_lookups

Apache HostnameLookups On|Off. When true, the framework populates $g->server['REMOTE_HOST'] via gethostbyaddr($g->server['REMOTE_ADDR']) on each request. **WARNING**: this performs a blocking reverse-DNS lookup per request (mitigated by OpenSwoole's coroutine hook converting it to a non-blocking async resolve, but still a measurable per-request cost). Off by default — Apache's own default since 1.3.

public static bool $hostname_lookups = false

$ignore_php_ext

When true (default), URLs ending in .php get a 403. The framework encourages extensionless URLs as the canonical public surface (matches Apache RewriteRule \.php$ - [F] parity). Set false to allow direct *.php routing — useful when porting an existing app that links to /foo.php from external sources.

public static bool $ignore_php_ext = true

$include_isolation

Per-request require_once cache reset. Clears EG(included_files) so files loaded via require_once on request N re-execute on request N+1.

public static bool $include_isolation = false

Functions and classes defined by those files stay loaded (they live in CG(function_table)/CG(class_table), not in included_files). Pair with silentRedeclare(true) so the re-executed function/class/constant declarations are silently skipped instead of E_COMPILE_ERROR.

Solves the "WordPress template-loader runs once then becomes a no-op" class of bug — any app that puts per-request logic inside a require_once'd file needs this.

Safe in ALL modes (sync, coroutine, hybrid). Implemented by ext-zealphp Stage 7: zealphp_include_isolation(true) installs a ZEND_INCLUDE_OR_EVAL opcode hook that, for any require_once/include_once of a file NOT in the boot snapshot, drops it from EG(included_files) inline so it re-executes — bootstrap (snapshotted) files stay cached. This needs ZERO per-request cleanup (it replaces the older per-request zealphp_process_state_clean(1) files-wipe). Requires a snapshot to have been taken (zealphp_process_state_snapshot() in onWorkerStart); the framework takes it automatically when this flag is on.

$initial_error_reporting

Initial error_reporting level captured at boot — referenced by the per-coroutine override.

public static int $initial_error_reporting = E_ALL

$keep_globals

Keep user-defined $GLOBALS across requests within the same worker.

public static bool $keep_globals = false

Default false: at the end of every request the session manager calls zealphp_globals_clean() to drop user-defined entries back to the parent baseline. This matches FPM's "fresh process per request" semantic at the request boundary.

When true: that cleanup is SKIPPED. $GLOBALS['wp_object_cache'], $GLOBALS['wp_did_header'], $GLOBALS['wpdb'], and similar process-persistent state stays alive across requests within the worker's lifetime — matching how WordPress, Drupal 7/8, MediaWiki, Magento, and other globals-heavy legacy apps were designed to work under long-running mod_php / single-process SAPI. The same semantic FrankenPHP's worker mode provides.

When to use:

  • WordPress, Drupal, MediaWiki, Magento — apps with explicit process-persistent globals
  • Any procedural PHP app where $wp_did_header-style boot sentinels gate the bootstrap chain

Sharp edge to know about:

  • Apps that mistakenly store REQUEST-SCOPED data in $GLOBALS (e.g., $GLOBALS['current_user_id'] = $_SESSION['uid']) will observe cross-request bleed. This is a pre-existing bad pattern that's also unsafe under FPM with opcache + persistent connections. The fix is to use $_SERVER, session, or a per-request DI container — not $GLOBALS.

Bounded by worker recycle: OpenSwoole's max_request cap (typical 10,000–50,000) gives the same eventual "fresh process" reset that FPM does, just at the worker level instead of per-request.

Set via App::keepGlobals(true) BEFORE App::run().

$limit_request_field_size

Apache LimitRequestFieldSize — maximum byte length of a single request header line. **NOT enforced by ZealPHP.** OpenSwoole's C-layer HTTP parser owns all wire-level framing; ZealPHP only sees the already-parsed $request->header array. The http_header_buffer_size option was explicitly NOT passed to OpenSwoole (its option validator rejects it at boot — see App::run() ~line 3748). Changing this value has no effect on the actual per-header byte limit, which is governed by OpenSwoole's global header-buffer size (~8 KiB default). This property is retained for documentation and future compatibility only.

public static int $limit_request_field_size = 8190

$limit_request_fields

Apache LimitRequestFields — maximum number of request header fields a single request may carry. Enforced at the PHP application layer: requests carrying more than this many headers are rejected with 400 before route dispatch. Set to 0 to disable the check (unlimited). Default 100 matches Apache's compiled-in default.

public static int $limit_request_fields = 100

$limit_request_line

Apache LimitRequestLine — maximum byte length of the HTTP request line (method + URI + protocol). **NOT enforced by ZealPHP.** OpenSwoole's C parser reads the request line before any PHP code runs; there is no per-request-line cap that ZealPHP can apply after the fact. OpenSwoole's global http_header_buffer_size governs this limit at the wire level.

public static int $limit_request_line = 8190

This property is retained for documentation and future compatibility only.

$middleware_aliases

Named middleware registry — Traefik's "named & shared" middleware and Laravel's route-middleware aliases. Maps a short name to either a ready MiddlewareInterface instance or a factory callable(...$args) that returns one. Populated by App::middlewareAlias() at boot; resolved to instances once at App::run() (single-coroutine, so the hot path never does a registry lookup or instantiation). Reused across routes — middleware objects MUST be stateless (request state lives in $g/RequestContext, never on the middleware) because one instance is shared by every concurrent coroutine that uses the alias.

public static array<string, MiddlewareInterface|callable> $middleware_aliases = []

$middleware_stack

The PSR-15 middleware stack handler, built during App::run() from the registered middleware list. null before run(). Generally read via the public App::middleware() accessor; this property is public for advanced introspection (e.g. /healthz dumps).

public static StackHandler|null $middleware_stack = null

$middleware_wait_stack

Middleware queued via App::addMiddleware() BEFORE App::run().

public static array<int, MiddlewareInterface> $middleware_wait_stack = []

Reversed at boot and fed to OpenSwoole's StackHandler (whose add() prepends and whose handle() runs index 0 first) so the first middleware you add wraps outermost and runs first; ResponseMiddleware (the router) always runs innermost. Public for apps that want to inspect / mutate the stack at boot time.

$path_info

Apache PATH_INFO — when /script.php/extra/path, expose /extra/path as PATH_INFO.

public static bool $path_info = true

$port

public int $port

$preload_classes

Extra classes to compile at worker start so they are NEVER cold-autoloaded under request concurrency. APPENDED to the framework's own request-path warmup set (see preloadRequestPathClasses()).

public static array<int, class-string> $preload_classes = []

WHY THIS EXISTS — in coroutine-legacy mode, a class first compiled by several overlapping coroutines at once (the first concurrent cold wave) can intermittently fail to register durably → transient Class "X" not found 500s on the cold burst, then fine once warm. Classes loaded at boot / worker-start (single-coroutine, no overlap) are immune. The framework warms its own request/response path; YOUR controllers, services, and any lazily-instantiated class on a hot path must be warmed too.

Register BEFORE App::run(): App::preloadClasses(App\Controller\Home::class, App\Service\Auth::class);

No-op outside coroutine-legacy (other modes don't have the race).

$preload_classmap

When true, warm EVERY class in Composer's classmap in the MASTER process (before $server->start() forks the workers), so a user app's own controllers/services (autoloaded on demand, deep inside handlers — "the app is just the server") are born LINKED and copy-on-write-forked into every worker, never compiled on the concurrent cold path. This is the structural fix for the present-but-unlinked inheritance race: the whole dependency graph is bound in a single process with NO coroutine scheduler, so nothing can yield and let a worker interleave a cold compile. Same idea as PHP's opcache.preload. Validated: 0 failures across cold bursts with the framework's own onWorkerStart preload disabled (classmap-only).

public static bool $preload_classmap = false

Requires an OPTIMIZED Composer classmap to be complete — run composer dump-autoload --optimize (or --classmap-authoritative). A plain PSR-4 autoloader has a sparse classmap; for that, list hot classes with App::preloadClasses() or warm a tree with App::preloadDir(). A pure require_once legacy app (no Composer/autoloader at all — classic WordPress) can't be warmed this way; run it in legacy-cgi mode, which is process-isolated and has NO coroutine race in the first place.

Off by default (it trades a slower BOOT + higher baseline RSS for the guarantee). Enable BEFORE App::run() via App::preloadClassmap().

$preload_dirs

Source directory trees to warm at worker start (PSR-4 roots / app source whose symbols a registered autoloader can resolve). Each .php file's declared symbols are extracted via the tokenizer and autoloaded+linked single-coroutine. Append via App::preloadDir().

public static array<int, string> $preload_dirs = []

$process_isolation

Per-include CGI process-isolation override. null means "follow $superglobals" (true → CGI subprocess via cgi_worker.php; false → in-process via executeFile()), which preserves today's default coupling. Set via App::processIsolation(bool) — see that method for the trade-offs. App::run() resolves this into the backing $coproc_implicit_request_handler flag right before the server starts.

public static bool|null $process_isolation = null

$reloading

True only while App::reloadRoutes() is re-including route/*.php files.

public static bool $reloading = false

Infrastructure-registration calls that route files also make at boot (Store::make, App::ws excluded as it is idempotent, App::onWorkerStart, App::addProcess, App::subscribe, App::onSignal) check this flag and skip re-wiring — they were wired once at boot and a reload only swaps the route table, never the worker's timers/processes/subscribers.

$run_has_started

Set true at the top of App::run() so the four lifecycle setters (superglobals, processIsolation, enableCoroutine, hookAll) can refuse mutations made AFTER the server has booted.

public static bool $run_has_started = false

Why: those four knobs decide the SessionManager class, the enable_coroutine server setting, and OpenSwoole\Runtime::HOOK_ALL — all of which are frozen at run() boot. But the static-property backing stores are re-read PER-REQUEST in executeFile() and App::include(). Mutating them mid-game leaves the framework in a Schrödinger state — coroutines still active, but superglobals reads now say "I'm in CGI mode". Concurrent coroutines race on $_GET/ $_POST/$_SESSION. validateLifecycleCombination() only fires at boot so it doesn't catch this. The guard closes that footgun.

$sapi_name

mod_php-parity SAPI identity for the php_sapi_name() override. Default null returns the real PHP_SAPI ("cli") — no behavior change. Set to a web SAPI string (e.g. 'apache2handler', 'fpm-fcgi') so legacy code branching on php_sapi_name() takes its web path. The PHP_SAPI *constant* is unaffected (uopz cannot redefine it). Configure via App::sapiName() before App::init().

public static string|null $sapi_name = null

$server

The active OpenSwoole server instance after App::run() constructs it; null before run(). Returned as a WebSocket\Server when any App::ws() route was registered (the framework upgrades from Http\Server automatically), Http\Server for pure HTTP apps.

public static Server|Server|null $server

Use App::getServer() for the public accessor.

$server_admin

Apache ServerAdmin webmaster@example.com. When set, the framework's default 500/error page mentions this contact. null disables the contact line.

public static string|null $server_admin = null

$server_tokens

Apache ServerTokens. Controls how much detail the X-Powered-By response header advertises: 'Full' (default) → ZealPHP + OpenSwoole 'Prod' / 'Major' / 'Minor' / 'Min' / 'OS'ZealPHP 'None' (or '') → header omitted entirely (info-leak hardening) Set via App::serverTokens() before App::init().

public static string $server_tokens = 'Full'

$session_data_size

Maximum serialized session size in bytes when using TableSessionHandler.

public static int $session_data_size = 16384

Default 16384 (16 KB) — fits most modern sessions including OAuth tokens, cart state, user preferences. Larger sessions overflow to file backing only.

$session_handler

Session storage backend. One of: - null (default) — the framework inline **file** path in ALL modes (flock read-merge-write under $session_save_path). #295: deliberately NOT auto-promoted to TableSessionHandler; the unconfigured default is file-backed everywhere. Opt into a concurrent-safe backend explicitly.

public static string|SessionHandlerInterface|null $session_handler = null
  • 'table' — TableSessionHandler (concurrent-safe, in-memory + file backing).
    • 'file' — FileSessionHandler (simple, key-level merge).
    • 'redis' — RedisSessionHandler (cross-node, WATCH/MULTI).
    • SessionHandlerInterface instance — bring your own.

Resolved via App::resolveActiveSessionHandler(). Set via App::sessionHandler('table') BEFORE App::run().

$session_lifecycle

Whether ZealPHP's per-request session lifecycle runs. Default true: the SessionManager / CoSessionManager OnRequest wrapper reads the PHPSESSID cookie, calls zeal_session_start(), optionally emits the Set-Cookie header, and closes the session at request end. Set to false when another framework (e.g. Symfony's NativeSessionStorage via the zealphp-symfony bridge) owns the session lifecycle — ZealPHP then skips the session-specific work but still does request-context setup ($g->openswoole_request, $g->zealphp_response, error-stack reset, etc.).

public static bool $session_lifecycle = true

The underlying zeal_session_* uopz-overridden functions remain installed and callable from user code either way; this toggle only controls whether the SessionManager wrapper drives the lifecycle automatically for every request.

$session_max_rows

Maximum concurrent sessions in OpenSwoole\Table when using TableSessionHandler. Default 65536 (64K) — accommodates medium-scale deployments without re-tuning. Each row costs `$session_data_size + ~64 bytes` of shared memory (one allocation per OpenSwoole server, NOT per worker). Default config = 64K × 16KB ≈ 1 GB shared memory.

public static int $session_max_rows = 65536

Bump for high-traffic; sessions beyond the cap fall through to file backing (still functional, just slower).

$session_save_path

File-backing directory for session storage. Default /var/lib/php/sessions (matches PHP's default). Used by FileSessionHandler and TableSessionHandler's file backing layer.

public static string $session_save_path = '/var/lib/php/sessions'

$session_strict_mode

PHP session.use_strict_mode parity (#244). When true (the default — security-first) a CLIENT-SUPPLIED session id (from a PHPSESSID cookie or query param) whose backing store loads an EMPTY session is treated as untrusted: the session managers mint a fresh server-generated id and switch the client to it. This defeats session FIXATION — an attacker who plants a known id into the victim's browser can no longer have it promoted to an authenticated session, because the framework rotates any unrecognised id before the victim ever authenticates under it. A well-formed id that DOES resolve to a non-empty stored session is preserved unchanged.

public static bool $session_strict_mode = true

CAVEAT — storage topology: the empty-session signal is only meaningful when the id's store is visible to the node handling the request. That holds for single-node (TableSessionHandler, the coroutine-mode default) and for shared storage (Redis/Tiered-backed sessions). A MULTI-NODE deployment using the per-server TableSessionHandler WITHOUT sticky load-balancing or shared session storage is already broken (sessions don't persist across nodes); with strict mode on it will ALSO rotate the id on every cross-node hop. Such setups should switch to Redis-backed sessions (so every node sees the same store) or opt out via App::sessionStrictMode(false). Set via the fluent setter App::sessionStrictMode().

$session_ttl

Session TTL in seconds. Default 7200 (2 hours — modern-app reasonable; PHP's stock 1440 / 24 min is too short for typical workflows).

public static int $session_ttl = 7200

Set via App::sessionTtl(3600) BEFORE App::run().

$silent_redeclare

Stage 3 — silent-redeclare opcode hooks. When enabled, ext-zealphp's ZEND_DECLARE_FUNCTION / ZEND_DECLARE_CLASS / _DELAYED opcode handlers check if the target symbol already exists in EG(function_table) / CG(class_table). If it does, the opcode is silently skipped instead of throwing E_COMPILE_ERROR ("Cannot redeclare …"). First declaration wins — matches what FPM gets "for free" by forking a fresh process per request.

public static bool $silent_redeclare = false

Closes the dominant Mode 3/4/5 failure mode on the 32-app sweep: conditional function foo() } / class Bar } in legacy code that re-runs on every request. Top-level (file-scope) function declarations are still compile-time-bound by Zend and not covered by this hook; those need OPcache enabled OR Mode 1 Pool.

Requires ext-zealphp 0.3.8+. Set via App::silentRedeclare(true) BEFORE App::run(). Off by default to keep existing semantics.

$static_handler_locations

Static handler URL-prefix whitelist. Empty = serve any path under document_root (Apache default).

public static array<int, string> $static_handler_locations = []

$strip_trailing_slash

Apache RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.+)/$ /$1 [R=301,L].

public static bool $strip_trailing_slash = false

When true, non-directory URIs ending in / are 301-redirected to the no-slash form. Inverse of $directory_slash. Default false (keeps current behaviour).

$superglobals

Per-request lifecycle mode (the dial that picks $g storage + SessionManager + enable_coroutine + HOOK_ALL default). See the "Lifecycle modes" matrix in CLAUDE.md — short version:

public static bool $superglobals = true
  • true (default): $g lives in process-wide PHP superglobals ($_GET/$_POST/$_SESSION etc.) — Apache mod_php parity. One request at a time per worker; coroutine scheduler OFF by default. Unmodified WordPress / Drupal work here.
    • false: per-coroutine $g via Coroutine::getContext(). Concurrent coroutine handling enabled; superglobals NOT populated. Modern apps that want OpenSwoole concurrency pick this.

Set via App::superglobals(bool) BEFORE App::init().

$trace_enabled

Apache TraceEnable — defaults to OFF for security. When false (default) ResponseMiddleware refuses HTTP TRACE with 405 regardless of any matching route definition. Set to true only if you know you need TRACE.

public static bool $trace_enabled = false

$trusted_proxies

CIDR list of proxy IPs whose X-Forwarded-For / X-Real-IP headers App::clientIp() will trust. Empty (the default) means no proxies trusted — App::clientIp() always returns REMOTE_ADDR. Critical for production deploys behind Traefik/Caddy/nginx; without it rate limiters and access logs see the proxy IP instead of the real client.

public static array<int, string> $trusted_proxies = []

Supports IPv4 (10.0.0.0/8, 192.168.1.42) and IPv6 (2001:db8::/32, ::1). A bare IP without /prefix is treated as /32 (v4) or /128 (v6).

$use_canonical_name

Apache UseCanonicalName On|Off. When true and $canonical_name is set, App::canonicalHost() returns the canonical name; otherwise it returns the request Host header. Default false (Apache's default since 2.0).

public static bool $use_canonical_name = false

$username_provider

Callback consulted by ZealAPI::getUsername(). Signature: fn(): ?string.

public static callable|null $username_provider = null

Default nullgetUsername() returns null. Set via App::usernameProvider().

$when_middleware

Path-scoped middleware registry — App::when($path, $middleware). Each entry scopes a chain to a URL path prefix (or a #...# PCRE), and runs for EVERY request whose normalized path matches — route or api alike, since api endpoints are just /api/... URLs on the same stack. Stored in registration order (first registered = outermost). Raw specs here; resolved to instances at App::run() into $when_middleware_compiled.

public static array<int, MiddlewareInterface|string>}> $when_middleware = []

$when_middleware_compiled

Boot-compiled App::when chains (alias→instance), read-only at request time so the hot path never does a registry lookup or new.

public static array<int, MiddlewareInterface>}> $when_middleware_compiled = []

$when_middleware_memo

Per-normalized-path memo of the flattened matching when chain (the registry is immutable after boot, so this is a write-once-per-path cache; concurrent same-path writes are idempotent). Capped at WHEN_MEMO_MAX entries so an attacker spraying distinct paths can't grow it without bound — past the cap, paths simply recompute the (cheap) prefix scan.

public static array<string, array<int, MiddlewareInterface>> $when_middleware_memo = []

$autoloadSerializerInstalled

True once the per-worker coroutine autoload serializer is installed.

protected static bool $autoloadSerializerInstalled = false

$bootedAt

Unix timestamp the master process booted at (set by run()).

protected static int|null $bootedAt = null

$host

Bind address for the OpenSwoole server (e.g. '0.0.0.0' or '127.0.0.1'). Set in __construct() from App::init().

protected string $host

$processBootWired

True once the onWorkerStart hook for process-pool is wired.

protected static bool $processBootWired = false

$processHandlers

protected static array<string, array{callable: callable, workers: int, coroutine: bool}> $processHandlers = []

$pubsubBootWired

True once the onWorkerStart hook for pubsub/streams is wired (one-time guard).

protected static bool $pubsubBootWired = false

$pubsubRegistry

protected static array<string, array<int, callable>> $pubsubRegistry = []

channel/pattern → handlers

$reliableRegistry

protected static array<string, array<int, array{group: string, handler: callable, blockMs: int, batchSize: int}>> $reliableRegistry = []

stream → consumers

$route_baseline

Snapshot of the route/middleware registries taken at App::run() *before* the route/*.php files + implicit routes are loaded — i.e. just the app.php-defined explicit routes/aliases/scopes. App::reloadRoutes() restores this baseline, then re-runs the file-based registration, so a route-file edit can be picked up without restarting the worker. Null until run() snapshots it.

protected MiddlewareInterface|callable>, backend_aliases?: array}>}|null $route_baseline = null

implicit holds the framework's own implicit routes (api dispatch, public file serving, …) captured as data so reload re-appends them after the re-included route files, preserving priority order without re-running their registration.

$routes

protected array<int, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}> $routes = []

$routes_by_exact_method

protected array<string, array<string, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>> $routes_by_exact_method = []

$routes_by_method

protected array<string, array<int, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>> $routes_by_method = []

$signalHandlers

protected static array<int, array<int, array{handler: callable, worker_only: bool}>> $signalHandlers = []

$task_worker_num

Resolved task-worker count after run() reads CLI/env/settings. Zero when task workers are disabled.

protected static int $task_worker_num = 0

$worker_num

Resolved worker counts after run() reads CLI/env/settings.

protected static int $worker_num = 0

$workerStartedAt

Unix timestamp (float) of the moment this worker's onWorkerStart callback finished — used by App::stats() to compute per-worker uptime. Zero until the first worker start fires.

protected static float $workerStartedAt = 0.0

$workerStartHooks

protected static array<int, callable> $workerStartHooks = []

$workerStopHooks

protected static array<int, callable> $workerStopHooks = []

$ws_routes

protected array<string, array{message: callable, open: callable|null, close: callable|null}> $ws_routes = []

$access_log_format_compiled

Parsed format spec cache (token list). Filled lazily by formatAccessLogLine() the first time it sees a given format string. Resets when accessLogFormat() is reassigned via the fluent setter.

private static array<int, array{kind: string, arg?: string}>|null $access_log_format_compiled = null

$configMap

ZealPHP config keys recognized in the run() $settings array.

private static array<string, string> $configMap = ['superglobals' => 'superglobals', 'process_isolation' => 'processIsolation', 'hook_exec' => 'hookExec', 'document_root' => 'documentRoot', 'trace_enabled' => 'traceEnabled', 'ignore_php_ext' => 'ignorePhpExt', 'default_charset' => 'defaultCharset', 'strip_trailing_slash' => 'stripTrailingSlash', 'server_admin' => 'serverAdmin', 'api_warn_collisions' => 'apiWarnCollisions', 'api_null_not_found' => 'apiNullNotFound', 'directory_slash' => 'directorySlash', 'hostname_lookups' => 'hostnameLookups']

Each maps to a static fluent setter; extracted before OpenSwoole sees the array. Add new framework-level knobs here.

$error_handlers

Status -> custom error handler registry (key 0 = catch-all).

private static array<int, array{handler: callable, param_map: array, raw: bool}> $error_handlers = []

$fallback_handler

private static array<string, mixed>|null $fallback_handler = null

$fatal_guard_inflight

#338 — in-flight raw responses, per worker process. A worker-killing FATAL (E_COMPILE_ERROR / E_ERROR / …) never reaches the normal emit path, so the client connection — held open by the master's reactor — would hang until the CLIENT's timeout (HTTP 000). Apache/mod_php answers 500. The session managers track every request's raw OpenSwoole response here and release it on normal completion; the native shutdown guard below answers whatever is left when a fatal tears the worker down. Coroutine mode can hold several entries at once — one fatal kills them all, so all of them get the 500.

private static array<int, Response> $fatal_guard_inflight = []

$instance

private static self|null $instance = null

$overridesRegistered

Guard preventing registerAllOverrides() from installing uopz/zealphp built-in overrides more than once per process.

private static bool $overridesRegistered = false

$resolved_session_handler

Memoised resolution of {@see $session_handler} (see resolveActiveSessionHandler).

private static SessionHandlerInterface|null $resolved_session_handler = null

$session_handler_resolved

Whether {@see $resolved_session_handler} has been computed yet.

private static bool $session_handler_resolved = false

Methods

__wakeup()

public __wakeup() : mixed

accessLogFormat()

Apache LogFormat. Resets the compiled-spec cache on set.

public static accessLogFormat([string|null $format = null ]) : string
Parameters
$format : string|null = null
Return values
string

addMiddleware()

public addMiddleware(MiddlewareInterface $middleware) : void
Parameters
$middleware : MiddlewareInterface

addProcess()

Register a long-running sidecar process — runs alongside the HTTP/WS server, managed by the OpenSwoole master (same fate-sharing: dies when the server stops, respawned on graceful reload). Different from task workers (which are queue consumers) and worker hooks (which run inside HTTP workers); these are independent processes for background work like log shippers, file watchers, scheduled-job runners, OAuth token refreshers, etc.

public static addProcess(string $name, callable $callable[, int $workers = 1 ][, bool $coroutine = true ]) : void
App::addProcess('log-shipper', function (\OpenSwoole\Process $p): void {
    while ($line = fgets(STDIN)) {
        shipToS3($line);
    }
}, workers: 1, coroutine: true);

The $callable receives the OpenSwoole\Process instance (call $p->exit() to shut down cleanly; respawn happens automatically when configured).

$workers spawns N independent copies under the same name (suffixed with index 0..N-1 internally). $coroutine enables OpenSwoole's coroutine runtime inside the sidecar — true by default so the same usleep / curl / PDO yield semantics work that the HTTP workers get.

Must be called BEFORE App::run() so registrations are visible at server-build time.

Mirrors $server->addProcess() from the OpenSwoole API.

Parameters
$name : string
$callable : callable
$workers : int = 1
$coroutine : bool = true

adminChecker()

Register a callback that ZealAPI::isAdmin() consults.

public static adminChecker([callable|null $fn = null ]) : callable|null

Same shape as authChecker()fn(): bool, default null.

Parameters
$fn : callable|null = null
Return values
callable|null

adoptRequestContext()

#42 — make the CURRENT (child) coroutine inherit the spawning request's context. Two layers: RequestContext::instance() walks the parent-coroutine chain automatically (so $g is the request's), and — in coroutine-legacy, where $g->server et al. are live aliases of the process superglobals — zealphp_superglobals_adopt() (ext-zealphp 0.3.43+) gives this coroutine its OWN superglobal snapshot lane: its first yield CAPTURES the live view (the spawning request's state) without clearing it, so $_SERVER/$_GET/… survive the child's own yields and the parent is never stolen from. Safe no-op outside a coroutine or without the ext function. Called automatically by App::go() / App::parallel() / App::parallelLimit().

public static adoptRequestContext() : void

after()

One-shot timer: calls $fn once after $ms milliseconds.

public static after(int $ms, callable $fn) : int
Parameters
$ms : int
$fn : callable
Return values
int

any()

public any(string $path, callable|array<int|string, mixed> $handler[, array<string, mixed> $options = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void
Parameters
$path : string
$handler : callable|array<int|string, mixed>
$options : array<string, mixed> = []
$raw : bool = false
$middleware : array<int, MiddlewareInterface|string> = []
$backend : array<string, mixed>|string|null = null

apiNullNotFound()

#347 — whether a ZealAPI handler returning null with no output, no explicit status and no streaming yields the Apache-parity 404 {"error":"method_not_found"} envelope instead of 200 + empty body. Default true. No-arg call returns the current value.

public static apiNullNotFound([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

apiWarnCollisions()

Whether ZealAPI logs a warning when a filename collides with an HTTP method keyword (e.g. get.php defining $get) or a filename-matched handler shadows per-method handlers. Default true. No-arg call returns the current value.

public static apiWarnCollisions([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

applySignalHandlersFor()

Internal: wire registered signal handlers into the OpenSwoole process lifecycle. Called from App::run() (master) and via onWorkerStart (workers).

public static applySignalHandlersFor(string $context) : void
Parameters
$context : string

authChecker()

Register a callback that ZealAPI::isAuthenticated() consults.

public static authChecker([callable|null $fn = null ]) : callable|null

Signature: fn(): bool. The callback decides whether the current request is authenticated — typically by reading $_SESSION, $g->session, or your own auth state.

Without this hook, ZealAPI::isAuthenticated() returns false (fail-closed default), so any API endpoint guarded by requirePostAuth() rejects every request. Fixes the gap surfaced in issue #13.

Pass null (or omit the argument and rely on the existing value) to read the current checker. Pass a callable to install one.

Example:

App::authChecker(fn() => !empty($_SESSION['user_id']));
App::authChecker(fn() => MyAuth::status() === MyAuth::LOGGED_IN);
Parameters
$fn : callable|null = null
Return values
callable|null

blockDotfiles()

Block any request whose path contains a dotfile component (.git, .env, .htaccess, etc.) with 403. Default true — matches Apache's convention of not serving hidden files. No-arg call returns the current value.

public static blockDotfiles([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

buildCgiEnv()

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

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

Thin public delegating shim — the implementation moved to Dispatcher::buildCgiEnv() (Phase 2 refactor). Kept on App for BC so external callers/tests reach it via App::buildCgiEnv().

Parameters
$server : array<string, mixed>

$g->server (OpenSwoole-populated)

$ctx : string

JSON-encoded ZEALPHP_REQUEST_CONTEXT

Return values
array<string, string>

canonicalHost()

Canonical host for absolute URL building. Returns $canonical_name when useCanonicalName() is on AND $canonical_name is set; otherwise returns the request Host header (falling back to SERVER_NAME, then ''). Used by absolute-redirect builders that need to decide between the configured server name and the client-provided Host.

public static canonicalHost() : string
Return values
string

canonicalName()

Apache ServerName. Canonical host advertised in absolute redirects when useCanonicalName() is on. Pass null/'' to clear.

public static canonicalName([string|null $name = null ]) : string|null
Parameters
$name : string|null = null
Return values
string|null

cgiBackendAlias()

Register a named CGI backend alias for the per-route backend: option — the App::middlewareAlias() of the CGI dispatch world. Lets a route say backend: 'wp-fork' instead of repeating an inline config.

public static cgiBackendAlias(string $name, array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|string $config) : void
App::cgiBackendAlias('wp-fork', 'fork');                         // bare mode
App::cgiBackendAlias('py', ['mode' => 'proc', 'interpreter' => '/usr/bin/python3']);
App::cgiBackendAlias('fpm', ['mode' => 'fcgi', 'address' => 'unix:/run/php-fpm.sock']);

Register aliases BEFORE the routes that reference them (e.g. in app.php before route/*.php load — the natural order). The config is resolved + validated here, so a bad mode / missing fcgi address / a lifecycle-mode name throws at registration, not at request time.

Parameters
$name : string

Alias name. Must be non-empty and not collide with a reserved mode (pool/proc/fork/fcgi).

$config : array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|string

A bare mode string, or an inline backend config array.

Tags
throws
InvalidArgumentException

on a reserved/empty name, a non-mode string config, an invalid mode, a lifecycle-mode name, or fcgi without an address.

cgiForkMaxConcurrent()

Max concurrent cgiMode('fork') children — a per-request fork ceiling, NOT the pre-spawned cgiPoolSize(). Default 16. No-arg returns the current value; with-arg sets and returns it. Env: ZEALPHP_CGI_FORK_MAX_CONCURRENT (applied at boot unless set explicitly).

public static cgiForkMaxConcurrent([int|null $n = null ]) : int
Parameters
$n : int|null = null
Return values
int

cgiMode()

Select how a process-isolated legacy include is dispatched: 'pool' (default) — pre-spawned PHP worker pool, mod_php-style isolation, ~1-3 ms warm.

public static cgiMode([CgiMode|string|null $mode = null ]) : string

'proc' — fresh PHP per request via proc_open (~30-50 ms cold; full compat). 'fork' — Apache MPM prefork: a fork-master forks a FRESH child per request at TRUE global scope (~1 ms). Unmodified-WordPress correctness (no class redeclare) at fork cost, not proc cold-start. EXPERIMENTAL (pcntl+posix). 'fcgi' — forward to a FastCGI backend via App::$fcgi_address (no child process). See App::$cgi_mode for the full trade-off. No-arg call returns the current mode. Only takes effect when processIsolation() is on.

Parameters
$mode : CgiMode|string|null = null
Return values
string

cgiOwnsSessions()

True when the CGI subprocess is the sole owner of the per-request session lifecycle — superglobals(true) + processIsolation(true).

public static cgiOwnsSessions() : bool

Issue #108 — when both the host (SessionManager) AND the subprocess (native PHP session_start in cgi_worker / pool_worker) drive session I/O on the same file, the host's session_write_close() in the finally block races the subprocess's exit-time flush. The host's stale in-memory $_SESSION wins and overwrites everything the subprocess wrote. The fix is to let the subprocess own the session fully in this lifecycle — the host SessionManager skips session_start, cookie emission, and session_write_close. The subprocess's native session machinery handles all three (it captures its own Set-Cookie via uopz; cgiPool / cgiSubprocess / cgiFcgi thread the captured cookies back into the outbound response).

Other lifecycle combos are unaffected:

  • Coroutine mode (superglobals(false)): CoSessionManager runs, no subprocess involved, no race.
  • Mixed-mode (superglobals(true) + processIsolation(false)): host runs everything in-process — host SessionManager IS the session owner. No subprocess race.
Return values
bool

cgiPoolEnvAllowlist()

Strict environment allowlist for cgiMode('pool') subprocesses — exact names and/or PREFIX* globs. A no-arg call returns the current list; a one-arg call sets it. Empty (default) passes the parent env through (legacy-app compatibility) minus the httpoxy HTTP_PROXY var; a non-empty list restricts the subprocess to matching vars only (the pool IPC var is always passed). Set BEFORE App::run().

public static cgiPoolEnvAllowlist([array<int, string>|null $names = null ]) : array<int, string>
Parameters
$names : array<int, string>|null = null
Return values
array<int, string>

cgiPoolMaxRequests()

Per-subprocess request count before recycle for cgiMode('pool').

public static cgiPoolMaxRequests([int|null $n = null ]) : int

FPM pm.max_requests parity. Default 500. Set to 1 for fresh-process semantics every request (slower; same isolation as cgiMode('proc')).

Parameters
$n : int|null = null
Return values
int

cgiPoolSize()

Worker count for cgiMode('pool') — the native FCGI-style subprocess pool. FPM pm.max_children parity. Default 4. Set BEFORE App::run().

public static cgiPoolSize([int|null $size = null ]) : int
Parameters
$size : int|null = null
Return values
int

cgiScriptAlias()

Register a ScriptAlias-style executable URL prefix (Apache ScriptAlias parity). Any file served under $urlPrefix is treated as executable, regardless of its extension or whether a per-extension backend exists.

public static cgiScriptAlias(string $urlPrefix, array<string, mixed> $config) : void
Parameters
$urlPrefix : string

URL path prefix (e.g. '/cgi-bin'), NOT a filesystem path. Passing a filesystem path (one that exists as a directory) throws \InvalidArgumentException.

$config : array<string, mixed>

'mode''proc' | 'fork' | 'fcgi' (defaults to 'proc') 'interpreter' — full path to interpreter binary (proc mode) 'address' — FastCGI backend address (fcgi mode) 'fcgi_params' — extra FCGI params (fcgi mode)

Tags
throws
InvalidArgumentException

when $urlPrefix is a filesystem path rather than a URL prefix.

cgiSubprocessAutoload()

Whether cgi_worker.php (proc-mode subprocess entry) loads Composer's vendor/autoload.php on startup. Default false — restores pre-v0.2.20 behaviour suitable for unmodified WordPress / Drupal / Joomla / plain PHP. Set to true when your public/*.php files explicitly need \ZealPHP\App or framework classes inside the CGI subprocess.

public static cgiSubprocessAutoload([bool|null $on = null ]) : bool

See the $cgi_subprocess_autoload property docblock for the WordPress wp-cron deadlock rationale (issue #18, v0.2.41 regression fix).

Parameters
$on : bool|null = null
Return values
bool

cgiTimeout()

Max seconds to wait for a proc-mode CGI subprocess to emit its metadata line before SIGTERM/SIGKILL. Apache CGIScriptTimeout parity. Default 60.

public static cgiTimeout([int|null $seconds = null ]) : int

No-arg returns the current value; with-arg sets and returns it. Env: ZEALPHP_CGI_TIMEOUT (applied at boot unless set explicitly here).

Parameters
$seconds : int|null = null
Return values
int

clearTimer()

Cancel a timer returned by tick() or after().

public static clearTimer(int $id) : void
Parameters
$id : int

clientIp()

Resolve the real client IP for the current request, honouring the $trusted_proxies allow-list. Behaviour:

public static clientIp() : string
  1. Read REMOTE_ADDR from $g->server (the direct peer).
  2. If REMOTE_ADDR is NOT in any trusted_proxies CIDR, return it as-is. The peer is untrusted, so any X-Forwarded-* header it sent is a lie.
  3. If REMOTE_ADDR IS in a trusted CIDR, walk X-Forwarded-For right-to-left (Apache mod_remoteip semantics) and return the rightmost IP that is NOT in trusted_proxies — that's the real client. If every entry is trusted, fall back to the socket peer (REMOTE_ADDR) — NOT the leftmost entry, which a client can forge by prepending (#249/#434).
  4. If X-Forwarded-For is absent but X-Real-IP is present (and the peer is trusted), return X-Real-IP.

Returns the empty string when no IP can be determined (REMOTE_ADDR missing entirely — only happens for non-request contexts like CLI invocation).

Return values
string

coerceStatusCode()

Coerce a handler's int return value to a valid HTTP status code.

public static coerceStatusCode(int $status) : int

Per the universal return contract, ints must be in 100-599 (RFC 7230). Out-of-range values are coerced to 500 with a warning logged via elog() so the bug surfaces in the debug log instead of silently downgrading. Matches Apache HTTP server's behavior (out-of-range → 500).

Parameters
$status : int
Return values
int

compileMiddlewareChain()

Resolve a normalized middleware spec (instances + alias strings) to a flat list of MiddlewareInterface instances. Called once per route at App::run() boot time, so the dispatch hot path never does a registry lookup or new.

public static compileMiddlewareChain(array<int, MiddlewareInterface|string> $spec) : array<int, MiddlewareInterface>
Parameters
$spec : array<int, MiddlewareInterface|string>
Return values
array<int, MiddlewareInterface>

composeRequestArray()

Compose $_REQUEST from the GET and POST bags per PHP's default request_order='GP' (#356).

public static composeRequestArray(array<string, mixed> $get, array<string, mixed> $post) : array<string, mixed>

PHP merges the sources left-to-right with LATER sources overwriting earlier ones, so for 'GP' (GET first, POST second) a key present in both takes the POST value — the form-submission-overrides-querystring convention. PHP's + array-union keeps the LEFT operand on a collision, so the POST-wins composition is $post + $get. COOKIE is deliberately excluded (matches PHP's 'GP', which omits C). The single source of truth for both the OnRequest populate and the CGI-context request builder.

Parameters
$get : array<string, mixed>
$post : array<string, mixed>
Return values
array<string, mixed>

coroutineCwdIsolation()

Per-coroutine CWD isolation (#323) — see the $coroutine_cwd_isolation docblock. The ext C-level flag is asserted at App::run() boot wiring (alongside the other isolation knobs) so the scheduler hooks are guaranteed installed and the worker baseline is captured pre-fork; setting it here only records the intent.

public static coroutineCwdIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

coroutineGlobalsIsolation()

Per-coroutine $GLOBALS isolation. See $coroutine_globals_isolation docblock. Requires ext-zealphp 0.3.6+.

public static coroutineGlobalsIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

coroutineGlobalsMemoryAdvisory()

One-time boot-time advisory for coroutineGlobalsIsolation(true).

public static coroutineGlobalsMemoryAdvisory() : void

Stage 2 COW (ext-zealphp v0.3.7+): shared parent snapshot taken once, per-coroutine state is just (deltas + tombstones) for keys the coro actually wrote or unset. Memory: O(parent) once + O(deltas) per coro, not O(N keys) per coro.

Stage 1 estimate is left in the message for context — most apps stay well under any threshold with Stage 2 unless they routinely write thousands of unique global keys per coroutine.

The advisory runs only at App::run() boot, never per-request.

coroutineLibxmlIsolation()

Per-coroutine libxml error-flag isolation — see the $coroutine_libxml_isolation docblock. Asserted at App::run() boot wiring.

public static coroutineLibxmlIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

coroutineLocaleIsolation()

Per-coroutine locale isolation — see the $coroutine_locale_isolation docblock. Asserted at App::run() boot wiring (pre-fork, so a boot-time setlocale() becomes the baseline); setting here records the intent.

public static coroutineLocaleIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

coroutineMbencIsolation()

Per-coroutine mb-internal-encoding isolation — see the $coroutine_mbenc_isolation docblock. Asserted at App::run() boot wiring.

public static coroutineMbencIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

coroutineStaticsIsolation()

Stage 5 per-coroutine function-static isolation. Opt-in — see $coroutine_statics_isolation docblock for the perf tradeoff. The ext C-level flag is asserted at App::run() boot wiring (alongside the other isolation knobs) so the scheduler hooks are guaranteed installed first; setting it here only records the intent.

public static coroutineStaticsIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

coroutineTimezoneIsolation()

Per-coroutine default-timezone isolation — see the $coroutine_tz_isolation docblock. Asserted at App::run() boot wiring.

public static coroutineTimezoneIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

coroutineUmaskIsolation()

Per-coroutine umask isolation — see the $coroutine_umask_isolation docblock. Asserted at App::run() boot wiring (pre-fork baseline).

public static coroutineUmaskIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

decodeUntilStable()

Percent-decode a path repeatedly until it stops changing.

public static decodeUntilStable(string $path[, int $maxIterations = 10 ]) : string

Apache normalises before each access check, so a double-encoded payload like %252e%252e (which decodes once to %2e%2e, then again to ..) is caught. A single rawurldecode() only peels one layer — leaving the traversal sequence intact after the first decode. Decoding until stable closes that gap. The iteration count is capped so a pathological input (%2525252525…) can't spin the CPU; once the cap is hit we return the partially-decoded form and let the caller's traversal/null-byte checks run against it (any surviving ../% is treated conservatively).

Parameters
$path : string
$maxIterations : int = 10
Return values
string

defaultCharset()

Apache AddDefaultCharset. Server-wide default.

public static defaultCharset([string|null $charset = null ]) : string
Parameters
$charset : string|null = null
Return values
string

defaultMimeType()

Apache DefaultType / PHP default_mimetype. The Content-Type CharsetMiddleware applies to responses that don't set one. Pass '' to disable. No-arg call returns the current value.

public static defaultMimeType([string|null $type = null ]) : string
Parameters
$type : string|null = null
Return values
string

defaultStaticHandlerLocations()

The built-in default static_handler_locations — DIRECTORY entries only, every one trailing-slash terminated so OpenSwoole's raw string-prefix match is segment-bounded (a bare /js would steal /json).

public static defaultStaticHandlerLocations() : array<int, string>

#367 — FILE entries (/favicon.ico, /robots.txt) are deliberately EXCLUDED: a file can't take a trailing slash, so OpenSwoole's prefix match over-reaches (/favicon.ico steals /favicon.icoX, shadowing a user route like /robots.txt-generator). favicon.ico + robots.txt are served as ordinary public/ files by the framework's implicit file routes instead. Used as the default when the app hasn't set App::staticHandlerLocations().

Return values
array<int, string>

defineIsolation()

Per-request define() isolation. Opt-in — see $define_isolation docblock.

public static defineIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

delete()

public delete(string $path, callable|array<int|string, mixed> $handler[, array<string, mixed> $options = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void
Parameters
$path : string
$handler : callable|array<int|string, mixed>
$options : array<string, mixed> = []
$raw : bool = false
$middleware : array<int, MiddlewareInterface|string> = []
$backend : array<string, mixed>|string|null = null

describeRoutes()

Introspect the routing + middleware topology — the data behind the "middleware visualizer" (think Traefik's dashboard for your own routes).

public describeRoutes() : array{global: list, aliases: list, when: list}>, routes: list, path: string, middleware: list, handler: string}>}

For each registered route it reports the HTTP methods, path, the resolved per-route middleware chain (outer → inner), and a handler label; plus the global middleware chain (which wraps every route) and the registered named aliases.

Works before OR after App::run(): after boot each route's middleware is resolved to instances (class short-names); before boot, alias strings are shown verbatim. The global chain lists App::$middleware_wait_stack in execution order (first-added = outermost), with the router innermost.

Also reports the App::when path-scoped chains (when) — each {scope, middleware} pair, in registration order (first = outermost). These wrap every matching request (route or /api/*), so they are not tied to a single route row.

Return values
array{global: list, aliases: list, when: list}>, routes: list, path: string, middleware: list, handler: string}>}

devReload()

Enable/disable dev route hot-reload. When on, each worker polls the route/*.php mtimes and calls reloadRoutes() on change — "save file → routes update" with no process restart. A no-arg call returns the resolved value; null (the default) falls back to the ZEALPHP_DEV env var. OFF in production, where the route table stays master-loaded + COW-shared.

public static devReload([bool|null $enabled = null ]) : bool

Heads-up: route-file re-includes pick up edits only if opcache lets them — set opcache.validate_timestamps=1 (revalidate_freq=0) in dev, or rely on the per-reload opcache_invalidate().

Parameters
$enabled : bool|null = null
Return values
bool

directoryIndex()

public static directoryIndex([array<int, string>|null $files = null ]) : array<int, string>
Parameters
$files : array<int, string>|null = null
Return values
array<int, string>

directorySlash()

Apache DirectorySlash — redirect /foo/foo/ when foo is a directory.

public static directorySlash([bool|null $on = null ]) : bool

Default true. No-arg call returns the current value.

Parameters
$on : bool|null = null
Return values
bool

dispatchTaskCallback()

Dispatch payload for the $server->on('task', …) callback.

public static dispatchTaskCallback(array<int, mixed> $rest) : array{task: array, result: mixed}|false

OpenSwoole 22.x calls task handlers with TWO different signatures depending on task_enable_coroutine:

true → ($server, OpenSwoole\Server\Task $task) // 2-arg false → ($server, $id, $worker_id, $data) // 4-arg

Our default is task_enable_coroutine => true, so the 2-arg form is the production hot path; apps that opt back out get the 4-arg form. Accepting both shapes here means neither a user override nor an OpenSwoole minor-version shift can throw ArgumentCountError mid-worker. See issue #103.

Parameters
$rest : array<int, mixed>

Variadic args excluding $server.

Return values
array{task: array, result: mixed}|false

display_errors()

public static display_errors([bool $display_errors = true ]) : void
Parameters
$display_errors : bool = true

displayErrors()

Whether framework error pages render the captured exception and stack trace inline. Secure-by-default (#412): a one-arg call sets the value explicitly (and wins forever after); a no-arg call returns the resolved value — when never set explicitly, null falls back to the ZEALPHP_DEV env var, so production (env unset) returns false and never leaks traces.

public static displayErrors([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

documentRoot()

Apache DocumentRoot equivalent. Relative path → resolved against cwd; absolute path → used as-is. Drives App::include() resolution and the implicit-route file lookups.

public static documentRoot([string|null $path = null ]) : string
Parameters
$path : string|null = null
Return values
string

emitEffectiveStatus()

Emit the EFFECTIVE response status and return the code that reached the wire. Resolves the raw header("HTTP/x.x <code> <reason>") override (#327): when the request carries RequestContext::$raw_status_code, that code is emitted — with its verbatim reason when one was given, else the IANA phrase — exactly as Apache mod_php forwards an explicit status line. Without an override this is emitStatus() on the PSR status. Callers use the returned code for body-forbidding rules and access logging so they agree with the wire.

public static emitEffectiveStatus(Response $response, int $psrStatus) : int
Parameters
$response : Response
$psrStatus : int
Return values
int

emitGeneratorStream()

Stream a \Generator response chunk-by-chunk to the live OpenSwoole response (SSR streaming), returning an empty placeholder Response once done. Shared by the route dispatcher (dispatchMatched) and ZealAPI's runHandlerWithContract so both stream identically. HEAD sends headers only (no body). Assumes the caller has already discarded its output buffer.

public static emitGeneratorStream(Generator<string|int, mixed> $object, string $method) : Response
Parameters
$object : Generator<string|int, mixed>
$method : string
Return values
Response

emitStatus()

Set the response status via OpenSwoole's two-arg form so codes its native list doesn't recognise still emit correctly on the wire.

public static emitStatus(Response $response, int $status) : void

Empty reason → defer to OpenSwoole's default (which has its own built-in phrasing for the common codes).

Parameters
$response : Response
$status : int

enableCoroutine()

OpenSwoole's enable_coroutine server setting — whether each inbound HTTP request is auto-wrapped in its own coroutine. When false, requests run synchronously one at a time per worker (a worker handling request N blocks any other inbound request until N completes). When true, requests can yield on hooked I/O and other requests dispatched on the same worker make progress.

public static enableCoroutine([bool|null $on = null ]) : bool

Default coupling is !App::$superglobals — running coroutines in superglobals mode races the process-wide $_GET/$_POST/$_SESSION arrays across concurrent requests, the original bug ZealPHP's per-coroutine $g context was designed to avoid. Setting this to true while $superglobals=true is REFUSED — App::run() throws RuntimeException at boot (v0.2.27+).

null follows the default coupling.

Parameters
$on : bool|null = null
Return values
bool

exec()

Coroutine-safe command execution.

public static exec(string $cmd[, float|null $timeout = null ]) : array{output: string, code: int, signal: int}

Inside an OpenSwoole coroutine (Coroutine::getCid() >= 0) this yields to the scheduler via Coroutine\System::exec() instead of blocking the worker. Outside a coroutine (boot / CLI) it falls back to the blocking App::rawExec() implementation. Either way the return shape is the same.

Parameters
$cmd : string

Shell command to run.

$timeout : float|null = null

Coroutine-mode timeout in seconds (null = no timeout).

Return values
array{output: string, code: int, signal: int}

exitHookAdvisory()

Boot-time advisory for App::hookExit(true) set without a coroutine scheduler (#454). The ext-zealphp exit() interception is scheduler-bound: with enableCoroutine off (mixed / in-process-sync modes) a userland exit()/die() still throws OpenSwoole\ExitException (extends \Exception) — which a legacy try { … exit; } catch (\Exception) router swallows — NOT ZealPHP\HaltException. Forcing the hook on there is silently inert, so surface it at boot rather than let the developer believe exit() is protected. Returns the advisory string, or null when N/A; the null (auto) default follows the scheduler and never warns. Testable seam.

public static exitHookAdvisory(bool|null $forced, bool $hasScheduler) : string|null
Parameters
$forced : bool|null
$hasScheduler : bool
Return values
string|null

fatalGuardRelease()

#338 — release a response tracked by fatalGuardTrack().

public static fatalGuardRelease(int $id) : void
Parameters
$id : int

fatalGuardTrack()

#338 — track an in-flight raw response; returns the release key.

public static fatalGuardTrack(Response $response) : int
Parameters
$response : Response
Return values
int

fatalResponseGuard()

#338 — native shutdown callback (registered in registerAllOverrides() BEFORE the register_shutdown_function override installs). On a fatal, answers every in-flight connection with a minimal 500 — mod_php parity — instead of leaving clients to time out, and surfaces the fatal in the PHP error log (debug.log misses engine fatals; that silence is what made ext-zealphp#36 a multi-day hunt).

public static fatalResponseGuard() : void

Uses only engine-native calls: during shutdown the coroutine scheduler (and thus elog()'s async channel) may be unavailable.

fcgiAddress()

FastCGI backend address for App::cgiMode('fcgi') dispatch.

public static fcgiAddress([string|null $address = null ]) : string

Accepts "host:port" (TCP) or "unix:/path/to/fpm.sock" (Unix socket). No-arg call returns the current address; with-arg sets and returns it. Must be configured before App::run() — changing it mid-request has no effect.

Parameters
$address : string|null = null
Return values
string

fileETag()

Apache FileETag. false ⇒ ETagMiddleware emits no ETag and never 304s (FileETag None). No-arg call returns the current value.

public static fileETag([bool|null $enabled = null ]) : bool
Parameters
$enabled : bool|null = null
Return values
bool

formatAccessLogLine()

Render one access-log line for the current request using App::$access_log_format.

public static formatAccessLogLine(int $status, int $length[, float|null $durationSec = null ]) : string

Called by ZealPHP\access_log() — direct callers are rare but the helper is public so user code (e.g. a custom logger middleware) can reuse it.

The format spec is compiled to a token list on first use and cached on App::$access_log_format_compiled; accessLogFormat() clears the cache when the format string is changed.

Parameters
$status : int

Final HTTP status code (after handler + middleware)

$length : int

Response body byte count (0 OK; %b emits '-' per CLF)

$durationSec : float|null = null

Request duration in seconds; pass null when unknown

Return values
string

fragment()

Declare a named region inside a template — the htmx-essay "template fragment" pattern. The same template renders the full page when called via App::render('page', $args), and just the named region when called via App::render('page', ['fragment' => $name] + $args). One file, two responses — no separate partial file required.

public static fragment(string $name, callable $fn) : void

Three behaviours depending on the parent render's fragment selector:

  • selector is null (normal full-page render) → $fn() runs inline, its echo flows into the surrounding template, its return value is discarded (the parent render's return owns the universal contract).
  • selector matches $name → the page-shell buffer is cleared, $fn() runs, its return is captured, then HaltException short-circuits the rest of the template. executeFile() propagates the return so the closure can return 404; / return ['k'=>'v']; / yield a Generator just like a route handler.
  • selector is set but does not match $name → skipped silently.

Same return contract as every other entry point: int=status, array=JSON, string=HTML, Generator=stream, Closure=invoked-and-recursed, null=use buffered output. See template/pages/responses.php#return-contract.

Example — htmx-style row swap:

// template/contacts/list.php
<ul>
  <?php foreach ($contacts as $contact): ?>
    <?php App::fragment("contact-{$contact->id}", function() use ($contact) { ?>
      <li id="contact-<?= $contact->id ?>"><?= htmlspecialchars($contact->name) ?></li>
    <?php }); ?>
  <?php endforeach; ?>
</ul>

Full page: App::render('contacts/list', ['contacts' => $all]). Single row (htmx swap response, same template): App::render('contacts/list', ['contacts' => $all, 'fragment' => "contact-{$id}"]).

Parameters
$name : string
$fn : callable

functionIsolation()

Per-request function/class/include isolation. Opt-in — see $function_isolation docblock.

public static functionIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

get()

public get(string $path, callable|array<int|string, mixed> $handler[, array<string, mixed> $options = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void
Parameters
$path : string
$handler : callable|array<int|string, mixed>
$options : array<string, mixed> = []
$raw : bool = false
$middleware : array<int, MiddlewareInterface|string> = []
$backend : array<string, mixed>|string|null = null

getCurrentFile()

Returns the current executing script name without extenstion

public static getCurrentFile([string|null $file = null ]) : string
Parameters
$file : string|null = null
Return values
string

getErrorHandler()

public static getErrorHandler(int $status) : array{handler: callable, param_map: array, raw: bool}|null
Parameters
$status : int
Return values
array{handler: callable, param_map: array, raw: bool}|null

getFallback()

public static getFallback() : array<string, mixed>|null
Return values
array<string, mixed>|null

getServer()

Return the underlying OpenSwoole server. Use this when you need to push to a WebSocket client from a context that didn't receive $server as a callback argument — e.g. from an App::subscribe pub/sub handler, an App::tick timer, or a sidecar process registered via App::addProcess.

public static getServer() : Server|Server|null

Returns WebSocket\Server when any App::ws() route was registered (the framework upgrades from Http\Server automatically), Http\Server for pure HTTP apps, or null BEFORE App::run() constructs it.

$server = App::getServer();
if ($server instanceof \OpenSwoole\WebSocket\Server && $server->isEstablished($fd)) {
    $server->push($fd, $payload);
}

For cluster-wide pushes prefer WSRouter::sendToClient($clientId, $payload) — it owns the cross-node routing fabric so you don't have to thread $server references around your code.

Return values
Server|Server|null

globalScopeInclude()

Stage 8 global-scope request include (coroutine-legacy). A no-arg call returns the current setting; a one-arg call sets it. See the $global_scope_include docblock for the contract. Set BEFORE App::run().

public static globalScopeInclude([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

go()

Request-aware go() — spawns a child coroutine that INHERITS the current request's context ($g + the live superglobals, #42). Use inside handlers instead of raw go() whenever the child reads $g->server / $_SERVER / $_GET etc. Returns the child's coroutine id, or false if creation failed.

public static go(callable $fn, mixed ...$args) : int|false
Parameters
$fn : callable
$args : mixed
Return values
int|false

group()

Route group — apply a shared URL prefix and/or a shared middleware chain to many routes at once (Traefik chains, Slim/Laravel route groups).

public group(string $prefix[, array<int, MiddlewareInterface|string>|callable $middleware = [] ][, callable|null $registrar = null ]) : void
$app->group('/admin', ['auth', 'admin-only'], function ($g) {
    $g->route('/users',    fn() => User::all());
    $g->route('/settings', fn() => Settings::get());
});

The group middleware wraps outside any route-level middleware, which wrap outside the handler. Groups nest — an inner $g->group() composes its prefix and middleware onto the outer group's. The callback receives a RouteGroup whose route()/nsRoute()/nsPathRoute()/patternRoute()/group() mirror App's, transparently prepending the prefix and prepending the shared middleware.

$middleware may be omitted: $app->group('/admin', fn ($g) => ...).

Parameters
$prefix : string
$middleware : array<int, MiddlewareInterface|string>|callable = []
$registrar : callable|null = null

hookAll()

OpenSwoole\Runtime::enableCoroutine($flags) — process-wide PHP I/O hooks that make blocking calls (fopen, fread, curl, mysqli, etc.) yield to the coroutine scheduler instead of blocking the worker. PDO is intentionally NOT hooked in OpenSwoole 22.1 / 26.2 regardless of this flag — Doctrine queries always block.

public static hookAll([bool|int|null $on = null ]) : int

Default coupling is !App::$superglobals (HOOK_ALL when coroutine mode, 0 when superglobals mode). Hooked I/O in superglobals mode is unsafe — yields can expose process-wide superglobal mutations to other concurrent coroutines. App::run() throws RuntimeException at boot for that combination (v0.2.27+).

Accepts:

  • null → follow default coupling
  • trueHOOK_ALL
  • false0 (no hooks)
  • int → explicit flag bitmask (HOOK_TCP | HOOK_FILE | ...)

Returns the resolved int flag bitmask currently in effect.

Parameters
$on : bool|int|null = null
Return values
int

hookExec()

Toggle the coroutine-safe exec family hook (backtick / shell_exec / exec / system / passthru). Pass null (or no arg) to read the current value; pass a non-null value to set and return it. null = auto = follow coroutine mode (resolves to self::$superglobals === false) at run() time; overriding these built-ins routes them through coroutine-safe equivalents.

public static hookExec([bool|null $value = null ]) : bool|null
Parameters
$value : bool|null = null
Return values
bool|null

hookExit()

Toggle the ext-zealphp exit()/die()ZealPHP\HaltException interception (ext#47). Pass null (or no arg) to read; non-null to set.

public static hookExit([bool|null $value = null ]) : bool|null

null = auto = follow the coroutine scheduler (on when enableCoroutine is effective) at App::run(). See App::$hook_exit.

Parameters
$value : bool|null = null
Return values
bool|null

hostnameLookups()

Apache HostnameLookups. Default false — blocking DNS is a perf cost.

public static hostnameLookups([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

ignorePhpExt()

Whether URLs ending in .php are blocked with 403. Default true (Apache RewriteRule \.php$ - [F] parity). Set false to allow direct *.php routing.

public static ignorePhpExt([bool|null $on = null ]) : bool

No-arg call returns the current value.

Parameters
$on : bool|null = null
Return values
bool

include()

Run a public/ file with Apache document-root parity and the framework's universal return contract.

public static include(string $publicPath[, array<string, mixed> $args = [] ]) : mixed

Path resolution: $publicPath is relative to App::$document_root (defaults to "public"). Leading slash optional — '/article.php' and 'article.php' both resolve to public/article.php. Same convention as a URL path.

Security: includeCheck() rejects paths outside the document root and dotfile segments (when App::$block_dotfiles is on); refused paths return int(403) so ResponseMiddleware can render the right status.

Apache parity: $g->server['PHP_SELF'], SCRIPT_NAME, SCRIPT_FILENAME are auto-populated before include so the file sees canonical $_SERVER values — callers no longer need the 3-line preamble.

In superglobals mode (legacy apps) dispatches via cgiSubprocess(); in coroutine mode runs in-process via executeFile(). Return value is the same shape in both modes (the subprocess metadata channel carries it).

Parameters
$publicPath : string
$args : array<string, mixed> = []

Extracted into the file's scope (coroutine mode only)

Tags
see
App::executeFile()

(private core) and the sibling methods (render / renderToString / renderStream).

includeCheck()

Checks if the given file path is safe to serve/execute from the document root. Apache ap_directory_walk / resolve_symlink parity:

public includeCheck(mixed $abs_file) : bool
  • Symlink escape (CRITICAL): we canonicalize BOTH the file and the document root with realpath() and require boundary-aware containment. realpath() follows every symlink to its target, so a link inside docroot pointing outside (e.g. /etc/passwd) resolves to a path that fails the containment check and is refused. Apache refuses such links at the C level unless Options +FollowSymLinks is set; ZealPHP refuses them unconditionally on the PHP-served path.
  • Non-regular files: device nodes, FIFOs and sockets are refused (Apache request.c:1286-1292 — only REG/DIR pass the directory walk).
  • Dotfile segments (.git, .env, .htaccess, …) are refused when App::$block_dotfiles is on.

Honest limitation: this guard only covers the PHP-served path (App::include() / serveDirectory() / the implicit file routes). Assets under the OpenSwoole built-in static handler prefixes (static_handler_ locations — /css/, /js/, …) are served by OpenSwoole's C-level handler before any PHP runs and have no FollowSymLinks guard; keep those directories symlink-free in production, or disable enable_static_handler and route assets through PHP so this check applies.

Parameters
$abs_file : mixed

The candidate file path. Callers pass a realpath() result (string|false) or a raw path; the value is validated and re-canonicalized here.

Return values
bool

Returns true if the file is a regular file within the document root, false otherwise.

includeFile()

public static includeFile(string $path) : mixed

since 0.2.18 — use App::include() with a public-relative path.

Legacy alias kept for the WordPress showcase and existing user scaffolds. Accepts an absolute path. For paths under the document root, delegates to App::include() (security check + $_SERVER preamble apply). For paths outside (e.g. test fixtures, embedded utilities), passes straight to the shared core so the return contract applies but no security gate fires — matching the historical includeFile() behaviour.

Parameters
$path : string

includeIsolation()

Per-request require_once cache reset. See $include_isolation docblock.

public static includeIsolation([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

init()

Initializes the application.

public static init([string $host = '0.0.0.0' ][, int $port = 8080 ][, string $cwd = null ]) : App
Parameters
$host : string = '0.0.0.0'

The host address to bind to. Defaults to '0.0.0.0'.

$port : int = 8080

The port number to bind to. Defaults to 8080.

$cwd : string = null

The current working directory. Defaults to the directory of the script.

Return values
App

instance()

public static instance() : App|null
Return values
App|null

isEnotdir()

ENOTDIR detection for Apache parity (request.c:1244-1250 — "deny rather than assume not found"). When a path component that should be a directory is actually a regular file (e.g. /home.php/extra), Apache returns 403, not 404, deliberately refusing to leak whether the deeper path exists.

public static isEnotdir(string $absPath) : bool

realpath() collapses both ENOENT and ENOTDIR to false, so we walk the uncanonicalized path: if any non-final ancestor exists and is NOT a directory, the request hit ENOTDIR. Symlinks are followed by is_dir()/ is_file(), matching the kernel's traversal.

Parameters
$absPath : string

The non-canonical absolute path the request mapped to.

Return values
bool

isolation()

The single knob that says HOW a request is isolated — folds the (processIsolation × enableCoroutine × hookAll × cgiMode) cross-product into one intention-revealing value. Pure sugar over the existing fluent setters (they all keep working unchanged); accepts the App::ISOLATION_* constant, an Isolation enum case, or a bare string ("no strong").

public static isolation([Isolation|string|null $mode = null ]) : string

App::isolation(App::ISOLATION_COROUTINE); // canonical App::isolation(Isolation::CgiProc); // enum App::isolation('cgi-pool'); // bare string (BC)

Mapping (each just calls the existing setters, so no forcing rule ever fires): coroutine → processIsolation(false) + enableCoroutine(true) + hookAll(true) cgi-pool → processIsolation(true) + cgiMode('pool') + enableCoroutine(false) + hookAll(false) cgi-proc → processIsolation(true) + cgiMode('proc') + enableCoroutine(false) + hookAll(false) cgi-fcgi → processIsolation(true) + cgiMode('fcgi') + enableCoroutine(false) + hookAll(false) none → processIsolation(false) + enableCoroutine(false) + hookAll(false)

No-arg call returns the currently-resolved isolation as an App::ISOLATION_* string (derived from the resolved processIsolation/enableCoroutine knobs, so the default — process for superglobals(true), coroutine for superglobals(false) — is reported faithfully).

Parameters
$mode : Isolation|string|null = null
Return values
string

keepGlobals()

Keep $GLOBALS across requests within the worker. See $keep_globals docblock for the full semantics + when to use it.

public static keepGlobals([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

limitRequestFields()

Apache LimitRequestFields.

public static limitRequestFields([int|null $n = null ]) : int
Parameters
$n : int|null = null
Return values
int

limitRequestFieldSize()

Apache LimitRequestFieldSize. Maps to OpenSwoole http_header_buffer_size.

public static limitRequestFieldSize([int|null $n = null ]) : int
Parameters
$n : int|null = null
Return values
int

limitRequestLine()

Apache LimitRequestLine. Advisory; OpenSwoole's header buffer covers it.

public static limitRequestLine([int|null $n = null ]) : int
Parameters
$n : int|null = null
Return values
int

middleware()

The assembled PSR-15 middleware stack (built at boot from App::$middleware_wait_stack by buildMiddlewareStack()).

public static middleware() : StackHandler|null

Self-heals: if App::$middleware_stack reads back null but middleware was queued via addMiddleware(), the stack is re-assembled on the fly from the wait stack — base ResponseMiddleware (the router) then each queued middleware in first-registered-outermost order, identical to buildMiddlewareStack(). This is a defence-in-depth backstop: if a per-request class-static reset ever zeroes App::$middleware_stack after boot without an exempting snapshot (issue #227 — the root fix is the reset gate perRequestStateResetsActive()), the accessor still returns a working stack instead of null. The rebuilt stack is deliberately NOT written back to the static — under isolation the write would not persist to the next coroutine, and in normal modes a null here means "called before init()", which the rebuild already satisfies without masking that ordering mistake. In the normal post-run() path the static is non-null, so this branch never runs (zero overhead, no behavioural change).

Return values
StackHandler|null

middlewareAlias()

Register a named, reusable middleware — the "named & shared" middleware vocabulary from Traefik, the route-middleware alias from Laravel.

public static middlewareAlias(string $name, MiddlewareInterface|callable $factory) : void

The alias can then be referenced by name in the middleware: route option (or a route group) instead of constructing the instance inline:

App::middlewareAlias('auth',       fn() => new BasicAuthMiddleware($verifier));
App::middlewareAlias('admin-only', new IpAccessMiddleware(['allow' => ['10.0.0.0/8']]));
App::middlewareAlias('throttle',   fn($n = '60') => new RateLimitMiddleware(limit: (int)$n));

$app->route('/admin/users', middleware: ['auth', 'admin-only', 'throttle:120'],
    handler: fn() => User::all());

Pass either a ready MiddlewareInterface instance (reused as-is) or a factory callable that returns one. Factories run once at App::run() (boot, single-coroutine — safe), and the resulting instance is shared across every request that uses the alias. A parameterised reference 'throttle:120' calls the factory with the comma-split args (fn('120')), mirroring Laravel's throttle:60,1.

Middleware instances MUST be stateless — one object serves all concurrent coroutines; per-request state belongs in $g (RequestContext), never on the middleware object.

Parameters
$name : string
$factory : MiddlewareInterface|callable

mode()

High-level mode preset — sets BOTH axes (superglobals + isolation) in one call. Sugar over the fine-grained setters; accepts an App::MODE_* constant or a bare string ("no strong"). All the individual setters remain available to override afterwards.

public static mode(string $mode) : void

legacy-cgi → superglobals(true) + isolation(cgi-pool) [unmodified WordPress/Drupal] coroutine → superglobals(false) + isolation(coroutine) [modern ZealPHP apps — default shape] coroutine-legacy → superglobals(true) + isolation(coroutine) [legacy code, concurrent — Mode 4] + silentRedeclare + includeIsolation + coroutineGlobalsIsolation + coroutineStaticsIsolation (re-included/re-declared code survives; $GLOBALS AND function-local static $x isolate per coroutine) mixed → superglobals(true) + isolation(none) [Symfony / Laravel bridge — sequential]

Parameters
$mode : string

normalizeMiddlewareSpec()

Validate + flatten a per-route middleware spec into a list, WITHOUT resolving aliases (resolution is deferred to App::run() so an alias may be registered after the route that references it). A single instance or alias string is wrapped into a one-element list.

public static normalizeMiddlewareSpec(mixed $spec) : array<int, MiddlewareInterface|string>
Parameters
$spec : mixed
Return values
array<int, MiddlewareInterface|string>

normalizeRequestPath()

Normalise a request path the way Apache's ap_normalize_path() does (server/util.c): collapse runs of // to a single / (MergeSlashes, on by default), drop /./ segments, and unwind /segment/../ back over the preceding segment. A .. that would climb above root is dropped (clamped at /), matching Apache's behaviour for the routing path.

public static normalizeRequestPath(string $path) : string

Operates on an already percent-decoded, query-stripped path. Returns a path that always starts with / for absolute inputs; a * (OPTIONS asterisk-form) or empty input is returned unchanged.

Parameters
$path : string
Return values
string

normalizeUploadedFiles()

Transpose OpenSwoole's $request->files into PHP/mod_php-canonical $_FILES (issue #304).

public static normalizeUploadedFiles(array<string|int, mixed> $files) : array<string, mixed>

OpenSwoole delivers a repeated/array file field (files[]) in index-major shape — ['files' => [0 => ['name'=>…,'tmp_name'=>…,…], 1 => […]]] — while PHP's RFC 1867 parser publishes the field-major shape every PHP app codes against: ['files' => ['name'=>[0=>…,1=>…], 'type'=>[…], 'tmp_name'=>[…], 'error'=>[…], 'size'=>[…], 'full_path'=>[…]]]. A SINGLE file field stays flat but gains the PHP 8.1+ full_path key (defaulting to name when OpenSwoole doesn't surface it). Nested names (doc[main]) are transposed recursively so the per-key sub-array mirrors the field structure.

Pure function — no side effects; safe to unit-test directly.

Parameters
$files : array<string|int, mixed>

OpenSwoole's $request->files.

Return values
array<string, mixed>

Field-major $_FILES-shaped tree.

nsPathRoute()

nsPathRoute: Define a route under a namespace but allow the last parameter to capture everything (including slashes).

public nsPathRoute(string $namespace, string $path[, array<string, mixed>|callable $options = [] ][, callable|array<int|string, mixed>|null $handler = null ][, array<int, string> $methods = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void

Here we assume the route is something like $app->nsPathRoute('api', ...) and the actual route will be /api/{path} with {path} capturing all trailing segments.

Example:

$app->nsPathRoute('api', ['methods' => ['GET']], function($path) {
    return "Full path under /api: $path";
});

Accessing /api/devices/set_pref will set $path = "devices/set_pref".

Parameters
$namespace : string
$path : string
$options : array<string, mixed>|callable = []
$handler : callable|array<int|string, mixed>|null = null
$methods : array<int, string> = []

Named-arg form of $options['methods'] (HTTP verbs); merged into $options.

$raw : bool = false

Named-arg form of $options['raw'] (skip output buffering).

$middleware : array<int, MiddlewareInterface|string> = []

Named-arg form of $options['middleware'] — per-route PSR-15 middleware (instances or registered aliases); combined with $options['middleware'].

$backend : array<string, mixed>|string|null = null

Per-route CGI dispatch backend for this route's App::include() — a bare mode ('pool'/'proc'/'fork'/'fcgi'), a registered App::cgiBackendAlias() name, or an inline config array (['mode'=>'proc','interpreter'=>'/usr/bin/python3']). Named-arg form of $options['backend'] (named arg wins). Rejects lifecycle-mode names (coroutine/coroutine-legacy/...) — those are process-wide, not per-route.

nsRoute()

nsRoute: Define a route under a specific namespace.

public nsRoute(string $namespace, string $path[, array<string, mixed>|callable $options = [] ][, callable|array<int|string, mixed>|null $handler = null ][, array<int, string> $methods = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void

e.g. $app->nsRoute('api', '/users', ['methods' => ['GET']], fn() => "User list"); This will create a route at /api/users

Parameters
$namespace : string
$path : string
$options : array<string, mixed>|callable = []
$handler : callable|array<int|string, mixed>|null = null
$methods : array<int, string> = []

Named-arg form of $options['methods'] (HTTP verbs); merged into $options.

$raw : bool = false

Named-arg form of $options['raw'] (skip output buffering).

$middleware : array<int, MiddlewareInterface|string> = []

Named-arg form of $options['middleware'] — per-route PSR-15 middleware (instances or registered aliases); combined with $options['middleware'].

$backend : array<string, mixed>|string|null = null

Per-route CGI dispatch backend for this route's App::include() — a bare mode ('pool'/'proc'/'fork'/'fcgi'), a registered App::cgiBackendAlias() name, or an inline config array (['mode'=>'proc','interpreter'=>'/usr/bin/python3']). Named-arg form of $options['backend'] (named arg wins). Rejects lifecycle-mode names (coroutine/coroutine-legacy/...) — those are process-wide, not per-route.

offPubSub()

BC alias for unsubscribe(). See onPubSub docblock.

public static offPubSub(string $channelOrPattern[, callable|null $handler = null ]) : int
Parameters
$channelOrPattern : string
$handler : callable|null = null
Return values
int

onProcess()

BC alias for addProcess(). The on*-prefixed name was a misnomer (this method REGISTERS a process — it isn't an event). New code should call App::addProcess(); pairs symmetrically with OpenSwoole's $server->addProcess() API.

public static onProcess(string $name, callable $callable[, int $workers = 1 ][, bool $coroutine = true ]) : void
Parameters
$name : string
$callable : callable
$workers : int = 1
$coroutine : bool = true

onPubSub()

BC alias for subscribe(). The original on*-prefixed name was a misnomer — the act IS subscribing, not an event. New code should call App::subscribe() directly; pairs symmetrically with Store::publish().

public static onPubSub(string $channelOrPattern, callable $handler) : void
Parameters
$channelOrPattern : string
$handler : callable

onReliableMessage()

BC alias for subscribeReliable(). See onPubSub docblock.

public static onReliableMessage(string $stream, callable $handler[, string|null $group = null ][, int $blockMs = 1000 ][, int $batchSize = 16 ]) : void
Parameters
$stream : string
$handler : callable
$group : string|null = null
$blockMs : int = 1000
$batchSize : int = 16

onSignal()

Register a signal handler. Fires in the master process by default; pass $workerOnly=true to fire only inside workers.

public static onSignal(int $signal, callable $handler[, bool $workerOnly = false ]) : void

Multiple handlers per signal allowed (called in registration order).

Built on OpenSwoole\Process::signal(). Common use cases:

  • SIGHUP → config reload
  • SIGUSR1 → stats dump
  • SIGUSR2 → debug snapshot
App::onSignal(SIGHUP, function (): void {
    // reload routing or config
});

Must be called BEFORE App::run().

Parameters
$signal : int
$handler : callable
$workerOnly : bool = false

onWorkerStart()

Register a callback to run inside every worker's workerStart event.

public static onWorkerStart(callable $fn) : void

Use this to start per-worker timers, warm caches, open connections, etc. Called as: $fn($server, $workerId)

Parameters
$fn : callable

onWorkerStop()

Register a per-worker shutdown hook. Runs inside the worker process when it exits (max_request recycle, graceful shutdown, or reload), BEFORE the process terminates — the reliable place to flush per-worker state (counters, buffered I/O, coverage dumps). Unlike register_shutdown_function, this fires on OpenSwoole's signal-driven worker stop.

public static onWorkerStop(callable $fn) : void

Called as: $fn($server, $workerId)

Parameters
$fn : callable

opcacheLegacyAdvisory()

Build the opcache + coroutine-legacy boot advisory string. Split out of opcacheLegacyBootCheck() as a pure (opcache-independent) seam so both dups_fix branches are unit-testable without opcache enabled in the SAPI.

public static opcacheLegacyAdvisory(bool $dupsFix, string $docRoot) : string
Parameters
$dupsFix : bool

whether opcache.dups_fix is on (the CLASS case is then handled)

$docRoot : string

document-root (already rtrim'd), used in the blacklist hint

Return values
string

opcacheLegacyBootCheck()

public static opcacheLegacyBootCheck() : string|null
Return values
string|null

options()

public options(string $path, callable|array<int|string, mixed> $handler[, array<string, mixed> $options = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void
Parameters
$path : string
$handler : callable|array<int|string, mixed>
$options : array<string, mixed> = []
$raw : bool = false
$middleware : array<int, MiddlewareInterface|string> = []
$backend : array<string, mixed>|string|null = null

parallel()

Fork-join helper — runs every closure in $tasks in its own coroutine in parallel and returns the results in input order.

public static parallel(array<int, callable(): T$tasks) : array<int, T|null>

Call inside a coroutine context (request handler, onWorkerStart, etc) — outside one, the call is wrapped in Coroutine::run() so sync-mode callers also work. Each task gets its own coroutine via go(); the caller blocks on a WaitGroup until all finish.

Exceptions in tasks propagate as null in the result slot AND the exception is re-thrown via array_walk from the caller's coroutine — failing fast on the first error.

Parameters
$tasks : array<int, callable(): T>
Tags
template
Return values
array<int, T|null>

parallelLimit()

Bounded fan-out — runs $fn over each item with at most $concurrency in-flight coroutines at a time. Results keyed by the input's original keys.

public static parallelLimit(array<string|int, mixed> $items, callable(V, K=): R $fn[, int $concurrency = 10 ]) : array<string|int, mixed>
Parameters
$items : array<string|int, mixed>
$fn : callable(V, K=): R
$concurrency : int = 10
Tags
template
Return values
array<string|int, mixed>

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}

Thin public delegating shim — the implementation moved to Dispatcher::parseCgiResponse() (Phase 2 refactor). Kept on App for BC so external callers/tests reach it via App::parseCgiResponse().

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

parseCookieHeader()

Parse a raw Cookie: header through PHP's cookie treat-data semantics (issue #305) — the same php_default_treat_data routine PHP applies to the query string, but with the cookie value-decoding rule:

public static parseCookieHeader(string $raw) : array<string, mixed>
  • Pairs split on ;; each pair split on its FIRST =. The name is left-trimmed; the value is everything after the first =.
  • Cookie NAMES get legacy mangling — leading whitespace stripped, . and space → _, and name[] / name[k] build nested arrays.
  • Cookie VALUES are %XX-decoded per RFC 6265 ONLY — a literal + is NOT turned into a space (that +→space rule is form-urlencoded-only).

Implementation re-uses PHP's parse_str() for the bracket-nesting + name-mangling, protecting each value's + / & / = so the assembled query survives the urldecode parse_str applies.

Pure function — no side effects; safe to unit-test directly.

Parameters
$raw : string
Return values
array<string, mixed>

PHP-canonical $_COOKIE tree.

parseCss()

Parses the given CSS file.

public static parseCss(string $file) : array<string, array<string, string>>
Parameters
$file : string

The path to the CSS file to be parsed.

Return values
array<string, array<string, string>>

The parsed CSS rules as an associative array.

patch()

public patch(string $path, callable|array<int|string, mixed> $handler[, array<string, mixed> $options = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void
Parameters
$path : string
$handler : callable|array<int|string, mixed>
$options : array<string, mixed> = []
$raw : bool = false
$middleware : array<int, MiddlewareInterface|string> = []
$backend : array<string, mixed>|string|null = null

pathInfo()

Apache PATH_INFO — expose the path suffix after a script name as PATH_INFO in $_SERVER (e.g. /script.php/extra/pathPATH_INFO=/extra/path).

public static pathInfo([bool|null $on = null ]) : bool

Default true. No-arg call returns the current value.

Parameters
$on : bool|null = null
Return values
bool

pathWithinRoot()

Boundary-aware containment test: is $candidate the same path as $root, or a descendant of it?

public static pathWithinRoot(string $candidate, string $root) : bool

Both arguments are expected to already be canonical (realpath'd) absolute paths — this is the pure decision the symlink-escape guard hangs on, kept separate so it can be unit-tested without a filesystem.

A plain strpos($candidate, $root) === 0 prefix match is unsafe: docroot /var/www/public would wrongly accept the sibling /var/www/public-data (shared string prefix, different directory). We require either an exact match or that $candidate begins with $root followed by the directory separator, so only true descendants pass.

Parameters
$candidate : string

Canonical absolute path under test.

$root : string

Canonical absolute document-root path (no trailing slash).

Return values
bool

patternRoute()

patternRoute: Allow full control of the pattern without {param} placeholders.

public patternRoute(string $regex[, array<string, mixed>|callable $options = [] ][, callable|array<int|string, mixed>|null $handler = null ][, array<int, string> $methods = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void

Here, the user provides a fully formed regex pattern (without anchors) and we anchor it internally. e.g. $app->patternRoute('/api/(.*)', ['methods'=>['GET']], fn() => "Pattern matched!"); This will match any route starting with /api/.

TODO: Allow users to provide variable names for the regex groups.

Parameters
$regex : string
$options : array<string, mixed>|callable = []
$handler : callable|array<int|string, mixed>|null = null
$methods : array<int, string> = []

Named-arg form of $options['methods'] (HTTP verbs); merged into $options.

$raw : bool = false

Named-arg form of $options['raw'] (skip output buffering).

$middleware : array<int, MiddlewareInterface|string> = []

Named-arg form of $options['middleware'] — per-route PSR-15 middleware (instances or registered aliases); combined with $options['middleware'].

$backend : array<string, mixed>|string|null = null

Per-route CGI dispatch backend for this route's App::include() — a bare mode ('pool'/'proc'/'fork'/'fcgi'), a registered App::cgiBackendAlias() name, or an inline config array (['mode'=>'proc','interpreter'=>'/usr/bin/python3']). Named-arg form of $options['backend'] (named arg wins). Rejects lifecycle-mode names (coroutine/coroutine-legacy/...) — those are process-wide, not per-route.

perRequestStateResetsActive()

True when the per-request state RESETS — zealphp_reset_request_rtcaches() / zealphp_reset_request_statics() / zealphp_reset_request_class_statics(), run in the session-manager finally block — are SAFE to execute.

public static perRequestStateResetsActive() : bool

Those resets restore user symbols to their boot template each request (the PHP-FPM "fresh process per request" contract). They are safe ONLY when the boot snapshot (zealphp_process_state_snapshot(), taken in onWorkerStart) exists to EXEMPT framework class statics — App::$routes, the middleware stack, Store/Counter backends, session handlers. That snapshot is gated on include- or function-isolation (App::run() boot wiring), so it is taken under coroutine-legacy (which enables includeIsolation(true)) but NOT under a bare silentRedeclare(true) that enables neither isolation.

Issue #227: gating the resets on $silent_redeclare ALONE let them fire under bare silentRedeclare(true) with no exempting snapshot, so the reset zeroed App::$middleware_stack (→ "handle() on null" on request 2+) and could heap-corrupt other framework statics. Requiring an active isolation (hence a snapshot) keeps the resets scoped to coroutine-legacy — exactly where they are intended and safe. Bare silentRedeclare(true) (the declare-opcode hook alone, for cron-worker redeclares) gets no resets, matching its documented "just the redeclare hook" contract.

Return values
bool

post()

public post(string $path, callable|array<int|string, mixed> $handler[, array<string, mixed> $options = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void
Parameters
$path : string
$handler : callable|array<int|string, mixed>
$options : array<string, mixed> = []
$raw : bool = false
$middleware : array<int, MiddlewareInterface|string> = []
$backend : array<string, mixed>|string|null = null

poweredByHeader()

Resolve the X-Powered-By header value for the current ServerTokens setting, or null when the header should be omitted. Consumed at the response-emission boundary; exposed for introspection/testing.

public static poweredByHeader() : string|null
Return values
string|null

preloadClasses()

Register classes to compile at worker start so they are never cold- autoloaded under request concurrency (coroutine-legacy mode). Call BEFORE App::run(). Idempotent; duplicates are harmless. See App::$preload_classes for the full rationale and the failure mode it prevents.

public static preloadClasses(class-string ...$classes) : void
Parameters
$classes : class-string

preloadClassmap()

Opt into warming the ENTIRE Composer classmap at worker start — the structural fix so a user app's own classes (autoloaded on demand inside handlers) are born LINKED, never compiled on the concurrent cold path.

public static preloadClassmap([bool $enable = true ]) : void

Call BEFORE App::run(). Requires composer dump-autoload --optimize for the classmap to be complete. See App::$preload_classmap.

Parameters
$enable : bool = true

preloadDir()

Register a source directory to warm at worker start: every class / interface / trait / enum declared under $dir (recursively) is autoloaded+linked single-coroutine before request concurrency. Use this for PSR-4 apps WITHOUT an optimized classmap, or any app whose own autoloader (not Composer's classmap) resolves these symbols. Call BEFORE App::run(). The symbol must still be resolvable by a registered autoloader — a pure require_once legacy app (no autoloader) won't warm this way; run such apps in legacy-cgi mode (no coroutine race) instead.

public static preloadDir(string $dir) : void
Parameters
$dir : string

prependToStreamable()

Combine a pre-yield buffered chunk with a Generator so the wire order is "echo first, then stream". Returns a new Generator that yields the buffered chunk before delegating to the original.

public static prependToStreamable(string $prefix, Generator $gen) : Generator
Parameters
$prefix : string
$gen : Generator
Return values
Generator

processIsolation()

Per-include CGI process isolation (Apache mod_php-style fresh process per file). When true (the default in superglobals mode), App::include() dispatches each .php file through cgi_worker.php via proc_open() — global state (defined classes, constants, ini_set, output handlers) is contained inside the subprocess. When false, runs in-process via executeFile() — saves the ~30-50ms proc_open + PHP startup + autoloader cost per call, but every include shares the worker's PHP arena.

public static processIsolation([bool|null $on = null ]) : bool

Set to false when the legacy code is well-behaved enough to coexist in a shared worker (Symfony, Laravel, modern PHP apps). Keep true for unmodified WordPress / Drupal where define()-heavy plugins assume a fresh process per request.

null (default) means "follow App::$superglobals" — preserves the historical pairing so callers that don't touch this knob see no behaviour change. App::run() resolves null into the backing $coproc_implicit_request_handler bool right before the server starts.

Parameters
$on : bool|null = null
Return values
bool

publish()

Fire-and-forget Redis pub/sub publish.

public static publish(string $channel, string $payload) : int

Scope = the entire cluster. When the Store backend is Redis, EVERY app instance on EVERY host that has App::subscribe'd to the channel receives the message — that's Redis pub/sub's native PUBLISH semantics. There's no "this server only" mode; route by channel name if you need per-server delivery (e.g. the ws:server:<id> pattern WSRouter::sendToClient uses).

Returns the receiver count Redis itself reported (typically subscribed workers × cluster instances). A return of 0 means no subscriber was listening at publish time. Throws StoreException on the Table backend (no pub/sub semantics; Table is single-server shared memory).

Pairs symmetrically with App::subscribe — "App publishes, App subscribes" — so the framework's pub/sub surface reads as one coherent API. Thin delegate to Store::publish (the lower-level primitive that owns the Redis I/O wire); use whichever shape reads better in your code.

// Cross-cluster broadcast — every subscribed worker on every host
// wakes up with this payload:
$count = App::publish('chat:42', json_encode(['user' => 'alice', 'msg' => 'hi']));
Parameters
$channel : string
$payload : string
Return values
int

publishReliable()

Reliable publish via Redis Streams (XADD) — at-least-once delivery via consumer groups. Returns the Redis-generated message id.

public static publishReliable(string $stream, string $payload[, int|null $maxLen = null ]) : string

Symmetric pair with App::subscribeReliable. Thin delegate to Store::publishReliable. Throws StoreException on Table backend.

Parameters
$stream : string
$payload : string
$maxLen : int|null = null
Return values
string

put()

public put(string $path, callable|array<int|string, mixed> $handler[, array<string, mixed> $options = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void
Parameters
$path : string
$handler : callable|array<int|string, mixed>
$options : array<string, mixed> = []
$raw : bool = false
$middleware : array<int, MiddlewareInterface|string> = []
$backend : array<string, mixed>|string|null = null

rawExec()

Raw blocking command execution via proc_open.

public static rawExec(string $cmd) : string|null

Deliberately uses proc_open — NOT shell_exec/exec/system/ passthru/popen — because those builtins are uopz-overridden by the CGI layer; routing through proc_open keeps this escape hatch recursion-safe regardless of those overrides.

Parameters
$cmd : string
Return values
string|null

Captured stdout, or null if the process failed to start.

reasonPhrase()

Look up an IANA reason phrase for the given status code. Used by emitStatus() to pass an explicit reason to OpenSwoole's two-arg $response->status($code, $reason) — required because the native one-arg form silently rejects codes missing from its internal C list (notably 451, even on ext 26.x), and the request emits HTTP 200 instead.

public static reasonPhrase(int $status) : string
Parameters
$status : int
Return values
string

reassertRotatedSessionId()

Re-assert a session id the session manager already rotated, over a freshly-parsed request-cookie map (#371, CWE-384 session fixation).

public static reassertRotatedSessionId(array<string, mixed> $cookie) : array<string, mixed>

The session manager (CoSessionManager/SessionManager) runs BEFORE the OnRequest superglobal populate. On a forged / strict-mode-rejected id it mints a fresh server id and records it in session_params['session_id'] (+ emits the rotated Set-Cookie). The populate then re-parses the RAW request cookie — which still carries the forged id — and writes it into $g->cookie, so a handler's session_start()zeal_session_id() would read the forged id and persist the session under the attacker's value. This re-asserts the manager's rotated id into the cookie map whenever it differs from the raw value (the only thing that makes them differ is a manager rotation, including the first-visit mint where the raw cookie is absent).

Parameters
$cookie : array<string, mixed>

freshly-parsed request cookie map

Return values
array<string, mixed>

rebindRequestInput()

Re-establish the request-input superglobals ($_GET / $_POST / $_COOKIE / $_SERVER / $_FILES / $_REQUEST) FROM the per-coroutine OpenSwoole request, in the coroutine that is about to read them. Called right before every handler / included-file dispatch in coroutine-legacy mode.

public static rebindRequestInput(RequestContext $g[, array<string, mixed>|null $serverOverlay = null ]) : void

WHY THIS EXISTS — coroutine-legacy populates these as PROCESS-GLOBAL arrays in the OnRequest closure (so unmodified $_GET['x'] code works). ext-zealphp's scheduler snapshots/restores them per coroutine across yields, but the snapshot is keyed by an OpenSwoole coroutine id that COLLIDES for requests multiplexed onto one shared connection coroutine (they all observe cid=2). Under request overlap, request B's populate overwrites request A's process-global $_GET before A's handler reads it → cross-request misroute (a response built for B answered to A).

The per-coroutine RequestContext ($g) and its zealphp_request ARE reliably isolated — stored via OpenSwoole\Coroutine::getContext(), not the colliding cid. Re-deriving the superglobals from $g->zealphp_request immediately before dispatch — with NO intervening yield — pins them to THIS request regardless of what a concurrent coroutine wrote to the process globals.

Uses ext-zealphp's zealphp_request_input_set, which writes BOTH EG(symbol_table) AND PG(http_globals) so the auto-global JIT (which reads PG) observes the corrected values too. No-op when coroutine-legacy isolation is off or the ext primitive is unavailable (the populate in the OnRequest closure already covers the non-overlapping single-worker case).

Parameters
$g : RequestContext
$serverOverlay : array<string, mixed>|null = null

Per-file $_SERVER keys (PHP_SELF / SCRIPT_NAME / SCRIPT_FILENAME) the caller set for an included file — these WIN over the request-derived rebuild so the included file sees its own canonical values, not the route's. Pass null at the route-dispatch boundary (pristine request $_SERVER).

refreshGlobalsBaseline()

Re-capture the per-coroutine $GLOBALS baseline from the current symbol table.

public static refreshGlobalsBaseline() : bool

Under coroutine-legacy, ext-zealphp snapshots the parent $GLOBALS baseline once — when coroutineGlobalsIsolation activates. Boot-time $GLOBALS writes that happen AFTER that (e.g. an app bootstrap include such as load.php at worker start) aren't in the baseline, so they'd be visible only to the FIRST request coroutine and vanish for every subsequent one once a yield resets $GLOBALS to the stale baseline (#26). The framework calls this once after the onWorkerStart hooks complete (where such bootstraps run) to fold those writes into the baseline; apps that populate $GLOBALS later in boot can call it explicitly afterwards.

No-op returning false when ext-zealphp lacks the function (< 0.3.33) or per-coroutine $GLOBALS isolation isn't active — always safe to call.

Return values
bool

True if the baseline was refreshed.

registerCgiBackend()

Register a per-extension CGI backend. Apache AddHandler/ProxyPassMatch + nginx fastcgi_pass-per-location parity.

public static registerCgiBackend(string $extension, array<string, mixed> $config) : void
Parameters
$extension : string

File extension including the dot, e.g. '.py', '.pl'.

$config : array<string, mixed>

'mode''pool' | 'proc' | 'fcgi' (required) 'interpreter' — full path to interpreter binary (proc mode only; null = direct exec via shebang) 'exec_paths' — URL path prefixes (e.g. '/cgi-bin'), NOT filesystem paths, where this extension may execute. Files outside these prefixes return 403 (Apache Options +ExecCGI parity). Passing a filesystem path (one that exists as a directory) throws \InvalidArgumentException — exec_paths are matched against the request URL, never the disk path. 'address' — FastCGI backend address, "host:port" or "unix:/path" (fcgi mode only) 'fcgi_params' — extra FCGI params merged into the CGI env after buildCgiEnv() (fcgi mode only)

Tags
throws
InvalidArgumentException

on invalid mode, fork-on-non-PHP, missing fcgi address, or an exec_paths entry that is a filesystem path rather than a URL prefix.

reloadRoutes()

**Hot-reload the route table from route/*.php WITHOUT restarting the worker process.** Restores the app.php-defined baseline (explicit routes + the alias / App::when registries), re-includes the route files (picking up edits — opcache is invalidated for them first), re-appends the framework's implicit routes in priority order, and rebuilds the dispatch table. Returns the new route count.

public reloadRoutes() : int

Scope (be precise): only route DEFINITIONS reload. app.php lifecycle config — mode/superglobals/worker-counts/the global middleware stack — is frozen at boot by OpenSwoole and is NOT affected. Infrastructure a route file wires at boot (Store::make, App::subscribe, App::onWorkerStart, App::addProcess, App::onSignal, timers) is NOT re-run: those calls detect App::$reloading and keep their boot registration; only route(), App::when(), App::middlewareAlias(), and App::ws() take effect.

Typically driven by the dev mtime-watcher (App::devReload(true)); also callable directly for a programmatic/CLI reload.

Return values
int

render()

Render a template with the provided data.

public static render([string $__template_file = 'index' ][, array<string, mixed> $__args = [] ][, string $__default_template_dir = 'template' ]) : mixed

Templates are looked up under ./template/ in the current working dir; PHP_SELF is consulted as a sub-directory prefix unless $tpl starts with /.

Return contract: see executeFile(). Templates may return int / array / string / Generator / Closure to participate in the universal contract.

Backwards compatibility: legacy callers expect render() to echo. When the template has no explicit return (the historical pattern in every public/*.php) the captured output is echoed back. Explicit non-void returns flow through to the caller unchanged.

Parameters
$__template_file : string = 'index'
$__args : array<string, mixed> = []
$__default_template_dir : string = 'template'
Tags
see
App::executeFile()

(private core) and the sibling methods (renderToString / renderStream / include).

renderError()

Render the response for an error status. Dispatches a user-registered handler if one exists (status-specific takes precedence over catch-all); otherwise returns the framework's default body (HTML or JSON per Accept).

public renderError(int $status[, Throwable|null $exception = null ]) : ResponseInterface

Handler exceptions are caught and logged — falls back to default body so a buggy 500 handler can't infinite-loop.

Parameters
$status : int
$exception : Throwable|null = null
Return values
ResponseInterface

renderHtmx()

htmx-aware render: return a fragment (partial) for an htmx request, the full page otherwise — a thin selector over {@see App::render()} that keeps the universal return contract and streaming intact (it does NOT touch executeFile(); it only chooses what to render).

public static renderHtmx(string $template[, array<string, mixed> $args = [] ][, string|null $fragmentName = null ][, string|null $fullPageTemplate = null ]) : mixed

The htmx "one URL, two responses" pattern, in one call:

  • htmx request (HX-Request: true) → render only the named region (via the App::fragment() mechanism), so the response is just the HTML that swaps in.
  • normal request → render the whole page shell.

Fragment selection for an htmx request:

  1. If $fragmentName is passed, that region is rendered.
  2. Otherwise the framework derives the region from the request: the HX-Target element id (a leading # is stripped), falling back to HX-Trigger-Name. If neither is present, the template is rendered with no fragment key — i.e. its bare partial output.

Called outside a request (no current zealphp_request), it falls back to the full-page path so server-side renders never break.

Two common shapes:

Same template, a App::fragment('results', …) region inside it:

// /search → full page; htmx (hx-target="#results") → just #results
$app->route('/search', fn() =>
    App::renderHtmx('search', ['q' => $q, 'hits' => $hits]));

A bare partial template for htmx + a separate full-page shell:

$app->route('/widget', fn() =>
    App::renderHtmx('widget/partial', ['w' => $w],
        fullPageTemplate: 'widget/page'));
Parameters
$template : string
$args : array<string, mixed> = []

Template args (param-injected as usual).

$fragmentName : string|null = null

Region to extract for htmx; null → derive from HX-Target / HX-Trigger-Name.

$fullPageTemplate : string|null = null

Template for non-htmx requests; null → $template.

Return values
mixed

The App::render() return value, riding the universal contract.

renderStream()

Render a template as a Generator. Streaming templates (return-a-Closure or return-a-Generator) yield directly; echo-style templates yield their buffered output once.

public static renderStream([string $__template_file = 'index' ][, array<string, mixed> $__args = [] ][, string $__default_template_dir = 'template' ]) : Generator

Compose multiple template streams with yield from:

return (function() {
    yield from App::renderStream('shell-open', ['title' => 'Users']);
    yield from App::renderStream('users/list', ['users' => $users]);
    yield from App::renderStream('shell-close');
})();
Parameters
$__template_file : string = 'index'
$__args : array<string, mixed> = []
$__default_template_dir : string = 'template'
Tags
see
App::executeFile()

(private core) and the sibling methods (render / renderToString / include).

Return values
Generator

renderToString()

Render a template and return the result as a string. Generators are consumed; Closures are invoked with param injection; arrays/objects are JSON-encoded.

public static renderToString([string $__template_file = 'index' ][, array<string, mixed> $__args = [] ][, string $__default_template_dir = 'template' ]) : string
Parameters
$__template_file : string = 'index'
$__args : array<string, mixed> = []
$__default_template_dir : string = 'template'
Tags
see
App::executeFile()

(private core) and the sibling methods (render / renderStream / include).

Return values
string

requestCookieMap()

mod_php-canonical cookie map for a request (issue #305).

public static requestCookieMap(Request $request) : array<string, mixed>

App::run() sets OpenSwoole's http_parse_cookie => false so OpenSwoole no longer parses cookies itself (its parser diverges from PHP — no array syntax, no ._ mangling, and a wrong +→space value decode). Instead it leaves the raw Cookie: header in $request->header['cookie'] and its own $request->cookie empty. This parses that raw header through parseCookieHeader() and WRITES THE RESULT BACK onto $request->cookie, so every consumer — the superglobal populate, both session managers' PHPSESSID lookup, WebSocket onOpen, and user handlers reading $request->cookie — sees the SAME PHP-canonical map.

Falls back to OpenSwoole's pre-parsed $request->cookie when there's no raw header (e.g. http_parse_cookie left on, or no Cookie sent). The write-back is idempotent — re-parsing the unchanged header is a no-op.

Parameters
$request : Request
Return values
array<string, mixed>

requestIsHttps()

Whether the request arrived over HTTPS. X-Forwarded-Proto is honoured ONLY when the immediate peer (REMOTE_ADDR) is a configured trusted proxy — parity with App::clientIp(). Public so the session layer (zeal_session_start's Secure-cookie auto-detect) shares this one gated source of truth instead of trusting the header from any client.

public static requestIsHttps(array<string, mixed> $srv) : bool
Parameters
$srv : array<string, mixed>
Return values
bool

resetCgiBackends()

Reset the CGI backend + ScriptAlias registries. Test-support helper — lets unit tests start from a clean registry without process recycling.

public static resetCgiBackends() : void

resolveActiveSessionHandler()

Resolve the configured session handler instance — the single source of truth the zeal_session_* overrides and both session managers consult.

public static resolveActiveSessionHandler() : SessionHandlerInterface|null

$session_handler is a string alias / instance / null. This memoises the resolution (one handler instance per worker) and returns it.

null preserves the framework file path (the historical observable default) — it is deliberately NOT promoted to TableSessionHandler here, so a session-wiring fix never silently changes the durability of an app that didn't configure a handler (#295). A non-null alias/instance is honoured: 'redis'/'table'/'file'/instance all wire through.

Returns null when unconfigured (callers keep their inline-file fallback).

Return values
SessionHandlerInterface|null

resolveCgiBackend()

Resolve the CGI backend config + execution permission for a given path.

public static resolveCgiBackend(string $absPath[, string $urlPath = '' ]) : array{backend: array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array, exec_paths?: array}, mayExecute: bool}

Resolution order (Apache parity):

  1. ScriptAlias prefixes ($cgi_script_aliases) — any file under a registered URL prefix is executable (mayExecute = true).
  2. Per-extension registry ($cgi_backends) — the backend is returned, but mayExecute is true only if the URL falls under one of the backend's exec_paths (ExecCGI scope).
  3. Unregistered — falls back to ['mode' => App::$cgi_mode] with mayExecute = false.
Parameters
$absPath : string
$urlPath : string = ''
Return values
array{backend: array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array, exec_paths?: array}, mayExecute: bool}

resolveCgiEnv()

Resolve the CGI subprocess-pool config from the environment at boot.

public static resolveCgiEnv() : void

Each variable is applied only when its knob was NOT set explicitly via the fluent setter, so precedence is: explicit code config > environment > hardcoded default — symmetric with ZEALPHP_WORKERSworker_num. Called once from App::run() in the master before workers fork; the resolved values COW-inherit into every worker and the CGI pools they lazily spawn. The testable seam (unit-tested without booting a server).

Env var Knob Setter
ZEALPHP_CGI_MODE cgi_mode cgiMode()
ZEALPHP_CGI_WORKERS cgi_pool_size cgiPoolSize()
ZEALPHP_CGI_MAX_REQUESTS cgi_pool_max_requests cgiPoolMaxRequests()
ZEALPHP_CGI_TIMEOUT cgi_timeout cgiTimeout()
ZEALPHP_FCGI_ADDRESS fcgi_address fcgiAddress()
ZEALPHP_CGI_FORK_MAX_CONCURRENT cgi_fork_max_concurrent cgiForkMaxConcurrent()

resolveDocumentRoot()

Resolve App::$document_root to an absolute path. Relative values are treated as ${App::$cwd}/$document_root; absolute values pass through.

public static resolveDocumentRoot() : string
Return values
string

resolveMaxRequest()

Resolve OpenSwoole's max_request from the raw ZEALPHP_MAX_REQUEST env value. #449 — getenv() returns false when unset and the string "0" when explicitly set to disable; the old getenv() ?: 100000 collapsed both to the default ("0" is falsy in ?:), so ZEALPHP_MAX_REQUEST=0 ("set 0 to disable", per docs/deployment.md) silently never reached the server. Test for presence (=== false) instead, so 0 is honoured (OpenSwoole: never recycle the worker).

public static resolveMaxRequest(string|false $env) : int
Parameters
$env : string|false

Raw getenv('ZEALPHP_MAX_REQUEST') result.

Return values
int

resolveWhenMiddleware()

Select the App::when middleware chain for a normalized request path — every matching scope's instances flattened in registration order (outermost first). Memoized per path; the registry is immutable after boot, so this never recomputes for a repeated path. Returns [] (the fast path) when nothing is registered or nothing matches.

public static resolveWhenMiddleware(string $normPath) : array<int, MiddlewareInterface>
Parameters
$normPath : string
Return values
array<int, MiddlewareInterface>

route()

Registers a route with the application.

public route(string $path[, array<string, mixed>|callable $options = [] ][, callable|array<int|string, mixed>|null $handler = null ][, array<int, string> $methods = [] ][, bool $raw = false ][, array<int, MiddlewareInterface|string> $middleware = [] ][, array<string, mixed>|string|null $backend = null ]) : void
Parameters
$path : string

The URL path pattern for the route. Flask-like {param} syntax can be used for named parameters.

$options : array<string, mixed>|callable = []
$handler : callable|array<int|string, mixed>|null = null
$methods : array<int, string> = []

Named-arg form of $options['methods'] (HTTP verbs); merged into $options.

$raw : bool = false

Named-arg form of $options['raw'] (skip output buffering).

$middleware : array<int, MiddlewareInterface|string> = []

Named-arg form of $options['middleware'] — per-route PSR-15 middleware (instances or registered aliases); combined with $options['middleware'].

$backend : array<string, mixed>|string|null = null

Per-route CGI dispatch backend for this route's App::include() — a bare mode ('pool'/'proc'/'fork'/'fcgi'), a registered App::cgiBackendAlias() name, or an inline config array (['mode'=>'proc','interpreter'=>'/usr/bin/python3']). Named-arg form of $options['backend'] (named arg wins). Rejects lifecycle-mode names (coroutine/coroutine-legacy/...) — those are process-wide, not per-route.

routes()

public routes() : array<int, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>
Return values
array<int, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>

routesByExactMethod()

public routesByExactMethod() : array<string, array<string, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>>
Return values
array<string, array<string, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>>

routesByMethod()

public routesByMethod() : array<string, array<int, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>>
Return values
array<string, array<int, MiddlewareInterface|string>, backend?: array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null}>>

run()

public run([array<string, mixed>|null $settings = null ]) : void
Parameters
$settings : array<string, mixed>|null = null

runRequestStaticsBeginRefresh()

Request-BEGIN function-static refresh (coroutine-legacy, #28).

public static runRequestStaticsBeginRefresh() : bool

The concurrency companion to the request-END zealphp_reset_request_statics(). The end-reset makes function-local static $x fresh-per-request SEQUENTIALLY, but under coroutine concurrency a peer can read a process-global static that another in-flight request mutated BEFORE that request's end-reset runs. The canonical break is WordPress's wp_start_object_cache(): its static $first_init is set false on the first run, so a concurrent peer reading that false skips wp_cache_init() and leaves its own $wp_object_cache null → "Call to a member function switch_to_blog() on null". Refreshing every registered static to its template at request begin makes THIS request coroutine's first read see the template; the per-yield static snapshot (S5a) then multiplexes per coroutine.

Gated identically to the end-resets (perRequestStateResetsActive()), and a no-op when the ext primitive is absent (older ext, or legacy-cgi/sync modes). Returns whether the refresh ran, so the call sites — CoSessionManager / SessionManager just before dispatch — stay one-liners and the behaviour is unit-testable without a live request. The ext primitive's refreshed-count return value is discarded (mirrors the sibling zealphp_reset_request_* calls), so no return-type plumbing crosses the ext boundary.

Return values
bool

True when the refresh ran (resets active + ext primitive present).

sapiName()

mod_php-parity SAPI name reported by the php_sapi_name() override.

public static sapiName([string|null $name = null ]) : string|null

No-arg call returns the current setting (null = report real PHP_SAPI); one-arg call opts in to a web SAPI string for legacy-app compatibility.

Parameters
$name : string|null = null
Return values
string|null

serveDirectory()

Apache DirectorySlash + DirectoryIndex behavior.

public serveDirectory(string $relDir, string $urlPrefix) : mixed

If the request hit a directory under public/, optionally 301-redirect to the trailing-slash form, then walk App::$directory_index until a file is found. .php files run via includeFile(); others are served via sendFile() (so Range/ETag work).

Returns: \Generator for streaming, int for status code, null when the route was handled inline (response already emitted), or false to indicate the directory has no servable index.

Parameters
$relDir : string
$urlPrefix : string
Return values
mixed

Generator|int|string|array|object|Closure|null|false — whatever App::include() returns for .php indexes, false when no index matched, null when a slash-redirect or sendFile was emitted.

serverAdmin()

Apache ServerAdmin. Contact email/identifier embedded in the framework's default error pages. Pass null (or '') to clear.

public static serverAdmin([string|null $admin = null ]) : string|null
Parameters
$admin : string|null = null
Return values
string|null

serverTokens()

Apache ServerTokens. Controls the X-Powered-By header detail.

public static serverTokens([string|null $tokens = null ]) : string

No-arg call returns the current setting. See App::$server_tokens.

Parameters
$tokens : string|null = null
Return values
string

sessionDataSize()

Max serialized session size in bytes for TableSessionHandler.

public static sessionDataSize([int|null $bytes = null ]) : int

See $session_data_size. Must be set BEFORE register() / App::run().

Parameters
$bytes : int|null = null
Return values
int

sessionHandler()

Session storage backend selector. See $session_handler docblock.

public static sessionHandler([string|SessionHandlerInterface|null $handler = null ]) : string|SessionHandlerInterface|null

Pass 'table', 'file', 'redis', or a SessionHandlerInterface instance. null (default) = the framework inline file path in ALL modes — #295: NOT auto-promoted to TableSessionHandler. Honored by both CoSessionManager and (since #295) SessionManager.

Parameters
$handler : string|SessionHandlerInterface|null = null
Return values
string|SessionHandlerInterface|null

sessionLifecycle()

Toggle ZealPHP's per-request session lifecycle. When disabled, the SessionManager / CoSessionManager OnRequest wrapper skips session_start / cookie emission / session write-close — request-context init (openswoole_request, zealphp_response, error-stack reset) still runs unconditionally. Use this when another framework (e.g. Symfony's NativeSessionStorage via the zealphp-symfony bridge) owns sessions and you don't want ZealPHP racing it for the PHPSESSID cookie. The zeal_session_* uopz overrides remain installed and callable from user code either way.

public static sessionLifecycle([bool|null $enabled = null ]) : bool
Parameters
$enabled : bool|null = null
Return values
bool

sessionMaxRows()

Max concurrent sessions in TableSessionHandler's OpenSwoole\Table.

public static sessionMaxRows([int|null $rows = null ]) : int

See $session_max_rows. Must be set BEFORE register() / App::run().

Parameters
$rows : int|null = null
Return values
int

sessionSavePath()

File-backing directory for session storage. Default /var/lib/php/sessions.

public static sessionSavePath([string|null $path = null ]) : string

Must be set BEFORE register() / App::run().

Parameters
$path : string|null = null
Return values
string

sessionStrictMode()

PHP session.use_strict_mode parity (#244). When on (the default), a client-supplied session id that loads an empty session is rotated to a fresh server-generated id, defeating session fixation. Pass false to accept client-supplied ids verbatim (only safe for multi-node setups without shared/sticky session storage — see App::$session_strict_mode).

public static sessionStrictMode([bool|null $on = null ]) : bool

No-arg call returns the current value.

Parameters
$on : bool|null = null
Return values
bool

sessionTtl()

Session TTL in seconds. Default 1440 (PHP's default). See $session_ttl.

public static sessionTtl([int|null $seconds = null ]) : int
Parameters
$seconds : int|null = null
Return values
int

setErrorHandler()

Register a custom error page handler — Apache's ErrorDocument equivalent.

public setErrorHandler(int|callable $statusOrHandler[, callable|null $handler = null ]) : void

Status-specific: $app->setErrorHandler(404, fn() => App::render('404')); Catch-all: $app->setErrorHandler(fn($status) => ...);

Handler signature supports param injection by name — any of: function() | function($status) | function($exception) | function($status, $exception, $request, $response)

Parameters
$statusOrHandler : int|callable
$handler : callable|null = null

setFallback()

Register a fallback handler for unmatched routes (like Apache's RewriteRule . /index.php [L]).

public setFallback(callable $handler) : void
Parameters
$handler : callable

silentRedeclare()

Stage 3 silent-redeclare. Opt-in — see $silent_redeclare docblock.

public static silentRedeclare([bool|null $on = null ]) : bool

When called, also flips the ext-zealphp C-level flag immediately if the function is available; the App::run() boot wiring re-asserts the flag for boot-after-set ordering.

Parameters
$on : bool|null = null
Return values
bool

staticHandlerLocations()

URL-prefix whitelist for static-file serving. Empty array (default) allows any path under document_root. When non-empty, only paths whose prefix matches one of the listed strings are served as static files; others fall through to route matching. No-arg call returns the current list.

public static staticHandlerLocations([array<int, string>|null $prefixes = null ]) : array<int, string>
Parameters
$prefixes : array<int, string>|null = null
Return values
array<int, string>

stats()

Aggregated framework health snapshot — backends, pool, workers, memory, uptime, plus per-subsystem counters (X-4). Designed for /healthz middleware exposure + Prometheus exposition (see App::onSchedule v0.3.0 P1.10 plan).

public static stats() : array<string, mixed>

Subsystems are queried defensively — each one is wrapped in a try/catch so a single subsystem's failure (e.g. WSRouter not initialised yet) doesn't take down /healthz.

Return values
array<string, mixed>

statusForbidsBody()

Whether a status code MUST be sent without a message body (RFC 7230 §3.3.2 / RFC 9110 §6.4.1): every 1xx informational response, plus 204 No Content and 304 Not Modified. For these, a server must emit neither a body nor a Content-Length / Content-Type header — a non-empty body is a framing violation that some clients treat as the start of the next response. The emit chokepoint uses this to drop any body a handler accidentally produced (#290).

public static statusForbidsBody(int $status) : bool
Parameters
$status : int
Return values
bool

stripTrailingSlash()

Apache RewriteCond %{REQUEST_FILENAME} !-d; RewriteRule ^(.+)/$ /$1 [R=301,L].

public static stripTrailingSlash([bool|null $on = null ]) : bool

Inverse of directorySlash(). When true, non-directory URIs ending in / 301-redirect to the no-slash form. Default off.

Parameters
$on : bool|null = null
Return values
bool

subscribe()

Register a Redis pub/sub handler — runs once for every message App::publish (or any other Redis client) sends on the channel.

public static subscribe(string $channelOrPattern, callable $handler) : void

Cluster-wide delivery. Once subscribed, THIS worker receives every message any app instance on any host publishes to the channel via the shared Redis. That's how cross-server WebSocket routing, federated chat rooms, and cluster-wide cache invalidation all work in ZealPHP — one process publishes, every subscribed process across the cluster picks it up.

Channels containing * are PSUBSCRIBE patterns (Redis glob); everything else is SUBSCRIBE exact. Multiple handlers per channel are allowed and all fire on each message.

Handler signature: function(string $payload, string $channel, ?string $pattern): void. Each invocation runs in its own go() so a slow handler can't block the next message. Throws inside the handler are caught + logged via elog.

Pairs symmetrically with App::publish ("App publishes, App subscribes"). The companion App::publish is a thin delegate to Store::publish — use whichever side of the layering reads better.

MUST be called BEFORE App::run() — calling after worker start is a documented no-op with an elog warning.

Requires the Redis backend (Store::defaultBackend('redis') OR ZEALPHP_STORE_BACKEND=redis env). If the backend is still Table at worker-start time, the subscriber is not spawned and a warning is logged (Table backend is single-server shared memory — no pub/sub semantics).

Parameters
$channelOrPattern : string
$handler : callable

subscribeReliable()

Register a Redis Streams consumer-group handler. At-least-once delivery via XREADGROUP. Handler signature: function(string $payload, string $messageId, string $stream, array $fields): bool Return true to XACK (message removed from pending). Return false OR throw to leave pending (retried on consumer recovery).

public static subscribeReliable(string $stream, callable $handler[, string|null $group = null ][, int $blockMs = 1000 ][, int $batchSize = 16 ]) : void

Default group name derives from canonicalHost() so all servers in a cluster share one group → round-robin load balancing across machines and workers.

Same backend requirement as subscribe().

Parameters
$stream : string
$handler : callable
$group : string|null = null
$blockMs : int = 1000
$batchSize : int = 16

superglobals()

Toggle the superglobals-mode lifecycle. See App::$superglobals for the full semantics. Must be called BEFORE App::run() — the method calls refuseAfterRun() and throws \RuntimeException if the server is already serving requests.

public static superglobals([bool $enable = true ]) : void
Parameters
$enable : bool = true

synthesizeRequestServerVars()

Synthesize the mod_php request-surface $_SERVER vars that OpenSwoole's raw $request->server omits or gets wrong (issue #306 + #307). Pure transform of an already-built server array — operates on the upper-cased keys buildServerVars() produces, so it is unit-testable in isolation:

public static synthesizeRequestServerVars(array<string, bool|float|int|string|null> $srv) : array<string, bool|float|int|string|null>
  • QUERY_STRING: always present; '' when the request has no query.
  • REQUEST_URI: full mod_php value — the original request target including the query string (#306). OpenSwoole delivers a path-only request_uri, so the query is re-appended here; the dispatch layer matches routes on a parse_url(PATH) of it, so carrying the query is safe.
  • CONTENT_TYPE / CONTENT_LENGTH: mirrored from the request body headers (HTTP_CONTENT_TYPE / HTTP_CONTENT_LENGTH) when present.
  • HTTP Basic/Digest auth (#307): Authorization: Basic <b64> decodes to PHP_AUTH_USER / PHP_AUTH_PW; Digest publishes PHP_AUTH_DIGEST. AUTH_TYPE is deliberately NOT set — mod_php only publishes it when an Apache auth module handles the request. HTTP_AUTHORIZATION is kept (Bearer flows rely on it).

PATH_INFO / PHP_SELF are NOT computed here — they depend on the matched .php script and are set where the script resolves (App::include() / the ResponseMiddleware PATH_INFO rewrite).

Parameters
$srv : array<string, bool|float|int|string|null>
Return values
array<string, bool|float|int|string|null>

tick()

Recurring timer: calls $fn every $ms milliseconds in this worker.

public static tick(int $ms, callable $fn) : int
Parameters
$ms : int
$fn : callable
Return values
int

traceEnabled()

Apache TraceEnable. Default OFF for security (XST attack vector).

public static traceEnabled([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

trustedProxies()

Trusted proxy CIDRs consulted by App::clientIp().

public static trustedProxies([array<int, string>|null $cidrs = null ]) : array<int, string>
Parameters
$cidrs : array<int, string>|null = null
Return values
array<int, string>

tryInclude()

Like App::include() but returns null instead of 403 when the requested file does not exist under the document root. Use for "try this file, fall through to something else if missing" patterns:

public static tryInclude(string $publicPath[, array<string, mixed> $args = [] ]) : mixed

$app->route('/{slug}', function($slug) use ($app) { $result = App::tryInclude("/articles/{$slug}.php"); if ($result === null) return App::tryInclude("/legacy/{$slug}.php") ?? 404; return $result; });

Security gating (dotfile/document-root checks) still applies — paths that exist but fail the security check return 403 just like include(). Only the "file missing" branch is rewritten to null.

Parameters
$publicPath : string
$args : array<string, mixed> = []

unsubscribe()

Unregister handlers for a channel/pattern. With $handler null, removes every registered handler for that channel. Returns the count removed.

public static unsubscribe(string $channelOrPattern[, callable|null $handler = null ]) : int
Parameters
$channelOrPattern : string
$handler : callable|null = null
Return values
int

useCanonicalName()

Apache UseCanonicalName. See $use_canonical_name docblock.

public static useCanonicalName([bool|null $on = null ]) : bool
Parameters
$on : bool|null = null
Return values
bool

usernameProvider()

Register a callback that ZealAPI::getUsername() consults.

public static usernameProvider([callable|null $fn = null ]) : callable|null

Signature: fn(): ?string. Default null → getUsername() returns null.

Parameters
$fn : callable|null = null
Return values
callable|null

when()

Scope a middleware chain to a URL **path** — the centralized, "think like Traefik" way to apply middleware to a slice of the site, **including the ZealAPI layer**. Because every request (route or api/** file) flows through the same stack and api/admin/x is just the URL /api/admin/x, one mechanism covers everything — there is no separate "api middleware".

public static when(string $pathPrefixOrRegex, MiddlewareInterface|string|array<int, MiddlewareInterface|string> $middleware) : void
App::middlewareAlias('auth', fn() => new BasicAuthMiddleware($verifier));

App::when('/',           ['request-id']);          // every request
App::when('/admin',      ['auth', 'admin-only']);  // /admin and /admin/*
App::when('/api/admin',  ['auth']);                // api/admin/*.php endpoints
App::when('/api/admin/users/delete', ['audit']);   // a single api endpoint
App::when('#^/api/v\d+/#', new CorsMiddleware());  // a PCRE scope

Scope syntax: a literal path prefix by default (matched on segment boundaries — /admin matches /admin and /admin/x but NOT /administrators); a PCRE when the string starts with #. '/' (or an empty string) matches everything. Regex scopes are matched unanchored (preg_match), so a guard intended for a subtree MUST anchor it — '#^/admin(/|$)#', not '#/admin#' (the latter also matches /x/badmin). Prefer a literal prefix unless you genuinely need a regex.

Accepts a MiddlewareInterface instance, a registered alias string (incl. parameterised 'throttle:120'), or a list mixing both.

Ordering: runs inside the request lifecycle after path normalization and after CORS/OPTIONS handling (so a when auth guard never blocks a preflight), wrapping route match + dispatch. Multiple when() registrations compose in registration order — first registered is outermost. The full per-request order is: global addMiddlewareApp::when → the route's own middleware: (or an api file's in-file $middleware) → handler; the response unwinds in reverse. A middleware that returns without calling the handler short-circuits.

Stateless contract: the resolved instance is shared across every concurrent request in scope — keep per-request state in $g (RequestContext), never on the middleware object.

Resolution (alias→instance) happens once at App::run(); the hot path only does a cheap, memoized path-prefix scan.

Parameters
$pathPrefixOrRegex : string
$middleware : MiddlewareInterface|string|array<int, MiddlewareInterface|string>

ws()

Register a WebSocket endpoint. Returns void — the OpenSwoole WebSocket\Server is owned by the framework lifecycle, not the route registration. To push to a client you have **two** ways to reach the server object:

public ws(string $path, callable $onMessage[, callable|null $onOpen = null ][, callable|null $onClose = null ]) : void
  1. Inside any callback — the first argument IS the server:

    $app->ws('/ws/chat',
        onMessage: function (\OpenSwoole\WebSocket\Server $server, $frame) {
            $server->push($frame->fd, "echo: {$frame->data}");
        },
    );
    
  2. From anywhere else (route handlers, App::subscribe handlers, sidecar processes, etc.) — call App::getServer():

    App::subscribe('chat:broadcast', function (string $payload) {
        $server = App::getServer();
        if ($server instanceof \OpenSwoole\WebSocket\Server) {
            foreach (yourLocalFds() as $fd) {
                if ($server->isEstablished($fd)) {
                    $server->push($fd, $payload);
                }
            }
        }
    });
    

For cluster-wide messaging across multiple ZealPHP processes, use the higher-level WSRouter::sendToClient($clientId, $payload)

  • WSRouter::room($name)->push($msg) — they handle the server-lookup + cross-node routing for you.
Parameters
$path : string

URI path, e.g. '/ws/chat'

$onMessage : callable

function(\OpenSwoole\WebSocket\Server $server, OpenSwoole\WebSocket\Frame $frame, G $g) — called for each message

$onOpen : callable|null = null

function(\OpenSwoole\WebSocket\Server $server, OpenSwoole\Http\Request $request, G $g) — called on connect

$onClose : callable|null = null

function(\OpenSwoole\WebSocket\Server $server, int $fd, G $g) — called on disconnect

wsRoutes()

public wsRoutes() : array<string, array{message: callable, open: callable|null, close: callable|null}>
Return values
array<string, array{message: callable, open: callable|null, close: callable|null}>

isExactRoutePath()

protected isExactRoutePath(string $path) : bool
Parameters
$path : string
Return values
bool

__clone()

private __clone() : mixed

__construct()

Private constructor — use App::init() to obtain the singleton instance.

private __construct([string $host = '0.0.0.0' ][, int $port = 8080 ][, string $cwd = __DIR__ ]) : mixed

Performs one-time per-process setup: validates that ext-zealphp or uopz is loaded, reads /etc/environment into $_ENV, captures the initial error_reporting() level, installs the process-level native error and exception handlers (which delegate to the per-coroutine RequestContext stack), primes PhpInfo::primeModuleText(), and calls registerAllOverrides() to replace PHP built-ins with ZealPHP's coroutine-safe equivalents.

Parameters
$host : string = '0.0.0.0'

Bind address (e.g. '0.0.0.0').

$port : int = 8080

TCP port.

$cwd : string = __DIR__

Project root — stored in App::$cwd.

Tags
throws
Exception

when neither ext-zealphp nor uopz is loaded.

activateIsolationRuntime()

Activate the ext-zealphp per-coroutine isolation runtime stack (superglobals / define / $GLOBALS / silent-redeclare / function-static / include isolation) and register the matching onWorkerStart hooks.

private activateIsolationRuntime(bool $enableCoroutine, int $hookFlags) : void

Extracted verbatim from App::run() (Phase 3 decomposition) — must run at the same point and read/write the same static state. Reads the two resolved lifecycle locals (enableCoroutine, hookFlags) passed in.

Parameters
$enableCoroutine : bool
$hookFlags : int

applyRouteBackend()

Apply the per-request route backend override (set by the matched route's backend: option in ResponseMiddleware::dispatchRoute()) over a resolveCgiBackend() result. A route that names a backend is itself the ExecCGI authorisation for its App::include(), so mayExecute is forced true. No override → the resolved backend passes through unchanged.

private static applyRouteBackend(array{backend: array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array, exec_paths?: array}, mayExecute: bool} $cgi) : array{backend: array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array, exec_paths?: array}, mayExecute: bool}
Parameters
$cgi : array{backend: array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array, exec_paths?: array}, mayExecute: bool}
Return values
array{backend: array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array, exec_paths?: array}, mayExecute: bool}

assertUrlPrefix()

Validate that an ExecCGI scope value is a URL path prefix, not a filesystem path. exec_paths / cgiScriptAlias prefixes are matched against the request URL (resolveCgiBackend()pathUnderPrefix()), so a filesystem path (e.g. '/var/www/cgi-bin') can never match the incoming URL and silently yields a bare 403 (GitHub #155). Fail fast at registration instead: reject anything that is not absolute (/-rooted) or that resolves to an existing directory on disk. A real URL prefix like '/cgi-bin' is not a directory on a normal host, so correct configs pass untouched.

private static assertUrlPrefix(string $value, string $label) : void
Parameters
$value : string
$label : string
Tags
throws
InvalidArgumentException

when $value looks like a filesystem path.

backendKind()

private static backendKind(object $backend) : string
Parameters
$backend : object
Return values
string

baseServerVars()

The static CGI/SAPI server vars mod_php exposes even OUTSIDE a request (PHP_SELF, SCRIPT_NAME, SCRIPT_FILENAME, REQUEST_URI, DOCUMENT_ROOT).

private static baseServerVars() : array<string, string>

Seeded into $g->server at worker start so app bootstrap that reads them at class-load — before the first request populates the real per-request values via buildServerVars() — doesn't hit "Undefined array key" (#270). buildServerVars() overlays the real per-request values on top.

Return values
array<string, string>

buildMiddlewareStack()

Reverse + add the queued middleware-wait-stack onto the live PSR-15 StackHandler. Extracted verbatim from App::run() (Phase 3) — runs at the same point, mutating self::$middleware_stack in the same first-registered- outermost order.

private static buildMiddlewareStack() : void

buildParamMap()

private buildParamMap(callable|array{0: object|string, 1: string} $handler) : array<int, array{name: string, has_default: bool, default: mixed}>
Parameters
$handler : callable|array{0: object|string, 1: string}
Return values
array<int, array{name: string, has_default: bool, default: mixed}>

buildServerVars()

Build the per-request $_SERVER array from an OpenSwoole request — mod_php parity. Merges $request->server (upper-cased keys), the HTTP_* header vars, and the constant CGI keys mod_php always provides (GATEWAY_INTERFACE, REQUEST_SCHEME, SCRIPT_FILENAME, SERVER_SOFTWARE, …).

private static buildServerVars(Request $request) : array<string, bool|float|int|string|null>

Single source of truth for $_SERVER: shared by the OnRequest populate path AND rebindRequestInput(), so both produce a byte-identical server array. Pure function of the request — no side effects, safe to call repeatedly within one request (the coroutine-legacy re-assert does).

Parameters
$request : Request
Return values
array<string, bool|float|int|string|null>

cidrContains()

private static cidrContains(string $cidr, string $ip) : bool
Parameters
$cidr : string
$ip : string
Return values
bool

clearHandlerHeaders()

Clear previously-accumulated response headers from a handler that then failed, keeping only the headers that Apache preserves across an error response (ap_send_error_response: apr_table_clear(r->headers_out) then re-instate headers required by HTTP protocol for specific status codes).

private clearHandlerHeaders(int $status) : void

Apache parity (http_protocol.c:1246-1292): Location — preserved from err_headers_out for redirect chains. WWW-Authenticate — preserved for 401 (mod_auth sets it in err_headers_out, http_request.c:604). Allow — Apache re-adds Allow for 405/501 inside ap_send_error_response after the table clear (http_protocol.c:1289-1292). We preserve any Allow header the framework set before calling renderError() (e.g. the 405 dispatch path) rather than clearing + re-adding.

Called at the top of renderError() so the policy applies to both custom handler dispatch and the default error body paths.

Parameters
$status : int

coerceToStream()

Coerce an executeFile() result to a Generator. Strings/scalars yield once; Generators yield-from; null yields nothing.

private static coerceToStream(mixed $result) : Generator
Parameters
$result : mixed
Return values
Generator

coerceToString()

Coerce an executeFile() result to a string. Generators are consumed and concatenated; arrays/objects are JSON-encoded; null becomes ''.

private static coerceToString(mixed $result) : string
Parameters
$result : mixed
Return values
string

collapseMappedIp()

Collapse an IPv4-mapped IPv6 address (::ffff:a.b.c.d) to its IPv4 form so an IPv4 CIDR matches a mapped peer (#433). Mirrors IpAccessMiddleware::normalizeIp byte-for-byte. Non-mapped input is returned unchanged.

private static collapseMappedIp(string $ip) : string
Parameters
$ip : string
Return values
string

compileAccessLogFormat()

Compile an Apache LogFormat string into a flat token list. Supported directive families (Apache mod_log_config subset): %h %l %u %t %r %s %>s %b %B %D %T %m %U %q %H %v %{NAME}i %{NAME}o %{NAME}e Unknown directives are passed through verbatim (Apache compatibility: mod_log_config logs '-' for unknown but compatibility matters less than surfacing typos to the operator).

private static compileAccessLogFormat(string $format) : array<int, array{kind: string, arg?: string}>
Parameters
$format : string
Return values
array<int, array{kind: string, arg?: string}>

compileRouteTable()

Resolve per-route + App::when middleware specs (alias → instance) and (re)build the method-indexed dispatch table. Idempotent — it resets the indexes first, so reloadRoutes() can call it to rebuild from scratch; at boot it runs exactly once.

private compileRouteTable() : void

defaultErrorResponse()

Default error body. Honors Accept: application/json for JSON envelope, otherwise emits HTML. Stack trace included only when App::$display_errors.

private defaultErrorResponse(int $status, Throwable|null $exception) : ResponseInterface
Parameters
$status : int
$exception : Throwable|null
Return values
ResponseInterface

executeFile()

Run a PHP file with the framework's universal return contract.

private static executeFile(string $absPath, array<string, mixed> $args) : mixed

Captures buffered output, then maps the included file's result: void+echo → buffered string return 404; (int) → int return ['ok' => true]; (array) → array return "html"; (string) → string (concatenated with prior echo) echo "shell"; return "body"; → "shellbody" return (function(){yield…})(); → Generator (prefixed with echo, if any) return function($req){yield…}; → Closure (param-injected at call site, then re-applied to result)

Throws bubble up to the caller — output buffer is dropped on throw so partial echo doesn't leak into the next response.

Parameters
$absPath : string

Already resolved absolute path

$args : array<string, mixed>

Extracted into the file's scope

fileEntryIsSingle()

True when $entry is a single PHP file struct (has a scalar/array tmp_name AND error directly on it), rather than an index/name-keyed group of nested file structs.

private static fileEntryIsSingle(array<string|int, mixed> $entry) : bool
Parameters
$entry : array<string|int, mixed>
Return values
bool

getFragmentState()

Read and narrow the current fragment-extraction state from $g->memo.

private static getFragmentState() : array{wanted: string, matched: bool, result: mixed}|null

$g->memo is array<string, mixed> so PHPStan can't see the shape of $g->memo['_fragment'] without help — this helper does the narrowing once and returns a typed array (or null when no fragment mode is set).

Return values
array{wanted: string, matched: bool, result: mixed}|null

globalScopeIncludeEffective()

Whether THIS request's App::include() should run at true global scope: the gate is on AND we're in coroutine-legacy (so the per-coroutine globals isolation stack is active). The ext-capability check (function_exists) is done inline at the executeFile() call site. Policy-only helper.

private static globalScopeIncludeEffective() : bool
Return values
bool

handlerDisplayName()

Human-readable label for a route handler — Class::method for an array callable, otherwise Closure/function.

private static handlerDisplayName(mixed $handler) : string
Parameters
$handler : mixed
Return values
string

includeRouteFiles()

Include the route/*.php files (the file-based route definitions).

private includeRouteFiles() : void

Re-runnable by reloadRoutes(); at boot it runs once.

installCoroutineAutoloadSerializer()

Coroutine-aware autoload serializer — the HAZARD-2 correctness fix for coroutine-legacy mode.

private static installCoroutineAutoloadSerializer() : void

THE RACE: under silentRedeclare the ext isolates EG(in_autoload) per coroutine, so two concurrent coroutines that both reference an as-yet- unloaded class each enter autoload, each compile it, and the first-wins merge orphans the loser class-entry. A loser-CE object can then escape to new, while the boot-compiled closure's type-hint cached the winner CE → TypeError: …must be of type X, X given → uncaught fatal → the worker dies → OpenSwoole respawns it → the fresh worker re-races its first concurrent batch → crash → an endless respawn cascade. (ASAN/Valgrind both confirm this is memory-safe — it is a class-identity correctness bug, not corruption.)

THE FIX: serialize autoload of any given class name so EXACTLY ONE coroutine compiles it; the rest wait and resolve to the single winner. The per-class gate channels live in a use-captured object — object property mutations bypass coroutineGlobalsIsolation/static isolation (which is why a $GLOBALS- or static-backed gate does NOT work here: each coroutine would see its own isolated copy and the gate would never actually be shared).

Installed per worker via onWorkerStart: it captures the autoloaders the app registered at bootstrap, unregisters them, and re-registers ONE wrapper that runs them under the gate. Generic — no Composer-specific API. Idempotent via a class-static flag (class statics are process-global; only function-local static $x is per-coroutine isolated).

invokeFallbackOrNotFound()

private invokeFallbackOrNotFound() : ResponseInterface
Return values
ResponseInterface

lifecycleBackendMessage()

The error shown when a route backend: (or a cgiBackendAlias) is given a process-wide lifecycle mode instead of a CGI dispatch strategy.

private static lifecycleBackendMessage(string $mode) : string
Parameters
$mode : string
Return values
string

logExecScopeMiss()

Emit a diagnostic when a request matched a registered CGI extension but fell outside its exec_paths scope (ExecCGI off → bare 403). Without this, a misconfigured exec_paths (e.g. a filesystem path that can never match the URL — GitHub #155) surfaces only as an opaque 403. Logs only for files whose extension HAS a registered backend, so unrelated unregistered-extension 403s stay quiet.

private static logExecScopeMiss(string $url, string $absPath) : void
Parameters
$url : string
$absPath : string

middlewareDisplayName()

Human-readable label for a middleware spec entry — the class short-name for an instance, the alias string (verbatim) for an unresolved reference.

private static middlewareDisplayName(mixed $mw) : string
Parameters
$mw : mixed
Return values
string

normalizeBackendConfig()

Normalise + validate one inline backend config array into the canonical shape (mode + the optional interpreter/address/fcgi_params). The single validation point for cgiBackendAlias() and resolveBackendSpec().

private static normalizeBackendConfig(array<string|int, mixed> $config) : array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}
Parameters
$config : array<string|int, mixed>
Tags
throws
InvalidArgumentException

on a lifecycle-mode name, an invalid mode, or fcgi without an address.

Return values
array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}

normalizeMethods()

Normalize a methods array (any shape) into a list of uppercase strings.

private static normalizeMethods(array<string|int, mixed> $methods) : array<int, string>
Parameters
$methods : array<string|int, mixed>
Return values
array<int, string>

normalizeSingleFileEntry()

Normalise one flat OpenSwoole file struct to PHP canonical shape, adding the PHP 8.1+ full_path key when absent (value = name).

private static normalizeSingleFileEntry(array<string|int, mixed> $entry) : array<string, mixed>
Parameters
$entry : array<string|int, mixed>
Return values
array<string, mixed>

overrideBuiltin()

Install a uopz or ext-zealphp override for a named PHP built-in function, routing calls to the given ZealPHP replacement. Uses zealphp_override() when ext-zealphp is loaded (preferred), falling back to uopz_set_return().

private static overrideBuiltin(string $name, callable-string $callable) : void

#457 — ext-zealphp denylists a small set of builtins it refuses to trampoline (session_set_save_handler); calling zealphp_override() on one emits an E_WARNING every boot and installs nothing. For those, skip the ext path and use uopz (which CAN override them — restoring the #295 custom-save-handler routing); if uopz is absent the override is simply skipped, with no warning.

Parameters
$name : string
$callable : callable-string

Fully-qualified ZealPHP replacement function name.

pathUnderPrefix()

Boundary-safe URL-prefix test. $url is "under" $prefix only when it equals the prefix exactly or begins with $prefix . '/' — so /cgi-bins does NOT match the /cgi-bin scope.

private static pathUnderPrefix(string $url, string $prefix) : bool
Parameters
$url : string
$prefix : string
Return values
bool

peerInTrustedProxies()

Match $ip against every entry in App::$trusted_proxies. Wrapper so the CIDR walk lives in one place; callers pass user-controlled input here so the per-entry guard inside cidrContains() is the only validation needed.

private static peerInTrustedProxies(string $ip) : bool
Parameters
$ip : string
Return values
bool

phpredisSubscribeWouldBlock()

H7 detection: would a phpredis SUBSCRIBE/PSUBSCRIBE block the whole worker?

private static phpredisSubscribeWouldBlock() : bool

phpredis's subscribe is a C-level blocking read that is only coroutinized under OpenSwoole\Runtime::HOOK_ALL. With HOOK_ALL off, a subscriber runner on phpredis would park the worker's single event loop forever. predis (pure-PHP socket) yields via the stream hooks regardless. Returns true when the resolved driver is phpredis AND HOOK_ALL is disabled — the signal for wirePubSubBoot() to force ['prefer' => 'predis'] on the runner.

Return values
bool

preloadRequestPathClasses()

Warm the per-request framework classes at worker start so the first concurrent request wave never autoloads them under coroutine overlap.

private static preloadRequestPathClasses() : void

The OnRequest closure and CoSessionManager instantiate the Request / Response wrappers and LazyServerRequest per request; cold concurrent autoload of these races to a transient "class not found" Fatal that hangs the client. Loaded here (worker start = single coroutine), they are defined before any request coroutine runs. See the onWorkerStart registration in run() for the full failure mode.

refuseAfterRun()

Throw if a lifecycle setter is being called after App::run() has started. The Session-manager class, OpenSwoole's enable_coroutine flag, and HOOK_ALL are all frozen at boot — mid-game mutation of $superglobals etc. leaves the framework in a partial state that races on $_GET/$_POST/$_SESSION. Boot-only is the contract.

private static refuseAfterRun(string $setter) : void

Called from superglobals(), processIsolation(), enableCoroutine(), and hookAll() setters when they receive a write (not a read).

Parameters
$setter : string
Tags
throws
RuntimeException

when called after App::run() has started.

registerAllOverrides()

Install all ZealPHP uopz/ext-zealphp built-in overrides in one shot.

private static registerAllOverrides() : void

Idempotent — guarded by $overridesRegistered so re-entrant calls (e.g. from tests that re-construct App) are no-ops. Covers response headers (header(), setcookie(), etc.), session functions (session_start() family), output control (flush(), ob_*), error handling (set_error_handler(), error_log()), and more. Called once from __construct().

registerImplicitRoutes()

Register the implicit framework routes (api dispatch, .php-ext block, dotfile block, index, CGI extension/ScriptAlias URL parity, and the public file/directory catch-alls).

private registerImplicitRoutes() : void

Extracted verbatim from App::run() (Phase 3). Registers in the SAME order via $this->route()/nsPathRoute()/patternRoute(), so the route-priority ordering — and the route_baseline array_slice() that follows in run() — are unchanged.

registerOnRequest()

Register the OnRequest event handler — the per-request entry point that populates RequestContext, runs the middleware stack, fires shutdown functions, and emits the response. Extracted verbatim from App::run() (Phase 3) — runs at the same point; the only captured locals are $server and the resolved session-manager class name.

private registerOnRequest(Server $server, CoSessionManager>|SessionManager> $sessionManager) : void
Parameters
$server : Server
$sessionManager : CoSessionManager>|SessionManager>

registerSessionGc()

Schedule the deterministic session garbage collector on a worker-0 timer.

private static registerSessionGc() : void

ZealPHP replaced PHP's probabilistic per-request session GC, so without this nothing ever reclaims expired sessions: default-storage sess_* files accumulate until inodes exhaust, and a leaked PHPSESSID stays replayable forever. Mirrors Cache::registerGc() — one worker (id 0) runs the sweep so N workers don't N-times-duplicate it. No-op when the session lifecycle is disabled (e.g. a Symfony/Laravel bridge owns sessions). Interval is env-tunable via ZEALPHP_SESSION_GC_INTERVAL (milliseconds, default 10 min); the max-lifetime is App::$session_ttl. Called from App::run() before $server->start().

registerTaskHandlers()

Register the task + finish OpenSwoole event handlers when task workers are configured. Extracted verbatim from App::run() (Phase 3) — runs at the same point, gated on the same effective_settings['task_worker_num'].

private registerTaskHandlers(Server $server, array<string, mixed> $effective_settings) : void
Parameters
$server : Server
$effective_settings : array<string, mixed>

registerWebSocketHandlers()

Register the WebSocket open/message/close/shutdown event handlers, sharing the per-worker fd → ws-path map across the closures. Extracted verbatim from App::run() (Phase 3); the $wsFdMap is local to these four closures.

private registerWebSocketHandlers(Server $server) : void
Parameters
$server : Server

registerWorkerStart()

Register the workerStart event handler — re-registers the php:// stream wrapper, fires user onWorkerStart hooks, and wires dev route hot-reload.

private registerWorkerStart(Server $server) : void

Extracted verbatim from App::run() (Phase 3); closes over $this for the dev-reload tick.

Parameters
$server : Server

registerWorkerStop()

Register the workerStop event handler — fires user onWorkerStop hooks then logs worker-recycle observability. Extracted verbatim from App::run() (Phase 3).

private registerWorkerStop(Server $server) : void
Parameters
$server : Server

renderAccessLogToken()

Render one compiled access-log token. Kept separate from the tokenizer so the hot path (per-request) only does the table-lookup half; the tokenize path runs once per format-string change.

private static renderAccessLogToken(array{kind: string, arg?: string} $token, RequestContext $g, int $status, int $length, float|null $durationSec) : string
Parameters
$token : array{kind: string, arg?: string}
$g : RequestContext
$status : int
$length : int
$durationSec : float|null
Return values
string

resolveBackendSpec()

Resolve a route backend: spec — a bare mode string, a registered alias name, or an inline config array — into a concrete backend config. null passes through (no backend). Called at route registration; the dispatch hot path never re-resolves.

private static resolveBackendSpec(array<string|int, mixed>|string $spec) : array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null
Parameters
$spec : array<string|int, mixed>|string
Tags
throws
InvalidArgumentException

on a lifecycle-mode name, an unknown alias, or an invalid inline config.

Return values
array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null

resolveClosureParams()

Resolve a Closure's parameters by name from $args, using each parameter's default value when the name is absent. Reflection is cached per file path so repeated calls (e.g. streaming templates yielded in a loop) pay only one reflection cost per worker.

private static resolveClosureParams(Closure $fn, array<string, mixed> $args, string $cacheKey) : array<int, mixed>
Parameters
$fn : Closure
$args : array<string, mixed>
$cacheKey : string
Return values
array<int, mixed>

resolveMiddleware()

Resolve one spec entry to an instance. An instance passes through; an alias string is looked up in the registry (name or name:arg1,arg2).

private static resolveMiddleware(MiddlewareInterface|string $entry) : MiddlewareInterface
Parameters
$entry : MiddlewareInterface|string
Return values
MiddlewareInterface

resolveTemplatePath()

Resolve a template-file name to an absolute path.

private static resolveTemplatePath(string $tpl, string $dir) : string

Lookup rules mirror the historical render() behaviour:

  • Leading slash ("/foo") = absolute lookup from $dir root
  • When the current request's PHP_SELF basename is a sub-directory under $dir, prefer "$dir/{basename}/$tpl.php"
  • Otherwise fall back to "$dir/$tpl.php"
Parameters
$tpl : string
$dir : string
Return values
string

restoreFragmentState()

Restore $g->memo['_fragment'] to its prior state. Called by executeFile() to undo fragment-mode setup for nested renders and on error paths. null means "no fragment mode was active before" — drop the slot entirely so the next App::fragment() call falls into the normal inline-render branch.

private static restoreFragmentState(mixed $previous) : void
Parameters
$previous : mixed

routeBackendSpec()

Combine the two ways a route can declare a backend — the 'backend' key in $options and the backend: named argument — and resolve to a concrete config. The named argument wins when both are present. Returns null (the fast path: no per-route backend) when neither is set.

private static routeBackendSpec(array<int|string, mixed> $options, array<string, mixed>|string|null $backendArg) : array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null
Parameters
$options : array<int|string, mixed>
$backendArg : array<string, mixed>|string|null
Return values
array{mode: string, interpreter?: string, address?: string, fcgi_params?: array}|null

routeFileWithTopLevelFunction()

The first route/*.php file that declares a top-level function (which cannot be re-included without a redeclaration fatal), or null if every route file is function-free and therefore hot-reloadable. The path is cwd-relative for logging. A $x = function(){} closure has no name and is not matched; a class method carrying a visibility keyword isn't either.

private static routeFileWithTopLevelFunction() : string|null
Return values
string|null

routeMiddlewareSpec()

Combine the two ways a route can declare middleware — the 'middleware' key of the $options array and the middleware: named argument — into a single normalized spec. When both are present the array-option entries run first (outermost). Stored on the route as the raw spec; resolved to instances at App::run().

private static routeMiddlewareSpec(array<int|string, mixed> $options, array<int, MiddlewareInterface|string> $middlewareArg) : array<int, MiddlewareInterface|string>
Parameters
$options : array<int|string, mixed>
$middlewareArg : array<int, MiddlewareInterface|string>
Return values
array<int, MiddlewareInterface|string>

runTasksSequentially()

Sequential fallback for parallel() when no coroutine scheduler is available (reactor worker in mixed / legacy-cgi — #429). Runs each task in input order; the first throw propagates immediately, matching parallel()'s fail-fast-on-first-error contract.

private static runTasksSequentially(array<int, callable(): T$tasks) : array<int, T>
Parameters
$tasks : array<int, callable(): T>
Tags
template
Return values
array<int, T>

runUserFile()

Run a user file (template / public page) in an ISOLATED scope and return its value (#458). The file is included HERE, not in executeFile(), so a page that reuses a common variable name — $g, $result, $output, $args, … — only shadows this helper's throwaway locals and never corrupts executeFile()'s framework state used after the include (which previously fatalled "Attempt to assign property _ob_floor on array" → 500 when a page did $g = …). Locals are $__zeal_-prefixed (and EXTR_SKIP keeps them safe during extract) to stay clear of app/template names.

private static runUserFile(string $__zeal_abs, array<string, mixed> $__zeal_args, object $__zeal_g, bool $__zeal_global) : mixed
Parameters
$__zeal_abs : string
$__zeal_args : array<string, mixed>

Template/route args, bound by name.

$__zeal_g : object

Request RequestContext (bound as $g).

$__zeal_global : bool

Run at true global scope (Stage 8).

safeStats()

private static safeStats(callable(): array<string, mixed> $fn) : array<string, mixed>
Parameters
$fn : callable(): array<string, mixed>
Return values
array<string, mixed>

serverNameFromHost()

Derive SERVER_NAME from the request Host header. SERVER_NAME is the server host NAME only (CGI/1.1, RFC 3875 §4.1.14) — the port belongs in SERVER_PORT. The Host header carries host[:port], so strip the port.

private static serverNameFromHost(string|null $host) : string

Bracketed IPv6 literals keep their brackets ([::1]:8080[::1], the canonical host form), and a trailing :segment is only treated as a port when numeric. A whitespace-only or absent Host falls back to the configured site host. Bracket-aware, mirroring HostRouterMiddleware::stripPort / Response::splitHostPort. (#459)

Parameters
$host : string|null
Return values
string

symbolDefined()

True if $name is already defined as a class, interface, or trait (autoload disabled). Extracted so the autoload serializer can re-test "is it loaded now?" after a concurrent load.

private static symbolDefined(string $name) : bool
Parameters
$name : string
Tags
phpstan-impure

— reads the process-global class/interface/trait tables, which a CONCURRENT coroutine's autoload mutates between calls; the result is NOT constant across repeated calls in the same scope (that is the whole point of the post-wait re-check in the serializer).

Return values
bool

symbolsInFile()

Extract fully-qualified class/interface/trait/enum names declared in a PHP file via the tokenizer — without executing it. Single namespace per file is the common case; multiple namespaces are handled.

private static symbolsInFile(string $path) : array<int, string>
Parameters
$path : string
Return values
array<int, string>

transposeFileGroup()

Transpose an index-major group of file structs (the OpenSwoole shape for files[] / doc[main]) into PHP's field-major layout. Recurses through nested name groups so ['main' => <struct>, 'thumb' => <struct>] yields ['name'=>['main'=>…,'thumb'=>…], 'tmp_name'=>[…], …].

private static transposeFileGroup(array<string|int, mixed> $group) : array<string, array<string|int, mixed>>
Parameters
$group : array<string|int, mixed>

Index/name-keyed file structs.

Return values
array<string, array<string|int, mixed>>

validateLifecycleCombination()

Validate lifecycle mode combinations at boot.

private static validateLifecycleCombination(bool $sg, int $hookFlags, bool $enableCo) : void

With ext-zealphp loaded, superglobals(true) + enableCoroutine(true) is now SAFE — the extension saves/restores $_GET/$_POST/$_SESSION per coroutine via zealphp_superglobals_save/restore(). This unlocks the "full superglobals + full coroutines" mode: legacy code using $_GET/$_SESSION just works, AND you get concurrent coroutine I/O.

Without ext-zealphp (uopz fallback), the old constraint applies: superglobals + coroutines would race process-wide arrays.

Parameters
$sg : bool
$hookFlags : int
$enableCo : bool
Tags
throws
RuntimeException

When an unsafe combination is used without ext-zealphp to make it safe.

warmBulkPreloads()

Bulk warming (whole Composer classmap + registered directory trees) — runs in the MASTER process before $server->start(), NOT in a worker coroutine. This is load-bearing: warming hundreds/thousands of arbitrary classes inside the coroutine onWorkerStart is unsafe — a class with load-time I/O (or anything the runtime hooks coroutinize) YIELDS, and the worker then accepts requests MID-WARMUP → cold concurrent compile → the duplicate-CE / unlinked race we are trying to avoid (empirically: a classmap warm in onWorkerStart reintroduced HAZARD-2 TypeErrors). The master has no coroutine scheduler, so every load is blocking + atomic; the warmed (linked) classes are then COW-forked into every worker. Same model as PHP's opcache.preload. No-op unless preloadClassmap()/preloadDir() registered something.

private static warmBulkPreloads() : void

warmClass()

Trigger one symbol's autoload+link, swallowing load errors (a class with an unmet dependency must not abort worker start). class_exists() fires the registered autoloader for classes/interfaces/traits/enums alike.

private static warmClass(string $name) : void
Parameters
$name : string

warmComposerClassmap()

Warm every class Composer's registered loaders know about. Iterates the classmap of each registered Composer\Autoload\ClassLoader and triggers its autoload (single-coroutine at worker start → whole hierarchy linked).

private static warmComposerClassmap() : void

warmDir()

Warm every PHP-declared symbol under a directory tree. Reads each .php file and extracts its namespace + class|interface|trait|enum names via the tokenizer (no file is executed), then triggers each symbol's autoload.

private static warmDir(string $dir) : void
Parameters
$dir : string

whenScopeMatches()

Whether an App::when scope matches a normalized path. Prefixes match on segment boundaries (the trailing / stops /admin matching /administrators); '/' matches all; regex scopes use preg_match.

private static whenScopeMatches(string $type, string $key, string $normPath) : bool
Parameters
$type : string
$key : string
$normPath : string
Return values
bool

wireProcessHandlers()

Internal: wire registered sidecar processes into the OpenSwoole server via $server->addProcess(). Called from App::run() after the server is constructed but before start().

private static wireProcessHandlers() : void

wirePubSubBoot()

One-time hook into onWorkerStart that builds + starts the RedisPubSub and RedisStreams runners based on what's in the registries. Re-callable; only wires once.

private static wirePubSubBoot() : void
On this page