1 Commits
3.0.0 ... 2.1.1

Author SHA1 Message Date
24fe47f1e7 fix postfix private 2023-11-18 01:23:21 +07:00
12 changed files with 502 additions and 1018 deletions

0
.gitignore vendored Executable file → Normal file
View File

11
LICENSE
View File

@@ -1,11 +0,0 @@
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.

View File

@@ -1,64 +0,0 @@
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>

11
composer.json Executable file → Normal file
View File

@@ -1,7 +1,7 @@
{ {
"name": "mirzaev/minimal", "name": "mirzaev/minimal",
"type": "framework", "type": "framework",
"description": "My vision of a good framework", "description": "Lightweight MVC framework that manages only the basic mechanisms, leaving the development of the programmer and not overloading the project",
"keywords": [ "keywords": [
"mvc", "mvc",
"framework", "framework",
@@ -14,7 +14,7 @@
"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": "Programmer" "role": "Developer"
} }
], ],
"support": { "support": {
@@ -22,11 +22,16 @@
"issues": "https://git.mirzaev.sexy/mirzaev/minimal/issues" "issues": "https://git.mirzaev.sexy/mirzaev/minimal/issues"
}, },
"require": { "require": {
"php": "~8.4" "php": "~8.2"
}, },
"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 Executable file → Normal file
View File

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

