Compare commits

...

18 Commits

Author SHA1 Message Date
Arsen Mirzaev Tatyano-Muradovich f936b70916 something 2024-11-19 13:03:40 +03:00
Arsen Mirzaev Tatyano-Muradovich c92838db5a resolved #4, resolved #5, resolved #6, resolved #7, resolved #8, resolved #14 2024-11-05 00:54:29 +03:00
Arsen Mirzaev Tatyano-Muradovich 6a4ea3f351 executing requests from a controller + boosted by PHP 8.4 + the router rebuild. hell fucking yeah 2024-10-31 23:34:35 +03:00
Arsen Mirzaev Tatyano-Muradovich f95e4082ef mega sex 2024-10-11 10:55:40 +03:00
Arsen Mirzaev Tatyano-Muradovich 9011ccf557 beauty 2024-10-11 10:45:56 +03:00
Arsen Mirzaev Tatyano-Muradovich e1a6483556 collectors, new router, errors handlers, refactoring 2024-10-11 10:14:33 +03:00
Arsen Mirzaev Tatyano-Muradovich 95ddffba30 fix postfix public 2024-01-04 04:08:44 +07:00
Arsen Mirzaev Tatyano-Muradovich 41bf8ab56a обновил чёто забыл уже 2023-12-21 23:14:34 +07:00
Arsen Mirzaev Tatyano-Muradovich 3d31c92628 a little fixes and transit into PHP 8.2 2023-03-20 21:46:41 +10:00
Arsen Mirzaev Tatyano-Muradovich 1f5685a20d Косметические изменения кода 2022-11-03 16:09:32 +10:00
Arsen Mirzaev Tatyano-Muradovich 22ad7304f9 Перенос с git.hood.su на git.mirzaev.sexy 2022-11-03 08:27:45 +10:00
Arsen Mirzaev Tatyano-Muradovich 7777d7af17 Баг при запросе маршрута с неправильным типом запроса 2022-03-04 07:15:52 +10:00
Arsen Mirzaev Tatyano-Muradovich 483814b6a5 Теперь понимает передачу файлов 2022-03-04 04:35:21 +10:00
Arsen Mirzaev Tatyano-Muradovich e7a6b9cebe Исправления для маршрута на главную страницу 2022-03-04 02:15:42 +10:00
Arsen Mirzaev Tatyano-Muradovich a4949ebc52 Мелкое исправление по переменным в контроллер 2022-02-28 04:40:03 +10:00
Arsen Mirzaev Tatyano-Muradovich aeed2d21cc Доработка передачи переменных в контроллер 2022-02-28 04:30:29 +10:00
Arsen Mirzaev Tatyano-Muradovich 81990de191 Маршрутизатор теперь умеет в переменные 2022-02-28 04:22:21 +10:00
Arsen Mirzaev Tatyano-Muradovich b6f90b7001 Исправление composer.json 2021-11-12 23:20:13 +10:00
18 changed files with 4831 additions and 3108 deletions

0
.gitignore vendored Normal file → Executable file
View File

11
LICENSE Executable file
View File

@ -0,0 +1,11 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

64
README.md Executable file
View File

@ -0,0 +1,64 @@
The MINIMAL framework that does **not limit your project with its own rules**, has **no dependencies**, implements the **best practices** of popular MVC-frameworks, it **VERY fast** and **optimized** for all the innovations in **PHP 8.2** 🤟
Can be configured to work with **any database** `core::$session` and **any HTML template engine** `$this->view`
*personally, i prefer **ArangoDB** and **Twig***
## Nearest plans (first half of 2025)
1. Add **middlewares** technology
2. Route sorting in the router `router::sort()`
3. Add trigger routes from within routes
4. Think about adding asynchronous executions
5. Write an article describing the principles of the framework
## Installation
Execute: `composer require mirzaev/minimal`
## Usage
*index.php*
```php
// Initializing the router
$router = new router;
// Initializing of routes
$router
->write('/', 'catalog', 'index', 'GET')
->write('/search', 'catalog', 'search', 'POST')
->write('/session/connect/telegram', 'session', 'telegram', 'POST')
->write('/product/$id', 'catalog', 'product', 'POST')
->write('/$categories...', 'catalog', 'index', 'POST'); // Collector (since 0.3.0)
// Initializing the core
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
// Handle the request
echo $core->start();
```
## Examples of projects based on MINIMAL
### ebala
**Repository:** https://git.mirzaev.sexy/mirzaev/ebala<br>
**Github mirror:** https://github.com/mature-woman/ebala<br>
*I earned more than a **million rubles** from this project*<br>
*Repositories **may** be closed at the request of the customer*<br>
### huesos
**Repository:** https://git.mirzaev.sexy/mirzaev/huesos<br>
**Guthub mirror:** https://github.com/mature-woman/huesos<br>
*The basis for developing chat-robots with Web App technology (for example for Telegram)*<br>
### arming_bot
**Repository:** https://git.mirzaev.sexy/mirzaev/arming_bot<br>
**Guthub mirror:** https://github.com/mature-woman/arming_bot<br>
*Chat-robot based on huesos*<br>
### notchat
**Repository:** https://git.mirzaev.sexy/mirzaev/notchat<br>
**Github mirror:** https://github.com/mature-woman/notchat<br>
*P2P chat project with different blockchains and smart stuff*<br>
### site-repression
**Link:** https://repression.mirzaev.sexy<br>
**Repository:** https://git.mirzaev.sexy/mirzaev/site-repression<br>
**Github mirror:** https://github.com/mature-woman/site-repression<br>
*A simple site for my article about **political repressions in Russia** and my **kidnapping by Wagner PMC operatives** from my house*<br>

