From c92838db5a0d28036b3af7a388cb7dd2e7371743 Mon Sep 17 00:00:00 2001 From: Arsen Mirzaev Tatyano-Muradovich Date: Tue, 5 Nov 2024 00:54:29 +0300 Subject: [PATCH] resolved #4, resolved #5, resolved #6, resolved #7, resolved #8, resolved #14 --- mirzaev/minimal/system/controller.php | 21 +- mirzaev/minimal/system/core.php | 235 ++------ .../system/http/enumerations/content.php | 94 ++++ .../system/http/enumerations/method.php | 42 ++ .../system/http/enumerations/protocol.php | 24 + .../system/http/enumerations/status.php | 241 +++++++++ mirzaev/minimal/system/http/request.php | 504 ++++++++++++++++++ mirzaev/minimal/system/http/response.php | 448 ++++++++++++++++ mirzaev/minimal/system/model.php | 4 +- mirzaev/minimal/system/route.php | 57 +- mirzaev/minimal/system/router.php | 55 +- mirzaev/minimal/system/traits/magic.php | 7 +- 12 files changed, 1499 insertions(+), 233 deletions(-) create mode 100644 mirzaev/minimal/system/http/enumerations/content.php create mode 100644 mirzaev/minimal/system/http/enumerations/method.php create mode 100644 mirzaev/minimal/system/http/enumerations/protocol.php create mode 100644 mirzaev/minimal/system/http/enumerations/status.php create mode 100755 mirzaev/minimal/system/http/request.php create mode 100755 mirzaev/minimal/system/http/response.php diff --git a/mirzaev/minimal/system/controller.php b/mirzaev/minimal/system/controller.php index 8fe351c..93fc563 100755 --- a/mirzaev/minimal/system/controller.php +++ b/mirzaev/minimal/system/controller.php @@ -7,7 +7,9 @@ namespace mirzaev\minimal; // Files of the project use mirzaev\minimal\model, mirzaev\minimal\core, - mirzaev\minimal\traits\magic; + mirzaev\minimal\traits\magic, + mirzaev\minimal\http\request, + mirzaev\minimal\http\enumerations\status; // Build-in libraries use exception; @@ -16,12 +18,11 @@ use exception; * Controller * * @var core $core An instance of the core + * @var request $request Request * @var model $model An instance of the model connected in the core * @var view $view View template engine instance (twig) - * @var core $core An instance of the core - * @var core $core An instance of the core * - * @method self __construct(core $core) Constructor + * @method void __construct(core $core) Constructor * * @package mirzaev\minimal * @@ -42,6 +43,16 @@ class controller get => $this->core; } + /** + * Request + * + * @var request $request Request + */ + public request $request { + // Read + get => $this->request; + } + /** * Model * @@ -77,7 +88,7 @@ class controller * * @param core $core The instance of the core * - * @return self + * @return void */ public function __construct(core $core) { // Writing the core into the property diff --git a/mirzaev/minimal/system/core.php b/mirzaev/minimal/system/core.php index 67b3547..0349479 100755 --- a/mirzaev/minimal/system/core.php +++ b/mirzaev/minimal/system/core.php @@ -8,14 +8,18 @@ namespace mirzaev\minimal; use mirzaev\minimal\router, mirzaev\minimal\route, mirzaev\minimal\controller, - mirzaev\minimal\model; + mirzaev\minimal\model, + mirzaev\minimal\http\request, + mirzaev\minimal\http\response, + mirzaev\minimal\http\enumerations\status; // Built-in libraries use exception, - BadMethodCallException as exception_method, + BadMethodCallException as exception_method, DomainException as exception_domain, InvalidArgumentException as exception_argument, UnexpectedValueException as exception_value, + LogicException as exception_logic, ReflectionClass as reflection; /** @@ -26,10 +30,11 @@ use exception, * @param model $model An instance of the model * @param router $router An instance of the router * - * @mathod self __construct(?string $namespace) Constructor + * @mathod void __construct(?string $namespace) Constructor * @method void __destruct() Destructor - * @method string|null request(?string $uri, ?string $method, array $variabls) Handle the request - * @method string|null route(route $route, string $method) Handle the route + * @method string|null start() Initialize request by environment and handle it + * @method string|null request(request $request, array $parameters = []) Handle request + * @method string|null route(route $route, string $method) Handle route * * @package mirzaev\minimal * @@ -84,7 +89,7 @@ final class core * * @param ?string $namespace Пространство имён системного ядра * - * @return self The instance of the core + * @return void */ public function __construct( ?string $namespace = null @@ -93,30 +98,41 @@ final class core $this->namespace = $namespace ?? (new reflection(self::class))->getNamespaceName(); } - /** * Destructor */ public function __destruct() {} /** - * Request + * Start * - * Handle the request - * - * @param string|null $uri URI of the request (value by default: $_SERVER['REQUEST_URI']) - * @param string|null $method Method of the request (GET, POST, PUT...) (value by default: $_SERVER["REQUEST_METHOD"]) - * @paam array $parameters parameters for merging with route parameters + * Initialize request by environment and handle it * * @return string|null Response */ - public function request(?string $uri = null, ?string $method = null, array $parameters = []): ?string + public function start(): ?string + { + // Handle request and exit (success) + return $this->request(new request(environment: true)); + } + + /** + * Request + * + * Handle request + * + * @param request $request The request + * @paam array $parameters parameters for merging with route parameters + * + * @return string|null Response + */ + public function request(request $request, array $parameters = []): ?string { // Matching a route - $route = $this->router->match($uri ??= $_SERVER['REQUEST_URI'] ?? '/', $method ??= $_SERVER["REQUEST_METHOD"]); + $route = $this->router->match($request); if ($route) { - // Initialized the route + // Initialized a route if (!empty($parameters)) { // Recaived parameters @@ -125,8 +141,11 @@ final class core $route->parameters = $parameters + $route->parameters; } - // Handling the route and exit (success) - return $this->route($route, $method); + // Writing request options from route options + $request->options = $route->options; + + // Handling a route and exit (success) + return $this->route($route, $request); } // Exit (fail) @@ -136,14 +155,18 @@ final class core /** * Route * - * Handle the route + * Handle route * * @param route $route The route - * @param string $method Method of requests (GET, POST, PUT, DELETE, COOKIE...) + * @param request $request The request + * + * @throws exception_domain if failed to find the controller or the model + * @throws exception_logic if not received the controller + * @throws exception_method if failed to find the method of the controller * * @return string|null Response, if generated */ - public function route(route $route, string $method = 'GET'): ?string + public function route(route $route, request $request): ?string { // Initializing name of the controller class $controller = $route->controller; @@ -159,12 +182,12 @@ final class core // Not found the controller and $route->controller has a value // Exit (fail) - throw new exception_domain('Failed to found the controller: ' . $route->controller); + throw new exception_domain("Failed to find the controller: $controller", status::not_implemented->value); } else { // Not found the controller and $route->controller is empty // Exit (fail) - throw new exception_argument('Failed to initialize the controller: ' . $route->controller); + throw new exception_logic('Not received the controller', status::internal_server_error->value); } // Deinitializing name of the controller class @@ -184,7 +207,7 @@ final class core // Not found the model and $route->model has a value // Exit (fail) - throw new exception_domain('Failed to initialize the model: ' . ($route->model ?? $route->controller)); + throw new exception_domain("Failed to find the model: $model", status::not_implemented->value); } // Deinitializing name of the model class @@ -197,165 +220,19 @@ final class core $route->controller->model = $route->model; } - if ($method === 'POST') { - // POST + // Writing the request to the controller + $route->controller->request = $request; - if (method_exists($route->controller, $route->method)) { - // Found the method of the controller + if (method_exists($route->controller, $route->method)) { + // Found the method of the controller - // Executing method of the controller that handles the route and exit (success) - return $route->controller->{$route->method}($route->parameters + $_POST, $_FILES); - } else { - // Not found the method of the controller - - // Exit (fail) - throw new exception('Failed to find the method of the controller: ' . $route->method); - } - } else if ($method === 'GET') { - // GET - - if (method_exists($route->controller, $route->method)) { - // Found the method of the controller - - if ($_SERVER["CONTENT_TYPE"] === 'multipart/form-data' || $_SERVER["CONTENT_TYPE"] === 'application/x-www-form-urlencoded') { - // The requeset content type is the "multipart/form-data" or "application/x-www-form-urlencoded" - - // Analysis of the request - [$_GET, $_FILES] = request_parse_body($route->options); - } - - // Executing method of the controller that handles the route and exit (success) - return $route->controller->{$route->method}($route->parameters + $_GET, $_FILES); - } else { - // Not found the method of the controller - - // Exit (fail) - throw new exception('Failed to find the method of the controller: ' . $route->method); - } - } else if ($method === 'PUT') { - // PUT - - if (method_exists($route->controller, $route->method)) { - // Found the method of the controller - - if ($_SERVER["CONTENT_TYPE"] === 'multipart/form-data' || $_SERVER["CONTENT_TYPE"] === 'application/x-www-form-urlencoded') { - // The requeset content type is the "multipart/form-data" or "application/x-www-form-urlencoded" - - // Analysis of the request - [$_PUT, $_FILES] = request_parse_body($route->options); - } - - // Executing method of the controller that handles the route and exit (success) - return $route->controller->{$route->method}($route->parameters + $_PUT, $_FILES); - } else { - // Not found the method of the controller - - // Exit (fail) - throw new exception('Failed to find the method of the controller: ' . $route->method); - } - } else if ($method === 'DELETE') { - // DELETE - - if (method_exists($route->controller, $route->method)) { - // Found the method of the controller - - if ($_SERVER["CONTENT_TYPE"] === 'multipart/form-data' || $_SERVER["CONTENT_TYPE"] === 'application/x-www-form-urlencoded') { - // The requeset content type is the "multipart/form-data" or "application/x-www-form-urlencoded" - - // Analysis of the request - [$_DELETE, $_FILES] = request_parse_body($route->options); - } - - // Executing method of the controller that handles the route and exit (success) - return $route->controller->{$route->method}($route->parameters + $_DELETE, $_FILES); - } else { - // Not found the method of the controller - - // Exit (fail) - throw new exception('Failed to find the method of the controller: ' . $route->method); - } - } else if ($method === 'PATCH') { - // PATCH - - if (method_exists($route->controller, $route->method)) { - // Found the method of the controller - - if ($_SERVER["CONTENT_TYPE"] === 'multipart/form-data' || $_SERVER["CONTENT_TYPE"] === 'application/x-www-form-urlencoded') { - // The requeset content type is the "multipart/form-data" or "application/x-www-form-urlencoded" - - // Analysis of the request - [$_PATCH, $_FILES] = request_parse_body($route->options); - } - - // Executing method of the controller that handles the route and exit (success) - return $route->controller->{$route->method}($route->parameters + $_PATCH, $_FILES); - } else { - // Not found the method of the controller - - // Exit (fail) - throw new exception('Failed to find the method of the controller: ' . $route->method); - } - } else if ($method === 'HEAD') { - // HEAD - - if (method_exists($route->controller, $route->method)) { - // Found the method of the controller - - // Executing method of the controller that handles the route and exit (success) - return $route->controller->{$route->method}($route->parameters); - } else { - // Not found the method of the controller - - // Exit (fail) - throw new exception('Failed to find the method of the controller: ' . $route->method); - } - } else if ($method === 'OPTIONS') { - // OPTIONS - - if (method_exists($route->controller, $route->method)) { - // Found the method of the controller - - // Executing method of the controller that handles the route and exit (success) - return $route->controller->{$route->method}($route->parameters); - } else { - // Not found the method of the controller - - // Exit (fail) - throw new exception('Failed to find the method of the controller: ' . $route->method); - } - } else if ($method === 'CONNECT') { - // CONNECT - - if (method_exists($route->controller, $route->method)) { - // Found the method of the controller - - // Executing method of the controller that handles the route and exit (success) - return $route->controller->{$route->method}($route->parameters); - } else { - // Not found the method of the controller - - // Exit (fail) - throw new exception('Failed to find the method of the controller: ' . $route->method); - } - } else if ($method === 'TRACE') { - // TRACE - - if (method_exists($route->controller, $route->method)) { - // Found the method of the controller - - // Executing method of the controller that handles the route and exit (success) - return $route->controller->{$route->method}($route->parameters); - } else { - // Not found the method of the controller - - // Exit (fail) - throw new exception('Failed to find the method of the controller: ' . $route->method); - } + // Executing method of the controller and exit (success) + return $route->controller->{$route->method}(...($route->parameters + $request->parameters)); } else { - // Not recognized method of the request - + // Not found the method of the controller + // Exit (fail) - throw new exception_value('Failed to recognize the method: ' . $method); + throw new exception_method('Failed to find method of the controller: ' . $route->controller::class . "->$route->method()", status::not_implemented->value); } // Exit (fail) diff --git a/mirzaev/minimal/system/http/enumerations/content.php b/mirzaev/minimal/system/http/enumerations/content.php new file mode 100644 index 0000000..f67585c --- /dev/null +++ b/mirzaev/minimal/system/http/enumerations/content.php @@ -0,0 +1,94 @@ + + */ +enum content: string +{ + case any = '*/*'; + + // Text + + case txt = 'text/plain'; + case css = 'text/css'; + case csv = 'text/csv'; + case html = 'text/html'; + case js = 'text/javascript'; // js + mjs (https://www.rfc-editor.org/rfc/rfc9239#name-text-javascript) + + // Applications + + case binary = 'application/octet-stream'; + case encoded = 'application/x-www-form-urlencoded'; + case json = 'application/json'; + case rdf = 'application/ld+json'; + case xml = 'application/xml'; + case ogx = 'application/ogg'; + case pdf = 'application/pdf'; + case xls = 'application/vnd.ms-excel'; + case xlsx = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + case tar = 'application/x-tar'; + case zip = 'application/zip'; + case zip7 = 'application/x-7z-compressed'; + case rar = 'application/vnd.rar'; + case jar = 'application/java-archive'; + case odp = 'application/vnd.oasis.opendocument.presentation'; + case ods = 'application/vnd.oasis.opendocument.spreadsheet'; + case odt = 'application/vnd.oasis.opendocument.text'; + case php = 'application/x-httpd-php'; + case sh = 'application/x-sh'; + case xhtml = 'application/xhtml+xml'; + + // Audio + + case aac = 'audio/aac'; + case mp3 = 'audio/mpeg'; + case wav = 'audio/wav'; + case oga = 'audio/ogg'; + case weba = 'audio/webm'; + + // Images + + case gif = 'image/gif'; + case jpeg = 'image/jpeg'; + case png = 'image/png'; + case apng = 'image/apng'; + case tiff = 'image/tiff'; + case svg = ' image/svg+xml'; + case webp = 'image/webp'; + case avif = 'image/avif'; + case bmp = 'image/bmp'; + case ico = 'image/vnd.microsoft.icon'; + + // Videos + + case avi = 'video/x-msvideo'; + case mp4 = 'video/mp4'; + case mpeg = 'video/mpeg'; + case ogv = 'video/ogg'; + case ts = 'video/mp2t'; + + // Fonts + + case otf = 'font/otf'; + case ttf = 'font/ttf'; + case woff = 'font/woff'; + case woff2 = 'font/woff2'; + + // Multipart + + case form = 'multipart/form-data'; + case mixed = 'multipart/mixed'; + case alternative = 'multipart/alternative'; + case related = 'multipart/related'; +} diff --git a/mirzaev/minimal/system/http/enumerations/method.php b/mirzaev/minimal/system/http/enumerations/method.php new file mode 100644 index 0000000..1b2d56c --- /dev/null +++ b/mirzaev/minimal/system/http/enumerations/method.php @@ -0,0 +1,42 @@ + + */ +enum method: string +{ + case post = 'POST'; + case get = 'GET'; + case put = 'PUT'; + case delete = 'DELETE'; + case patch = 'PATCH'; + case head = 'HEAD'; + case options = 'OPTIONS'; + case connect = 'CONNECT'; + case trace = 'TRACE'; + + /** + * Body + * + * @return bool Request with this method may has body? + */ + public function body(): bool + { + // Exit (success) + return match ($this) { + self::post, self::put, self::delete, self::patch => true, + default => false + }; + } +} diff --git a/mirzaev/minimal/system/http/enumerations/protocol.php b/mirzaev/minimal/system/http/enumerations/protocol.php new file mode 100644 index 0000000..29912d8 --- /dev/null +++ b/mirzaev/minimal/system/http/enumerations/protocol.php @@ -0,0 +1,24 @@ + + */ +enum protocol: string +{ + case HTTP_3 = 'HTTP/3'; + case HTTP_2 = 'HTTP/2'; + case HTTP_1_1 = 'HTTP/1.1'; + case HTTP_1_0 = 'hTTP/1.0'; + case HTTP_0_9 = 'HTTP/0.9'; +} diff --git a/mirzaev/minimal/system/http/enumerations/status.php b/mirzaev/minimal/system/http/enumerations/status.php new file mode 100644 index 0000000..f4e4f7b --- /dev/null +++ b/mirzaev/minimal/system/http/enumerations/status.php @@ -0,0 +1,241 @@ + + */ +enum status: int +{ + // 1XX + + case continue = 100; + case switching_protocols = 101; + case processing = 102; + case early_hints = 103; + case among_us = 112; + + // 2XX + + case ok = 200; // ok + case created = 201; + case accepted = 202; + case non_authoritative_information = 203; + case no_content = 204; + case reset_content = 205; + case partial_content = 206; + case multi_status = 207; + case already_reported = 208; + case im_used = 226; // bruh + + // 3XX + + case multiple_choises = 300; + case moved_permanently = 301; + case found = 302; // Previously "Moved temporarily" + case see_other = 303; + case not_modified = 304; + case use_proxy = 305; + case switch_proxy = 306; + case temporary_redirect = 307; + case permanent_redirect = 308; + + // 4XX + + case bad_request = 400; + case unauthorized = 401; + case payment_required = 402; // Are you fucking commerce? + case forbidden = 403; + case not_found = 404; // our celebrity + case method_not_allowed = 405; + case not_acceptable = 406; + case proxy_authentication_required = 407; + case request_timeout = 408; + case conflict = 409; + case gone = 410; + case length_required = 411; + case precondition_failed = 412; + case payload_too_large = 413; + case uri_too_long = 414; + case unsupported_media_type = 415; + case range_not_satisfiable = 416; + case expectation_failed = 417; + case i_am_a_teapot = 418; + case misdirected_request = 421; + case unprocessable_content = 422; + case locked = 423; + case failed_dependency = 424; + case too_early = 425; + case upgrade_required = 426; + case precondition_required = 428; + case too_many_requests = 429; + case request_header_fields_too_large = 431; + case unavailable_for_legal_reasons = 451; // @see self::failed_state + case bruh = 441; + + // 5XX + + case internal_server_error = 500; + case not_implemented = 501; + case bad_gateway = 502; + case service_unawaiable = 503; + case gateway_timeout = 504; + case http_version_not_supported = 505; + case variant_also_negotiates = 506; + case insufficient_storage = 507; + case loop_detected = 508; + case not_extended = 510; + case network_authentication_required = 511; + + // 9XX + + case failed_state = 911; + + /** + * Label + * + * The result will be in ucwords() format - first character uppercase, rest lowercase + * + * You might want to do strtoupper() - convert all characters to uppercase + * "HTTP/2 200 Ok" after strtoupper() will be "HTTP/2 200 OK" + * + * It is common for "OK" to have both characters uppercase, + * and for the other status texts only the first letter of each word uppercase. + * This is universalized here, so the result will be "Ok" instead of "OK". + * + * If you want to get "OK" without using strtoupper(), + * just use the literal "OK" instead of self::ok->label() + * + * The uppercase letter on each word makes it easier and faster to read the status text, + * even if it violates the grammar rules we are accustomed to + * + * I also indignantly conducted a test and tried to use "Early hints" instead of "Early Hints", + * as well as "Already reported" instead of "Already Reported". + * The result of my tests was that the readability of such status texts is greatly reduced. + * + * Also note the following: + * 1. "Non-Authoritative Information" -> "Non Authoritative Information" + * 2. "Multi-Status" -> "Multi Status" + * 3. "IM Used" -> "I Am Used" + * 4. "I`m a teapot" -> "I Am A Teapot" (Initially, i wanted to leave it as in honor of tradition, + * but i decided that this is, first of all, one of our current and working status codes to this day, + * so it should also be universalized. + * 5. "URI" retains its case because it is an abbreviation. + * 6. "HTTP" retaints its case because it is an abbreviation. + * + * If you do not like my changes, just fork MINIMAL and edit this file. + * You will be able to get updates without any problems, which probably will not touch this file. + * + * Or you can write a BASH/FISH script with the creation of a link to your version of the file, + * which will be executed after `composer install`. Of course, you should not do this, + * but this is a completely working solution that is unlikely to break anything + * and is completely automatic and portable. + * + * @throws exception_domain if failed to recognize status + * + * @return string Label + */ + public function label(): string + { + // Exit (success) + return match ($this) { + // 1XX + + self::continue => 'Continue', + self::switching_protocols => 'Switching Protocols', + self::processing => 'Processing', + self::early_hints => 'Early Hints', + self::among_us => 'Among Us', + + // 2XX + + self::ok => 'Ok', + self::created => 'Created', + self::accepted => 'Accepted', + self::non_authoritative_information => 'Non Authoritative Information', + self::no_content => 'No Content', + self::reset_content => 'Reset Content', + self::partial_content => 'Partial Content', + self::multi_status => 'Multi Status', + self::already_reported => 'Already Reported', + self::im_used => 'I Am Used', + + // 3XX + + self::multiple_choises => 'Multiple Choices', + self::moved_permanently => 'Moved Permanently', + self::found => 'Found', // Previously "Moved Temporarily" + self::see_other => 'See Other', + self::not_modified => 'Not Modified', + self::use_proxy => 'Use Proxy', + self::switch_proxy => 'Switch Proxy', + self::temporary_redirect => 'Temporary Redirect', + self::permanent_redirect => 'Permanent Redirect', + + // 4XX + + self::bad_request => 'Bad Request', + self::unauthorized => 'Unauthorized', + self::payment_required => 'Payment Required', // do not make me angry + self::forbidden => 'Forbidden', + self::not_found => 'Not Found', + self::method_not_allowed => 'Method Not Allowed', + self::not_acceptable => 'Not Acceeptable', + self::proxy_authentication_required => 'Proxy Authentication Required', + self::request_timeout => 'Request Timeout', + self::conflict => 'Conflict', + self::gone => 'Gone', + self::length_required => 'Length Reuired', + self::precondition_failed => 'Precondition Failed', + self::payload_too_large => 'Payload Too Large', + self::uri_too_long => 'URI Too Long', + self::unsupported_media_type => 'Unsupported Media Type', + self::range_not_satisfiable => 'Range Not Satisfiable', + self::expectation_failed => 'Exception Failed', + self::i_am_a_teapot => 'I Am A Teapot', + self::misdirected_request => 'Misdirected Request', + self::unprocessable_content => 'Unprocessable Content', + self::locked => 'Locked', + self::failed_dependency => 'Failed Dependency', + self::too_early => 'Too Early', + self::upgrade_required => 'Upgrade Required', + self::precondition_required => 'Precondition Required', + self::too_many_requests => 'Too Many Requests', + self::request_header_fields_too_large => 'Request Header Fields Too Large', + self::unavailable_for_legal_reasons => 'Unavaiable For Legal Reasons', // Fucking disgrease. + self::bruh => 'Bruh', + + // 5XX + + self::internal_server_error => 'Internal Server Error', + self::not_implemented => 'Not Implemented', + self::bad_gateway => 'Bad Gateway', + self::service_unawaiable => 'Service Unawaiable', + self::gateway_timeout => 'Gateway Timeout', + self::http_version_not_supported => 'HTTP Version Not Supported', + self::variant_also_negotiates => 'Variant Also Negotiates', + self::insufficient_storage => 'Insufficient Storage', + self::loop_detected => 'Loop Detected', + self::not_extended => 'Not Extended', + self::network_authentication_required => 'Network Authentication Required', + + // 9XX + + self::failed_state => 'Failed State', + + default => throw new exception_domain('Failed to recognize status', self::not_found->value) + }; + } +} diff --git a/mirzaev/minimal/system/http/request.php b/mirzaev/minimal/system/http/request.php new file mode 100755 index 0000000..02b5c14 --- /dev/null +++ b/mirzaev/minimal/system/http/request.php @@ -0,0 +1,504 @@ + + */ +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 ($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 (match($this->headers['content-type']) { content::form->value, content::encoded->value => true, default => false }) { + // 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; + } +} diff --git a/mirzaev/minimal/system/http/response.php b/mirzaev/minimal/system/http/response.php new file mode 100755 index 0000000..c1ae4b7 --- /dev/null +++ b/mirzaev/minimal/system/http/response.php @@ -0,0 +1,448 @@ + + */ +final class response +{ + /** + * 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::{$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); + } + } + } + } + + /** + * Status + * + * @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 status + * + * @var status $status Status + */ + public status $status { + // Write + set (status|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 status) { + // Received implementation of status + + // Writing + $this->status = $value; + } else { + // Received a string literal (excected name of status) + + // Initializing implementator of status + $status = status::{$value}; + + if ($status instanceof status) { + // Initialized implementator of status + + // Writing + $this->status = $status; + } else { + // Not initialized implementator of status + + // Exit (fail) + throw new exception_domain("Failed to recognize status: $value", status::internal_server_error->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; + } + + /** + * Body + * + * @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks) + * + * @var string $body Serialized content + */ + public string $body = '' { + // Write + set (string $value) { + // Writing + $this->body = $value; + }; + + // Read + &get => $this->body; + } + + /** + * Constructor + * + * @param protocol|string|null $protocol version of http protocol + * @param status|null $status Status + * @param array|null $headers Headers + * @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 a required property + * + * @return void + */ + public function __construct( + protocol|string|null $protocol = null, + ?status $status = null, + ?array $headers = null, + bool $environment = false + ) { + // Writing verstion of HTTP protocol from argument into the property + if (isset($protocol)) $this->protocol = $protocol; + + // Writing status from argument into the property + if (isset($status)) $this->status = $status; + + 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); + } + + if ($environment) { + // Requested to write values from environment + + // Writing verstion of HTTP protocol from environment into the property + $this->protocol ??= $_SERVER['SERVER_PROTOCOL']; + + // Writing status from environment into the property + $this->status ??= status::ok; + } + + // Validating of required properties + if (!isset($this->protocol)) throw new exception_argument('Failed to initialize HTTP version of the request', status::internal_server_error->value); + if (!isset($this->status)) throw new exception_argument('Failed to initialize status of the request', status::internal_server_error->value); + } + + /** + * Server-Sent Events (SSE) + * + * Writes headers for SSE implementation + * + * @return self The instance from which the method was called (fluent interface) + */ + public function sse(): self + { + // Writing headers to the headers property (https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2) + $this->headers['x-accel-buffering'] = 'no'; + $this->headers['content-encoding'] = 'none'; + + // Exit (success) + return $this; + } + + /** + * JSON + * + * Writes headers for JSON implementation + * Concatenates with the response body property if $content argument was received + * + * @param mixed $content JSON or content that will be serialized via json_encode() + * + * @return self The instance from which the method was called (fluent interface) + */ + public function json(mixed $content): self + { + // Writing headers to the headers property (https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2) + $this->headers['content-type'] = content::json->value; + + if (!empty($content)) { + // Received content + + if (is_string($content) && json_validate($content, 512)) { + // Validated as JSON + + // Writing to the response body property + $this->body .= $content; + } else { + // Not validated as JSON + + // Writing to the response body property + $this->body .= json_encode($content, depth: 512); + } + } + + // Exit (success) + return $this; + } + + /** + * Write + * + * Concatenates with the response body property + * + * @param string $value Value that will be concatenated with the response body property + * + * @return self The instance from which the method was called (fluent interface) + */ + public function write(string $value): self + { + // Writing to the response body property + $this->body .= $value; + + // Exit (success) + return $this; + } + + /** + * Body + * + * Transfer the contents of the body property to the output buffer + * + * @return self The instance from which the method was called (fluent interface) + */ + public function body(): self + { + // Writing to the output buffer + echo $this->body; + + // Exit (success) + return $this; + } + + /** + * Validate + * + * Validate response by request + * + * @param request $request The request for validating with it + * + * @return self|false The instance from which the method was called if validated (fluent interface) + */ + public function validate(request $request): self|false + { + if (str_contains($request->headers['accept'], $this->headers['content-type'] ?? '')) { + // Validated with "accept" and "content-type" + + // Exit (success) + return $this; + } + + // Exit (fail) + return false; + + // Exit (fail) + return false; + } + + /** + * Status line + * + * Generates the status line (HTTP/2 200 OK) + * + * @return string The status line + */ + public function status(): string + { + // Exit (success) + return $this->protocol->value . ' ' . $this->status->value . ' ' . $this->status->label(); + } + + /** + * 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; + } + + /** + * Start + * + * Initializes response headers and output buffer + * + * @return self The instance from which the method was called (fluent interface) + */ + public function start(): self + { + // Initializing the heaader string + header($this->status()); + + // Initializing headers + foreach ($this->headers ?? [] as $name => $value) header("$name: $value", replace: true); + + // Initializing the output buffer + ob_start(); + + // Exit (success) + return $this; + } + + /** + * Clean + * + * Delete everything in the output buffer + * + * @return self The instance from which the method was called (fluent interface) + */ + public function clean(): self + { + // Flushing the output buffer + ob_clean(); + + // Exit (success) + return $this; + } + + /** + * End + * + * Initializes response headers and flushes the output buffer + * + * @return self The instance from which the method was called (fluent interface) + */ + public function end(): self + { + // Calculate length of the output buffer and write to the header (https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2) + header('content-length: ' . ob_get_length()); + + // Sending response and flushing the output buffer + ob_end_flush(); + flush(); + + // Deinitializing headers property + unset($this->headers); + + // Deinitializing headers + header_remove(); + + // Exit (success) + return $this; + } +} diff --git a/mirzaev/minimal/system/model.php b/mirzaev/minimal/system/model.php index dff3327..2a39b95 100755 --- a/mirzaev/minimal/system/model.php +++ b/mirzaev/minimal/system/model.php @@ -10,7 +10,7 @@ use mirzaev\minimal\traits\magic; /** * Model * - * @method self __construct() Constructor + * @method void __construct() Constructor * * @package mirzaev\minimal * @@ -24,7 +24,7 @@ class model /** * Constructor * - * @return self + * @return void */ public function __construct() {} } diff --git a/mirzaev/minimal/system/route.php b/mirzaev/minimal/system/route.php index 710c4ed..d88aad9 100755 --- a/mirzaev/minimal/system/route.php +++ b/mirzaev/minimal/system/route.php @@ -13,7 +13,7 @@ namespace mirzaev\minimal; * @param array $parameters Arguments for the $this->method (will be concatenated together with generated request parameters) * @param array $options Options for `request_parse_body($options)` * - * @method self __construct(string $controller,?string $method, ?string $model, array $variables, array $options) Constructor + * @method void __construct(string|controller $controller, ?string $method, string|model|null $model, array $parameters, array $options) Constructor * * @package mirzaev\minimal * @@ -25,7 +25,7 @@ final class route /** * Controller * - * @var string|model $controller Name of the controller or an instance of the controller + * @var string|controller $controller Name of the controller or an instance of the controller */ public string|controller $controller { // Read @@ -54,38 +54,55 @@ final class route /** * Parameters + * + * @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks) * * @var array $parameters Arguments for the $this->method (will be concatenated together with generated request parameters) */ - public array $parameters { + public array $parameters = [] { // Read - get => $this->parameters; + &get => $this->parameters; } /** * Options * - * Used only for GET, PUT, PATCH and DELETE + * 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 string $options Options for `request_parse_body($options)` + * @var array $options Options for `request_parse_body($options)` */ public array $options { // Write - set (array $value) => 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 => false - }, - ARRAY_FILTER_USE_KEY - ); + 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; + get => $this->options ?? []; } /** @@ -97,7 +114,7 @@ final class route * @param array $parameters Arguments for the $method (will be concatenated together with generated request parameters) * @param array $options Options for `request_parse_body` (Only for POST method) * - * @return self + * @return void */ public function __construct( string|controller $controller, diff --git a/mirzaev/minimal/system/router.php b/mirzaev/minimal/system/router.php index c61b350..85755db 100755 --- a/mirzaev/minimal/system/router.php +++ b/mirzaev/minimal/system/router.php @@ -6,6 +6,7 @@ namespace mirzaev\minimal; // Files of the project use mirzaev\minimal\route, + mirzaev\minimal\http\request, mirzaev\minimal\traits\singleton; // Build-ing libraries @@ -14,9 +15,10 @@ use InvalidArgumentException as exception_argument; /** * Router * - * @param array $routes The registry of routes + * @param array $routes Registry of routes * - * @method self write(string $uri, string $method) + * @method self write(string $urn, route $route, string|array $method) Write route to registry of routes (fluent interface) + * @method route|null match(request $request) Match request URI with registry of routes * @method self sort() Sort routes (DEV) * @method string universalize(string $urn) Universalize URN * @@ -32,35 +34,38 @@ final class router /** * Routes * - * @var array $routes The registry of routes + * @var array $routes Registry of routes */ - protected array $routes = []; + protected array $routes = [] { + // Read + &get => $this->routes; + } /** - * Write a route + * Write route + * + * Write route to registry of routes * * @param string $urn URN of the route ('/', '/page', '/page/$variable', '/page/$collector...'...) * @param route $route The route - * @param string|array $method Method of requests (GET, POST, PUT, DELETE, COOKIE...) + * @param string|array $method Method of requests * * @return self The instance from which the method was called (fluent interface) */ - public function write( - string $urn, - route $route, - null|string|array $method = 'GET' - ): self { + public function write(string $urn, route $route, string|array $method): self + { foreach (is_array($method) ? $method : [$method] as $method) { // Iterate over methods of requests - // Validating method - $method = match ($method) { - 'POST', 'GET', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE' => $method, - default => throw new exception_argument("Failed to initialize method: \"$method\"") - }; + // Initializing the request + $request = new request( + uri: $urn, + method: $method, + environment: false + ); // Writing to the registry of routes - $this->routes[$urn][$method] = $route; + $this->routes[$request->uri][$request->method->value] = $route; } // Exit (success) (fluent interface) @@ -72,12 +77,12 @@ final class router * * Match request URI with registry of routes * - * @param string $uri URI (protocol://domain/foo/bar) - * @param string $method Method of the request (GET, POST, PUT...) + * @param request $request The request * * @return route|null Route, if found */ - public function match(string $uri, string $method): ?route { + public function match(request $request): ?route + { // Declaration of the registry of routes directoies $routes = []; @@ -93,7 +98,7 @@ final class router // Initialized the registry of routes directoies // Universalization of URN (/foo/bar) - $urn = self::universalize(parse_url(urldecode($uri), PHP_URL_PATH)); + $urn = self::universalize(parse_url(urldecode($request->uri), PHP_URL_PATH)); // Search directories of URN (explode() creates empty value in array) preg_match_all('/(^\/$|[^\/]+)/', $urn, $directories); @@ -112,7 +117,7 @@ final class router foreach ($this->routes as $route => $data) { // Iteration over routes - if (isset($data[$method])) { + if (isset($data[$request->method->value])) { // The request method matches the route method // Universalization of route @@ -173,12 +178,12 @@ final class router // The directory is a variable ($variable) // Запись в реестр переменных и перещапись директории в маршруте - $data[$method]->variables[trim($route_directory, '$')] = $directories[$i]; + $data[$request->method->value]->variables[trim($route_directory, '$')] = $directories[$i]; } else if (preg_match('/^\$([a-zA-Z_\x80-\xff]+\.\.\.)$/', $route_directory) === 1) { // The directory of route is a collector ($variable...) // Инициализаия ссылки на массив сборщика - $collector = &$data[$method]->variables[trim($route_directory, '$.')]; + $collector = &$data[$request->method->value]->variables[trim($route_directory, '$.')]; // Инициализаия массива сборщика $collector ??= []; @@ -198,7 +203,7 @@ final class router } // Exit (success or fail) - return $data[$method] ?? null; + return $data[$request->method->value] ?? null; } } diff --git a/mirzaev/minimal/system/traits/magic.php b/mirzaev/minimal/system/traits/magic.php index ba67085..6b29e40 100755 --- a/mirzaev/minimal/system/traits/magic.php +++ b/mirzaev/minimal/system/traits/magic.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace mirzaev\minimal\traits; +// Files of the project +use mirzaev\minimal\http\enumerations\status; + // Built-in libraries use exception; @@ -33,7 +36,7 @@ trait magic public function __set(string $name, mixed $value = null): void { match ($name) { - default => throw new exception('Property "' . static::class . "::\$$name\" not found", 404) + default => throw new exception('Failed to find property: ' . static::class . "::\$$name", status::not_found->value) }; } @@ -47,7 +50,7 @@ trait magic public function __get(string $name): mixed { return match ($name) { - default => throw new exception('Property "' . static::class . "::\$$name\" not found", 404) + default => throw new exception('Failed to find property: ' . static::class . "::\$$name", status::not_found->value) }; }