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

Response
in package

Thin wrapper around \OpenSwoole\Http\Response. The __call / __get / __set proxies forward to the underlying response — these @method annotations expose the forwarded signatures to static analysis so call sites are statically typed instead of treated as mixed.

Table of Contents

Constants

MAX_RANGES  : mixed = 200
Maximum number of comma-separated range specs honoured in a single Range header. Mirrors Apache's AP_DEFAULT_MAX_RANGES (byterange_filter.c).
REDIRECT_REASONS  : mixed = [301 => 'Moved Permanently', 302 => 'Found', 30...

Properties

$cookiesList  : array<int, array{0: string, 1: string, 2: int, 3: string, 4: string, 5: bool, 6: bool, 7: string, 8: string}>
$headersList  : array<int, array{0: string, 1: string}>
$parent  : Response
$rawCookiesList  : array<int, array{0: string, 1: string, 2: int, 3: string, 4: string, 5: bool, 6: bool, 7: string, 8: string}>
$fileMimeResolver  : MimeResolver|null
Serve a file with Range request support using OpenSwoole's zero-copy sendfile.
$g  : RequestContext
$htmxBuilder  : HtmxResponse|null
$statusCode  : int|null

Methods

__call()  : mixed
Forward method calls to the underlying OpenSwoole response.
__construct()  : mixed
__get()  : mixed
Proxy property reads to the underlying OpenSwoole response.
__set()  : mixed
Proxy property writes to the underlying OpenSwoole response.
close()  : void
cookie()  : bool
create()  : Response|false
detach()  : bool
end()  : bool
flush()  : bool
header()  : bool
Queue a response header.
htmx()  : HtmxResponse
Returns a fluent HtmxResponse builder that queues HX-* headers into this response. Each call returns the same builder instance (one per Response object) so callers can chain or call multiple times.
ifRangeDateMatches()  : bool
Shared If-Range HTTP-date evaluator — the SINGLE source of truth for date-form If-Range strong validation across both range-serving paths (this Response::sendFile() zero-copy path AND the buffered RangeMiddleware path), so they can never diverge on the same request (#258).
isWritable()  : bool
json()  : void
Emit a JSON response and end the connection.
parseRange()  : array{status: "ignore"|"unsatisfiable"|"ok", ranges: array}
Parse a Range header value into a normalised list of [start, end] byte ranges (inclusive, clamped to the resource size).
push()  : bool
rawCookie()  : bool
recv()  : bool
redirect()  : void
Send an HTTP redirect.
sendfile()  : int|false
sendFile()  : void
serializeCookie()  : string
Serialize ONE Set-Cookie header value byte-identical to PHP 8.4's php_setcookie() (#293) — verified live against Apache 2.4.67 + mod_php 8.4:
setFileMimeResolver()  : void
Override the suffix→metadata resolver sendFile() uses — pass your own MimeResolver (e.g. extra types, no language map), or null to restore the built-in Apache-parity default.
sse()  : void
Server-Sent Events endpoint. Sets the required headers and delegates to stream(). The $fn callback receives an $emit() closure: $emit(string $data, string $event = '', string $id = '') which formats and sends one SSE message.
status()  : bool
stream()  : void
Stream a response body in chunks. Headers are flushed immediately.
trailer()  : bool
upgrade()  : void
write()  : bool
coalesceRanges()  : array<int, array{0: int, 1: int}>
Coalesce a list of [start, end] byte ranges (inclusive) into the minimal set of disjoint ranges covering the same bytes.
defaultFileMimeResolver()  : MimeResolver
Apache stock-conf parity maps: common mime.types entries, the AddEncoding compression suffixes, and the default-conf AddLanguage set. Kept deliberately modest — the full IANA registry belongs to a user-supplied resolver via {@see setFileMimeResolver()}.
defaultPort()  : int
Default port for a URL scheme (RFC 6454 origin). 0 = unknown scheme.
emitMultiHeader()  : void
Emit a single header name with multiple values as one OpenSwoole header() call carrying an ARRAY value, so the ext writes one wire line per value (#260). Isolated in its own method because the openswoole/ide-helper stub types the value param as string while the real ext accepts any zval (incl. an array) — the array-value behaviour is a documented ext-vs-stub mismatch (see phpstan.neon ignoreErrors).
emitQueuedHeaders()  : void
Emit every queued header to the underlying OpenSwoole response, grouping same-name entries so multiple appends survive on the wire (#260).
fileMimeResolver()  : MimeResolver
ifRangeMatches()  : bool
Evaluate an If-Range header value against the resource's validators.
isSameOrigin()  : bool
RFC 6454 same-origin test for the open-redirect (CWE-601) guard. An origin is the triple (scheme, host, port); two URLs are same-origin iff all three match (#432). This is **strict by design**: ZealPHP commonly runs several instances on the same host at different ports (`php app.php -p 8081 vs -p 8082`), so a same-host different-PORT target is a *different instance* and must be treated as cross-origin — and a scheme/port downgrade to an attacker-controlled service on the same host is a genuine CWE-601 hole. Same-origin absolute redirects keep working because the request's own (scheme, host, port) is compared, not a default — fixing the prior port-blind over-block (a same-origin redirect 500'd on every non-default port) and the host-only under-block together.
resolveFileMetadata()  : array{type: string, encoding: ?string, languages: list}
Resolve a file's response metadata for sendFile() (#317): walk the suffix chain via {@see MimeResolver} (Apache mod_mime semantics), then fall back to magic-bytes sniffing only when no suffix mapped a type.
sendMultipart()  : void
Emit a 206 multipart/byteranges response for two or more ranges, streaming each slice straight from the file. Body framing mirrors RangeMiddleware so both paths produce byte-identical multipart output.
splitHostPort()  : array{0: string, 1: ?int}
Split a host[:port] authority into [bare host, port]. Handles bracketed IPv6 literals ([::1], [::1]:8080) and the host:port form. A missing or non-numeric port yields null for the port (the caller applies the scheme default).

Constants

MAX_RANGES

Maximum number of comma-separated range specs honoured in a single Range header. Mirrors Apache's AP_DEFAULT_MAX_RANGES (byterange_filter.c).

private mixed MAX_RANGES = 200

Beyond this the Range header is ignored and the full body is served (200), matching Apache's CVE-2011-3192 mitigation.

REDIRECT_REASONS

private mixed REDIRECT_REASONS = [301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect']

Properties

$cookiesList

public array<int, array{0: string, 1: string, 2: int, 3: string, 4: string, 5: bool, 6: bool, 7: string, 8: string}> $cookiesList = []

$headersList

public array<int, array{0: string, 1: string}> $headersList = []

$rawCookiesList

public array<int, array{0: string, 1: string, 2: int, 3: string, 4: string, 5: bool, 6: bool, 7: string, 8: string}> $rawCookiesList = []

$fileMimeResolver

Serve a file with Range request support using OpenSwoole's zero-copy sendfile.

private static MimeResolver|null $fileMimeResolver = null

The default mod_mime suffix maps sendFile() resolves against (#317) — an Apache mime.types / AddEncoding / AddLanguage stock-conf subset. Built once per worker; override with setFileMimeResolver().

$statusCode

private int|null $statusCode = null

Methods

__call()

Forward method calls to the underlying OpenSwoole response.

public __call(string $name, array<int, mixed> $arguments) : mixed
Parameters
$name : string
$arguments : array<int, mixed>

__construct()

public __construct(Response $response) : mixed
Parameters
$response : Response

__get()

Proxy property reads to the underlying OpenSwoole response.

public & __get(string $name) : mixed
Parameters
$name : string

__set()

Proxy property writes to the underlying OpenSwoole response.

public __set(string $name, mixed $value) : mixed
Parameters
$name : string
$value : mixed
public cookie(string $key[, string $value = '' ][, int $expire = 0 ][, string $path = '/' ][, string $domain = '' ][, bool $secure = false ][, bool $httponly = false ][, string $samesite = '' ][, string $priority = '' ]) : bool
Parameters
$key : string
$value : string = ''
$expire : int = 0
$path : string = '/'
$domain : string = ''
$secure : bool = false
$httponly : bool = false
$samesite : string = ''
$priority : string = ''
Return values
bool

create()

public static create([int $fd = = '-1' ]) : Response|false
Parameters
$fd : int = = '-1'
Return values
Response|false

detach()

public detach() : bool
Return values
bool

end()

public end([string|null $data = null ]) : bool
Parameters
$data : string|null = null
Return values
bool

flush()

public flush() : bool
Return values
bool

header()

Queue a response header.

public header(string $key, string $value[, bool $replace = true ]) : bool

Mirrors PHP's native header($value, $replace = true) semantics (#260): $replace = true (the default) drops any previously-queued header of the same name before adding this one — last write wins, as every framework caller that sets a single header (Content-Type, Cache-Control, …) relies on. $replace = false is the APPEND form (header($h, false)) — it keeps prior same-name entries, so multiple Link / WWW-Authenticate / CSP headers all survive (flush() groups same-name appends into one array-valued OpenSwoole call so they all reach the wire).

Parameters
$key : string
$value : string
$replace : bool = true
Return values
bool

htmx()

Returns a fluent HtmxResponse builder that queues HX-* headers into this response. Each call returns the same builder instance (one per Response object) so callers can chain or call multiple times.

public htmx() : HtmxResponse
Return values
HtmxResponse

ifRangeDateMatches()

Shared If-Range HTTP-date evaluator — the SINGLE source of truth for date-form If-Range strong validation across both range-serving paths (this Response::sendFile() zero-copy path AND the buffered RangeMiddleware path), so they can never diverge on the same request (#258).

public static ifRangeDateMatches(string $ifRange, int $mtime, int|null $reqTime) : bool

Implements RFC 9110 §13.1.5's strong-validation requirement via Apache's one-minute clock-skew rule (ap_condition_if_range): a Last-Modified date is only a STRONG validator if it is at least 60 s older than the time the representation was served — a value younger than that window is treated as weak and a weak validator MUST NOT be used to short-circuit a Range request. The Range is honoured only when the supplied date matches the resource mtime exactly AND that 60 s skew window has elapsed.

Parameters
$ifRange : string

Raw If-Range header value (already known to be the HTTP-date form, not an entity-tag).

$mtime : int

Resource last-modified time (unix seconds).

$reqTime : int|null

Time the response is/was served (unix seconds) — from the response Date header when available; null → use wall-clock now.

Return values
bool

true → the date is a strong match, honour the Range.

isWritable()

public isWritable() : bool
Return values
bool

json()

Emit a JSON response and end the connection.

public json(mixed $data[, int $status = 200 ]) : void
Parameters
$data : mixed
$status : int = 200

parseRange()

Parse a Range header value into a normalised list of [start, end] byte ranges (inclusive, clamped to the resource size).

public static parseRange(string $rangeHeader, int $total) : array{status: "ignore"|"unsatisfiable"|"ok", ranges: array}

Mirrors RangeMiddleware's spec handling so both the buffered and the zero-copy file paths agree on suffix / open-end / bounded semantics.

Returns one of:

  • ['status' => 'ignore', 'ranges' => []] Header is not a bytes= range, contains a syntactically invalid spec (RFC 7233 §2.1: an invalid spec invalidates the whole header), contains no spec at all (bytes=, — the empty byte-range-set, also invalid per §2.1, #365), or the range count exceeds MAX_RANGES (CVE-2011-3192 mitigation). Caller serves the full body (200).
  • ['status' => 'unsatisfiable', 'ranges' => []] Every well-formed spec is out of bounds (416).
  • ['status' => 'ok', 'ranges' => array<int, array{0: int, 1: int}>]
Parameters
$rangeHeader : string
$total : int
Return values
array{status: "ignore"|"unsatisfiable"|"ok", ranges: array}

push()

public push(string $filename[, int $opcode = = '1' ][, bool $finish = = 'true' ]) : bool
Parameters
$filename : string
$opcode : int = = '1'
$finish : bool = = 'true'
Return values
bool

rawCookie()

public rawCookie(string $key[, string $value = '' ][, int $expire = 0 ][, string $path = '/' ][, string $domain = '' ][, bool $secure = false ][, bool $httponly = false ][, string $samesite = '' ][, string $priority = '' ]) : bool
Parameters
$key : string
$value : string = ''
$expire : int = 0
$path : string = '/'
$domain : string = ''
$secure : bool = false
$httponly : bool = false
$samesite : string = ''
$priority : string = ''
Return values
bool

recv()

public recv([float $timeout = = '0' ]) : bool
Parameters
$timeout : float = = '0'
Return values
bool

redirect()

Send an HTTP redirect.

public redirect(string $url[, int $status = 302 ][, bool $allowExternal = false ]) : void
Parameters
$url : string

Destination URL (absolute or relative)

$status : int = 302

301 Moved Permanently, 302 Found (default), 307 Temporary Redirect, 308 Permanent Redirect

$allowExternal : bool = false

Permit cross-origin / protocol-relative targets. Default false — safe-by-default: a //evil.com or different-host absolute URL is REFUSED (open-redirect / CWE-601 guard). Set true only when an external redirect is genuinely intended (and validate user input against an allowlist first).

sendfile()

public sendfile(string $filename[, int $offset = = '0' ][, int $length = = '0' ]) : int|false
Parameters
$filename : string
$offset : int = = '0'
$length : int = = '0'
Return values
int|false

sendFile()

public sendFile(string $path[, string $filename = '' ]) : void
Parameters
$path : string

Absolute path to the file

$filename : string = ''

Optional download filename (triggers Content-Disposition: attachment)

serializeCookie()

Serialize ONE Set-Cookie header value byte-identical to PHP 8.4's php_setcookie() (#293) — verified live against Apache 2.4.67 + mod_php 8.4:

public static serializeCookie(string $name[, string $value = '' ][, int $expire = 0 ][, string $path = '' ][, string $domain = '' ][, bool $secure = false ][, bool $httponly = false ][, string $samesite = '' ][, string $priority = '' ][, bool $raw = false ]) : string
  • expires uses the IMF-fixdate form with SPACES (Wed, 18 May 2033 …), not the legacy dashed Netscape date.
  • Max-Age=max(0, expire - now) is emitted alongside expires (RFC 6265 §5.2.2 — Max-Age wins where supported).
  • The value is rawurlencode()d — space is %20 (never +; cookies are not form-encoded, and a literal + corrupts base64 payloads like JWTs).
  • PHP's attribute order and casing: path, domain, secure, HttpOnly, SameSite.
  • An EMPTY value emits PHP's canonical deletion form: name=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0.

#319: SameSite=None without Secure is emitted AS-IS — PHP/Apache do not auto-coerce, and parity wins — but a warning is logged, because modern browsers (Chrome 80+, RFC 6265bis) silently drop such cookies.

Parameters
$name : string

Cookie name (validated upstream by setcookie/setrawcookie).

$value : string = ''

Cookie value; '' → PHP's deletion form.

$expire : int = 0

Unix expiry; 0 → session cookie (no expires/Max-Age).

$path : string = ''

path= attribute ('' → omitted).

$domain : string = ''

domain= attribute ('' → omitted).

$secure : bool = false

Emit secure.

$httponly : bool = false

Emit HttpOnly.

$samesite : string = ''

SameSite= attribute ('' → omitted).

$priority : string = ''

Priority= attribute (Chrome extension; '' → omitted).

$raw : bool = false

true → value passes through verbatim (setrawcookie).

Return values
string

setFileMimeResolver()

Override the suffix→metadata resolver sendFile() uses — pass your own MimeResolver (e.g. extra types, no language map), or null to restore the built-in Apache-parity default.

public static setFileMimeResolver(MimeResolver|null $resolver) : void
Parameters
$resolver : MimeResolver|null

sse()

Server-Sent Events endpoint. Sets the required headers and delegates to stream(). The $fn callback receives an $emit() closure: $emit(string $data, string $event = '', string $id = '') which formats and sends one SSE message.

public sse(callable $fn) : void
Parameters
$fn : callable

status()

public status(int $statusCode[, string $reason = '' ]) : bool
Parameters
$statusCode : int
$reason : string = ''
Return values
bool

stream()

Stream a response body in chunks. Headers are flushed immediately.

public stream(callable $fn) : void

The $fn callback receives a $write(string $chunk) closure; call it for each piece of content. The response is closed when $fn returns.

Use inside a coroutine route — co::sleep() or channel ops between $write() calls yield the event loop so other requests aren't blocked.

Parameters
$fn : callable

trailer()

public trailer(string $key, string $value) : bool
Parameters
$key : string
$value : string
Return values
bool

write()

public write(string $content) : bool
Parameters
$content : string
Return values
bool

coalesceRanges()

Coalesce a list of [start, end] byte ranges (inclusive) into the minimal set of disjoint ranges covering the same bytes.

private static coalesceRanges(array<int, array{0: int, 1: int}> $ranges) : array<int, array{0: int, 1: int}>

Sorts by start, then merges any spec that overlaps OR is immediately adjacent to the running union (next.start <= cur.end + 1). Every input spec is already clamped to [0, total-1] by parseRange(), so the union of the result can never exceed the resource size — the DoS-amplification cap (#230). Result is start-ascending (RFC 9110 §14.2 permits the server to reorder/merge multipart ranges). Kept in lock-step with RangeMiddleware::coalesceRanges().

Parameters
$ranges : array<int, array{0: int, 1: int}>
Return values
array<int, array{0: int, 1: int}>

defaultFileMimeResolver()

Apache stock-conf parity maps: common mime.types entries, the AddEncoding compression suffixes, and the default-conf AddLanguage set. Kept deliberately modest — the full IANA registry belongs to a user-supplied resolver via {@see setFileMimeResolver()}.

private static defaultFileMimeResolver() : MimeResolver
Return values
MimeResolver

defaultPort()

Default port for a URL scheme (RFC 6454 origin). 0 = unknown scheme.

private defaultPort(string $scheme) : int
Parameters
$scheme : string
Return values
int

emitMultiHeader()

Emit a single header name with multiple values as one OpenSwoole header() call carrying an ARRAY value, so the ext writes one wire line per value (#260). Isolated in its own method because the openswoole/ide-helper stub types the value param as string while the real ext accepts any zval (incl. an array) — the array-value behaviour is a documented ext-vs-stub mismatch (see phpstan.neon ignoreErrors).

private emitMultiHeader(string $name, array<int, string> $values) : void
Parameters
$name : string
$values : array<int, string>

emitQueuedHeaders()

Emit every queued header to the underlying OpenSwoole response, grouping same-name entries so multiple appends survive on the wire (#260).

private emitQueuedHeaders() : void

header($name, $value, false) (PHP's append form) queues each value as a separate $headersList entry — correct. But OpenSwoole's scalar $response->header($name, $scalar) OVERWRITES any prior entry of the same name (it stores under the header name as the array key), so emitting one scalar call per entry collapses Link / WWW-Authenticate / Content-Security-Policy multi-value headers to the LAST value only.

OpenSwoole DOES support multiple same-name headers — pass an ARRAY value and the ext emits one wire line per element (the same mechanism that makes multiple Set-Cookie work). So we group by name (first-seen order preserved), then emit a scalar for single-value names and an array for names with >1 value.

ifRangeMatches()

Evaluate an If-Range header value against the resource's validators.

private ifRangeMatches(string $ifRange, string $etag, int $mtime) : bool
Parameters
$ifRange : string

Raw If-Range header value

$etag : string

The resource's current ETag (entity-tag form)

$mtime : int

The resource's last-modified time (unix seconds)

Return values
bool

true → the validator still matches, honour the Range

isSameOrigin()

RFC 6454 same-origin test for the open-redirect (CWE-601) guard. An origin is the triple (scheme, host, port); two URLs are same-origin iff all three match (#432). This is **strict by design**: ZealPHP commonly runs several instances on the same host at different ports (`php app.php -p 8081 vs -p 8082`), so a same-host different-PORT target is a *different instance* and must be treated as cross-origin — and a scheme/port downgrade to an attacker-controlled service on the same host is a genuine CWE-601 hole. Same-origin absolute redirects keep working because the request's own (scheme, host, port) is compared, not a default — fixing the prior port-blind over-block (a same-origin redirect 500'd on every non-default port) and the host-only under-block together.

private isSameOrigin(string $url) : bool

A target with no host (relative URL) is same-origin; an undeterminable request host preserves the historical "allow" so a working relative redirect never regresses.

Parameters
$url : string
Return values
bool

resolveFileMetadata()

Resolve a file's response metadata for sendFile() (#317): walk the suffix chain via {@see MimeResolver} (Apache mod_mime semantics), then fall back to magic-bytes sniffing only when no suffix mapped a type.

private static resolveFileMetadata(string $path) : array{type: string, encoding: ?string, languages: list}

When the chain yielded an ENCODING but no type (README.gz), magic bytes must not label the encoding as the type (application/gzip + Content-Encoding: gzip would double-decode) — emit octet-stream.

Parameters
$path : string
Return values
array{type: string, encoding: ?string, languages: list}

sendMultipart()

Emit a 206 multipart/byteranges response for two or more ranges, streaming each slice straight from the file. Body framing mirrors RangeMiddleware so both paths produce byte-identical multipart output.

private sendMultipart(string $path, array<int, array{0: int, 1: int}> $ranges, int $total, string $mime) : void
Parameters
$path : string
$ranges : array<int, array{0: int, 1: int}>
$total : int
$mime : string

splitHostPort()

Split a host[:port] authority into [bare host, port]. Handles bracketed IPv6 literals ([::1], [::1]:8080) and the host:port form. A missing or non-numeric port yields null for the port (the caller applies the scheme default).

private splitHostPort(string $authority) : array{0: string, 1: ?int}
Parameters
$authority : string
Return values
array{0: string, 1: ?int}
On this page