CMS, CMF … CMG (Content management generator)

от автора

Основная идея CMG (Content management generator) — не выполнять в Runtime то, что можно сгенерировать в виде статического PHP кода.

Введение

Каждый сайт состоит из следующих компонентов:

Маршруты

Служат для определения скрипта, который необходимо вызывать для генерации содержимого страницы в зависимости от указанного адреса. Текущий подход к маршрутизации очень простой:

  1. Задаются параметры маршрутов в файле настроек.
  2. Все запросы клиента перенаправляются в файл index.php с помощью файла .htaccess
    # Send Requests To Front Controller... RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L]
  3. Скрипт читает настройки и определяет нужный маршрут

При этом вызов скрипта 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));             });         });     } }
Файл .htaccess

    #-- 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>';     } }

Пример для дерева настроек выше (+маршрут с параметром $id)

Настройки

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

В файле .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));             });         });     } }
Файл .htaccess

    #-- 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

Сервис 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             ]);         }     } } 
Twig шаблон сервиса

<?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;     } }
Сгенерированный класс реализации сервиса IPath

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));             });         });     } }
Файл .htaccess

    #-- 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 (с настройкой генератора)

Некоторые сервисы требуют дополнительных настроек для своей генерации. К примеру класс реализации сервиса шаблонизатора 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;     } }
Twig шаблон сервиса

{{ 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);     } }
Сгенерированный класс реализации сервиса ITwig

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');                 });         });     } }
Файл .htaccess

    #-- 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]);             });         });     } }
Файл .htaccess

В директории /@/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/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *