forked from mirzaev/minimal
508 lines
16 KiB
PHP
Executable File
508 lines
16 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace mirzaev\minimal\http;
|
|
|
|
// Files of the project
|
|
use mirzaev\minimal\http\enumerations\method,
|
|
mirzaev\minimal\http\enumerations\protocol,
|
|
mirzaev\minimal\http\enumerations\status,
|
|
mirzaev\minimal\http\enumerations\content,
|
|
mirzaev\minimal\http\response;
|
|
|
|
// Built-in libraries
|
|
use DomainException as exception_domain,
|
|
InvalidArgumentException as exception_argument,
|
|
RuntimeException as exception_runtime,
|
|
LogicException as exception_logic;
|
|
|
|
/**
|
|
* Request
|
|
*
|
|
* @package mirzaev\minimal\http
|
|
*
|
|
* @param method $method Method
|
|
* @param string $uri URI
|
|
* @param protocol $protocol Version of HTTP protocol
|
|
* @param array $headers Headers
|
|
* @param array $parameters Deserialized parameters from URI and body
|
|
* @param array $files Deserialized files from body
|
|
* @param array $options Options for `request_parse_body($options)`
|
|
*
|
|
* @method void __construct(method|string|null $method, ?string $uri, protocol|string|null $protocol, array $headers, array $parameters, array $files, bool $environment) Constructor
|
|
* @method response response() Generate response for request
|
|
* @method self header(string $name, string $value) Write a header to the headers property
|
|
*
|
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
*/
|
|
final class request
|
|
{
|
|
/**
|
|
* Method
|
|
*
|
|
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
|
|
*
|
|
* @throws exception_runtime if reinitialize the property
|
|
* @throws exception_domain if failed to recognize method
|
|
*
|
|
* @var method $method Method
|
|
*/
|
|
public method $method {
|
|
// Write
|
|
set(method|string $value) {
|
|
if (isset($this->{__PROPERTY__})) {
|
|
// The property is already initialized
|
|
|
|
// Exit (fail)
|
|
throw new exception_runtime('The property is already initialized: ' . __PROPERTY__, status::internal_server_error->value);
|
|
}
|
|
|
|
if ($value instanceof method) {
|
|
// Received implementation of the method
|
|
|
|
// Writing
|
|
$this->method = $value;
|
|
} else {
|
|
// Received a string literal (excected name of the method)
|
|
|
|
// Initializing implementator of the method
|
|
$method = method::{strtolower($value)};
|
|
|
|
if ($method instanceof method) {
|
|
// Initialized implementator of the method
|
|
|
|
// Writing
|
|
$this->method = $method;
|
|
} else {
|
|
// Not initialized implementator of the method
|
|
|
|
// Exit (fail)
|
|
throw new exception_domain('Failed to recognize method: ' . $value, status::not_implemented->value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* URI
|
|
*
|
|
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
|
|
*
|
|
* @throws exception_runtime if reinitialize the property
|
|
*
|
|
* @var string $uri URI
|
|
*/
|
|
public string $uri {
|
|
// Write
|
|
set(string $value) {
|
|
if (isset($this->{__PROPERTY__})) {
|
|
// The property is already initialized
|
|
|
|
// Exit (fail)
|
|
throw new exception_runtime('The property is already initialized: ' . __PROPERTY__, status::internal_server_error->value);
|
|
}
|
|
|
|
// Writing
|
|
$this->uri = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Protocol
|
|
*
|
|
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
|
|
*
|
|
* @throws exception_runtime if reinitialize the property
|
|
* @throws exception_domain if failed to recognize HTTP version
|
|
*
|
|
* @var protocol $protocol Version of HTTP protocol
|
|
*/
|
|
public protocol $protocol {
|
|
// Write
|
|
set(protocol|string $value) {
|
|
if (isset($this->{__PROPERTY__})) {
|
|
// The property is already initialized
|
|
|
|
// Exit (fail)
|
|
throw new exception_runtime('The property is already initialized: ' . __PROPERTY__, status::internal_server_error->value);
|
|
}
|
|
|
|
if ($value instanceof protocol) {
|
|
// Received implementation of HTTP version
|
|
|
|
// Writing
|
|
$this->protocol = $value;
|
|
} else {
|
|
// Received a string literal (excected name of HTTP version)
|
|
|
|
// Initializing implementator of HTTP version
|
|
$protocol = protocol::tryFrom($value);
|
|
|
|
if ($protocol instanceof protocol) {
|
|
// Initialized implementator of HTTP version
|
|
|
|
// Writing
|
|
$this->protocol = $protocol;
|
|
} else {
|
|
// Not initialized implementator of HTTP version
|
|
|
|
// Exit (fail)
|
|
throw new exception_domain('Failed to recognize HTTP version: ' . $value, status::http_version_not_supported->value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Headers
|
|
*
|
|
* @see https://www.rfc-editor.org/rfc/rfc7540
|
|
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
|
|
*
|
|
* @var array $headers Headers
|
|
*/
|
|
public array $headers = [] {
|
|
// Read
|
|
&get => $this->headers;
|
|
}
|
|
|
|
/**
|
|
* Parameters
|
|
*
|
|
* For "application/json" will store json_decode(file_get_contents('php://input'))
|
|
* For method::post will store the value from $_POST ?? []
|
|
* For other methods with $this->method->body() === true and "multipart/form-data" will store the result of executing request_parse_body($this->options)[0] (>=PHP 8.4)
|
|
* For other methods with $this->method->body() === true and "application/x-www-form-urlencoded" will store the result of executing request_parse_body($this->options)[0] (>=PHP 8.4)
|
|
* For other methods with $this->method->body() === true and other Content-Type will store $GLOBALS['_' . $this->method->value] ?? []
|
|
* For other methods with $this->method->body() === false will store the value from $GLOBALS['_' . $this->method->value] ?? []
|
|
*
|
|
* @see https://wiki.php.net/rfc/rfc1867-non-post about request_parse_body()
|
|
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
|
|
*
|
|
* @throws exception_runtime if reinitialize the property
|
|
*
|
|
* @var array $parameters Deserialized parameters from URI and body
|
|
*/
|
|
public array $parameters {
|
|
// Write
|
|
set(array $value) {
|
|
if (isset($this->{__PROPERTY__})) {
|
|
// The property is already initialized
|
|
|
|
// Exit (fail)
|
|
throw new exception_runtime('The property is already initialized: ' . __PROPERTY__, status::internal_server_error->value);
|
|
}
|
|
|
|
// Writing
|
|
$this->parameters = $value;
|
|
}
|
|
|
|
// Read
|
|
get => $this->parameters ?? [];
|
|
}
|
|
|
|
/**
|
|
* Files
|
|
*
|
|
* For method::post will store the value from $_FILES ?? []
|
|
* For other methods with $this->method->body() === true and "multipart/form-data" will store the result of executing request_parse_body($this->options)[1] (>=PHP 8.4)
|
|
* For other methods with $this->method->body() === true and "application/x-www-form-urlencoded" will store the result of executing request_parse_body($this->options)[1] (>=PHP 8.4)
|
|
* For other methods with $this->method->body() === true and other Content-Type will store $_FILES ?? []
|
|
*
|
|
* @see https://wiki.php.net/rfc/rfc1867-non-post about request_parse_body()
|
|
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
|
|
*
|
|
* @throws exception_runtime if reinitialize the property
|
|
* @throws exception_runtime if $this->method is not initialized
|
|
* @throws exception_logic if request with that method can not has files
|
|
*
|
|
* @var array $files Deserialized files from body
|
|
*/
|
|
public array $files {
|
|
// Write
|
|
set(array $value) {
|
|
if (isset($this->{__PROPERTY__})) {
|
|
// The property is already initialized
|
|
|
|
// Exit (fail)
|
|
throw new exception_runtime('The property is already initialized: ' . __PROPERTY__, status::internal_server_error->value);
|
|
}
|
|
|
|
if (isset($this->method)) {
|
|
// Initialized method
|
|
|
|
if ($this->method->body()) {
|
|
// Request with this method can has body
|
|
|
|
// Writing
|
|
$this->files = $value;
|
|
} else {
|
|
// Request with this method can not has body
|
|
|
|
// Exit (fail)
|
|
throw new exception_logic('Request with ' . $this->method->value . ' method can not has body therefore can not has files', status::internal_server_error->value);
|
|
}
|
|
} else {
|
|
// Not initialized method
|
|
|
|
// Exit (fail)
|
|
throw new exception_runtime('Method of the request is not initialized', status::internal_server_error->value);
|
|
}
|
|
}
|
|
|
|
// Read
|
|
get => $this->files ?? [];
|
|
}
|
|
|
|
/**
|
|
* Options
|
|
*
|
|
* Required if $this->method !== method::post
|
|
*
|
|
* @see https://wiki.php.net/rfc/rfc1867-non-post about request_parse_body()
|
|
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
|
|
*
|
|
* @throws exception_runtime if reinitialize the property
|
|
*
|
|
* @var array $options Options for `request_parse_body($options)`
|
|
*/
|
|
public array $options {
|
|
// Write
|
|
set(array $value) {
|
|
if (isset($this->{__PROPERTY__})) {
|
|
// The property is already initialized
|
|
|
|
// Exit (fail)
|
|
throw new exception_runtime('The property is already initialized: ' . __PROPERTY__, status::internal_server_error->value);
|
|
}
|
|
|
|
// Writing
|
|
$this->options = array_filter(
|
|
$value,
|
|
fn(string $key) => match ($key) {
|
|
'post_max_size',
|
|
'max_input_vars',
|
|
'max_multipart_body_parts',
|
|
'max_file_uploads',
|
|
'upload_max_filesize' => true,
|
|
default => throw new exception_domain("Failed to recognize option: $key", status::internal_server_error->value)
|
|
},
|
|
ARRAY_FILTER_USE_KEY
|
|
);
|
|
}
|
|
|
|
// Read
|
|
get => $this->options ?? [];
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param method|string|null $method Name of the method
|
|
* @param string|null $uri URI
|
|
* @param protocol|string|null $protocol Version of HTTP protocol
|
|
* @param array|null $headers Headers
|
|
* @param array|null $parameters Deserialized parameters from URI and body
|
|
* @param array|null $files Deserialized files from body
|
|
* @param bool $environment Write values from environment to properties?
|
|
*
|
|
* @throws exception_domain if failed to normalize name of header
|
|
* @throws exception_argument if failed to initialize JSON
|
|
* @throws exception_argument if failed to initialize a required property
|
|
*
|
|
* @return void
|
|
*/
|
|
public function __construct(
|
|
method|string|null $method = null,
|
|
?string $uri = null,
|
|
protocol|string|null $protocol = null,
|
|
?array $headers = null,
|
|
?array $parameters = null,
|
|
?array $files = null,
|
|
bool $environment = false
|
|
) {
|
|
// Writing method from argument into the property
|
|
if (isset($method)) $this->method = $method;
|
|
|
|
// Writing URI from argument into the property
|
|
if (isset($uri)) $this->uri = $uri;
|
|
|
|
// Writing verstion of HTTP protocol from argument into the property
|
|
if (isset($protocol)) $this->protocol = $protocol;
|
|
|
|
if (isset($headers)) {
|
|
// Received headers
|
|
|
|
// Declaring the buffer of headers
|
|
$buffer = [];
|
|
|
|
foreach ($headers ?? [] as $name => $value) {
|
|
// Iterating over headers
|
|
|
|
// Normalizing name of header (https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2)
|
|
$name = mb_strtolower($name, 'UTF-8');
|
|
|
|
if (empty($name)) {
|
|
// Not normalized name of header
|
|
|
|
// Exit (fail)
|
|
throw new exception_domain('Failed to normalize name of header', status::internal_server_error->value);
|
|
}
|
|
|
|
// Writing into the buffer of headers
|
|
$buffer[$name] = $value;
|
|
}
|
|
|
|
// Writing headers from argument into the property
|
|
$this->headers = $buffer;
|
|
|
|
// Deinitializing the buffer of headers
|
|
unset($buffer);
|
|
}
|
|
|
|
// Writing parameters from argument into the property
|
|
if (isset($parameters)) $this->parameters = $parameters;
|
|
|
|
// Writing files from argument into the property
|
|
if (isset($files)) $this->files = $files;
|
|
|
|
if ($environment) {
|
|
// Requested to write values from environment
|
|
|
|
// Writing method from environment into the property
|
|
$this->method ??= $_SERVER["REQUEST_METHOD"];
|
|
|
|
// Writing URI from environment into the property
|
|
$this->uri ??= $_SERVER['REQUEST_URI'];
|
|
|
|
// Writing verstion of HTTP protocol from environment into the property
|
|
$this->protocol ??= $_SERVER['SERVER_PROTOCOL'];
|
|
|
|
if (!isset($headers)) {
|
|
// Received headers
|
|
|
|
// Declaring the buffer of headers
|
|
$buffer = [];
|
|
|
|
foreach (getallheaders() ?? [] as $name => $value) {
|
|
// Iterating over headers
|
|
|
|
// Normalizing name of header (https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2)
|
|
$name = mb_strtolower($name, 'UTF-8');
|
|
|
|
if (empty($name)) {
|
|
// Not normalized name of header
|
|
|
|
// Exit (fail)
|
|
throw new exception_domain('Failed to normalize name of header', status::internal_server_error->value);
|
|
}
|
|
|
|
// Writing into the buffer of headers
|
|
$buffer[$name] = $value;
|
|
}
|
|
|
|
// Writing headers from environment into the property
|
|
$this->headers = $buffer;
|
|
|
|
// Deinitializing the buffer of headers
|
|
unset($buffer);
|
|
}
|
|
|
|
if (str_starts_with($this->headers['content-type'], content::json->value)) {
|
|
// The body contains "application/json"
|
|
|
|
// Initializing data from the input buffer
|
|
$input = file_get_contents('php://input');
|
|
|
|
if (json_validate($input, 512)) {
|
|
// Validated JSON
|
|
|
|
// Decoding JSON and writing parameters into the property (array type for universalization)
|
|
$this->parameters = json_decode($input, true, 512);
|
|
} else {
|
|
// Not validated JSON
|
|
|
|
// Exit (false)
|
|
throw new exception_argument('Failed to validate JSON from the input buffer', status::unprocessable_content->value);
|
|
}
|
|
|
|
// Writing parameters from environment into the property
|
|
$this->parameters = $_POST ?? [];
|
|
} else if ($this->method === method::post) {
|
|
// POST method
|
|
|
|
// Writing parameters from environment into the property
|
|
$this->parameters = $_POST ?? [];
|
|
|
|
// Writing files from environment into the property
|
|
$this->files = $_FILES ?? [];
|
|
} else if ($this->method->body()) {
|
|
// Non POST method and can has body
|
|
|
|
if (
|
|
str_starts_with($this->headers['content-type'], content::form->value) ||
|
|
str_starts_with($this->headers['content-type'], content::encoded->value)
|
|
) {
|
|
// Non POST method and the body content type is "multipart/form-data" or "application/x-www-form-urlencoded"
|
|
|
|
// Writing parameters and files from environment into the properties
|
|
[$this->parameters, $this->files] = request_parse_body($this->options);
|
|
} else {
|
|
// Non POST method and the body content type is not "multipart/form-data" or "application/x-www-form-urlencoded"
|
|
|
|
// Writing parameters from environment into the property
|
|
$this->parameters = $GLOBALS['_' . $this->method->value] ?? [];
|
|
|
|
// Writing files from environment into the property
|
|
$this->files = $_FILES ?? [];
|
|
}
|
|
} else {
|
|
// Non POST method and can not has body
|
|
|
|
// Writing parameters from environment into the property
|
|
$this->parameters = $GLOBALS['_' . $this->method->value] ?? [];
|
|
}
|
|
}
|
|
|
|
// Validating of required properties
|
|
if (empty($this->method)) throw new exception_argument('Failed to initialize method of the request', status::internal_server_error->value);
|
|
if (empty($this->uri)) throw new exception_argument('Failed to initialize URI of the request', status::internal_server_error->value);
|
|
}
|
|
|
|
/**
|
|
* Response
|
|
*
|
|
* Generate response for request
|
|
*
|
|
* @return response Reponse for request
|
|
*/
|
|
public function response(): response
|
|
{
|
|
// Exit (success)
|
|
return new response(protocol: $this->protocol, status: status::ok);
|
|
}
|
|
|
|
/**
|
|
* Header
|
|
*
|
|
* Write a header to the headers property
|
|
*
|
|
* @see https://www.rfc-editor.org/rfc/rfc7540
|
|
*
|
|
* @param string $name Name
|
|
* @param string $value Value
|
|
*
|
|
* @return self The instance from which the method was called (fluent interface)
|
|
*/
|
|
public function header(string $name, string $value): self
|
|
{
|
|
// Normalizing name of header and writing to the headers property (https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2)
|
|
$this->headers[mb_strtolower($name, 'UTF-8')] = $value;
|
|
|
|
// Exit (success)
|
|
return $this;
|
|
}
|
|
}
|