28
composer.json Normal file → Executable file
View File

@ -1,44 +1,32 @@
{ {
"name": "mirzaev/minimal", "name": "mirzaev/minimal",
"type": "framework", "type": "framework",
"description": "Легковесный MVC фреймворк который следует твоим правилам, а не диктует свои", "description": "My vision of a good framework",
"keywords": [ "keywords": [
"mvc", "mvc",
"framework" "framework",
"lightweight"
], ],
"homepage": "https://git.hood.su/mirzaev/minimal",
"license": "WTFPL", "license": "WTFPL",
"homepage": "https://git.mirzaev.sexy/mirzaev/minimal",
"authors": [ "authors": [
{ {
"name": "Arsen Mirzaev Tatyano-Muradovich", "name": "Arsen Mirzaev Tatyano-Muradovich",
"email": "arsen@mirzaev.sexy", "email": "arsen@mirzaev.sexy",
"homepage": "https://mirzaev.sexy", "homepage": "https://mirzaev.sexy",
"role": "Developer" "role": "Programmer"
} }
], ],
"support": { "support": {
"docs": "https://git.hood.su/mirzaev/minimal/manual", "docs": "https://git.mirzaev.sexy/mirzaev/minimal/wiki",
"issues": "https://git.hood.su/mirzaev/minimal/issues" "issues": "https://git.mirzaev.sexy/mirzaev/minimal/issues"
}, },
"require": { "require": {
"php": "~8.0", "php": "~8.4"
"psr/log": "~3.0",
"twig/twig": "^3.3"
},
"require-dev": {
"phpunit/phpunit": "~9.5"
},
"suggest": {
"ext-PDO": "Для работы с базами данных на SQL (MySQL, PostreSQL...)"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"mirzaev\\minimal\\": "mirzaev/minimal/system" "mirzaev\\minimal\\": "mirzaev/minimal/system"
} }
},
"autoload-dev": {
"psr-4": {
"mirzaev\\minimal\\tests\\": "mirzaev/minimal/tests"
}
} }
} }

0
composer.lock generated Normal file → Executable file
View File

241
mirzaev/minimal/system/controller.php Normal file → Executable file
View File

@ -4,176 +4,115 @@ declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
use mirzaev\minimal\model; // Files of the project
use mirzaev\minimal\model,
mirzaev\minimal\core,
mirzaev\minimal\traits\magic,
mirzaev\minimal\http\request,
mirzaev\minimal\http\enumerations\status;
use Exception; // Built-in libraries
use exception,
RuntimeException as exception_runtime;
/** /**
* Контроллер * Controller
* *
* @package mirzaev\minimal * @package mirzaev\minimal
*
* @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)
*
* @method void __construct(core $core) Constructor
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/ */
class controller class controller
{ {
/** use magic;
* Постфикс
*/
private string $postfix = '_controller';
/** /**
* Модель * Core
*
* @var core $core An instance of the core
*/ */
protected model $model; public core $core {
// Read
get => $this->core;
}
/** /**
* Шаблонизатор представления * Request
*
* @var request $request Request
*/ */
protected object $view; public request $request {
// Read
get => $this->request;
}
/** /**
* Конструктор * Model
*
* @throws exception_runtime if reinitialize the property
* @throws exception_runtime if an attempt to write null
*
* @var model $model An instance of the model connected in the core
*/
public ?model $model = null {
// Write
set (model|null $model) {
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 ($model instanceof model) {
// Validated model
// Writing
$this->model = $model;
} else {
// Not validated model
// Exit (fail)
throw new exception_runtime('The property must be an instance of model', status::internal_server_error->value);
}
}
// Read
get => $this->model;
}
/**
* View
*
* @var view $view View template engine instance (twig)
*/
public object $view {
// Write
set (object $view) {
$this->view ??= $view;
}
// Read
get => $this->view;
}
/**
* Constructor
*
* @param core $core The instance of the core
* *
* @return void * @return void
*/ */
public function __construct() public function __construct(core $core) {
{ // Writing the core into the property
} $this->core = $core;
/**
* Записать свойство
*
* @param string $name Название
* @param mixed $value Значение
*/
public function __set(string $name, mixed $value = null): void
{
match ($name) {
'model' => (function () use ($value) {
if (isset($this->model)) {
// Свойство уже было инициализировано
// Выброс исключения (неудача)
throw new exception('Запрещено реинициализировать модель ($this->model)', 500);
} else {
// Свойство ещё не было инициализировано
if ($value instanceof model) {
// Передано подходящее значение
// Запись свойства (успех)
$this->model = $value;
} else {
// Передано неподходящее значение
// Выброс исключения (неудача)
throw new exception('Модель ($this->model) должна хранить инстанцию "mirzaev\minimal\model"', 500);
}
}
})(),
'view' => (function () use ($value) {
if (isset($this->view)) {
// Свойство уже было инициализировано
// Выброс исключения (неудача)
throw new exception('Запрещено реинициализировать шаблонизатор представления ($this->view)', 500);
} else {
// Свойство ещё не было инициализировано
if (is_object($value)) {
// Передано подходящее значение
// Запись свойства (успех)
$this->view = $value;
} else {
// Передано неподходящее значение
// Выброс исключения (неудача)
throw new exception('Шаблонизатор представлений ($this->view) должен хранить объект', 500);
}
}
})(),
'postfix' => (function () use ($value) {
if (isset($this->postfix)) {
// Свойство уже было инициализировано
// Выброс исключения (неудача)
throw new exception('Запрещено реинициализировать постфикс ($this->postfix)', 500);
} else {
// Свойство ещё не было инициализировано
if ($value = filter_var($value, FILTER_SANITIZE_STRING)) {
// Передано подходящее значение
// Запись свойства (успех)
$this->postfix = $value;
} else {
// Передано неподходящее значение
// Выброс исключения (неудача)
throw new exception('Постфикс ($this->postfix) должен быть строкой', 500);
}
}
})(),
default => throw new exception("Свойство \"\$$name\" не найдено", 404)
};
}
/**
* Прочитать свойство
*
* Записывает значение по умолчанию, если свойство не инициализировано
*
* @param string $name Название
*
* @return mixed Содержимое
*/
public function __get(string $name): mixed
{
return match ($name) {
'postfix' => (function () {
if (isset($this->postfix)) {
// Свойство уже было инициализировано
} else {
// Свойство ещё не было инициализировано
// Инициализация со значением по умолчанию
$this->__set('postfix', '_controller');
}
// Возврат (успех)
return $this->postfix;
})(),
'view' => $this->view ?? throw new exception("Свойство \"\$$name\" не инициализировано", 500),
'model' => $this->model ?? throw new exception("Свойство \"\$$name\" не инициализировано", 500),
default => throw new exception("Свойство \"\$$name\" не обнаружено", 404)
};
}
/**
* Проверить свойство на инициализированность
*
* @param string $name Название
*/
public function __isset(string $name): bool
{
return match ($name) {
default => isset($this->{$name})
};
}
/**
* Удалить свойство
*
* @param string $name Название
*/
public function __unset(string $name): void
{
match ($name) {
default => (function () use ($name) {
// Удаление
unset($this->{$name});
})()
};
} }
} }

