statusCode < 0 ? 200 : $this->statusCode; } public function setStatusCode(int $statusCode): void { $this->statusCode = $statusCode; } public function hasStatusCode(): bool { return $this->statusCode >= 100; } public function getStatusText(): string { return $this->statusText ?? self::STATUS[$this->getStatusCode()] ?? 'Unknown Status'; } public function setStatusText(string $statusText): void { $this->statusText = (string)$statusText; } public function clearStatusText(): void { $this->statusText = null; } public function addCookie( string $name, mixed $value, DateTimeInterface|int|null $expires = null, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = false, bool $sameSiteStrict = false ): void { $cookie = rawurlencode($name) . '=' . rawurlencode($value) . '; SameSite=' . ($sameSiteStrict ? 'Strict' : 'Lax'); if($expires !== null) { if(!($expires instanceof DateTime)) { if(is_int($expires)) $expires = DateTime::fromUnixTimeSeconds($expires); else $expires = DateTime::createFromInterface($expires); } $now = DateTime::now(); $maxAge = $expires->isLessThanOrEqual($now) ? -1 : $expires->difference($now)->totalSeconds(); $expires = $expires->toCookieString(); $cookie .= '; Expires=' . $expires . '; Max-Age=' . $maxAge; } if(!empty($domain)) $cookie .= '; Domain=' . $domain; if(!empty($path)) $cookie .= '; Path=' . $path; if($secure) $cookie .= '; Secure'; if($httpOnly) $cookie .= '; HttpOnly'; $this->addHeader('Set-Cookie', $cookie); } public function removeCookie( string $name, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = false, bool $sameSiteStrict = false ): void { $this->addCookie($name, '', -9001, $path, $domain, $secure, $httpOnly, $sameSiteStrict); } public function redirect(string $to, bool $permanent = false): void { $this->setStatusCode($permanent ? 301 : 302); $this->setHeader('Location', $to); } public function addVary(string|array $headers): void { if(!is_array($headers)) $headers = [$headers]; foreach($headers as $header) { $header = $header; if(!in_array($header, $this->vary)) $this->vary[] = $header; } $this->setHeader('Vary', implode(', ', $this->vary)); } public function setPoweredBy(string $poweredBy): void { $this->setHeader('X-Powered-By', $poweredBy); } public function setEntityTag(string $eTag, bool $weak = false): void { $eTag = '"' . $eTag . '"'; if($weak) $eTag = 'W/' . $eTag; $this->setHeader('ETag', $eTag); } public function setServerTiming(Timings $timings): void { $laps = $timings->getLaps(); $timings = []; foreach($laps as $lap) { $timing = $lap->getName(); if($lap->hasComment()) $timing .= ';desc="' . strtr($lap->getComment(), ['"' => '\\"']) . '"'; $timing .= ';dur=' . round($lap->getDurationTime(), 5); $timings[] = $timing; } $this->setHeader('Server-Timing', implode(', ', $timings)); } public function hasContentType(): bool { return $this->hasHeader('Content-Type'); } public function setContentType(MediaType|string $mediaType): void { $this->setHeader('Content-Type', (string)$mediaType); } public function setTypeStream(): void { $this->setContentType('application/octet-stream'); } public function setTypePlain(string $charset = 'us-ascii'): void { $this->setContentType('text/plain; charset=' . $charset); } public function setTypeHTML(string $charset = 'utf-8'): void { $this->setContentType('text/html; charset=' . $charset); } public function setTypeJson(string $charset = 'utf-8'): void { $this->setContentType('application/json; charset=' . $charset); } public function setTypeXML(string $charset = 'utf-8'): void { $this->setContentType('application/xml; charset=' . $charset); } public function setTypeCSS(string $charset = 'utf-8'): void { $this->setContentType('text/css; charset=' . $charset); } public function setTypeJS(string $charset = 'utf-8'): void { $this->setContentType('application/javascript; charset=' . $charset); } public function sendFile(string $absolutePath): void { $this->setHeader('X-Sendfile', $absolutePath); } public function accelRedirect(string $uri): void { $this->setHeader('X-Accel-Redirect', $uri); } public function setFileName(string $fileName, bool $attachment = false): void { $this->setHeader( 'Content-Disposition', sprintf( '%s; filename="%s"', ($attachment ? 'attachment' : 'inline'), $fileName ) ); } public function clearSiteData(string ...$directives): void { $this->setHeader( 'Clear-Site-Data', implode(', ', array_map(fn($directive) => sprintf('"%s"', $directive), $directives)) ); } public function setCacheControl(string ...$directives): void { $this->setHeader('Cache-Control', implode(', ', $directives)); } public function toResponse(): HttpResponse { return new HttpResponse( $this->getHttpVersion(), $this->getStatusCode(), $this->getStatusText(), $this->getHeaders(), $this->getContent() ); } private const STATUS = [ 100 => 'Continue', 101 => 'Switching Protocols', 103 => 'Early Hints', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', 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 Required', 413 => 'Payload Too Large', 414 => 'URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Range Not Satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 425 => 'Too Early', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', 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', ]; }