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

Table of Contents

Constants

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.
REASON_PHRASES  : mixed = [ // 1xx Informational 100 => 'Continue', 101 =...
IANA-registered HTTP status reason phrases (RFC 9110 §15).

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
$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).
$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.
$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_backends  : array<string, array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array}>
Per-extension CGI backend registry. Apache AddHandler/ProxyPassMatch + nginx fastcgi_pass-per-location parity.
$cgi_mode  : string
How a process-isolated legacy include is dispatched, when processIsolation() is on:
$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.
$coproc_implicit_request_handler  : bool
$cwd  : string
$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
$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
$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'.
$file_etag  : bool
Apache FileETag. When false, ETagMiddleware emits no ETag header and never returns 304 (equivalent to FileETag None). Default true.
$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.
$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
$initial_error_reporting  : int
Initial error_reporting level captured at boot — referenced by the per-coroutine override.
$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_stack  : StackHandler|null
$middleware_wait_stack  : array<int, MiddlewareInterface>
$path_info  : bool
Apache PATH_INFO — when /script.php/extra/path, expose /extra/path as PATH_INFO.
$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.
$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
$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_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.).
$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
$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
$host  : string
$port  : int
$routes  : array<int, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>
$routes_by_exact_method  : array<string, array<string, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>>
$routes_by_method  : array<string, array<int, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>>
$workerStartedAt  : float
$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.
$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
$instance  : self|null

Methods

