Основная идея 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. В настройках задаётся список пространств имен и папки шаблонов, после чего генерируется процесс создания сервиса + сам класс реализации сервиса в который прописываются эти самые шаблоны. Т.е. список шаблонов хранится не где-то в настройках, а создаётся статический класс со ссылками на эти шаблоны.