476
mirzaev/minimal/system/core.php Normal file → Executable file
View File

@ -4,313 +4,251 @@ declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
use mirzaev\minimal\router; // Files of the project
use mirzaev\minimal\controller; use mirzaev\minimal\router,
use mirzaev\minimal\model; mirzaev\minimal\route,
mirzaev\minimal\controller,
mirzaev\minimal\model,
mirzaev\minimal\http\request,
mirzaev\minimal\http\response,
mirzaev\minimal\http\enumerations\status;
use exception; // Built-in libraries
use exception,
RuntimeException as exception_runtime,
BadMethodCallException as exception_method,
DomainException as exception_domain,
InvalidArgumentException as exception_argument,
UnexpectedValueException as exception_value,
LogicException as exception_logic,
ReflectionClass as reflection;
/** /**
* Ядро * Core
* *
* @package mirzaev\minimal * @package mirzaev\minimal
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
* *
* @todo * @param string $namespace Namespace where the core was initialized from
* 1. Добавить __isset() и __unset() * @param controller $controller An instance of the controller
* @param model $model An instance of the model
* @param router $router An instance of the router
*
* @mathod void __construct(?string $namespace) Constructor
* @method void __destruct() Destructor
* @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
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/ */
final class core final class core
{ {
/** /**
* Соединение с базой данных * Namespace
*/
private object $storage;
/**
* Маршрутизатор
*/
private router $router;
/**
* Контроллер
*/
private controller $controller;
/**
* Модель
*/
private model $model;
/**
* Пространство имён проекта
* *
* Используется для поиска файлов по спецификации PSR-4 * @var string $namespace Namespace where the core was initialized from
*
* @see https://www.php-fig.org/psr/psr-4/
*/ */
private string $namespace; public string $namespace {
// Read
get => $this->namespace;
}
/** /**
* Конструктор * Controller
* *
* @param object $storage Хранилище * @var controller $controller An instance of the controller
* @param router $router Маршрутизатор
* @param string $uri Маршрут
*/ */
public function __construct(object $storage = null, router $router = null, controller $controller = null, model $model = null, string $namespace = null) private controller $controller {
// Read
get => $this->controller ??= new controller;
}
/**
* Model
*
* @var model $model An instance of the model
*/
private model $model {
// Read
get => $this->model ??= new model;
}
/**
* Router
*
* @var router $router An instance of the router
*/
public router $router {
get => $this->router ??= router::initialize();
}
/**
* Constrictor
*
* @param ?string $namespace Пространство имён системного ядра
*
* @return void
*/
public function __construct(
?string $namespace = null
) {
// Writing a namespace to the property
$this->namespace = $namespace ?? (new reflection(self::class))->getNamespaceName();
}
/**
* Destructor
*/
public function __destruct() {}
/**
* Start
*
* Initialize request by environment and handle it
*
* @return string|null Response
*/
public function start(): ?string
{ {
if (isset($storage)) { // Handle request and exit (success)
// Переданы данные для хранилища return $this->request(new request(environment: true));
// Проверка и запись
$this->__set('storage', $storage);
}
if (isset($router)) {
// Переданы данные для маршрутизатора
// Проверка и запись
$this->__set('router', $router);
}
if (isset($controller)) {
// Переданы данные для контроллера
// Проверка и запись
$this->__set('controller', $controller);
}
if (isset($model)) {
// Переданы данные для модели
// Проверка и запись
$this->__set('model', $model);
}
if (isset($namespace)) {
// Переданы данные для пространства имён
// Проверка и запись
$this->__set('namespace', $namespace);
}
} }
/** /**
* Деструктор * Request
* *
* Handle request
*
* @param request $request The request
* @paam array $parameters parameters for merging with route parameters
*
* @return string|null Response
*/ */
public function __destruct() public function request(request $request, array $parameters = []): ?string
{ {
// Matching a route
$route = $this->router->match($request);
if ($route) {
// Initialized a route
if (!empty($parameters)) {
// Recaived parameters
// Merging parameters with route parameters
$route->parameters = $parameters + $route->parameters;
} }
public function start(string $uri = null): ?string // Writing request options from route options
{ $request->options = $route->options;
// Обработка запроса
return $this->__get('router')->handle($uri, core: $this); // Handling a route and exit (success)
return $this->route($route, $request);
}
// Exit (fail)
return null;
} }
/** /**
* Записать свойство * Route
* *
* @param string $name Название * Handle route
* @param mixed $value Значение *
* @param route $route The route
* @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 __set(string $name, mixed $value = null): void public function route(route $route, request $request): ?string
{ {
match ($name) { // Initializing name of the controller class
'storage', 'db', 'database' => (function () use ($value) { $controller = $route->controller;
if (isset($this->storage)) {
// Свойство уже было инициализировано
// Выброс исключения (неудача) if ($route->controller instanceof controller) {
throw new exception('Запрещено реинициализировать хранилище ($this->storage)', 500); // Initialized the controller
} else if (class_exists($controller = "$this->namespace\\controllers\\$controller")) {
// Found the controller by its name
// Initializing the controller
$route->controller = new $controller(core: $this);
} else if (!empty($route->controller)) {
// Not found the controller and $route->controller has a value
// Exit (fail)
throw new exception_domain("Failed to find the controller: $controller", status::not_implemented->value);
} else { } else {
// Свойство ещё не было инициализировано // Not found the controller and $route->controller is empty
if (is_object($value)) { // Exit (fail)
// Передано подходящее значение throw new exception_logic('Not received the controller', status::internal_server_error->value);
}
// Deinitializing name of the controller class
unset($controller);
if (!isset($route->controller->model)) {
//
// Initializing name if the model class
$model = $route->model;
if ($route->model instanceof model) {
// Initialized the model
} else if (class_exists($model = "$this->namespace\\models\\$model")) {
// Found the model by its name
// Initializing the model
$route->model = new $model;
} else if (!empty($route->model)) {
// Not found the model and $route->model has a value
// Exit (fail)
throw new exception_domain("Failed to find the model: $model", status::not_implemented->value);
}
// Deinitializing name of the model class
unset($model);
if ($route->model instanceof model) {
// Initialized the model
// Writing the model to the controller
$route->controller->model = $route->model;
}
}
// Writing the request to the controller
$route->controller->request = $request;
if (method_exists($route->controller, $route->method)) {
// Found the method of the controller
try {
// Executing method of the controller and exit (success)
return $route->controller->{$route->method}(...($route->parameters + $request->parameters));
} catch (exception $e) {
// Catched an exception
// Exit (fail)
throw new exception_runtime(...$e);
}
// Запись свойства (успех)
$this->storage = $value;
} else { } else {
// Передано неподходящее значение // Not found the method of the controller
// Выброс исключения (неудача) // Exit (fail)
throw new exception('Хранилище ($this->storage) должно хранить объект', 500); throw new exception_method('Failed to find method of the controller: ' . $route->controller::class . "->$route->method()", status::not_implemented->value);
}
}
})(),
'router' => (function () use ($value) {
if (isset($this->router)) {
// Свойство уже было инициализировано
// Выброс исключения (неудача)
throw new exception('Запрещено реинициализировать маршрутизатор ($this->router)', 500);
} else {
// Свойство ещё не было инициализировано
if ($value instanceof router) {
// Передано подходящее значение
// Запись свойства (успех)
$this->router = $value;
} else {
// Передано неподходящее значение
// Выброс исключения (неудача)
throw new exception('Маршрутизатор ($this->router) должен хранить инстанцию "mirzaev\minimal\router"', 500);
}
}
})(),
'controller' => (function () use ($value) {
if (isset($this->controller)) {
// Свойство уже было инициализировано
// Выброс исключения (неудача)
throw new exception('Запрещено реинициализировать контроллер ($this->controller)', 500);
} else {
// Свойство ещё не было инициализировано
if ($value instanceof controller) {
// Передано подходящее значение
// Запись свойства (успех)
$this->controller = $value;
} else {
// Передано неподходящее значение
// Выброс исключения (неудача)
throw new exception('Контроллер ($this->controller) должен хранить инстанцию "mirzaev\minimal\controller"', 500);
}
}
})(),
'model' => (function () use ($value) {
if (isset($this->model)) {
// Свойство уже было инициализировано
// Выброс исключения (неудача)
throw new exception('Запрещено реинициализировать модель ($this->model)', 500);
} else {
// Свойство ещё не было инициализировано
if ($value instanceof model) {
// Передано подходящее значение
// Запись свойства (успех)
$this->model = $value;
} else {
// Передано неподходящее значение
// Выброс исключения (неудача)
throw new exception('Модель ($this->model) должен хранить инстанцию "mirzaev\minimal\model"', 500);
}
}
})(),
'namespace' => (function () use ($value) {
if (isset($this->namespace)) {
// Свойство уже было инициализировано
// Выброс исключения (неудача)
throw new exception('Запрещено реинициализировать пространство имён ($this->namespace)', 500);
} else {
// Свойство ещё не было инициализировано
if (is_string($value)) {
// Передано подходящее значение
// Запись свойства (успех)
$this->namespace = $value;
} else {
// Передано неподходящее значение
// Выброс исключения (неудача)
throw new exception('Пространство имён ($this->namespace) должно хранить строку', 500);
}
}
})(),
default => throw new exception("Свойство \"\$$name\" не найдено", 404)
};
} }
/** // Exit (fail)
* Прочитать свойство return null;
*
* Записывает значение по умолчанию, если свойство не инициализировано
*
* @param string $name Название
*
* @return mixed Содержимое
*/
public function __get(string $name): mixed
{
return match ($name) {
'storage', 'db', 'database' => $this->storage ?? throw new exception("Свойство \"\$$name\" не инициализировано", 500),
'router' => (function () {
if (isset($this->router)) {
// Свойство уже было инициализировано
} else {
// Свойство ещё не было инициализировано
// Инициализация со значением по умолчанию
$this->__set('router', new router);
}
// Возврат (успех)
return $this->router;
})(),
'controller' => (function () {
if (isset($this->controller)) {
// Свойство уже было инициализировано
} else {
// Свойство ещё не было инициализировано
// Инициализация со значением по умолчанию
$this->__set('controller', new controller);
}
// Возврат (успех)
return $this->controller;
})(),
'model' => (function () {
if (isset($this->model)) {
// Свойство уже было инициализировано
} else {
// Свойство ещё не было инициализировано
// Инициализация со значением по умолчанию
$this->__set('model', new model);
}
// Возврат (успех)
return $this->model;
})(),
'namespace' => $this->namespace ?? throw new exception("Свойство \"\$$name\" не инициализировано", 500),
default => throw new exception("Свойство \"\$$name\" не найдено", 404)
};
}
/**
* Проверить свойство на инициализированность
*
* @param string $name Название
*/
public function __isset(string $name): bool
{
return match ($name) {
default => isset($this->{$name})
};
}
/**
* Удалить свойство
*
* @param string $name Название
*/
public function __unset(string $name): void
{
match ($name) {
default => (function () use ($name) {
// Удаление
unset($this->{$name});
})()
};
} }
} }

