Основная идея 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; } }
Заключение
Данная статья написана для структурирования своих мыслей. Зафиксировать уже реализованное и попытаться понять что я упустил. Следующий этап — довести разработку до состояния когда можно будет сгенерировать сайт и выложить его на хостинге.
ссылка на оригинал статьи https://habr.com/ru/post/717706/

Добавить комментарий