index/src/Http/HttpResponseBuilder.php

278 lines
8.2 KiB
PHP

<?php
// HttpResponseBuilder.php
// Created: 2022-02-08
// Updated: 2022-02-27
namespace Index\Http;
use DateTimeInterface;
use Index\DateTime;
use Index\UrlEncoding;
use Index\MediaType;
use Index\Performance\Timings;
class HttpResponseBuilder extends HttpMessageBuilder {
private int $statusCode = -1;
private ?string $statusText;
private array $vary = [];
public function getStatusCode(): int {
return $this->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',
];
}