7 Commits
2.0.0 ... 3.0.0

12 changed files with 3487 additions and 3179 deletions

0
.gitignore vendored Normal file → Executable file
View File

11
LICENSE Normal 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 Normal 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>

71
composer.json Normal file → Executable file
View File

@@ -1,39 +1,32 @@
{ {
"name": "mirzaev/minimal", "name": "mirzaev/minimal",
"type": "framework", "type": "framework",
"description": "Lightweight MVC framework that manages only the basic mechanisms, leaving the development of the programmer and not overloading the project", "description": "My vision of a good framework",
"keywords": [ "keywords": [
"mvc", "mvc",
"framework" "framework",
], "lightweight"
"license": "WTFPL", ],
"homepage": "https://git.mirzaev.sexy/mirzaev/minimal", "license": "WTFPL",
"authors": [ "homepage": "https://git.mirzaev.sexy/mirzaev/minimal",
{ "authors": [
"name": "Arsen Mirzaev Tatyano-Muradovich", {
"email": "arsen@mirzaev.sexy", "name": "Arsen Mirzaev Tatyano-Muradovich",
"homepage": "https://mirzaev.sexy", "email": "arsen@mirzaev.sexy",
"role": "Developer" "homepage": "https://mirzaev.sexy",
} "role": "Programmer"
], }
"support": { ],
"docs": "https://git.mirzaev.sexy/mirzaev/minimal/wiki", "support": {
"issues": "https://git.mirzaev.sexy/mirzaev/minimal/issues" "docs": "https://git.mirzaev.sexy/mirzaev/minimal/wiki",
}, "issues": "https://git.mirzaev.sexy/mirzaev/minimal/issues"
"require": { },
"php": "~8.1" "require": {
}, "php": "~8.4"
"suggest": { },
"ext-PDO": "To work with SQL-based databases (MySQL, PostreSQL...)" "autoload": {
}, "psr-4": {
"autoload": { "mirzaev\\minimal\\": "mirzaev/minimal/system"
"psr-4": { }
"mirzaev\\minimal\\": "mirzaev/minimal/system" }
} }
},
"autoload-dev": {
"psr-4": {
"mirzaev\\minimal\\tests\\": "mirzaev/minimal/tests"
}
}
}

4640
composer.lock generated Normal file → Executable file

File diff suppressed because it is too large Load Diff

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

@@ -1,181 +1,86 @@
<?php <?php
declare(strict_types=1); 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;
use exception;
// Build-in libraries
/** use exception;
* Контроллер
* /**
* @package mirzaev\minimal * Controller
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> *
*/ * @var core $core An instance of the core
class controller * @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
*/ *
private string $postfix = '_controller'; * @method self __construct(core $core) Constructor
*
/** * @package mirzaev\minimal
* Модель *
*/ * @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
protected model $model; * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
/** class controller
* Шаблонизатор представления {
*/ use magic;
protected object $view;
/**
/** * Core
* Конструктор *
* * @var core $core An instance of the core
* @return void */
*/ public core $core {
public function __construct() // Read
{ get => $this->core;
} }
/** /**
* Записать свойство * Model
* *
* @param string $name Название * @var model $model An instance of the model connected in the core
* @param mixed $value Значение */
*/ public model $model {
public function __set(string $name, mixed $value = null): void // Write
{ set (model $model) {
match ($name) { $this->model ??= $model;
'model' => (function () use ($value) { }
if ($this->__isset('model')) {
// Свойство уже было инициализировано // Read
get => $this->model;
// Выброс исключения (неудача) }
throw new exception('Запрещено реинициализировать модель ($this->model)', 500);
} else { /**
// Свойство ещё не было инициализировано * View
*
if ($value instanceof model) { * @var view $view View template engine instance (twig)
// Передано подходящее значение */
public object $view {
// Запись свойства (успех) // Write
$this->model = $value; set (object $view) {
} else { $this->view ??= $view;
// Передано неподходящее значение }
// Выброс исключения (неудача) // Read
throw new exception('Модель ($this->model) должна хранить инстанцию "mirzaev\minimal\model"', 500); get => $this->view;
} }
}
})(), /**
'view' => (function () use ($value) { * Constructor
if ($this->__isset('view')) { *
// Свойство уже было инициализировано * @param core $core The instance of the core
*
// Выброс исключения (неудача) * @return self
throw new exception('Запрещено реинициализировать шаблонизатор представления ($this->view)', 500); */
} else { public function __construct(core $core) {
// Свойство ещё не было инициализировано // Writing the core into the property
$this->core = $core;
if (is_object($value)) { }
// Передано подходящее значение }
// Запись свойства (успех)
$this->view = $value;
} else {
// Передано неподходящее значение
// Выброс исключения (неудача)
throw new exception('Шаблонизатор представлений ($this->view) должен хранить объект', 500);
}
}
})(),
'postfix' => (function () use ($value) {
if ($this->__isset('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 ($this->__isset('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});
})()
};
}
}

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

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

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

