7 Commits

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

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

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

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

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

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

@@ -1,112 +1,34 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace mirzaev\minimal; namespace mirzaev\minimal;
use Exception; // Files of the project
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 Название /**
* @param mixed $value Значение * Postfix of file names
*/ */
public function __set(string $name, mixed $value = null): void public const string POSTFIX = '_model';
{
match ($name) { /**
'postfix' => (function () use ($value) { * Constructor
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});
})()
};
}
}

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

@@ -1,206 +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
* @package mirzaev\shop *
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
* @todo */
* 1. Доработать обработку ошибок final class router
* 2. Добавить __set(), __get(), __isset() и __unset() {
*/ /**
final class router * @var array $router Реестр маршрутов
{ */
/** /* protected array $routes = []; */
* @var array $router Маршруты public array $routes = [];
*/
public array $routes = []; /**
* Записать маршрут
/** *
* Записать маршрут * @param string $route Маршрут
* * @param string $handler Обработчик - инстанции контроллера и модели (не обязательно), без постфиксов
* @param string $route Маршрут * @param ?string $method Вызываемый метод в инстанции контроллера обработчика
* @param string $target Обработчик (контроллер и модель, без постфиксов) * @param ?string $request HTTP-метод запроса (GET, POST, PUT...)
* @param string|null $method Метод * @param ?string $model Инстанция модели (переопределение инстанции модели в $target)
* @param string|null $type Тип * @param array $variables
* @param string|null $model Модель *
*/ * @return static The instance from which the method was called (fluent interface)
public function write(string $route, string $target, string $method = null, string $type = 'GET', string $model = null): void */
{ public function write(
// Запись в реестр string $route,
$this->routes[$route][$type] = [ string $handler,
'target' => $target, ?string $method = 'index',
'method' => $method ?? '__construct' ?string $request = 'GET',
]; ?string $model = null,
} array $variables = []
): static {
/** // Запись в реестр
* Обработать маршрут $this->routes[$route][$request] = [
* 'controller' => $handler,
* @param string $route Маршрут 'model' => $model ?? $handler,
*/ 'method' => $method,
public function handle(string $uri = null, core $core = null): ?string 'variables' => $variables
{ ];
// Запись полученного URI или из данных веб-сервера
$uri = $uri ?? $_SERVER['REQUEST_URI'] ?? ''; // Exit (success) (fluent interface)
return $this;
// Инициализация URL }
$url = parse_url($uri, PHP_URL_PATH);
/**
// Универсализация * Handle a request
$url = self::universalization($url); *
* @param string|null $uri URI (protocol://domain/foo/bar)
// Сортировка массива маршрутов от большего ключа к меньшему (кешируется) * @param string|null $method Method (GET, POST, PUT...)
krsort($this->routes); * @param core|null $core Instence of the system core
*
// Поиск директорий в ссылке * @return string|int|null Response
preg_match_all('/[^\/]+/', $url, $directories); */
public function handle(
// Инициализация директорий ?string $uri = null,
$directories = $directories[0]; ?string $method = null,
?core $core = new core
foreach ($this->routes as $route => $data) { ): string|int|null {
// Перебор маршрутов // Declaration of the registry of routes directoies
$routes = [];
// Универсализация
$route = self::universalization($route); foreach ($this->routes as $route => $data) {
// Iteration over routes
// Поиск директорий в маршруте
preg_match_all('/[^\/]+/', $route, $data['directories']); // Search directories of route (explode() creates empty value in array)
preg_match_all('/(^\/$|[^\/]+)/', $route, $data['directories']);
// Инициализация директорий $routes[$route] = $data['directories'][0];
$data['directories'] = $data['directories'][0]; }
if (count($directories) === count($data['directories'])) { if (count($routes) === count($this->routes)) {
// Совпадает количество директорий у ссылки и маршрута (вероятно эта ссылка на этот маршрут) // Initialized the registry of routes directoies
// Инициализация массива переменных // Initializing default values
$data['vars'] = []; $uri ??= $_SERVER['REQUEST_URI'] ?? '/';
$method ??= $_SERVER["REQUEST_METHOD"];
foreach ($data['directories'] as $index => &$directory) {
// Перебор найденных переменных // Initializing of URN (/foo/bar)
$urn = parse_url(urldecode($uri), PHP_URL_PATH);
if (preg_match('/\$([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]+)/', $directory) === 1) {
// Переменная // Universalization of URN
$urn = self::universalize($urn);
// Запись в массив переменных и перезапись переменной значением из ссылки
$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 }
{
// Если не записан "/" в начале, то записать /**
$url = preg_replace('/^([^\/])/', '/$1', $url); * Initialization of route handlers
*/
// Если записан "/" в конце, то удалить
$url = preg_replace('/(.+)\/$/', '$1', $url); if (array_key_exists($method, $data)) {
// Идентифицирован метод маршрута (GET, POST, PUT...)
return $url;
} // Инициализация обработчиков (controller, model, method)
} $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})
};
}
}