6 Commits
2.0.0 ... 2.3.2

10 changed files with 3186 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>

76
composer.json Normal file → Executable file
View File

@@ -1,39 +1,37 @@
{ {
"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": "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",
], "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": "Developer"
], }
"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.2"
"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": {
"autoload-dev": { "mirzaev\\minimal\\tests\\": "mirzaev/minimal/tests"
"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

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

@@ -1,181 +1,94 @@
<?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\traits\magic;
// Встроенные библиотеки
use exception; // Встроенные библиотеки
use exception;
/**
* Контроллер /**
* * Controller (base)
* @package mirzaev\minimal *
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * @package mirzaev\minimal
*/ *
class controller * @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
{ * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
/** */
* Постфикс class controller
*/ {
private string $postfix = '_controller'; use magic;
/** /**
* Модель * Postfix of file names
*/ */
protected model $model; public const string POSTFIX = '_controller';
/** /**
* Шаблонизатор представления * Instance of the model connected in the router
*/ */
protected object $view; protected model $model;
/** /**
* Конструктор * View template engine instance (twig)
* */
* @return void protected object $view;
*/
public function __construct() /**
{ * Constructor
} */
public function __construct() {}
/**
* Записать свойство /**
* * Write property
* @param string $name Название *
* @param mixed $value Значение * @param string $name Name of the property
*/ * @param mixed $value Value of the property
public function __set(string $name, mixed $value = null): void *
{ * @return void
match ($name) { */
'model' => (function () use ($value) { public function __set(string $name, mixed $value = null): void
if ($this->__isset('model')) { {
// Свойство уже было инициализировано match ($name) {
'model' => (function () use ($value) {
// Выброс исключения (неудача) if ($this->__isset('model')) throw new exception('Can not reinitialize property: ' . static::class . '::$model', 500);
throw new exception('Запрещено реинициализировать модель ($this->model)', 500); else {
} else { // Property not initialized
// Свойство ещё не было инициализировано
if (is_object($value)) $this->model = $value;
if ($value instanceof model) { else throw new exception('Property "' . static::class . '::view" should store an instance of a model', 500);
// Передано подходящее значение }
})(),
// Запись свойства (успех) 'view' => (function () use ($value) {
$this->model = $value; if ($this->__isset('view')) throw new exception('Can not reinitialize property: ' . static::class . '::$view', 500);
} else { else {
// Передано неподходящее значение // Property not initialized
// Выброс исключения (неудача) if (is_object($value)) $this->view = $value;
throw new exception('Модель ($this->model) должна хранить инстанцию "mirzaev\minimal\model"', 500); else throw new exception('Property "' . static::class . '::view" should store an instance of a view template engine', 500);
} }
} })(),
})(), default => throw new exception('Property "' . static::class . "::\$$name\" not found", 404)
'view' => (function () use ($value) { };
if ($this->__isset('view')) { }
// Свойство уже было инициализировано
/**
// Выброс исключения (неудача) * Read property
throw new exception('Запрещено реинициализировать шаблонизатор представления ($this->view)', 500); *
} else { * @param string $name Name of the property
// Свойство ещё не было инициализировано *
* @return mixed Value of the property
if (is_object($value)) { */
// Передано подходящее значение public function __get(string $name): mixed
{
// Запись свойства (успех) return match ($name) {
$this->view = $value; 'model' => $this->model ?? throw new exception('Property "' . static::class . '::$model" is not initialized', 500),
} else { 'view' => $this->view ?? throw new exception('Property "' . static::class . '::$view" is not initialized', 500),
// Передано неподходящее значение default => throw new exception('Property "' . static::class . "::\$$name\" not found", 404)
};
// Выброс исключения (неудача) }
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});
})()
};
}
}

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