__wakeup()  : mixed
accessLogFormat()  : string
Apache LogFormat. Resets the compiled-spec cache on set.
addMiddleware()  : void
adminChecker()  : callable|null
Register a callback that ZealAPI::isAdmin() consults.
after()  : int
One-shot timer: calls $fn once after $ms milliseconds.
authChecker()  : callable|null
Register a callback that ZealAPI::isAuthenticated() consults.
blockDotfiles()  : bool
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.
cgiMode()  : string
Select how a process-isolated legacy include is dispatched: 'proc' (default) — fresh PHP per request via proc_open (full WordPress/Drupal compat).
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.
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.
directoryIndex()  : array<int, string>
directorySlash()  : bool
display_errors()  : void
displayErrors()  : bool
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.
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.
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.
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
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.
hostnameLookups()  : bool
Apache HostnameLookups. Default false — blocking DNS is a perf cost.
ignorePhpExt()  : bool
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
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.
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
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.
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.
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.
parseCss()  : array<string, array<string, string>>
Parses the given CSS file.
pathInfo()  : bool
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.
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.
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.
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.
registerCgiBackend()  : void
Register a per-extension CGI backend. Apache AddHandler/ProxyPassMatch + nginx fastcgi_pass-per-location parity.
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).
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.
resolveCgiBackend()  : array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array}
Resolve the CGI backend config for a given file path.
resolveDocumentRoot()  : string
Resolve App::$document_root to an absolute path. Relative values are treated as ${App::$cwd}/$document_root; absolute values pass through.
route()  : void
Registers a route with the application.
routes()  : array<int, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>
routesByExactMethod()  : array<string, array<string, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>>
routesByMethod()  : array<string, array<int, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>>
run()  : void
Runs the ZealPHP application.
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.
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.
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]).
staticHandlerLocations()  : array<int, string>
stripTrailingSlash()  : bool
Apache RewriteCond %{REQUEST_FILENAME} !-d; RewriteRule ^(.+)/$ /$1 [R=301,L].
superglobals()  : void
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:
useCanonicalName()  : bool
Apache UseCanonicalName. See $use_canonical_name docblock.
usernameProvider()  : callable|null
Register a callback that ZealAPI::getUsername() consults.
ws()  : void
Register a WebSocket endpoint.
wsRoutes()  : array<string, array{message: callable, open: callable|null, close: callable|null}>
isExactRoutePath()  : bool
parseCliArgs()  : array<string, mixed>
__clone()  : mixed
__construct()  : mixed
buildParamMap()  : array<int, array{name: string, has_default: bool, default: mixed}>
cgiFcgi()  : mixed
Dispatch a legacy include to a FastCGI backend (e.g. php-fpm) via the FCGI binary protocol. Used when App::cgiMode() === 'fcgi'.
cgiFork()  : mixed
Warm-fork variant of cgiSubprocess(): instead of proc_open()-ing a fresh PHP interpreter per request, OpenSwoole\Process forks the already-booted worker (copy-on-write — the interpreter, Composer autoloader, and opcache are inherited, so PHP startup + autoload are NOT re-paid). ~5× faster than 'proc' mode on a trivial file.
cgiSubprocess()  : mixed
Run a PHP file in a separate process at true global scope (CGI-style).
cidrContains()  : bool
Does $ip fall within $cidr? Supports IPv4 and IPv6. A bare IP without /prefix is treated as a single-host range (/32 v4, /128 v6). Returns false on any parse failure rather than throwing — defensive for header- sourced input.
claimOrphanIfAny()  : bool
Recover from an orphaned-daemon situation: pid file missing or stale but the port is still held. If the listener is a ZealPHP process, graceful-then-force kill it. Returns true when an orphan was cleaned up, false when there's nothing to do (port free, or held by something that isn't ours).
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).
cliHelp()  : void
cliLogs()  : void
cliStatus()  : void
cliStatusOne()  : void
cliStop()  : void
cliStopAuto()  : void
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 ''.
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).
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.
extractPortFromPidFile()  : int
Pull the port number from a default-shaped pid file path like /tmp/zealphp/zealphp_8080.pid. Returns 0 when the caller passed a --pid-file override that doesn't match the convention.
findPortOwnerPid()  : int|null
Returns the PID listening on $port, or null when nothing's listening or it can't be determined (non-Linux, /proc unreadable). Linux-only: /proc/net/tcp + tcp6 give the LISTEN-state socket inode; /proc/[pid]/fd/* resolves inode → owner pid. We deliberately avoid stream_socket_server / socket_bind here — those are intercepted by OpenSwoole's runtime hook (HOOK_ALL) and become coroutine-only, which would crash this CLI path.
forkStartupReporter()  : void
For daemonized start/restart: fork so the terminal-attached parent polls for the new daemon's PID file and prints a confirmation line BEFORE the shell prompt returns, while the child goes on to boot the (self-daemonizing) server. The parent never touches OpenSwoole — it only watches the PID file and exits, so the confirmation is always the last thing written to the terminal (fixes the issue #17 race where the prompt returned first and the message overlapped the next command).
getFragmentState()  : array{wanted: string, matched: bool, result: mixed}|null
Read and narrow the current fragment-extraction state from $g->memo.
invokeFallbackOrNotFound()  : ResponseInterface
normalizeMethods()  : array<int, string>
Normalize a methods array (any shape) into a list of uppercase strings.
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.
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.
processIsZealphp()  : bool
True when /proc/$pid/cmdline looks like a ZealPHP daemon: argv[0] is a real php binary (php, php8.3, etc.) AND the args reference app.php.
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.
requestIsHttps()  : bool
Detect whether the request arrived over TLS, for deriving REQUEST_SCHEME / HTTPS in the $_SERVER builder. Mirrors the session-cookie secure detection (src/Session/utils.php): a direct HTTPS=on, an X-Forwarded-Proto: https from a proxy, or SERVER_PORT 443.
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.
resolvePidFile()  : string
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.
validateLifecycleCombination()  : void
Refuse to start with lifecycle combinations that race process-wide superglobals across concurrent coroutines.

Constants

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', ]

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).

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

public static callable|null $admin_checker = null

$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

$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.

$block_dotfiles

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

public static bool $block_dotfiles = true

$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_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}> $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_mode

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

public static string $cgi_mode = 'proc'

'proc' (default) — proc_open() spawns a FRESH PHP interpreter per request (src/cgi_worker.php). True global scope: top-level $x = ... is visible via global $x in functions, so unmodified WordPress/Drupal work. Cost: cold PHP startup + autoload per request (~tens of ms).

'fork'OpenSwoole\Process forks the already-booted worker (copy-on- write: warm interpreter, loaded autoloader, hot opcache). ~5× faster than 'proc' because nothing re-execs. TRADE-OFF: the file runs in the fork closure's FUNCTION scope, so bare top-level vars are NOT visible via the global keyword ($wpdb-style patterns break). Superglobals ($_GET/$_POST/$_SESSION/$_SERVER) work normally. Use for "modernised legacy" apps that read request state via superglobals and don't lean on bare-global wiring; keep 'proc' for unmodified WordPress/Drupal.

Set via App::cgiMode('fork'|'proc'|'fcgi'). Default 'proc' — no behaviour change for existing isolation users.

'fcgi' — forward to a FastCGI backend (e.g. php-fpm) via the FCGI binary protocol over a TCP or Unix socket. The target address is configured with App::fcgiAddress(). No child process is spawned; the OpenSwoole coroutine socket keeps the event loop unblocked. Best choice when ZealPHP sits in front of an existing php-fpm pool.

$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

$coproc_implicit_request_handler

public static bool $coproc_implicit_request_handler = false

$cwd

public static string $cwd

$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

public static string|null $default_php_self = 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

public static bool $display_errors = true

$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.

$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().

$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

$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

public static bool $ignore_php_ext = true

$initial_error_reporting

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

public static int $initial_error_reporting = E_ALL

$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_stack

public static StackHandler|null $middleware_stack = null

$middleware_wait_stack

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

$path_info

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

public static bool $path_info = true

$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

$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

public static Server|Server|null $server

$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_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.

$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

public static bool $superglobals = true

$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

public static callable|null $username_provider = null

$host

protected string $host

$port

protected int $port

$routes

protected array<int, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}> $routes = []

$routes_by_exact_method

protected array<string, array<string, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>> $routes_by_exact_method = []

$routes_by_method

protected array<string, array<int, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>> $routes_by_method = []

$workerStartedAt

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

$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

$instance

private static self|null $instance = null

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

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

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

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()

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>

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

Parameters
$server : array<string, mixed>

$g->server (OpenSwoole-populated)

$ctx : string

JSON-encoded ZEALPHP_REQUEST_CONTEXT

Return values
array<string, string>

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

cgiMode()

Select how a process-isolated legacy include is dispatched: 'proc' (default) — fresh PHP per request via proc_open (full WordPress/Drupal compat).

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

'fork' — warm OpenSwoole\Process fork (~5× faster; function-scope only). '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 : string|null = null
Return values
string

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 leftmost address.
  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

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

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()

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

display_errors()

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

displayErrors()

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

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

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

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()

public static getServer() : Server|Server|null
Return values
Server|Server|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

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()

public static ignorePhpExt([bool|null $on = null ]) : bool
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

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

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()

public static middleware() : StackHandler|null
Return values
StackHandler|null

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

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|null $handler = 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|null = null

nsRoute()

nsRoute: Define a route under a specific namespace.

public nsRoute(string $namespace, string $path[, array<string, mixed>|callable $options = [] ][, callable|null $handler = 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|null = null

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

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.

pathInfo()

public static pathInfo([bool|null $on = null ]) : bool
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|null $handler = 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|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

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

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

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''proc' | 'fork' | 'fcgi' (required) 'interpreter' — full path to interpreter binary (proc mode only; null = direct exec via shebang) '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, or missing fcgi address.

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

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

resolveCgiBackend()

Resolve the CGI backend config for a given file path.

public static resolveCgiBackend(string $path) : array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array}

Looks up the extension in the per-extension registry ($cgi_backends). Falls back to ['mode' => App::$cgi_mode] for unregistered extensions.

Parameters
$path : string
Return values
array{mode: string, interpreter?: string|null, address?: string, fcgi_params?: array}

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

route()

Registers a route with the application.

public route(string $path[, array<string, mixed>|callable $options = [] ][, callable|null $handler = 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|null = null

routes()

public routes() : array<int, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>
Return values
array<int, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>

routesByExactMethod()

public routesByExactMethod() : array<string, array<string, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>>
Return values
array<string, array<string, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>>

routesByMethod()

public routesByMethod() : array<string, array<int, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>>
Return values
array<string, array<int, array{path: string, pattern: string, methods: array, handler: callable|null, param_map: array>, raw: bool}>>

run()

Runs the ZealPHP application.

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

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

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

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

staticHandlerLocations()

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

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

superglobals()

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

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> = []

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

ws()

Register a WebSocket endpoint.

public ws(string $path, callable $onMessage[, callable|null $onOpen = null ][, callable|null $onClose = null ]) : void
Parameters
$path : string

URI path, e.g. '/ws/chat'

$onMessage : callable

function($server, $frame, $g) — called for each message

$onOpen : callable|null = null

function($server, $request, $g) — called on connect

$onClose : callable|null = null

function($server, $fd, $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

parseCliArgs()

protected static parseCliArgs() : array<string, mixed>
Return values
array<string, mixed>

__clone()

private __clone() : mixed

__construct()

private __construct([string $host = '0.0.0.0' ][, int $port = 8080 ][, string $cwd = __DIR__ ]) : mixed
Parameters
$host : string = '0.0.0.0'
$port : int = 8080
$cwd : string = __DIR__

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}>

cgiFcgi()

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

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

Builds CGI env via App::buildCgiEnv(), adds SCRIPT_FILENAME / SCRIPT_NAME, reads the request body, calls FastCgiClient::request(), maps the response (status, headers, body, stderr) back through the universal return contract.

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

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

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

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

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

cgiFork()

Warm-fork variant of cgiSubprocess(): instead of proc_open()-ing a fresh PHP interpreter per request, OpenSwoole\Process forks the already-booted worker (copy-on-write — the interpreter, Composer autoloader, and opcache are inherited, so PHP startup + autoload are NOT re-paid). ~5× faster than 'proc' mode on a trivial file.

private static cgiFork(string $path) : mixed

Isolation: the child is a fresh process that runs the file and exits, so define()/class/ini mutations and any die()/exit() die WITH the child — the worker is never affected (this is actually safer for die()-heavy legacy code than the in-process executeFile() path).

TRADE-OFF vs 'proc': the file is included inside the fork closure, so it runs in FUNCTION scope, not true global scope. Superglobals ($_GET/$_POST/$_SESSION/$_SERVER) work (they're always global), but a bare top-level $x = ... is NOT visible via global $x in a function — so unmodified WordPress/Drupal (global $wpdb;) need 'proc'. See App::$cgi_mode.

Streaming note: the child runs to completion and ships one buffered payload back, so incremental SSE does not stream chunk-by-chunk in fork mode (an infinite event-stream loop would hang). Use 'proc' mode or a native coroutine SSE route for live streaming.

Parameters
$path : string

cgiSubprocess()

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

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

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

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

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

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

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

cidrContains()

Does $ip fall within $cidr? Supports IPv4 and IPv6. A bare IP without /prefix is treated as a single-host range (/32 v4, /128 v6). Returns false on any parse failure rather than throwing — defensive for header- sourced input.

private static cidrContains(string $cidr, string $ip) : bool
Parameters
$cidr : string
$ip : string
Return values
bool

claimOrphanIfAny()

Recover from an orphaned-daemon situation: pid file missing or stale but the port is still held. If the listener is a ZealPHP process, graceful-then-force kill it. Returns true when an orphan was cleaned up, false when there's nothing to do (port free, or held by something that isn't ours).

private static claimOrphanIfAny(int $port) : bool

Orphan recovery is rare and notable, so messages always print even when called from a "quiet" code path (cliStop during restart) — silent recovery hides the fact that the system was in a degraded state that needed self-healing.

Parameters
$port : int
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

cliHelp()

private static cliHelp() : void

cliLogs()

private static cliLogs(array<string, mixed> $flags) : void
Parameters
$flags : array<string, mixed>

cliStatus()

private static cliStatus(array<string, mixed> $flags) : void
Parameters
$flags : array<string, mixed>

cliStatusOne()

private static cliStatusOne(string $pidFile) : void
Parameters
$pidFile : string

cliStop()

private static cliStop(string $pidFile[, bool $quiet = false ]) : void
Parameters
$pidFile : string
$quiet : bool = false

cliStopAuto()

private static cliStopAuto() : void

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

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}>

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

extractPortFromPidFile()

Pull the port number from a default-shaped pid file path like /tmp/zealphp/zealphp_8080.pid. Returns 0 when the caller passed a --pid-file override that doesn't match the convention.

private static extractPortFromPidFile(string $pidFile) : int
Parameters
$pidFile : string
Return values
int

findPortOwnerPid()

Returns the PID listening on $port, or null when nothing's listening or it can't be determined (non-Linux, /proc unreadable). Linux-only: /proc/net/tcp + tcp6 give the LISTEN-state socket inode; /proc/[pid]/fd/* resolves inode → owner pid. We deliberately avoid stream_socket_server / socket_bind here — those are intercepted by OpenSwoole's runtime hook (HOOK_ALL) and become coroutine-only, which would crash this CLI path.

private static findPortOwnerPid(int $port) : int|null
Parameters
$port : int
Return values
int|null

forkStartupReporter()

For daemonized start/restart: fork so the terminal-attached parent polls for the new daemon's PID file and prints a confirmation line BEFORE the shell prompt returns, while the child goes on to boot the (self-daemonizing) server. The parent never touches OpenSwoole — it only watches the PID file and exits, so the confirmation is always the last thing written to the terminal (fixes the issue #17 race where the prompt returned first and the message overlapped the next command).

private static forkStartupReporter(string $pidFile, int $port, string $verb) : void

No-op when pcntl is unavailable or the fork fails: start proceeds without a confirmation line (prior behaviour), never silently broken.

Parameters
$pidFile : string
$port : int
$verb : string

e.g. "Restarted" or "Started ZealPHP in detached mode"

Forks + polls the daemon PID file and exits in the child — neither unit-testable in-process (pcntl_fork/exit kills the test runner) nor dumpable as a subprocess (the OpenSwoole server suppresses the PHP shutdown coverage flush). Verified manually + by the CLI behaviour.

Tags
codeCoverageIgnore

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

invokeFallbackOrNotFound()

private invokeFallbackOrNotFound() : ResponseInterface
Return values
ResponseInterface

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>

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

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.

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

processIsZealphp()

True when /proc/$pid/cmdline looks like a ZealPHP daemon: argv[0] is a real php binary (php, php8.3, etc.) AND the args reference app.php.

private static processIsZealphp(int $pid) : bool

Used to gate kill operations so a recycled PID belonging to an unrelated process is never targeted.

The stricter argv[0] check matters because bash wrappers that spawn php app.php … as a child carry "app.php" in their own cmdline — a naive substring match falsely identifies them as ZealPHP daemons and would happily kill them.

Non-Linux returns true (be permissive — caller still has posix_kill).

Parameters
$pid : int
Return values
bool

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

requestIsHttps()

Detect whether the request arrived over TLS, for deriving REQUEST_SCHEME / HTTPS in the $_SERVER builder. Mirrors the session-cookie secure detection (src/Session/utils.php): a direct HTTPS=on, an X-Forwarded-Proto: https from a proxy, or SERVER_PORT 443.

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

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>

resolvePidFile()

private static resolvePidFile(array<string, mixed> $flags) : string
Parameters
$flags : array<string, mixed>
Return values
string

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

validateLifecycleCombination()

Refuse to start with lifecycle combinations that race process-wide superglobals across concurrent coroutines.

private static validateLifecycleCombination(bool $sg, int $hookFlags, bool $enableCo) : void

History: pre-v0.2.27 these were elog()'d at warn level so they landed in /tmp/zealphp/debug.log but didn't refuse — the rationale was "users may have niche reasons (security audits, debugging)". In practice the warning was invisible to anyone not actively reading the debug log, and the unsafe configuration is how cross-request state-leak bugs ship to production. v0.2.27 changes this to a hard throw at App::run() boot — fail loud, fail fast, before any request can be served against a broken contract.

Parameters
$sg : bool
$hookFlags : int
$enableCo : bool
Tags
throws
RuntimeException

When superglobals(true) is combined with enableCoroutine(true) or hookAll(non-zero) — both expose $_GET/$_POST/$_SESSION (process-wide PHP arrays) to concurrent coroutine writes, which races across requests.

On this page