@@ -4,83 +4,119 @@ declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
// Files of the project // Файлы проекта
use mirzaev\minimal\model, use mirzaev\minimal\model;
mirzaev\minimal\core,
mirzaev\minimal\traits\magic;
// Build-in libraries // Встроенные библиотеки
use exception; use exception;
/** /**
* Controller * Контроллер
*
* @var core $core An instance of the core
* @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
* *
* @package mirzaev\minimal * @package mirzaev\minimal
*
* @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; /**
* Постфикс
*/
public const POSTFIX = '_controller';
/** /**
* Core * Инстанция модели
* */
* @var core $core An instance of the core protected model $model;
*/
public core $core {
// Read
get => $this->core;
}
/** /**
* Model * Инстанция шаблонизатора представления
* */
* @var model $model An instance of the model connected in the core protected object $view;
*/
public model $model {
// Write
set (model $model) {
$this->model ??= $model;
}
// Read /**
get => $this->model; * Конструктор
} */
public function __construct()
{
}
/** /**
* View * Записать свойство
* *
* @var view $view View template engine instance (twig) * @param string $name Название
*/ * @param mixed $value Значение
public object $view { *
// Write * @return void
set (object $view) { */
$this->view ??= $view; public function __set(string $name, mixed $value = null): void
} {
match ($name) {
'model' => (function () use ($value) {
if ($this->__isset('model')) throw new exception('Запрещено реинициализировать свойство с инстанцией модели ($this->model)', 500);
else {
// Свойство не инициализировано
// Read if (is_object($value)) $this->model = $value;
get => $this->view; else throw new exception('Свойство $this->model должно хранить инстанцию модели (объект)', 500);
} }
})(),
'view' => (function () use ($value) {
if ($this->__isset('view')) throw new exception('Запрещено реинициализировать свойство с инстанцией шаблонизатора представления ($this->view)', 500);
else {
// Свойство не инициализировано
/** if (is_object($value)) $this->view = $value;
* Constructor else throw new exception('Свойство $this->view должно хранить инстанцию шаблонизатора представления (объект)', 500);
* }
* @param core $core The instance of the core })(),
* default => throw new exception("Свойство \"\$$name\" не найдено", 404)
* @return self };
*/ }
public function __construct(core $core) {
// Writing the core into the property /**
$this->core = $core; * Прочитать свойство
} *
* @param string $name Название
*
* @return mixed Содержимое
*/
public function __get(string $name): mixed
{
return match ($name) {
'model' => $this->model ?? throw new exception("Свойство \"\$model\" не инициализировано", 500),
'view' => $this->view ?? throw new exception("Свойство \"\$view\" не инициализировано", 500),
default => throw new exception("Свойство \"\$$name\" не обнаружено", 404)
};
}
/**
* Проверить свойство на инициализированность
*
* @param string $name Название
*
* @return bool Инициализировано свойство?
*/
public function __isset(string $name): bool
{
return match ($name) {
default => isset($this->{$name})
};
}
/**
* Удалить свойство
*
* @param string $name Название
*
* @return void
*/
public function __unset(string $name): void
{
match ($name) {
default => (function () use ($name) {
// Удаление
unset($this->{$name});
})()
};
}
} }

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

@@ -4,361 +4,227 @@ declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
// Files of the project // Файлы проекта
use mirzaev\minimal\router, use mirzaev\minimal\router,
mirzaev\minimal\route, mirzaev\minimal\controller,
mirzaev\minimal\controller, mirzaev\minimal\model;
mirzaev\minimal\model;
// Built-in libraries // Встроенные библиотеки
use exception, use exception,
BadMethodCallException as exception_method, ReflectionClass as reflection;
DomainException as exception_domain,
InvalidArgumentException as exception_argument,
UnexpectedValueException as exception_value,
ReflectionClass as reflection;
/** /**
* Core * Ядро
*
* @param string $namespace Namespace where the core was initialized from
* @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 self __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
* *
* @package mirzaev\minimal * @package mirzaev\minimal
*
* @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. Добавить __isset() и __unset()
*/ */
final class core final class core
{ {
/** /**
* Namespace * Инстанция соединения с базой данных
* */
* @var string $namespace Namespace where the core was initialized from private object $db;
*
* @see https://www.php-fig.org/psr/psr-4/
*/
public string $namespace {
// Read
get => $this->namespace;
}
/** /**
* Controller * Инстанция маршрутизатора
* */
* @var controller $controller An instance of the controller private readonly router $router;
*/
private controller $controller {
// Read
get => $this->controller ??= new controller;
}
/** /**
* Model * Инстанция ядра контроллера
* */
* @var model $model An instance of the model private readonly controller $controller;
*/
private model $model {
// Read
get => $this->model ??= new model;
}
/** /**
* Router * Инстанция ядра модели
* */
* @var router $router An instance of the router private readonly model $model;
*/
public router $router {
get => $this->router ??= router::initialize();
}
/** /**
* Constrictor * Путь пространства имён (системное)
* *
* @param ?string $namespace Пространство имён системного ядра * Используется для поиска файлов по спецификации PSR-4
* */
* @return self The instance of the core private readonly string $namespace;
*/
public function __construct(
?string $namespace = null
) {
// Writing a namespace to the property
$this->namespace = $namespace ?? (new reflection(self::class))->getNamespaceName();
}
/**
* Конструктор
*
* @param ?object $db Инстанция соединения с базой данных
* @param ?router $router Маршрутизатор
* @param ?controller $controller Инстанция ядра контроллера
* @param ?model $model Инстанция ядра модели
* @param ?string $namespace Пространство имён системного ядра
*
* @return self Инстанция ядра
*/
public function __construct(
?object $db = null,
?router $router = null,
?controller $controller = null,
?model $model = null,
?string $namespace = null
) {
// Инициализация свойств
if (isset($db)) $this->__set('db', $db);
if (isset($router)) $this->__set('router', $router);
if (isset($controller)) $this->__set('controller', $controller);
if (isset($model)) $this->__set('model', $model);
$this->__set('namespace', $namespace ?? (new reflection(self::class))->getNamespaceName());
}
/** /**
* Destructor * Деструктор
*/ *
public function __destruct() {} */
public function __destruct()
{
}
/** /**
* Request * Запуск
* *
* Handle the request * @param ?string $uri Маршрут
* *
* @param string|null $uri URI of the request (value by default: $_SERVER['REQUEST_URI']) * @return ?string Сгенерированный ответ (HTML, JSON...)
* @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 public function start(string $uri = null): ?string
* {
* @return string|null Response // Обработка запроса
*/ return $this->__get('router')->handle($uri, core: $this);
public function request(?string $uri = null, ?string $method = null, array $parameters = []): ?string }
{
// Matching a route
$route = $this->router->match($uri ??= $_SERVER['REQUEST_URI'] ?? '/', $method ??= $_SERVER["REQUEST_METHOD"]);
if ($route) { /**
// Initialized the route * Записать свойство
*
* @param string $name Название
* @param mixed $value Содержимое
*
* @return void
*/
public function __set(string $name, mixed $value = null): void
{
match ($name) {
'db', 'database' => (function () use ($value) {
if ($this->__isset('db')) throw new exception('Запрещено реинициализировать инстанцию соединения с базой данных ($this->db)', 500);
else {
// Свойство ещё не было инициализировано
if (!empty($parameters)) { if (is_object($value)) $this->db = $value;
// Recaived parameters else throw new exception('Свойство $this->db должно хранить инстанцию соединения с базой данных', 500);
}
})(),
'router' => (function () use ($value) {
if ($this->__isset('router')) throw new exception('Запрещено реинициализировать инстанцию маршрутизатора ($this->router)', 500);
else {
// Свойство ещё не было инициализировано
// Merging parameters with route parameters if ($value instanceof router) $this->router = $value;
$route->parameters = $parameters + $route->parameters; else throw new exception('Свойство $this->router должно хранить инстанцию маршрутизатора (mirzaev\minimal\router)"', 500);
} }
})(),
'controller' => (function () use ($value) {
if ($this->__isset('controller')) throw new exception('Запрещено реинициализировать инстанцию ядра контроллеров ($this->controller)', 500);
else {
// Свойство не инициализировано
// Handling the route and exit (success) if ($value instanceof controller) $this->controller = $value;
return $this->route($route, $method); else throw new exception('Свойство $this->controller должно хранить инстанцию ядра контроллеров (mirzaev\minimal\controller)', 500);
} }
})(),
'model' => (function () use ($value) {
if ($this->__isset('model')) throw new exception('Запрещено реинициализировать инстанцию ядра моделей ($this->model)', 500);
else {
// Свойство не инициализировано
// Exit (fail) if ($value instanceof model) $this->model = $value;
return null; else throw new exception('Свойство $this->model должно хранить инстанцию ядра моделей (mirzaev\minimal\model)', 500);
} }
})(),
'namespace' => (function () use ($value) {
if ($this->__isset('namespace')) throw new exception('Запрещено реинициализировать путь пространства имён ($this->namespace)', 500);
else {
// Свойство не инициализировано
/** if (is_string($value)) $this->namespace = $value;
* Route else throw new exception('Свойство $this->namespace должно хранить строку с путём пространства имён', 500);
* }
* Handle the route })(),
* default => throw new exception("Свойство \"\$$name\" не найдено", 404)
* @param route $route The route };
* @param string $method Method of requests (GET, POST, PUT, DELETE, COOKIE...) }
*
* @return string|null Response, if generated
*/
public function route(route $route, string $method = 'GET'): ?string
{
// Initializing name of the controller class
$controller = $route->controller;
if ($route->controller instanceof controller) {
// 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 * Записывает значение по умолчанию, если свойство не инициализировано
*
* @param string $name Название
*
* @return mixed Содержимое
*/
public function __get(string $name): mixed
{
return match ($name) {
'db', 'database' => $this->db ?? throw new exception("Свойство \"\$$name\" не инициализировано", 500),
'router' => (function () {
// Инициализация со значением по умолчанию
if (!$this->__isset('router')) $this->__set('router', new router);
// Exit (fail) // Возврат (успех)
throw new exception_domain('Failed to found the controller: ' . $route->controller); return $this->router;
} else { })(),
// Not found the controller and $route->controller is empty 'controller' => (function () {
// Инициализация со значением по умолчанию
if (!$this->__isset('controller')) $this->__set('controller', new controller);
// Exit (fail) // Возврат (успех)
throw new exception_argument('Failed to initialize the controller: ' . $route->controller); return $this->controller;
} })(),
'model' => (function () {
// Инициализация со значением по умолчанию
if (!$this->__isset('model')) $this->__set('model', new model);
// Deinitializing name of the controller class // Возврат (успех)
unset($controller); return $this->model;
})(),
'namespace' => $this->namespace ?? throw new exception("Свойство \"\$$name\" не инициализировано", 500),
default => throw new exception("Свойство \"\$$name\" не найдено", 404)
};
}
// Initializing name if the model class /**
$model = $route->model; * Проверить свойство на инициализированность
*
* @param string $name Название
*
* @return bool Инициализировано свойство?
*/
public function __isset(string $name): bool
{
return match ($name) {
default => isset($this->{$name})
};
}
if ($route->model instanceof model) { /**
// Initialized the model * Удалить свойство
} else if (class_exists($model = "$this->namespace\\models\\$model")) { *
// Found the model by its name * @param string $name Название
*
// Initializing the model * @return void
$route->model = new $model; */
} else if (!empty($route->model)) { public function __unset(string $name): void
// Not found the model and $route->model has a value {
match ($name) {
// Exit (fail) default => (function () use ($name) {
throw new exception_domain('Failed to initialize the model: ' . ($route->model ?? $route->controller)); // Удаление
} unset($this->{$name});
})()
// 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;
}
if ($method === 'POST') {
// POST
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);
}
} else {
// Not recognized method of the request
// Exit (fail)
throw new exception_value('Failed to recognize the method: ' . $method);
}
// Exit (fail)
return null;
}
} }

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