@@ -1,318 +1,227 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
// Файлы проекта // Файлы проекта
use mirzaev\minimal\router, use mirzaev\minimal\router,
mirzaev\minimal\controller, mirzaev\minimal\controller,
mirzaev\minimal\model; mirzaev\minimal\model;
// Встроенные библиотеки // Встроенные библиотеки
use exception; use exception,
ReflectionClass as reflection;
/**
* Ядро /**
* * Core
* @package mirzaev\minimal *
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * @package mirzaev\minimal
* *
* @todo * @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* 1. Добавить __isset() и __unset() * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/ */
final class core final class core
{ {
/** /**
* Соединение с базой данных * Инстанция соединения с базой данных
*/ */
private object $storage; private object $db;
/** /**
* Маршрутизатор * Инстанция маршрутизатора
*/ */
private router $router; private readonly router $router;
/** /**
* Контроллер * Инстанция ядра контроллера
*/ */
private controller $controller; private readonly controller $controller;
/** /**
* Модель * Инстанция ядра модели
*/ */
private model $model; private readonly model $model;
/** /**
* Пространство имён проекта * Путь пространства имён (системное)
* *
* Используется для поиска файлов по спецификации PSR-4 * Используется для поиска файлов по спецификации PSR-4
*/ */
private string $namespace; private readonly string $namespace;
/** /**
* Конструктор * Конструктор
* *
* @param object $storage Хранилище * @param ?object $db Инстанция соединения с базой данных
* @param router $router Маршрутизатор * @param ?router $router Маршрутизатор
* @param string $uri Маршрут * @param ?controller $controller Инстанция ядра контроллера
*/ * @param ?model $model Инстанция ядра модели
public function __construct(object $storage = null, router $router = null, controller $controller = null, model $model = null, string $namespace = null) * @param ?string $namespace Пространство имён системного ядра
{ *
if (isset($storage)) { * @return self Инстанция ядра
// Переданы данные для хранилища */
public function __construct(
// Проверка и запись ?object $db = null,
$this->__set('storage', $storage); ?router $router = null,
} ?controller $controller = null,
?model $model = null,
if (isset($router)) { ?string $namespace = null
// Переданы данные для маршрутизатора ) {
// Инициализация свойств
// Проверка и запись if (isset($db)) $this->__set('db', $db);
$this->__set('router', $router); if (isset($router)) $this->__set('router', $router);
} if (isset($controller)) $this->__set('controller', $controller);
if (isset($model)) $this->__set('model', $model);
if (isset($controller)) { $this->__set('namespace', $namespace ?? (new reflection(self::class))->getNamespaceName());
// Переданы данные для контроллера }
// Проверка и запись /**
$this->__set('controller', $controller); * Деструктор
} *
*/
if (isset($model)) { public function __destruct() {}
// Переданы данные для модели
/**
// Проверка и запись * Запуск
$this->__set('model', $model); *
} * @param ?string $uri Маршрут
*
if (isset($namespace)) { * @return string|int|null Ответ
// Переданы данные для пространства имён */
public function start(string $uri = null): string|int|null
// Проверка и запись {
$this->__set('namespace', $namespace); // Обработка запроса
} return $this->__get('router')->handle($uri, core: $this);
} }
/** /**
* Деструктор * Записать свойство
* *
*/ * @param string $name Название
public function __destruct() * @param mixed $value Содержимое
{ *
} * @return void
*/
public function start(string $uri = null): ?string public function __set(string $name, mixed $value = null): void
{ {
// Обработка запроса match ($name) {
return $this->__get('router')->handle($uri, core: $this); 'db', 'database' => (function () use ($value) {
} if ($this->__isset('db')) throw new exception('Запрещено реинициализировать инстанцию соединения с базой данных ($this->db)', 500);
else {
/** // Свойство ещё не было инициализировано
* Записать свойство
* if (is_object($value)) $this->db = $value;
* @param string $name Название else throw new exception('Свойство $this->db должно хранить инстанцию соединения с базой данных', 500);
* @param mixed $value Значение }
*/ })(),
public function __set(string $name, mixed $value = null): void 'router' => (function () use ($value) {
{ if ($this->__isset('router')) throw new exception('Запрещено реинициализировать инстанцию маршрутизатора ($this->router)', 500);
match ($name) { else {
'storage', 'db', 'database' => (function () use ($value) { // Свойство ещё не было инициализировано
if ($this->__isset('storage')) {
// Свойство уже было инициализировано if ($value instanceof router) $this->router = $value;
else throw new exception('Свойство $this->router должно хранить инстанцию маршрутизатора (mirzaev\minimal\router)"', 500);
// Выброс исключения (неудача) }
throw new exception('Запрещено реинициализировать хранилище ($this->storage)', 500); })(),
} else { 'controller' => (function () use ($value) {
// Свойство ещё не было инициализировано if ($this->__isset('controller')) throw new exception('Запрещено реинициализировать инстанцию ядра контроллеров ($this->controller)', 500);
else {
if (is_object($value)) { // Свойство не инициализировано
// Передано подходящее значение
if ($value instanceof controller) $this->controller = $value;
// Запись свойства (успех) else throw new exception('Свойство $this->controller должно хранить инстанцию ядра контроллеров (mirzaev\minimal\controller)', 500);
$this->storage = $value; }
} else { })(),
// Передано неподходящее значение 'model' => (function () use ($value) {
if ($this->__isset('model')) throw new exception('Запрещено реинициализировать инстанцию ядра моделей ($this->model)', 500);
// Выброс исключения (неудача) else {
throw new exception('Хранилище ($this->storage) должно хранить объект', 500); // Свойство не инициализировано
}
} if ($value instanceof model) $this->model = $value;
})(), else throw new exception('Свойство $this->model должно хранить инстанцию ядра моделей (mirzaev\minimal\model)', 500);
'router' => (function () use ($value) { }
if ($this->__isset('router')) { })(),
// Свойство уже было инициализировано 'namespace' => (function () use ($value) {
if ($this->__isset('namespace')) throw new exception('Запрещено реинициализировать путь пространства имён ($this->namespace)', 500);
// Выброс исключения (неудача) else {
throw new exception('Запрещено реинициализировать маршрутизатор ($this->router)', 500); // Свойство не инициализировано
} else {
// Свойство ещё не было инициализировано if (is_string($value)) $this->namespace = $value;
else throw new exception('Свойство $this->namespace должно хранить строку с путём пространства имён', 500);
if ($value instanceof router) { }
// Передано подходящее значение })(),
default => throw new exception("Свойство \"\$$name\" не найдено", 404)
// Запись свойства (успех) };
$this->router = $value; }
} else {
// Передано неподходящее значение /**
* Прочитать свойство
// Выброс исключения (неудача) *
throw new exception('Маршрутизатор ($this->router) должен хранить инстанцию "mirzaev\minimal\router"', 500); * Записывает значение по умолчанию, если свойство не инициализировано
} *
} * @param string $name Название
})(), *
'controller' => (function () use ($value) { * @return mixed Содержимое
if ($this->__isset('controller')) { */
// Свойство уже было инициализировано public function __get(string $name): mixed
{
// Выброс исключения (неудача) return match ($name) {
throw new exception('Запрещено реинициализировать контроллер ($this->controller)', 500); 'db', 'database' => $this->db ?? throw new exception("Свойство \"\$$name\" не инициализировано", 500),
} else { 'router' => (function () {
// Свойство ещё не было инициализировано // Инициализация со значением по умолчанию
if (!$this->__isset('router')) $this->__set('router', new router);
if ($value instanceof controller) {
// Передано подходящее значение // Возврат (успех)
return $this->router;
// Запись свойства (успех) })(),
$this->controller = $value; 'controller' => (function () {
} else { // Инициализация со значением по умолчанию
// Передано неподходящее значение if (!$this->__isset('controller')) $this->__set('controller', new controller);
// Выброс исключения (неудача) // Возврат (успех)
throw new exception('Контроллер ($this->controller) должен хранить инстанцию "mirzaev\minimal\controller"', 500); return $this->controller;
} })(),
} 'model' => (function () {
})(), // Инициализация со значением по умолчанию
'model' => (function () use ($value) { if (!$this->__isset('model')) $this->__set('model', new model);
if ($this->__isset('model')) {
// Свойство уже было инициализировано // Возврат (успех)
return $this->model;
// Выброс исключения (неудача) })(),
throw new exception('Запрещено реинициализировать модель ($this->model)', 500); 'namespace' => $this->namespace ?? throw new exception("Свойство \"\$$name\" не инициализировано", 500),
} else { default => throw new exception("Свойство \"\$$name\" не найдено", 404)
// Свойство ещё не было инициализировано };
}
if ($value instanceof model) {
// Передано подходящее значение /**
* Проверить свойство на инициализированность
// Запись свойства (успех) *
$this->model = $value; * @param string $name Название
} else { *
// Передано неподходящее значение * @return bool Инициализировано свойство?
*/
// Выброс исключения (неудача) public function __isset(string $name): bool
throw new exception('Модель ($this->model) должен хранить инстанцию "mirzaev\minimal\model"', 500); {
} return match ($name) {
} default => isset($this->{$name})
})(), };
'namespace' => (function () use ($value) { }
if ($this->__isset('namespace')) {
// Свойство уже было инициализировано /**
* Удалить свойство
// Выброс исключения (неудача) *
throw new exception('Запрещено реинициализировать пространство имён ($this->namespace)', 500); * @param string $name Название
} else { *
// Свойство ещё не было инициализировано * @return void
*/
if (is_string($value)) { public function __unset(string $name): void
// Передано подходящее значение {
match ($name) {
// Запись свойства (успех) default => (function () use ($name) {
$this->namespace = $value; // Удаление
} else { unset($this->{$name});
// Передано неподходящее значение })()
};
// Выброс исключения (неудача) }
throw new exception('Пространство имён ($this->namespace) должно хранить строку', 500); }
}
}
})(),
default => throw new exception("Свойство \"\$$name\" не найдено", 404)
};
}
/**
* Прочитать свойство
*
* Записывает значение по умолчанию, если свойство не инициализировано
*
* @param string $name Название
*
* @return mixed Содержимое
*/
public function __get(string $name): mixed
{
return match ($name) {
'storage', 'db', 'database' => $this->storage ?? throw new exception("Свойство \"\$$name\" не инициализировано", 500),
'router' => (function () {
if ($this->__isset('router')) {
// Свойство уже было инициализировано
} else {
// Свойство ещё не было инициализировано
// Инициализация со значением по умолчанию
$this->__set('router', new router);
}
// Возврат (успех)
return $this->router;
})(),
'controller' => (function () {
if ($this->__isset('controller')) {
// Свойство уже было инициализировано
} else {
// Свойство ещё не было инициализировано
// Инициализация со значением по умолчанию
$this->__set('controller', new controller);
}
// Возврат (успех)
return $this->controller;
})(),
'model' => (function () {
if ($this->__isset('model')) {
// Свойство уже было инициализировано
} else {
// Свойство ещё не было инициализировано
// Инициализация со значением по умолчанию
$this->__set('model', new model);
}
// Возврат (успех)
return $this->model;
})(),
'namespace' => $this->namespace ?? throw new exception("Свойство \"\$$name\" не инициализировано", 500),
default => throw new exception("Свойство \"\$$name\" не найдено", 404)
};
}
/**
* Проверить свойство на инициализированность
*
* @param string $name Название
*/
public function __isset(string $name): bool
{
return match ($name) {
default => isset($this->{$name})
};
}
/**
* Удалить свойство
*
* @param string $name Название
*/
public function __unset(string $name): void
{
match ($name) {
default => (function () use ($name) {
// Удаление
unset($this->{$name});
})()
};
}
}

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

@@ -1,113 +1,34 @@
<?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;
/** // Built-in libraries
* Модель use exception;
*
* @package mirzaev\minimal /**
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * Model (base)
*/ *
class model * @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 string $postfix = '_model'; class model
{
/** use magic;
* Записать свойство
* /**
* @param string $name Название * Postfix of file names
* @param mixed $value Значение */
*/ public const string POSTFIX = '_model';
public function __set(string $name, mixed $value = null): void
{ /**
match ($name) { * Constructor
'postfix' => (function () use ($value) { */
if (isset($this->postfix)) { public function __construct() {}
// Свойство уже было инициализировано }
// Выброс исключения (неудача)
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});
})()
};
}
}

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

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

View File

@@ -0,0 +1,81 @@
<?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 = null) 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
* @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})
};
}
}