Основная идея CMG (Content management generator) — не выполнять в Runtime то, что можно сгенерировать в виде статического PHP кода. Т.е. мы кэшируем все данные в генерируемом коде. Это происходит во всех современных фреймворках, но в данном случае это происходит не при развертывании на хостинге, а при кодогенерации. Код сайта генерируется с помощью конфига в виде кода. На мой взгляд для программиста это удобнее + больше гибкости чем при работе с конфигом.
Введение
Каждый сайт состоит из следующих компонентов:
Служат для определения скрипта, который необходимо вызывать для генерации содержимого страницы в зависимости от указанного адреса. Текущий подход к маршрутизации очень простой:
- Задаются параметры маршрутов в файле настроек.
- Все запросы клиента перенаправляются в файл index.php с помощью файла .htaccess
# Send Requests To Front Controller... RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L]
- Скрипт читает настройки и определяет нужный маршрут
При этом вызов скрипта index.php явно лишний, так как маршрутизация может быть выполнена непосредственно в файле .htaccess
Служат для выполнения требуемых функций: работа с БД, работа с КЕШем, работа с шаблонизатором. Фактически сервис — эта функционал, который расширяет возможности разработчика для написания сайта. Для создания сервисов используется Reflection API. Т.е. при запросе какого-то сервиса определяется список параметров его конструктора и если там указаны другие сервисы, то они тут же создаются и передаются в конструктор в качестве параметра. Сервис задаётся интерфейсом для которого указывается реализация в настройках. При этом функционал не будет измениться при изменении реализации (разве что будут добавлены новые функции в интерфейс).
На основе Reflection API можно создать статический код для создания сервисов. В этом случае нет необходимости в Runtime определять параметры конструкторов, это будет сделано при генерации кода создания сервисов.
Посредники служат для обработки запросов до того как он придёт в скрипт генерации страницы, а также обработки содержимого страницы после её генерации. Используют Reflection API для работы с сервисами.
Можно сгенерировать php-код в котором будут прописаны вызовы всех посредников маршрута, а также вызов непосредственно скрипта для генерации содержимого страницы.
Служат для генерации содержимого страницы. Для создания контроллера используется Reflection API. Т.е. при вызове какого-то контроллера определяется список параметров его конструктора и если там указаны сервисы, то они тут же создаются и передаются в конструктор в качестве параметра.
На основе Reflection API можно создать статический код для создания контроллеров. В этом случае нет необходимости в Runtime определять параметры конструкторов, это будет сделано при генерации кода создания контроллера.
Как видно из описания при работе с Сервисами, Посредниками и контроллерами используется Reflection API, а значит, сгенерировав код сайта, мы можем исключить вызовы этого API. Маршрутизацию можно перенести из кода PHP в файл .htaccess.
Схема работы
Для генерации кодовой базы сайта необходимо знать параметры каждого маршрута. Настройки сайта задаются в виде дерева настроек, где каждый лист представляется одним из следующих типов:
- Сайт — родительский узел. Только один.
- Домен — содержит имя хоста домена
- Путь — часть маски маршрута
- Маршрут — непосредственно сам маршрут.
Выше на картинке в настройках заданы следующие домены и маршруты:
- host1.ru/
- host1.ru/favicon.ico
- host2.ru/
- host2.ru/forum
- host2.ru/forum/edit
- host2.ru/forum/view/$id
Каждый узел содержит (и позволяет изменить) следующие параметры:
- Список сервисов — общий список для маршрута формируется, начиная с листа маршрута. Каждый сервис устанавливается только один раз. Т.е. если для одного сервиса будет установлен класс реализации в узле Сайт и Домен, то будет взят тот класс, что указан в Домене, так как он ближе к листу Маршрут.
- Посредники — посредники объединяются в одну очередь, начиная от Сайта. Можно задать приоритеты для каждого посредника в случае, если необходимо чтобы они выполнялись в определенной последовательности.
- Контроллер — класс и метод контроллера задаётся в Маршруте
- Тип запроса (GET, POST, HEAD или их комбинация). Тип берется из того листа, что ближе к Маршруту и если он указан. Если не указан, то по умолчанию значение = GET.
Также в каждый лист дерева настроек можно добавить Генератор. Генератор наследуется от абстрактного класса и содержит абстрактный метод onGenerate который необходимо реализовать. В этом методе вы можете задать свой код, который будет изменять дерево настроек. Т.е. в методе генерации можно добавлять свои узлы в дерево настроек. Эти новые узлы будут учтены при генерации кодовой базы сайта.
Т.е. генератор позволяет создавать свои модули, которые можно переиспользовать.
Примеры
Для тех кто ничего не понял, несколько примеров с кодом.
Сайт содержит один маршрут s-cmg1.ru/test
Сначала вызывается настройка и генерируется сайт. Запрос приходит в файл .htaccess и после обработки вызывается код создания запроса, отправки ответа и создания сервиса, который содержит трейт генерации содержимого страницы. Трейт генерации содержимого вызывает контроллер.
class SiteExample1 extends SiteGenerator
{
// Конструктор
public function __construct()
{
parent::__construct();
// Добавить домен
$this->addDomain('s-cmg1.ru', function (DomainGenerator $domain) {
// Добавить путь
$domain->addPath('test', null, function (PathGenerator $path) {
// Добавить маршрут
$path->addRoute(new RouteGeneratorController(TestController::class));
});
});
}
}
#-- s-cmg1.ru/test
RewriteCond %{HTTP_HOST} s-cmg1.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^test$ /@/s-cmg1.ru/f684f00a.php [L]
Создание запроса и его обработка происходит в методе run. Также данный класс содержит методы для получения сервисов, определенных для данного маршрута.
// Приложение маршрута
class AppRoute
{
// Трейт с функцией onRequest которая генерирует ответ
use \Shasoft\SamoyedCMG\AppRouteOnRequest;
// Соответствие Сервис => Имя класса
protected static array $serviceName2Classname = [
"Shasoft\\SamoyedCMG\\Service\\IRequest" => "Shasoft_SamoyedCMG_Service_Request",
"Shasoft\\SamoyedCMG\\Service\\IService" => "Shasoft_SamoyedCMG_Service_Request",
IRoute::class => "Shasoft_SamoyedCMG_Route"
];
// Список созданных сервисов
protected static array $instances = [];
// Получить сервис (или false в случае его отсутствия)
static public function getServiceSafe(string $serviceName): IService|false
{
// Преобразовать идентификатор сервиса/имя интерфейса => имя класса
if (array_key_exists($serviceName, self::$serviceName2Classname)) {
// Имя метода создания
$methodName = self::$serviceName2Classname[$serviceName];
// А может сервис уже создан?
if (array_key_exists($methodName, self::$instances)) {
// Вернуть созданный ранее сервис
return self::$instances[$methodName];
} else {
// Проверить существование метода создания
if (method_exists(__CLASS__, $methodName)) {
// Вызвать метод по имени для создания сервиса
$ret = self::$methodName();
// Записать сервис в КЕШ
self::$instances[$methodName] = $ret;
// Вернуть сервис
return $ret;
}
}
}
// Исключение
return false;
}
// Получить сервис
static public function getService(string $serviceName): IService
{
$ret = self::getServiceSafe($serviceName);
// Если сервис не определен
if ($ret === false) {
// Исключение
throw new \Exception("Сервиса " . $serviceName . " не существует!");
}
return $ret;
}
// Сервис с информацией о маршруте
static protected function Shasoft_SamoyedCMG_Route(): \Shasoft\SamoyedCMG\Route
{
return new \Shasoft\SamoyedCMG\Route([]);
}
// Shasoft\SamoyedCMG\Service\Request
static protected function Shasoft_SamoyedCMG_Service_Request(): \Shasoft\SamoyedCMG\Service\Request
{
return new \Shasoft\SamoyedCMG\Service\Request();
}
// Запуск
static public function run()
{
//--------------------------------------------------------------------------------
try {
// Запрос
$request = self::getService(IRequest::class);
// Сформировать ответ
$response = self::onRequest($request);
// Если ответ в виде строки
if (is_string($response)) {
// то преобразовать строку в HTTP ответ
$response = new Response($response);
$response->prepare($request->getSymfonyRequest());
}
// Отправить ответ пользователю
$response->send();
} catch (\Exception $e) {
s_dump_error('Исключение ' . get_class($e), $e);
} catch (\Error $e) {
s_dump_error('Ошибка ' . get_class($e), $e);
}
}
}
// Запустить обработку запроса
AppRoute::run();
trait AppRouteOnRequest
{
// Обработка запроса
static protected function onRequest(IRequest $request): Response
{
// Создать контроллер
$controller = new \Shasoft\SExample\Controller\TestController();
// Выполнить метод контролера
$response = $controller->run( );
// Если вернули null
if (is_null($response)) {
$response = '<h3><abbr style="color:red" title="Контроллер вернул null">null</abbr></h3>';
}
// Если ответ в виде строки
if (is_string($response)) {
// то преобразовать строку в HTTP ответ
$response = new Response($response);
$response->prepare($request->getSymfonyRequest());
}
// Вернуть ответ
return $response;
}
}
class TestController
{
// Конструктор
public function __construct()
{
}
// Запуск
public function run()
{
// Вывод содержимого шаблона
return 'Страница на основе контроллера. Маршрут без параметров. <b>' . microtime() . '</b>';
}
}
class SiteExample0 extends SiteGenerator
{
// Конструктор
public function __construct()
{
parent::__construct();
// Добавить домены
$this
->addDomain('host1.ru', function (DomainGenerator $domain) {
// Добавить маршрут host1.ru/
$domain->addRoute(new RouteGeneratorController(TestController::class));
// Добавить путь
$domain->addPath('/favicon.ico', null, function (PathGenerator $path) {
// Добавить маршрут host1.ru/favicon.ico
$path->addRoute(new RouteGeneratorController(TestController::class));
});
})->addDomain('host2.ru', function (DomainGenerator $domain) {
// Добавить маршрут host2.ru/
$domain->addRoute(new RouteGeneratorController(TestController::class));
// Добавить путь
$domain->addPath('forum', null, function (PathGenerator $path) {
// Добавить маршрут host2.ru/forum
$path->addRoute(new RouteGeneratorController(TestController::class));
// Добавить путь
$path->addPath('edit', null, function (PathGenerator $path) {
// Добавить маршрут host2.ru/forum/edit
$path
->addRoute(new RouteGeneratorController(TestController::class))
->setHttpMethod('GET|POST');
});
// Добавить путь
$path->addPath('view/$id', null, function (PathGenerator $path) {
// Добавить маршрут host2.ru/forum/view/$id
$path->addRoute(new RouteGeneratorController(TestController::class));
});
});
});
}
}
В файле .htaccess генерируется RegExp для получения параметра $id и передачи его значения в скрипт обработки.
#-- host1.ru/
RewriteCond %{HTTP_HOST} host1.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^$ /@/host1.ru/164b9b11.php [L]
#-- host1.ru/favicon.ico
RewriteCond %{HTTP_HOST} host1.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^favicon.ico$ /@/host1.ru/3bc202d3.php [L]
#-- host2.ru/
RewriteCond %{HTTP_HOST} host2.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^$ /@/host2.ru/164b9b11.php [L]
#-- host2.ru/forum
RewriteCond %{HTTP_HOST} host2.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^forum$ /@/host2.ru/473c6874.php [L]
#-- host2.ru/forum/edit
RewriteCond %{HTTP_HOST} host2.ru
RewriteCond %{REQUEST_METHOD} GET|POST
RewriteRule ^forum/edit$ /@/host2.ru/79849227.php [L]
#-- host2.ru/forum/view/$id
RewriteCond %{HTTP_HOST} host2.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^forum/view/([^/]+)$ /@/host2.ru/dcacf4a6.php?Z5a50cb9c0=$1 [QSA,L]
В трейте происходит передача параметра $id из скрипта в контроллер
trait AppRouteOnRequest
{
// Обработка запроса
static protected function onRequest(IRequest $request): Response
{
// Создать контроллер
$controller = new \Shasoft\SExample\Controller\TestArgController();
// Выполнить метод контролера
$response = $controller->run(self::$argsRoute['id']);
// Если вернули null
if (is_null($response)) {
$response = '<h3><abbr style="color:red" title="Контроллер вернул null">null</abbr></h3>';
}
// Если ответ в виде строки
if (is_string($response)) {
// то преобразовать строку в HTTP ответ
$response = new Response($response);
$response->prepare($request->getSymfonyRequest());
}
// Вернуть ответ
return $response;
}
}
контроллер получает параметр маршрута $id.
class TestArgController
{
// Конструктор
public function __construct()
{
}
// Запуск
static public function run($id)
{
// Вывод содержимого шаблона
return 'Страница на основе контроллера. Маршрут с параметром $id = [<b>' . $id . '</>] <b>' . microtime() . '</b>';
}
}
Сайт содержит один маршрут s-cmg2.ru/test
class SiteExample2 extends SiteGenerator
{
// Конструктор
public function __construct()
{
parent::__construct();
// Добавить домен
$this->addDomain('s-cmg2.ru', function (DomainGenerator $domain) {
$this->addMiddleware(TestMiddleware::class, ['role' => 'admin']);
// Добавить путь
$domain->addPath('test', null, function (PathGenerator $path) {
$this->addMiddleware(TestMiddleware2::class, ['x' => 1]);
// Добавить маршрут
$path->addRoute(new RouteGeneratorController(TestController::class));
});
});
}
}
#-- s-cmg2.ru/test
RewriteCond %{HTTP_HOST} s-cmg2.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^test$ /@/s-cmg2.ru/f684f00a.php [L]
Вот тут появились изменения по сравнению с маршрутом без посредников. Появился метод onMiddlewares в котором происходит вызов посредников + вызывается метод генерации содержимого страницы onRequest. При отсутствии посредников вместо метода onMiddlewares сразу вызывается метод onRequest.
class AppRoute
{
// Трейт с функцией onRequest которая генерирует ответ
use \Shasoft\SamoyedCMG\AppRouteOnRequest;
// Соответствие Сервис => Имя класса
protected static array $serviceName2Classname = [
"Shasoft\\SamoyedCMG\\Service\\IRequest" => "Shasoft_SamoyedCMG_Service_Request",
"Shasoft\\SamoyedCMG\\Service\\IService" => "Shasoft_SamoyedCMG_Service_Request",
IRoute::class => "Shasoft_SamoyedCMG_Route"
];
// Список созданных сервисов
protected static array $instances = [];
// Получить сервис (или false в случае его отсутствия)
static public function getServiceSafe(string $serviceName): IService|false
{
// Преобразовать идентификатор сервиса/имя интерфейса => имя класса
if (array_key_exists($serviceName, self::$serviceName2Classname)) {
// Имя метода создания
$methodName = self::$serviceName2Classname[$serviceName];
// А может сервис уже создан?
if (array_key_exists($methodName, self::$instances)) {
// Вернуть созданный ранее сервис
return self::$instances[$methodName];
} else {
// Проверить существование метода создания
if (method_exists(__CLASS__, $methodName)) {
// Вызвать метод по имени для создания сервиса
$ret = self::$methodName();
// Записать сервис в КЕШ
self::$instances[$methodName] = $ret;
// Вернуть сервис
return $ret;
}
}
}
// Исключение
return false;
}
// Получить сервис
static public function getService(string $serviceName): IService
{
$ret = self::getServiceSafe($serviceName);
// Если сервис не определен
if ($ret === false) {
// Исключение
throw new \Exception("Сервиса " . $serviceName . " не существует!");
}
return $ret;
}
// Сервис с информацией о маршруте
static protected function Shasoft_SamoyedCMG_Route(): \Shasoft\SamoyedCMG\Route
{
return new \Shasoft\SamoyedCMG\Route([]);
}
// Shasoft\SamoyedCMG\Service\Request
static protected function Shasoft_SamoyedCMG_Service_Request(): \Shasoft\SamoyedCMG\Service\Request
{
return new \Shasoft\SamoyedCMG\Service\Request();
}
// Посредники
static protected function onMiddlewares(IRequest $request): Response
{
// Выполнить посредник Shasoft\SExample\Middleware\TestMiddleware
return (new \Shasoft\SExample\Middleware\TestMiddleware())->handle($request, function ($request) {
// Выполнить посредник Shasoft\SExample\Middleware\TestMiddleware2
return (new \Shasoft\SExample\Middleware\TestMiddleware2())->handle($request, function ($request) {
// Вызвать основную функцию генерации
$response = self::onRequest($request);
// Если ответ в виде строки
if (is_string($response)) {
// то преобразовать строку в HTTP ответ
$response = new Response($response);
$response->prepare($request->getSymfonyRequest());
}
return $response;
}, array(
'x' => 1,
));
}, array(
'role' => 'admin',
));
}
// Запуск
static public function run()
{
//--------------------------------------------------------------------------------
try {
// Запрос
$request = self::getService(IRequest::class);
// Сформировать ответ
$response = self::onMiddlewares($request);
// Отправить ответ пользователю
$response->send();
} catch (\Exception $e) {
s_dump_error('Исключение ' . get_class($e), $e);
} catch (\Error $e) {
s_dump_error('Ошибка ' . get_class($e), $e);
}
}
}
// Запустить обработку запроса
AppRoute::run();
trait AppRouteOnRequest
{
// Обработка запроса
static protected function onRequest(IRequest $request): Response
{
// Создать контроллер
$controller = new \Shasoft\SExample\Controller\TestController();
// Выполнить метод контролера
$response = $controller->run( );
// Если вернули null
if (is_null($response)) {
$response = '<h3><abbr style="color:red" title="Контроллер вернул null">null</abbr></h3>';
}
// Если ответ в виде строки
if (is_string($response)) {
// то преобразовать строку в HTTP ответ
$response = new Response($response);
$response->prepare($request->getSymfonyRequest());
}
// Вернуть ответ
return $response;
}
}
class TestController
{
// Конструктор
public function __construct()
{
}
// Запуск
public function run()
{
// Вывод содержимого шаблона
return 'Страница на основе контроллера. Маршрут без параметров. <b>' . microtime() . '</b>';
}
}
Сервис IPath служит для получения директорий маршрута (именно маршрута(!), а не сайта в целом. Т.е. для каждого маршрута можно задать свои пути).
Все сервисы должны наследоваться от сервиса IService
// Пути маршрута
interface IPath extends IService
{
// Директория сайта
public function site(?string $pathX = null): string;
// Директория пакетов
public function vendor(?string $pathX = null): string;
// Директория пакета
public function package(string $packageName, ?string $pathX = null): string;
// Временная директория
public function temp(?string $pathX = null): string;
// Директория сайта в публичной директории
public function public_html(?string $pathX = null): string;
// Хранилище
public function storage(?string $pathX = null): string;
}
Генератор создаёт класс из шаблона. Важный момент: генерация сервиса происходит только если при установке в качестве реализации сервиса IPath текущего класса произошло изменение. Т.е. при повторном вызове генерация не произойдет. Это важно, так как если генератор произвел какие-то изменения, то происходит перезапуск всех генераторов, даже тех, которые уже вызывались. Поэтому необходимо отслеживать чтобы генератор не генерировал то, что он уже генерировал ранее.
Т.е. в данном случае происходит установка класса в качестве реализации сервиса с помощью метода setService, который возвращает TRUE только если произошло изменение.
class ServiceGeneratorPath extends ServiceCodeGenerator
{
// Конструктор
public function __construct(protected string $dataKey)
{
parent::__construct();
}
// Генерация
protected function onGenerate(ApiMake $api): void
{
// Имя класса
$classname = str_replace('IPath', 'Path', IPath::class);
// Генератор добавлен для узла Сайт?
if ($api->hasType('site')) {
$host = null;
} else {
$host = $api->parentObject()->domain()->host();
}
// Установить класс в качестве сервиса и если произошло изменение сервисов
if ($api->setService(IPath::class, $classname)) {
// То сгенерировать класс по шаблону
$api->addScript(__DIR__ . '/../../../twig/Service/Path.php.twig', [
'host' => $host,
'classname' => $classname,
'dataKey' => $this->dataKey
]);
}
}
}
<?php
{{ classname | namespace }}
use Shasoft\Support\File;
use Shasoft\SamoyedCMG\Service\IPath;
// Пути
class {{ classname | class }} implements IPath
{
// Базовая директория
protected string $basepath;
// Конструктор
public function __construct()
{
// Базовая директория сайта
$this->basepath = File::normalize(__DIR__ . "/..", true);
}
// Директория сайта
public function site(?string $pathX = null): string
{
$ret = $this->basepath;
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
// Директория пакетов
public function vendor(?string $pathX = null): string
{
$ret = $this->site('vendor');
$ret = '';
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
// Директория пакета
public function package(string $packageName, ?string $pathX = null): string
{
$ret = $this->vendor($packageName);
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
// Временная директория
public function temp(?string $pathX = null): string
{
$ret = $this->site('~.temp/{{ dataKey }}');
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
// Директория сайта в публичной директории
public function public_html(?string $pathX = null): string
{
{% if host %}
$ret = $this->site('public_html/@/{{ host }}');
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
{% else %}
throw new \Exception('Генератор добавлен для узла Сайт поэтмоу данная функция недоступна!');
return '';
{% endif %}
}
// Файловое хранилище
public function storage(?string $pathX = null): string
{
$ret = $this->site('storage/{{ dataKey }}');
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
}
class Path implements IPath
{
// Базовая директория
protected string $basepath;
// Конструктор
public function __construct()
{
// Базовая директория сайта
$this->basepath = File::normalize(__DIR__ . "/..", true);
}
// Директория сайта
public function site(?string $pathX = null): string
{
$ret = $this->basepath;
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
// Директория пакетов
public function vendor(?string $pathX = null): string
{
$ret = $this->site('vendor');
$ret = '';
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
// Директория пакета
public function package(string $packageName, ?string $pathX = null): string
{
$ret = $this->vendor($packageName);
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
// Временная директория
public function temp(?string $pathX = null): string
{
$ret = $this->site('~.temp/demo3');
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
// Директория сайта в публичной директории
public function public_html(?string $pathX = null): string
{
$ret = $this->site('public_html/@/s-cmg3.ru');
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
// Файловое хранилище
public function storage(?string $pathX = null): string
{
$ret = $this->site('storage/demo3');
if (!is_null($pathX)) {
$ret .= '/' . $pathX;
}
return $ret;
}
}
class SiteExample3 extends SiteGenerator
{
// Конструктор
public function __construct()
{
parent::__construct();
// Добавить домен
$this->addDomain('s-cmg3.ru', function (DomainGenerator $domain) {
// Добавить генератор сервиса IPath
$domain->addGenerator(new ServiceGeneratorPath('demo3'));
// Добавить путь
$domain->addPath('test', null, function (PathGenerator $path) {
// Добавить маршрут
$path->addRoute(new RouteGeneratorController(TestIPathController::class));
});
});
}
}
#-- s-cmg3.ru/test
RewriteCond %{HTTP_HOST} s-cmg3.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^test$ /@/s-cmg3.ru/f684f00a.php [L]
В список сервисов добавился новый сервис IPath в дополнение к стандартным.
class AppRoute
{
// Трейт с функцией onRequest которая генерирует ответ
use \Shasoft\SamoyedCMG\AppRouteOnRequest;
// Соответствие Сервис => Имя класса
protected static array $serviceName2Classname = [
"Shasoft\\SamoyedCMG\\Service\\IPath" => "Shasoft_SamoyedCMG_Service_Path",
"Shasoft\\SamoyedCMG\\Service\\IService" => "Shasoft_SamoyedCMG_Service_Path",
"Shasoft\\SamoyedCMG\\Service\\IRequest" => "Shasoft_SamoyedCMG_Service_Request",
IRoute::class => "Shasoft_SamoyedCMG_Route"
];
// Список созданных сервисов
protected static array $instances = [];
// Получить сервис (или false в случае его отсутствия)
static public function getServiceSafe(string $serviceName): IService|false
{
// Преобразовать идентификатор сервиса/имя интерфейса => имя класса
if (array_key_exists($serviceName, self::$serviceName2Classname)) {
// Имя метода создания
$methodName = self::$serviceName2Classname[$serviceName];
// А может сервис уже создан?
if (array_key_exists($methodName, self::$instances)) {
// Вернуть созданный ранее сервис
return self::$instances[$methodName];
} else {
// Проверить существование метода создания
if (method_exists(__CLASS__, $methodName)) {
// Вызвать метод по имени для создания сервиса
$ret = self::$methodName();
// Записать сервис в КЕШ
self::$instances[$methodName] = $ret;
// Вернуть сервис
return $ret;
}
}
}
// Исключение
return false;
}
// Получить сервис
static public function getService(string $serviceName): IService
{
$ret = self::getServiceSafe($serviceName);
// Если сервис не определен
if ($ret === false) {
// Исключение
throw new \Exception("Сервиса " . $serviceName . " не существует!");
}
return $ret;
}
// Сервис с информацией о маршруте
static protected function Shasoft_SamoyedCMG_Route(): \Shasoft\SamoyedCMG\Route
{
return new \Shasoft\SamoyedCMG\Route([]);
}
// Shasoft\SamoyedCMG\Service\Path
static protected function Shasoft_SamoyedCMG_Service_Path(): \Shasoft\SamoyedCMG\Service\Path
{
return new \Shasoft\SamoyedCMG\Service\Path();
}
// Shasoft\SamoyedCMG\Service\Request
static protected function Shasoft_SamoyedCMG_Service_Request(): \Shasoft\SamoyedCMG\Service\Request
{
return new \Shasoft\SamoyedCMG\Service\Request();
}
// Запуск
static public function run()
{
//--------------------------------------------------------------------------------
try {
// Запрос
$request = self::getService(IRequest::class);
// Сформировать ответ
$response = self::onRequest($request);
// Если ответ в виде строки
if (is_string($response)) {
// то преобразовать строку в HTTP ответ
$response = new Response($response);
$response->prepare($request->getSymfonyRequest());
}
// Отправить ответ пользователю
$response->send();
} catch (\Exception $e) {
s_dump_error('Исключение ' . get_class($e), $e);
} catch (\Error $e) {
s_dump_error('Ошибка ' . get_class($e), $e);
}
}
}
// Запустить обработку запроса
AppRoute::run();
Происходит передача сервиса в контроллер. При этом можно указывать не только в конструкторе нужный сервис, но и в методе контроллера. Это удобно если метод контроллера статический.
trait AppRouteOnRequest
{
// Обработка запроса
static protected function onRequest(IRequest $request): Response
{
// Создать контроллер
$controller = new \Shasoft\SExample\Controller\TestIPathController(\Shasoft\SamoyedCMG\AppRoute::getService(\Shasoft\SamoyedCMG\Service\IPath::class));
// Выполнить метод контролера
$response = $controller->run( \Shasoft\SamoyedCMG\AppRoute::getService(\Shasoft\SamoyedCMG\Service\IPath::class));
// Если вернули null
if (is_null($response)) {
$response = '<h3><abbr style="color:red" title="Контроллер вернул null">null</abbr></h3>';
}
// Если ответ в виде строки
if (is_string($response)) {
// то преобразовать строку в HTTP ответ
$response = new Response($response);
$response->prepare($request->getSymfonyRequest());
}
// Вернуть ответ
return $response;
}
}
В контроллере можно запрашивать сервис как в контсрукторе, так и в методе контроллера. Это удобно для случая если метод контроллера статический.
class TestIPathController
{
// Конструктор
public function __construct(protected IPath $path)
{
}
// Запуск
public function run(IPath $path)
{
// Вывод содержимого шаблона
return 'Директория storage:<br/>' .
$this->path->storage() . '<br/>' .
$path->storage() . '<br/>' .
'<b>' . microtime() . '</b>';
}
}
Некоторые сервисы требуют дополнительных настроек для своей генерации. К примеру класс реализации сервиса шаблонизатора ITwig требует указания директорий с шаблонами. Возможность настройки генератора решается с помощью метода tune который доступен у всех листов в дерева настроек.
// Шаблонизатор
interface ITemplate extends IService
{
// Сгенерировать
public function render(string $templateName, array $args = []): string;
}
// Шаблонизатор Twig
interface ITwig extends ITemplate
{
}
class ServiceGeneratorTwig extends ServiceCodeGenerator
{
// Все пространстава имён
protected array $namespaces = [];
// Конструктор
public function __construct()
{
parent::__construct();
}
// Генерация
protected function onGenerate(ApiMake $api): void
{
// Имя класса
$classname = str_replace('ITwig', 'Twig', ITwig::class);
// Установить класс в качестве сервиса
if ($api->setService(ITwig::class, $classname)) {
// Сгенерировать класс по шаблону
$api->addScript(__DIR__ . '/../twig/Twig.php.twig', [
'classname' => $classname,
'namespaces' => $this->namespaces,
'dev' => $api->isDev()
]);
}
}
// Добавить пространство имён
public function addNamespace(string $name, string $path): static
{
// Нормализовать путь
$path = File::normalize($path, true);
// Если такого пространства имён нет ИЛИ путь изменяется
if (!array_key_exists($name, $this->namespaces) || $this->namespaces[$name] != $path) {
// Внести изменение
$this->namespaces[$name] = $path;
// Установить флаг изменения
$this->setModify();
}
// Вернуть указатель на себя
return $this;
}
// Добавить пространства имён
public function addNamespaces(array $namespaces): static
{
foreach ($namespaces as $name => $path) {
$this->addNamespace($name, $path);
}
// Вернуть указатель на себя
return $this;
}
}
{{ classname | namespace }}
use Shasoft\SamoyedCMG\Service\IPath;
use Shasoft\STwig\ITwig;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
// Шаблонизатор Twig
class {{ classname | class }} implements ITwig
{
// Указатель на объект работы с шаблонами Twig
protected ?Environment $_twig = null;
// Папка с компилированными шаблонами
protected string $path;
// Конструктор
public function __construct(IPath $path)
{
// Директория кеширования откомпилированных шаблонов
$this->path = $path->temp('twig');
}
// Вывести содержимое
public function render(string $templateName, array $args = []): string
{
// Если шаблонизатор не создан
if( is_null($this->_twig) ) {
// Загрузчик шаблонов
$loader = new FilesystemLoader();
{% if namespaces %}
// Добавить все пути
{% for name, path in namespaces %}
$loader->addPath( __DIR__ . '/{{ path | relScript }}', '{{ name }}');
{% endfor %}
{% endif %}
// Создать объект шаблонизатора
$this->_twig = new Environment($loader, [
// Директория кеширования откомпилированных шаблонов
'cache' => $this->path,
// Режим отладки
'debug' => {{ dev | var_export }},
]);
}
return $this->_twig->render($templateName, $args);
}
}
class Twig implements ITwig
{
// Указатель на объект работы с шаблонами Twig
protected ?Environment $_twig = null;
// Папка с компилированными шаблонами
protected string $path;
// Конструктор
public function __construct(IPath $path)
{
// Директория кеширования откомпилированных шаблонов
$this->path = $path->temp('twig');
}
// Вывести содержимое
public function render(string $templateName, array $args = []): string
{
// Если шаблонизатор не создан
if (is_null($this->_twig)) {
// Загрузчик шаблонов
$loader = new FilesystemLoader();
// Добавить все пути
$loader->addPath(__DIR__ . '/../vendor/shasoft/s-examples/@twig', 'demo4');
// Создать объект шаблонизатора
$this->_twig = new Environment($loader, [
// Директория кеширования откомпилированных шаблонов
'cache' => $this->path,
// Режим отладки
'debug' => true,
]);
}
return $this->_twig->render($templateName, $args);
}
}
В методе tune необходимо указать тип генератора. В указанную функцию обратного вызова будут переданы все генераторы заданного типа, который есть в указанном узле или в вышестоящих родительских узлах. В текущем примере в методе настройки в генератор добавляется пространство имен шаблонов + директория с шаблонами.
class SiteExample4 extends SiteGenerator
{
// Конструктор
public function __construct()
{
parent::__construct();
// Добавить генератор сервиса ITag
$this->addGenerator(new ServiceGeneratorTwig);
// Добавить домен
$this->addDomain('s-cmg4.ru', function (DomainGenerator $domain) {
$domain
// Добавить генератор сервиса IPath
->addGenerator(new ServiceGeneratorPath('demo4'))
// Добавить маршрут
->addRoute(new RouteGeneratorController(TestControllerTwig::class))
// Настройка генератора сервиса ITag
->tune(function (ServiceGeneratorTwig $twig) {
// Добавить в генератор директорию с шаблонами с пространством имен demo4
$twig->addNamespace('demo4', __DIR__ . '/../@twig');
});
});
}
}
#-- s-cmg4.ru/
RewriteCond %{HTTP_HOST} s-cmg4.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^$ /@/s-cmg4.ru/164b9b11.php [L]
Добавился сервис ITemplate и ITwig. При этом в качестве реализации выступает один и тот же класс. И будет создан один объект данного класса.
class AppRoute
{
// Трейт с функцией onRequest которая генерирует ответ
use \Shasoft\SamoyedCMG\AppRouteOnRequest;
// Соответствие Сервис => Имя класса
protected static array $serviceName2Classname = [
"Shasoft\\SamoyedCMG\\Service\\IPath" => "Shasoft_SamoyedCMG_Service_Path",
"Shasoft\\SamoyedCMG\\Service\\IService" => "Shasoft_SamoyedCMG_Service_Path",
"Shasoft\\SamoyedCMG\\Service\\IRequest" => "Shasoft_SamoyedCMG_Service_Request",
"Shasoft\\STwig\\ITwig" => "Shasoft_STwig_Twig",
"Shasoft\\SamoyedCMG\\Service\\ITemplate" => "Shasoft_STwig_Twig",
IRoute::class => "Shasoft_SamoyedCMG_Route"
];
// Список созданных сервисов
protected static array $instances = [];
// Получить сервис (или false в случае его отсутствия)
static public function getServiceSafe(string $serviceName): IService|false
{
// Преобразовать идентификатор сервиса/имя интерфейса => имя класса
if (array_key_exists($serviceName, self::$serviceName2Classname)) {
// Имя метода создания
$methodName = self::$serviceName2Classname[$serviceName];
// А может сервис уже создан?
if (array_key_exists($methodName, self::$instances)) {
// Вернуть созданный ранее сервис
return self::$instances[$methodName];
} else {
// Проверить существование метода создания
if (method_exists(__CLASS__, $methodName)) {
// Вызвать метод по имени для создания сервиса
$ret = self::$methodName();
// Записать сервис в КЕШ
self::$instances[$methodName] = $ret;
// Вернуть сервис
return $ret;
}
}
}
// Исключение
return false;
}
// Получить сервис
static public function getService(string $serviceName): IService
{
$ret = self::getServiceSafe($serviceName);
// Если сервис не определен
if ($ret === false) {
// Исключение
throw new \Exception("Сервиса " . $serviceName . " не существует!");
}
return $ret;
}
// Сервис с информацией о маршруте
static protected function Shasoft_SamoyedCMG_Route(): \Shasoft\SamoyedCMG\Route
{
return new \Shasoft\SamoyedCMG\Route([]);
}
// Shasoft\SamoyedCMG\Service\Path
static protected function Shasoft_SamoyedCMG_Service_Path(): \Shasoft\SamoyedCMG\Service\Path
{
return new \Shasoft\SamoyedCMG\Service\Path();
}
// Shasoft\SamoyedCMG\Service\Request
static protected function Shasoft_SamoyedCMG_Service_Request(): \Shasoft\SamoyedCMG\Service\Request
{
return new \Shasoft\SamoyedCMG\Service\Request();
}
// Shasoft\STwig\Twig
static protected function Shasoft_STwig_Twig(): \Shasoft\STwig\Twig
{
return new \Shasoft\STwig\Twig(\Shasoft\SamoyedCMG\AppRoute::getService(\Shasoft\SamoyedCMG\Service\IPath::class));
}
// Запуск
static public function run()
{
//--------------------------------------------------------------------------------
try {
// Запрос
$request = self::getService(IRequest::class);
// Сформировать ответ
$response = self::onRequest($request);
// Если ответ в виде строки
if (is_string($response)) {
// то преобразовать строку в HTTP ответ
$response = new Response($response);
$response->prepare($request->getSymfonyRequest());
}
// Отправить ответ пользователю
$response->send();
} catch (\Exception $e) {
s_dump_error('Исключение ' . get_class($e), $e);
} catch (\Error $e) {
s_dump_error('Ошибка ' . get_class($e), $e);
}
}
}
// Запустить обработку запроса
AppRoute::run();
trait AppRouteOnRequest
{
// Обработка запроса
static protected function onRequest(IRequest $request): Response
{
// Создать контроллер
$controller = new \Shasoft\SExample\Controller\TestControllerTwig();
// Выполнить метод контролера
$response = $controller->run(\Shasoft\SamoyedCMG\AppRoute::getService(\Shasoft\SamoyedCMG\Service\ITemplate::class));
// Если вернули null
if (is_null($response)) {
$response = '<h3><abbr style="color:red" title="Контроллер вернул null">null</abbr></h3>';
}
// Если ответ в виде строки
if (is_string($response)) {
// то преобразовать строку в HTTP ответ
$response = new Response($response);
$response->prepare($request->getSymfonyRequest());
}
// Вернуть ответ
return $response;
}
}
class TestControllerTwig
{
// Конструктор
public function __construct()
{
}
// Запуск
public function run(ITemplate $template)
{
// Вывод содержимого шаблона
return $template->render('@demo4/Test.twig', [
'Title' => 'Заголовок страницы из шаблона!',
'datetime' => microtime()
]);
}
}
Все примеры выше использовали контроллер для генерации содержимого страницы. Однако это не единственная возможность. Можно создавать страницу на основе файла/директории. К примеру для генерации ссылок на ресурсы. Если маршрут не содержит посредники, то будет сгенерирован просто ссылка на статический файл.
class SiteExample5 extends SiteGenerator
{
// Конструктор
public function __construct()
{
parent::__construct();
// Добавить домен
$this->addDomain('s-cmg5.ru', function (DomainGenerator $domain) {
// Добавить маршрут: файл без посредников
$domain->addRoute(new RouteGeneratorLink(__DIR__ . '/../@assets/favicon.ico'), function (RouteGeneratorLink $route) {
$route->setRoute('link/file.ico');
});
// Добавить маршрут: файл с посредниками
$domain->addRoute(new RouteGeneratorLink(__DIR__ . '/../@assets/favicon.ico'), function (RouteGeneratorLink $route) {
$route->setRoute('link/file/middleware.ico');
$route->addMiddleware(TestMiddleware::class, ['a' => 1]);
});
// Добавить маршрут: директорис без посредников
$domain->addRoute(new RouteGeneratorLink(__DIR__ . '/../@assets/images'), function (RouteGeneratorLink $route) {
$route->setRoute('link/folder');
});
// Добавить маршрут: директорис с посредниками
$domain->addRoute(new RouteGeneratorLink(__DIR__ . '/../@assets/images'), function (RouteGeneratorLink $route) {
$route->setRoute('link/folder/middleware');
$route->addMiddleware(TestMiddleware::class, ['b' => 2]);
});
});
}
}
В директории /@/s-cmg5.ru/ при отсутствии посредников создаются ссылки на соответствующий файл/директорию.
#-- s-cmg5.ru/link/file.ico
RewriteCond %{HTTP_HOST} s-cmg5.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^link/file.ico$ /@/s-cmg5.ru/96495f91.ico [L]
#-- s-cmg5.ru/link/file/middleware.ico
RewriteCond %{HTTP_HOST} s-cmg5.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^link/file/middleware.ico$ /@/s-cmg5.ru/469c3124.php [L]
#-- s-cmg5.ru/link/folder
RewriteCond %{HTTP_HOST} s-cmg5.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^link/folder/(.*)$ /@/s-cmg5.ru/5e754869/$1 [L]
#-- s-cmg5.ru/link/folder/middleware
RewriteCond %{HTTP_HOST} s-cmg5.ru
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^link/folder/middleware/(.*)$ /@/s-cmg5.ru/ea49e04d.php?Z7c0d736e0=$1 [QSA,L]
trait AppRouteOnRequest
{
// Обработка запроса
static protected function onRequest(IRequest $request): Response
{
// Имя файла с контентом
$filepath = __DIR__ . '/../../vendor/shasoft/s-examples/@assets/favicon.ico';
// Проверим наличие файла?
if( is_file($filepath) && file_exists($filepath) )
{
// В качестве ответа вернуть файл
$response = new \Symfony\Component\HttpFoundation\BinaryFileResponse($filepath);
// Установить параметры ответа по параметрам запроса
$response->prepare($request->getSymfonyRequest());
} else {
// Страница не существует
$response = new Response('Файла не существует!', Response::HTTP_NOT_FOUND);
}
// Вернуть ответ
return $response;
}
}
trait AppRouteOnRequest
{
// Обработка запроса
static protected function onRequest(IRequest $request): Response
{
// Имя файла с контентом
$filepath = __DIR__ . '/../../vendor/shasoft/s-examples/@assets/images/'.str_replace('../','/',self::$argsRoute['filename']);
// Проверим наличие файла?
if( is_file($filepath) && file_exists($filepath) )
{
// В качестве ответа вернуть файл
$response = new \Symfony\Component\HttpFoundation\BinaryFileResponse($filepath);
// Установить параметры ответа по параметрам запроса
$response->prepare($request->getSymfonyRequest());
} else {
// Страница не существует
$response = new Response('Файла не существует!', Response::HTTP_NOT_FOUND);
}
// Вернуть ответ
return $response;
}
}
Заключение
Данная статья написана для структурирования своих мыслей. Зафиксировать уже реализованное и попытаться понять что я упустил. Следующий этап — довести разработку до состояния когда можно будет сгенерировать сайт и выложить его на хостинге.
Добавление по результатам комментариев
В комментариях очень точно описали процесс работы — генерирование кода сайта на основе PHP кода конфига. На мой взгляд это удобнее, чем конфиг в виде PHP массива (как минимум есть подсказки в виде типов данных для классов настройки).
Также указали на то что современные фреймворки генерируют в КЕШе код для исключения вызовов Reflection API. Т.е. тут я иду в "ногу со всеми современными фреймворками". Однако у меня упор делается на то, что можно не просто закешировать процесс создания сервиса, а можно сам сервис сгенерировать динамически. В качестве примера (он есть выше) я привожу сервис шаблонизатора ITwig. В настройках задаётся список пространств имен и папки шаблонов, после чего генерируется процесс создания сервиса + сам класс реализации сервиса в который прописываются эти самые шаблоны. Т.е. список шаблонов хранится не где-то в настройках, а создаётся статический класс со ссылками на эти шаблоны.
Комментарии (7)
BetsuNo
00.00.0000 00:00+2Основная идея CMG (Content management generator) — не выполнять в Runtime то, что можно сгенерировать в виде статического PHP кода.
Что-то мне подсказывает, что .htaccess не является PHP-кодом. К тому же на apache свет клином не сошёлся. По крайней мере я в своей практике за последние 10 лет вообще ни разу с ним не встречался.
Зафиксировать уже реализованное и попытаться понять что я упустил.
Из прочитанного сложилось впечатление, что вы предлагаете писать конфиг в виде кода. А после этот код будет генерировать другой код.
В целом не понятно на кого это рассчитано. Если на пользователей CMF, то слишком много кода, который как раз прячется за привычными роутерами и DI в фреймворке. Если на пользователей CMS, то тоже слишком много кода, который в принципе надо писать, что для пользователей CMS в целом задача не тривиальная.
В обоих случаях я бы предпочёл видеть какую-то декларативную расширяемую систему (либо просто конфиг в случае с CMF, либо визуальный редактор для CMS). На основе этого конгфига уже можно было бы генерить какой-то код. Кстати, это мало чем отличается текущего положения вещей в современных фреймворках.
Так же не понятно, каким образом получилось избавиться от Reflection API при использовании сервисов.
Ещё заметил, что ваш код отдаёт статику. Но это же тоже можно на этапе роутинга в HTTP-сервере решить.
shasoftX Автор
00.00.0000 00:00+1К тому же на apache свет клином не сошёлся.
Так ведь можно генерировать и не только для apache. Делаю для него так как мой хостинг его поддерживает.
Из прочитанного сложилось впечатление, что вы предлагаете писать конфиг в виде кода. А после этот код будет генерировать другой код.
Да. Так и есть.
Так же не понятно, каким образом получилось избавиться от Reflection API при использовании сервисов
Reflection API вызывается на этапе генерации и генерирует PHP код в котором уже прописаны все вызовы конструкторов нужных объектов.
Ещё заметил, что ваш код отдаёт статику. Но это же тоже можно на этапе роутинга в HTTP-сервере решить.
Вся статика и отдаётся как статика. Собственно в простейшем случае мы получаем генератор статического сайта.
FanatPHP
00.00.0000 00:00+3Если всё это только ради
не выполнять в Runtime то, что можно сгенерировать в виде статического PHP кода.
То вынужден огорчить. Все современные фреймворки и так генерируют РНР код без рефлексии. Рекомендую заглянуть в папочку cache.
francyfox
Использовать хабр в качестве среды для заметок не лучшая идея, тебе тока репу понизят
shasoftX Автор
Это не просто несвязанные заметки, а описание подхода.
UnknownQq
..заметками, имхо.
FanatPHP
С одной стороны, я согласен. С другой — в этом хабе такой чахлый трафик, что на безрыбье и заметки сгодятся.
Плюс весь опыт педагогики говорит нам о том, что нельзя подавлять креативные устремления детей, и пусть сначала идеи так себе, но важен сам факт стремления к новому. Так что вполне можно эти идеи и обсудить. Зачастую ведь самое интересное бывает как раз в комментариях.