@@ -4,27 +4,86 @@ declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
// Files of the project // Встроенные библиотеки
use mirzaev\minimal\traits\magic; use exception;
/** /**
* Model * Модель
*
* @method self __construct() Constructor
* *
* @package mirzaev\minimal * @package mirzaev\minimal
*
* @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; /**
* Постфикс
*/
public const POSTFIX = '_model';
/** /**
* Constructor * Конструктор
* */
* @return self public function __construct()
*/ {
public function __construct() {} }
/**
* Записать свойство
*
* @param string $name Название
* @param mixed $value Содержимое
*
* @return void
*/
public function __set(string $name, mixed $value = null): void
{
match ($name) {
default => throw new exception("Свойство \"\$$name\" не найдено", 404)
};
}
/**
* Прочитать свойство
*
* @param string $name Название
*
* @return mixed Содержимое
*/
public function __get(string $name): mixed
{
return match ($name) {
default => throw new exception("Свойство \"\$$name\" не обнаружено", 404)
};
}
/**
* Проверить свойство на инициализированность
*
* @param string $name Название
*
* @return bool Инициализировано свойство?
*/
public function __isset(string $name): bool
{
return match ($name) {
default => isset($this->{$name})
};
}
/**
* Удалить свойство
*
* @param string $name Название
*
* @return void
*/
public function __unset(string $name): void
{
match ($name) {
default => (function () use ($name) {
// Удаление
unset($this->{$name});
})()
};
}
} }