@@ -1,113 +1,30 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
// Встроенные библиотеки // Files of the project
use exception; use mirzaev\minimal\traits\magic;
/** /**
* Модель * Model
* *
* @package mirzaev\minimal * @method self __construct() Constructor
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> *
*/ * @package mirzaev\minimal
class model *
{ * @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
/** * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
* Постфикс */
*/ class model
private string $postfix = '_model'; {
use magic;
/**
* Записать свойство /**
* * Constructor
* @param string $name Название *
* @param mixed $value Значение * @return self
*/ */
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 ($this->__isset('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});
})()
};
}
}

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

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

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

@@ -1,208 +1,285 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
// Файлы проекта // Files of the project
use mirzaev\minimal\core; use mirzaev\minimal\route,
mirzaev\minimal\traits\singleton;
// Встроенные библиотеки
use ReflectionClass; // Build-ing libraries
use InvalidArgumentException as exception_argument;
/**
* Маршрутизатор /**
* * Router
* @package mirzaev\shop *
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * @param array $routes The registry of routes
* *
* @todo * @method self write(string $uri, string $method)
* 1. Доработать обработку ошибок * @method self sort() Sort routes (DEV)
* 2. Добавить __set(), __get(), __isset() и __unset() * @method string universalize(string $urn) Universalize URN
*/ *
final class router * @package mirzaev\minimal
{ *
/** * @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @var array $router Маршруты * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/ */
public array $routes = []; final class router
{
/** use singleton;
* Записать маршрут
* /**
* @param string $route Маршрут * Routes
* @param string $target Обработчик (контроллер и модель, без постфиксов) *
* @param string|null $method Метод * @var array $routes The registry of routes
* @param string|null $type Тип */
* @param string|null $model Модель protected array $routes = [];
*/
public function write(string $route, string $target, string $method = null, string $type = 'GET', string $model = null): void /**
{ * Write a route
// Запись в реестр *
$this->routes[$route][$type] = [ * @param string $urn URN of the route ('/', '/page', '/page/$variable', '/page/$collector...'...)
'target' => $target, * @param route $route The route
'method' => $method ?? '__construct' * @param string|array $method Method of requests (GET, POST, PUT, DELETE, COOKIE...)
]; *
} * @return self The instance from which the method was called (fluent interface)
*/
/** public function write(
* Обработать маршрут string $urn,
* route $route,
* @param string $route Маршрут null|string|array $method = 'GET'
*/ ): self {
public function handle(string $uri = null, core $core = null): ?string foreach (is_array($method) ? $method : [$method] as $method) {
{ // Iterate over methods of requests
// Запись полученного URI или из данных веб-сервера
$uri = $uri ?? $_SERVER['REQUEST_URI'] ?? ''; // Validating method
$method = match ($method) {
// Инициализация URL 'POST', 'GET', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE' => $method,
$url = parse_url($uri, PHP_URL_PATH); default => throw new exception_argument("Failed to initialize method: \"$method\"")
};
// Универсализация
$url = self::universalization($url); // Writing to the registry of routes
$this->routes[$urn][$method] = $route;
// Сортировка массива маршрутов от большего ключа к меньшему (кешируется) }
krsort($this->routes);
// Exit (success) (fluent interface)
// Поиск директорий в ссылке return $this;
preg_match_all('/[^\/]+/', $url, $directories); }
// Инициализация директорий /**
$directories = $directories[0]; * Match
*
foreach ($this->routes as $route => $data) { * 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...)
$route = self::universalization($route); *
* @return route|null Route, if found
// Поиск директорий в маршруте */
preg_match_all('/[^\/]+/', $route, $data['directories']); public function match(string $uri, string $method): ?route {
// Declaration of the registry of routes directoies
// Инициализация директорий $routes = [];
$data['directories'] = $data['directories'][0];
foreach ($this->routes as $route => $data) {
if (count($directories) === count($data['directories'])) { // Iteration over routes
// Совпадает количество директорий у ссылки и маршрута (вероятно эта ссылка на этот маршрут)
// Search directories of route (explode() creates empty value in array)
// Инициализация массива переменных preg_match_all('/(^\/$|[^\/]+)/', $route, $data['directories']);
$data['vars'] = []; $routes[$route] = $data['directories'][0];
}
foreach ($data['directories'] as $index => &$directory) {
// Перебор найденных переменных if (count($routes) === count($this->routes)) {
// Initialized the registry of routes directoies
if (preg_match('/\$([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]+)/', $directory) === 1) {
// Переменная // Universalization of URN (/foo/bar)
$urn = self::universalize(parse_url(urldecode($uri), PHP_URL_PATH));
// Запись в массив переменных и перезапись переменной значением из ссылки
$directory = $data['vars'][trim($directory, '$')] = $directories[$index]; // Search directories of URN (explode() creates empty value in array)
} preg_match_all('/(^\/$|[^\/]+)/', $urn, $directories);
} $directories = $directories[0];
// Реиницилазция маршрута /**
$route = self::universalization(implode('/', $data['directories'])); * Initialization of the route
*/
// Маршрут оказался пустым
if (empty($route)) $route = '/'; // Initializing the buffer of matches of route directories with URN directories
$matches = [];
if (mb_stripos($route, $url, 0, "UTF-8") === 0 && mb_strlen($route, 'UTF-8') <= mb_strlen($url, 'UTF-8')) {
// Найден маршрут, а так же его длина не меньше длины запрошенного URL foreach ($directories as $i => $urn_directory) {
// Iteration over directories of URN
// Инициализация маршрута
if (array_key_exists($_SERVER["REQUEST_METHOD"], $data)) { foreach ($this->routes as $route => $data) {
// Найдены настройки для полученного типа запроса // Iteration over routes
// Запись маршрута if (isset($data[$method])) {
$route = $data[$_SERVER["REQUEST_METHOD"]]; // The request method matches the route method
} else {
// Не найдены настройки для полученного типа запроса // Universalization of route
$route = self::universalize($route);
// Деинициализация
unset($route); // Skipping unmatched routes based on results of previous iterations
} if (isset($matches[$route]) && $matches[$route] === false) continue;
// Выход из цикла // Initializing of route directory
break; $route_directory = $routes[$route][$i] ?? null;
}
} if (isset($route_directory)) {
// Initialized of route directory
// Деинициализация
unset($route); if ($urn_directory === $route_directory) {
} // The directory of URN is identical to the directory of route
if (!empty($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) {
if (class_exists($controller = ($core->namespace ?? (new core)->namespace) . '\\controllers\\' . $route['target'] . $core->controller->postfix ?? (new core())->controller->postfix)) { // 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])
if (class_exists($model = ($core->namespace ?? (new core)->namespace) . '\\models\\' . $route['target'] . $core->model->postfix ?? (new core())->model->postfix)) { && 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
$controller->model = new $model;
} // Writing
$matches[$route] = 'collector';
if (empty($response = $controller->{$route['method']}($data['vars'] + $_REQUEST, $_FILES))) { } else $matches[$route] = false;
// Не удалось получить ответ после обработки контроллера } else if ($matches[$route] === 'collector') {
} else $matches[$route] = false;
// Возврат (неудача) }
return $this->error($core); }
} }
// Возврат (успех) // Finding a priority route from match results
return $response; foreach ($matches as $route => $match) if ($match !== false) break;
}
} if ($route && !empty($data = $this->routes[$route])) {
// Route found
// Возврат (неудача)
return $this->error($core); // Universalization of route
} $route = self::universalize($route);
/** /**
* Контроллер ошибок * Initialization of route variables
* */
* @param core $core Ядро фреймворка
* foreach ($routes[$route] as $i => $route_directory) {
* @return string|null HTML-документ с ошибкой // Iteration over directories of route
*/
private function error(core $core = null): ?string if (preg_match('/^\$([a-zA-Z_\x80-\xff]+)$/', $route_directory) === 1) {
{ // The directory is a variable ($variable)
if (
class_exists($class = '\\' . ($core->namespace ?? (new ReflectionClass(core::class))->getNamespaceName()) . '\\controllers\\errors' . $core->controller->postfix ?? (new core())->controller->postfix) && // Запись в реестр переменных и перещапись директории в маршруте
method_exists($class, $method = 'error404') $data[$method]->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...)
// Возврат (вызов метода для обработки ошибки) // Инициализаия ссылки на массив сборщика
return (new $class(basename($class)))->$method(); $collector = &$data[$method]->variables[trim($route_directory, '$.')];
} else {
// Не существует контроллер ошибок или метод для обработки ошибки // Инициализаия массива сборщика
$collector ??= [];
// Никаких исключений не вызывать, отдать пустую страницу,
// либо вызвать, но отображать в зависимости от включенного дебаг режима !!!!!!!!!!!!!!!!!!!! см. @todo // Запись в реестр переменных
return null; $collector[] = $directories[$i];
}
} foreach (array_slice($directories, ++$i, preserve_keys: true) as &$urn_directory) {
// Перебор директорий URN
/**
* Универсализация URL // Запись в реестр переменных
* $collector[] = $urn_directory;
* @param string $url Ссылка }
*
* @return string Универсализированная ссылка break;
*/ }
private function universalization(string $url): string }
{
// Если не записан "/" в начале, то записать // Exit (success or fail)
$url = preg_replace('/^([^\/])/', '/$1', $url); return $data[$method] ?? null;
}
// Если записан "/" в конце, то удалить }
$url = preg_replace('/(.+)\/$/', '$1', $url);
// Exit (fail)
return $url; 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

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

@@ -0,0 +1,85 @@
<?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');
}
}