View File

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace mirzaev\minimal\http\enumerations;
// Files of the project
use mirzaev\minimal\http\enumerations\status;
// Built-in libraries
use InvalidArgumentException as exception_argument,
DomainException as exception_domain;
/**
* Content
*
* Implementation of "Content-Type" header
*
* @package mirzaev\minimal\http\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
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';
/**
* Extension
*
* Returns the file extension without a dot
*
* @throws exception_argument if content can not have file extension
* @throws exception_domain if failed to recognize content
*
* @return string File extension
*/
public function extension(): string
{
// Exit (success)
return match ($this) {
self::jpeg => 'jpg',
self::png => 'png',
self::form, self::mixed, self::alternative, self::related => throw new exception_argument('Content can not have file extension', status::internal_server_error->value),
default => throw new exception_domain('Failed to recognize content: ' . $this->value, status::not_found->value)
};
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace mirzaev\minimal\http\enumerations;
/**
* Method
*
* Methods of HTTP request
*
* @package mirzaev\minimal\http\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
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
};
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace mirzaev\minimal\http\enumerations;
/**
* Protocol
*
* Versions of HTTP
*
* @package mirzaev\minimal\http\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
enum protocol: string
{
case http_3 = 'HTTP/3';
case http_2 = 'HTTP/2';
case http_1_1 = 'HTTP/1.1';
case http_1 = 'hTTP/1.0';
case http_0_9 = 'HTTP/0.9';
}

View File

@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace mirzaev\minimal\http\enumerations;
// Built-in libraries
use DomainException as exception_domain;
/**
* Status
*
* Status codes and status texts of HTTP response
*
* @package mirzaev\minimal\http\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
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)
};
}
}

View File

@ -0,0 +1,504 @@
<?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 ($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;
}
}

View File

@ -0,0 +1,464 @@
<?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\content,
mirzaev\minimal\http\enumerations\status,
mirzaev\minimal\http\header;
// Built-in libraries
use DomainException as exception_domain,
InvalidArgumentException as exception_argument,
RuntimeException as exception_runtime,
LogicException as exception_logic;
/**
* Response
*
* @package mirzaev\minimal\http
*
* @param protocol $protocol Version of HTTP protocol
* @param status $status Status
* @param array $headers Headers
* @param string $body Body
*
* @method self __construct(protocol|string|null $protocol, ?status $status, ?array $headers, bool $environment) Constructor
* @method self sse() Writes headers for SSE implementation
* @method self json(mixed $content) Writes headers for JSON implementation
* @method self write(string $value) Concatenates with the response body property
* @method self body() Transfer the contents of the body property to the output buffer
* @method self|false validate(request $request) Validate response by request
* @method string status() Generates the status line (HTTP/2 200 OK)
* @method self header(string $name, string $value) Write a header to the headers property
* @method self start() Initializes response headers and output buffer
* @method self clean() Delete everything in the output buffer
* @method self end() Initializes response headers and flushes the output buffer
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class response
{
/**
* Protocol
*
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
*
* @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 ($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_domain if failed to recognize status
*
* @var status $status Status
*/
public status $status {
// Write
set (status|string $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)
*
* @param protocol|null $protocol Version of HTTP
* @param status|null $status Status code and status text
*
* @return string The status line
*/
public function status(?protocol $protocol = null, ?status $status = null): string
{
// Declaring buffer of status line
$buffer = '';
if ($protocol instanceof protocol) {
// Received version of HTTP
// Writing to buffer of status line
$buffer .= $protocol->value . ' ';
} else {
// Not received version of HTTP
// Writing to buffer of status line
$buffer .= $this->protocol->value . ' ';
}
if ($status instanceof status) {
// Received status
// Writing to buffer of status line
$buffer .= $status->value . ' ' . $status->label();
} else {
// Not received status
// Writing to buffer of status line
$buffer .= $this->status->value . ' ' . $this->status->label();
}
// Exit (success)
return $buffer;
}
/**
* 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;
}
}

104
mirzaev/minimal/system/model.php Normal file → Executable file
View File

@ -4,109 +4,27 @@ declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
use Exception; // Files of the project
use mirzaev\minimal\traits\magic;
/** /**
* Модель * Model
* *
* @package mirzaev\minimal * @package mirzaev\minimal
*
* @method void __construct() Constructor
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/ */
class model class model
{ {
/** use magic;
* Постфикс
*/
private string $postfix = '_model';
/** /**
* Записать свойство * Constructor
* *
* @param string $name Название * @return void
* @param mixed $value Значение
*/ */
public function __set(string $name, mixed $value = null): void public function __construct() {}
{
match ($name) {
'postfix' => (function () use ($value) {
if (isset($this->postfix)) {
// Свойство уже было инициализировано
// Выброс исключения (неудача)
throw new exception('Запрещено реинициализировать постфикс ($this->postfix)', 500);
} else {
// Свойство ещё не было инициализировано
if ($value = filter_var($value, FILTER_SANITIZE_STRING)) {
// Передано подходящее значение
// Запись свойства (успех)
$this->postfix = $value;
} else {
// Передано неподходящее значение
// Выброс исключения (неудача)
throw new exception('Постфикс ($this->postfix) должен быть строкой', 500);
}
}
})(),
default => throw new exception("Свойство \"\$$name\" не найдено", 404)
};
}
/**
* Прочитать свойство
*
* Записывает значение по умолчанию, если свойство не инициализировано
*
* @param string $name Название
*
* @return mixed Содержимое
*/
public function __get(string $name): mixed
{
return match ($name) {
'postfix' => (function() {
if (isset($this->postfix)) {
// Свойство уже было инициализировано
} else {
// Свойство ещё не было инициализировано
// Инициализация со значением по умолчанию
$this->__set('postfix', '_model');
}
// Возврат (успех)
return $this->postfix;
})(),
default => throw new exception("Свойство \"\$$name\" не обнаружено", 404)
};
}
/**
* Проверить свойство на инициализированность
*
* @param string $name Название
*/
public function __isset(string $name): bool
{
return match ($name) {
default => isset($this->{$name})
};
}
/**
* Удалить свойство
*
* @param string $name Название
*/
public function __unset(string $name): void
{
match ($name) {
default => (function () use ($name) {
// Удаление
unset($this->{$name});
})()
};
}
} }

144
mirzaev/minimal/system/route.php Executable file
View File

@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace mirzaev\minimal;
/**
* Route
*
* @package mirzaev\minimal
*
* @param string|controller $controller Name of the controller
* @param string $method Name of the method of the method of $this->controller
* @param string|model $model Name of the model
* @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 void __construct(string|controller $controller, ?string $method, string|model|null $model, array $parameters, array $options) Constructor
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class route
{
/**
* Controller
*
* @var string|controller $controller Name of the controller or an instance of the controller
*/
public string|controller $controller {
// Read
get => $this->controller;
}
/**
* Method
*
* @var string $method Name of the method of the method of $this->controller
*/
public string $method{
// Read
get => $this->method;
}
/**
* Model
*
* @var string|model|null $model Name of the model of an instance of the model
*/
public string|model|null $model {
// Read
get => $this->model;
}
/**
* 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 = [] {
// Read
&get => $this->parameters;
}
/**
* 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 string|controller $controller Name of the controller
* @param string|null $method Name of the method of the method of $controller
* @param string|model|null $model Name of the model
* @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 void
*/
public function __construct(
string|controller $controller,
?string $method = 'index',
string|model|null $model = null,
array $parameters = [],
array $options = []
) {
// Writing name of the controller
$this->controller = $controller;
// Writing name of the model
$this->model = $model;
// Writing name of the method of the controller
$this->method = $method;
// Writing parameters
$this->parameters = $parameters;
// Writing options
if (match ($method) {
'GET', 'PUT', 'PATCH', 'DELETE' => true,
default => false
}) $this->options = $options;
}
}

331
mirzaev/minimal/system/router.php Normal file → Executable file
View File

@ -4,134 +4,287 @@ declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
use mirzaev\minimal\core; // Files of the project
use mirzaev\minimal\route,
mirzaev\minimal\http\request,
mirzaev\minimal\traits\singleton;
use ReflectionClass; // Build-ing libraries
use InvalidArgumentException as exception_argument;
/** /**
* Маршрутизатор * Router
* *
* @package mirzaev\shop * @package mirzaev\minimal
*
* @param array $routes Registry of routes
*
* @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
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*
* @todo
* 1. Доработать обработку ошибок
* 2. Добавить __set(), __get(), __isset() и __unset()
*/ */
final class router final class router
{ {
/** use singleton;
* @var array $router Маршруты
*/
public array $routes = [];
/** /**
* Записать маршрут * Routes
* *
* @param string $route Маршрут * @var array $routes Registry of routes
* @param string $target Обработчик (контроллер и модель, без постфиксов)
* @param string|null $method Метод
* @param string|null $type Тип
* @param string|null $model Модель
*/ */
public function write(string $route, string $target, string $method = null, string $type = 'GET', string $model = null): void protected array $routes = [] {
{ // Read
// Запись в реестр &get => $this->routes;
$this->routes[$route][$type] = [
'target' => $target,
'method' => $method ?? '__construct'
];
} }
/** /**
* Обработать маршрут * Write route
* *
* @param string $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
*
* @return self The instance from which the method was called (fluent interface)
*/ */
public function handle(string $uri = null, core $core = null): ?string public function write(string $urn, route $route, string|array $method): self
{ {
// Запись полученного URI или из данных веб-сервера foreach (is_array($method) ? $method : [$method] as $method) {
$uri = $uri ?? $_SERVER['REQUEST_URI'] ?? ''; // Iterate over methods of requests
// Инициализация URL // Initializing the request
$url = parse_url($uri, PHP_URL_PATH); $request = new request(
uri: $urn,
method: $method,
environment: false
);
// Сортировка массива маршрутов от большего ключа к меньшему // Writing to the registry of routes
krsort($this->routes); $this->routes[$request->uri][$request->method->value] = $route;
}
foreach ($this->routes as $key => $value) { // Exit (success) (fluent interface)
// Перебор маршрутов return $this;
}
// Если не записан "/" в начале, то записать /**
$route_name = preg_replace('/^([^\/])/', '/$1', $key); * Match
$url = preg_replace('/^([^\/])/', '/$1', $url); *
* Match request URI with registry of routes
*
* @param request $request The request
*
* @return route|null Route, if found
*/
public function match(request $request): ?route
{
// Declaration of the registry of routes directoies
$routes = [];
// Если не записан "/" в конце, то записать foreach ($this->routes as $route => $data) {
$route_name = preg_replace('/([^\/])$/', '$1/', $route_name); // Iteration over routes
$url = preg_replace('/([^\/])$/', '$1/', $url);
if (mb_stripos($route_name, $url, 0, "UTF-8") === 0 && mb_strlen($route_name, 'UTF-8') <= mb_strlen($url, 'UTF-8')) { // Search directories of route (explode() creates empty value in array)
// Найден маршрут, а так же его длина не меньше длины запрошенного URL preg_match_all('/(^\/$|[^\/]+)/', $route, $data['directories']);
$routes[$route] = $data['directories'][0];
}
// Инициализация маршрута if (count($routes) === count($this->routes)) {
$route = $value[$_SERVER["REQUEST_METHOD"] ?? 'GET']; // Initialized the registry of routes directoies
// Universalization of URN (/foo/bar)
$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);
$directories = $directories[0];
/**
* Initialization of the route
*/
// Initializing the buffer of matches of route directories with URN directories
$matches = [];
foreach ($directories as $i => $urn_directory) {
// Iteration over directories of URN
foreach ($this->routes as $route => $data) {
// Iteration over routes
if (isset($data[$request->method->value])) {
// The request method matches the route method
// Universalization of route
$route = self::universalize($route);
// Skipping unmatched routes based on results of previous iterations
if (isset($matches[$route]) && $matches[$route] === false) continue;
// Initializing of route directory
$route_directory = $routes[$route][$i] ?? null;
if (isset($route_directory)) {
// Initialized of route directory
if ($urn_directory === $route_directory) {
// The directory of URN is identical to the directory of route
// Writing: end of URN directories XNOR end of route directories
$matches[$route] = !(isset($directories[$_i = $i + 1]) xor isset($routes[$route][$_i]));
} else if (preg_match('/^\$([a-zA-Z_\x80-\xff]+)$/', $route_directory) === 1) {
// The directory of route is a variable ($variable)
// Writing: end of URN directories XNOR end of route directories
$matches[$route] = !(isset($directories[$_i = $i + 1]) xor isset($routes[$route][$_i]));
} else if (
!isset($routes[$route][$i + 1])
&& preg_match('/^\$([a-zA-Z_\x80-\xff]+\.\.\.)$/', $route_directory) === 1
) {
// The directory of route is a collector ($variable...)
// AND this is the end of route directories
// Writing
$matches[$route] = 'collector';
} else $matches[$route] = false;
} else if ($matches[$route] === 'collector') {
} else $matches[$route] = false;
}
}
}
// Finding a priority route from match results
foreach ($matches as $route => $match) if ($match !== false) break;
if ($route && !empty($data = $this->routes[$route])) {
// Route found
// Universalization of route
$route = self::universalize($route);
/**
* Initialization of route variables
*/
foreach ($routes[$route] as $i => $route_directory) {
// Iteration over directories of route
if (preg_match('/^\$([a-zA-Z_\x80-\xff]+)$/', $route_directory) === 1) {
// The directory is a variable ($variable)
// Запись в реестр переменных и перещапись директории в маршруте
$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[$request->method->value]->variables[trim($route_directory, '$.')];
// Инициализаия массива сборщика
$collector ??= [];
// Запись в реестр переменных
$collector[] = $directories[$i];
foreach (array_slice($directories, ++$i, preserve_keys: true) as &$urn_directory) {
// Перебор директорий URN
// Запись в реестр переменных
$collector[] = $urn_directory;
}
// Выход из цикла (успех)
break; break;
} }
} }
if (!empty($route)) { // Exit (success or fail)
// Найден маршрут return $data[$request->method->value] ?? null;
if (class_exists($controller = ($core->namespace ?? (new core)->namespace) . '\\controllers\\' . $route['target'] . $core->controller->postfix ?? (new core())->controller->postfix)) {
// Найден контроллер
// Инициализация контроллера
$controller = new $controller;
if (class_exists($model = ($core->namespace ?? (new core)->namespace) . '\\models\\' . $route['target'] . $core->model->postfix ?? (new core())->model->postfix)) {
// Найдена модель
// Инициализация модели
$controller->model = new $model;
}
if (empty($response = $controller->{$route['method']}($_REQUEST))) {
// Не удалось получить ответ после обработки контроллера
// Возврат (неудача)
return $this->error($core);
}
// Возврат (успех)
return $response;
} }
} }
// Возврат (неудача) // Exit (fail)
return $this->error($core); return null;
} }
/** /**
* Контроллер ошибок * Sorting routes
*
* 1. Short routes
* 2. Long routes
* 3. Short routes with variables (position of variables from "right" to "left")
* 4. Long routes with variables (position of variables from "right" to "left")
* 5. Short routes with collector
* 6. Long routes with collector
* 7. Short routes with variables and collector (position of variables from "right" to "left" then by amount)
* 8. Long routes with variables and collector (position of variables from "right" to "left")
*
* Добавить чтобы сначала текст потом переменная затем после переменной первыми тексты и в конце перменные опять и так рекурсивно
*
* @return self The instance from which the method was called (fluent interface)
*
* @todo ПЕРЕДЕЛАТЬ ПОЛНОСТЬЮ
*/ */
private function error(core $core = null): ?string public function sort(): self
{ {
if ( uksort($this->routes, function (string $a, string $b) {
class_exists($class = (new ReflectionClass(core::class))->getNamespaceName() . '\\controllers\\errors' . $core->controller->postfix ?? (new core())->controller->postfix) && // Sorting routes
method_exists($class, $method = 'error404')
) {
// Существует контроллер ошибок и метод для обработки ошибки
// Возврат (вызов метода для обработки ошибки) // Initialization of string lengths (multibyte-safe)
return (new $class(basename($class)))->$method(); $length = [
} else { $a => mb_strlen($a),
// Не существует контроллер ошибок или метод для обработки ошибки $b => mb_strlen($b)
];
// Никаких исключений не вызывать, отдать пустую страницу, // Initialization of the presence of variables
// либо вызвать, но отображать в зависимости от включенного дебаг режима !!!!!!!!!!!!!!!!!!!! см. @todo $variables = [
return null; $a => preg_match('/\$([a-zA-Z_\x80-\xff]+)(\/|$)/', $a) === 1,
} $b => preg_match('/\$([a-zA-Z_\x80-\xff]+)(\/|$)/', $b) === 1
];
// Initialization of the presence of collectors
$collectors = [
$a => preg_match('/\$([a-zA-Z_\x80-\xff]+)\.\.\.$/', $a) === 1,
$b => preg_match('/\$([a-zA-Z_\x80-\xff]+)\.\.\.$/', $b) === 1
];
if ($variables[$a] && !$variables[$b]) return 1;
else if (!$variables[$a] && $variables[$b]) return -1;
else if ($variables[$a] && $variables[$b]) {
} else if ($collectors[$a] && !$collectors[$b]) return 1;
else if (!$collectors[$a] && $collectors[$b]) return -1;
else {
// NOR variables and XAND collectors (both are not found or both are found)
// The routes are the same length (no changes)
if ($length[$a] === $length[$b]) return 0;
// The longer route moves to the end
return $length[$a] > $length[$b] ? 1 : -1;
}
});
// Exit (success) (fluent interface)
return $this;
}
/**
* Universalize URN
*
* Always "/" at the beginning and never "/" at the end
*
* @param string $urn URN (/foo/bar)
*
* @return string Universalized URN
*/
private function universalize(string $urn): string
{
// Universalization of URN and exit (success)
return (string) '/' . mb_trim($urn, '/');
} }
} }

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace mirzaev\minimal\traits;
// Files of the project
use mirzaev\minimal\http\enumerations\status;
// Built-in libraries
use exception;
/**
* Trait of magical methods
*
* @package mirzaev\minimal\traits
*
* @method void __set(string $name, mixed $value) Write property
* @method mixed __get(string $name) Read property
* @method void __unset(string $name) Delete property
* @method bool __isset(string $name) Check property for initialization
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
trait magic
{
/**
* Write property
*
* @param string $name Name of the property
* @param mixed $value Value of the property
*
* @return void
*/
public function __set(string $name, mixed $value = null): void
{
match ($name) {
default => throw new exception('Failed to find property: ' . static::class . "::\$$name", status::not_found->value)
};
}
/**
* Read property
*
* @param string $name Name of the property
*
* @return mixed Value of the property
*/
public function __get(string $name): mixed
{
return match ($name) {
default => throw new exception('Failed to find property: ' . static::class . "::\$$name", status::not_found->value)
};
}
/**
* Delete property
*
* @param string $name Name of the property
*
* @return void
*/
public function __unset(string $name): void
{
match ($name) {
default => (function () use ($name) {
unset($this->{$name});
})()
};
}
/**
* Check property for initialization
*
* @param string $name Name of the property
*
* @return bool Is the property initialized?
*/
public function __isset(string $name): bool
{
return match ($name) {
default => isset($this->{$name})
};
}
}

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace mirzaev\minimal\traits;
// Built-in libraries
use exception;
/**
* Trait of singleton
*
* @package mirzaev\minimal\traits
*
* @method static initialize() Initialize an instance
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
trait singleton
{
/**
* Constructor (blocked)
*
* @return void
*/
final private function __construct()
{
// Initializing the indicator that an instance of static::class has already been initialized
static $instance = false;
if ($instance) {
// An instance of static has already been initialized
// Exit (fail)
throw new exception('Has already been initialized an instance of the ' . static::class);
}
// Writing the indicator that the instance of static been initialized
$instance = true;
}
/**
* Initialize an instance
*
* Create an instance of static::class, or return an already created static::class instance
*
* @return static
*/
public static function initialize(): static
{
// Initialize the buffer of the instance of static::class
static $instance;
// Exit (success)
return $instance ??= new static;
}
/**
* Clone (blocked)
*/
private function __clone()
{
// Exit (fail)
throw new exception('Cloning is inadmissible');
}
/**
* Sleep (blocked)
*/
public function __sleep()
{
// Exit (fail)
throw new exception('Serialization is inadmissible');
}
/**
* Wake up (blocked)
*/
public function __wakeup()
{
// Exit (fail)
throw new exception('Deserialisation is inadmissible');
}
}