View File

@@ -1,127 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\minimal;
/**
* Route
*
* @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 self __construct(string $controller,?string $method, ?string $model, array $variables, array $options) Constructor
*
* @package mirzaev\minimal
*
* @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|model $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
*
* @var array $parameters Arguments for the $this->method (will be concatenated together with generated request parameters)
*/
public array $parameters {
// Read
get => $this->parameters;
}
/**
* Options
*
* Used only for GET, PUT, PATCH and DELETE
*
* @var string $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
);
// 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 self
*/
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;
}
}

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

@@ -4,282 +4,170 @@ declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
// Files of the project // Файлы проекта
use mirzaev\minimal\route, use mirzaev\minimal\core;
mirzaev\minimal\traits\singleton;
// Build-ing libraries
use InvalidArgumentException as exception_argument;
/** /**
* Router * Маршрутизатор
*
* @param array $routes The registry of routes
*
* @method self write(string $uri, string $method)
* @method self sort() Sort routes (DEV)
* @method string universalize(string $urn) Universalize URN
* *
* @package mirzaev\minimal * @package mirzaev\minimal
*
* @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>
*/ */
final class router final class router
{ {
use singleton; /**
* @var array $router Реестр маршрутов
*/
protected array $routes = [];
/** /**
* Routes * Записать маршрут
* *
* @var array $routes The registry of routes * @param string $route Маршрут
*/ * @param string $handler Обработчик - инстанции контроллера и модели (не обязательно), без постфиксов
protected array $routes = []; * @param ?string $method Вызываемый метод в инстанции контроллера обработчика
* @param ?string $request HTTP-метод запроса (GET, POST, PUT...)
* @param ?string $model Инстанция модели (переопределение инстанции модели в $target)
*
* @return void
*/
public function write(
string $route,
string $handler,
?string $method = 'index',
?string $request = 'GET',
?string $model = null
): void {
// Запись в реестр
$this->routes[$route][$request] = [
'controller' => $handler,
'model' => $model ?? $handler,
'method' => $method
];
}
/** /**
* Write a route * Обработать маршрут
* *
* @param string $urn URN of the route ('/', '/page', '/page/$variable', '/page/$collector...'...) * @param ?string $uri URI запроса (https://domain.com/foo/bar)
* @param route $route The route * @param ?string $method Метод запроса (GET, POST, PUT...)
* @param string|array $method Method of requests (GET, POST, PUT, DELETE, COOKIE...) * @param ?core $core Инстанция системного ядра
* */
* @return self The instance from which the method was called (fluent interface) public function handle(?string $uri = null, ?string $method = null, ?core $core = new core): ?string
*/ {
public function write( // Инициализация значений по умолчанию
string $urn, $uri ??= $_SERVER['REQUEST_URI'] ?? '/';
route $route, $method ??= $_SERVER["REQUEST_METHOD"] ?? 'GET';
null|string|array $method = 'GET'
): self {
foreach (is_array($method) ? $method : [$method] as $method) {
// Iterate over methods of requests
// Validating method // Инициализация URL запроса (/foo/bar)
$method = match ($method) { $url = parse_url($uri, PHP_URL_PATH);
'POST', 'GET', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE' => $method,
default => throw new exception_argument("Failed to initialize method: \"$method\"")
};
// Writing to the registry of routes // Универсализация маршрута
$this->routes[$urn][$method] = $route; $url = self::universalize($url);
}
// Exit (success) (fluent interface) // Сортировка реестра маршрутов от большего ключа к меньшему (кешируется)
return $this; krsort($this->routes);
}
/** // Поиск директорий в ссылке
* Match preg_match_all('/[^\/]+/', $url, $directories);
*
* 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...)
*
* @return route|null Route, if found
*/
public function match(string $uri, string $method): ?route {
// Declaration of the registry of routes directoies
$routes = [];
foreach ($this->routes as $route => $data) { // Инициализация директорий
// Iteration over routes $directories = $directories[0];
// Search directories of route (explode() creates empty value in array) foreach ($this->routes as $route => $data) {
preg_match_all('/(^\/$|[^\/]+)/', $route, $data['directories']); // Перебор маршрутов
$routes[$route] = $data['directories'][0];
}
if (count($routes) === count($this->routes)) { // Универсализация маршрута
// Initialized the registry of routes directoies $route = self::universalize($route);
// Universalization of URN (/foo/bar) // Поиск директорий
$urn = self::universalize(parse_url(urldecode($uri), PHP_URL_PATH)); preg_match_all('/[^\/]+/', $route, $data['directories']);
// Search directories of URN (explode() creates empty value in array) // Инициализация директорий
preg_match_all('/(^\/$|[^\/]+)/', $urn, $directories); $data['directories'] = $data['directories'][0];
$directories = $directories[0];
/** if (count($directories) === count($data['directories'])) {
* Initialization of the route // Входит в диапазон маршрут (совпадает количество директорий у ссылки и маршрута)
*/
// Initializing the buffer of matches of route directories with URN directories // Инициализация реестра переменных
$matches = []; $data['vars'] = [];
foreach ($directories as $i => $urn_directory) { foreach ($data['directories'] as $index => &$directory) {
// Iteration over directories of URN // Перебор директорий
foreach ($this->routes as $route => $data) { if (preg_match('/\$([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]+)/', $directory) === 1) {
// Iteration over routes // Директория является переменной (.../$variable/...)
if (isset($data[$method])) { // Запись в реестр переменных
// The request method matches the route method $directory = $data['vars'][trim($directory, '$')] = $directories[$index];
}
}
// Universalization of route // Реиницилазция маршрута
$route = self::universalize($route); $route = self::universalize(implode('/', $data['directories']));
// Skipping unmatched routes based on results of previous iterations // Проверка на пустой маршрут
if (isset($matches[$route]) && $matches[$route] === false) continue; if (empty($route)) $route = '/';
// Initializing of route directory if (mb_stripos($route, $url, 0, "UTF-8") === 0 && mb_strlen($route, 'UTF-8') <= mb_strlen($url, 'UTF-8')) {
$route_directory = $routes[$route][$i] ?? null; // Идентифицирован маршрут (длина не меньше длины запрошенного URL)
if (isset($route_directory)) { if (array_key_exists($method, $data)) {
// Initialized of route directory // Идентифицирован метод маршрута (GET, POST, PUT...)
if ($urn_directory === $route_directory) { $route = $data[$method];
// The directory of URN is identical to the directory of route
// Writing: end of URN directories XNOR end of route directories if (class_exists($controller = $core->namespace . '\\controllers\\' . $route['controller'] . $core->controller::POSTFIX)) {
$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])); $controller = new $controller;
} 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'; if (class_exists($model = $core->namespace . '\\models\\' . $route['model'] . $core->model::POSTFIX)) $controller->model = new $model;
} 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; return $controller->{$route['method']}($data['vars'] + $_REQUEST, $_FILES);
}
}
if ($route && !empty($data = $this->routes[$route])) { // Выход из цикла (провал)
// Route found break;
}
}
}
// Universalization of route // Возврат (провал)
$route = self::universalize($route); return $this->error($core);
}
/** /**
* Initialization of route variables * Сгенерировать ответ с ошибкой
*/ *
* Вызывает метод error404 в инстанции контроллера ошибок
*
* @param ?core $core Инстанция системного ядра
*
* @return ?string HTML-документ
*/
private function error(core $core = new core): ?string
{
return class_exists($class = '\\' . $core->namespace . '\\controllers\\errors' . $core->controller::POSTFIX)
&& method_exists($class, $method = 'error404')
? (new $class)->$method()
: null;
}
foreach ($routes[$route] as $i => $route_directory) { /**
// Iteration over directories of route * Универсализировать маршрут
*
if (preg_match('/^\$([a-zA-Z_\x80-\xff]+)$/', $route_directory) === 1) { * @param string $route Маршрут
// The directory is a variable ($variable) *
* @return string Универсализированный маршрут
// Запись в реестр переменных и перещапись директории в маршруте */
$data[$method]->variables[trim($route_directory, '$')] = $directories[$i]; private function universalize(string $route): string
} else if (preg_match('/^\$([a-zA-Z_\x80-\xff]+\.\.\.)$/', $route_directory) === 1) { {
// The directory of route is a collector ($variable...) // Если не записан "/" в начале, то записать, затем, если записан "/" в конце, то удалить
return preg_replace('/(.+)\/$/', '$1', preg_replace('/^([^\/])/', '/$1', $route));
// Инициализаия ссылки на массив сборщика }
$collector = &$data[$method]->variables[trim($route_directory, '$.')];
// Инициализаия массива сборщика
$collector ??= [];
// Запись в реестр переменных
$collector[] = $directories[$i];
foreach (array_slice($directories, ++$i, preserve_keys: true) as &$urn_directory) {
// Перебор директорий URN
// Запись в реестр переменных
$collector[] = $urn_directory;
}
break;
}
}
// Exit (success or fail)
return $data[$method] ?? null;
}
}
// Exit (fail)
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 ПЕРЕДЕЛАТЬ ПОЛНОСТЬЮ
*/
public function sort(): self
{
uksort($this->routes, function (string $a, string $b) {
// Sorting routes
// Initialization of string lengths (multibyte-safe)
$length = [
$a => mb_strlen($a),
$b => mb_strlen($b)
];
// Initialization of the presence of variables
$variables = [
$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

@@ -1,83 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\minimal\traits;
// Built-in libraries
use exception;
/**
* Trait of magical methods
*
* @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
*
* @package mirzaev\minimal\traits
*
* @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('Property "' . static::class . "::\$$name\" not found", 404)
};
}
/**
* 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('Property "' . static::class . "::\$$name\" not found", 404)
};
}
/**
* 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

@@ -1,85 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\minimal\traits;
// Built-in libraries
use exception;
/**
* Trait of singleton
*
* @method static initialize() Initialize an instance
*
* @package mirzaev\minimal\traits
*
* @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');
}
}