В прошлой статье был описан процесс установки и запуска Samoyed CMG (Content Management Generator). Основная идея — генерация кода сайта на основе настроек заданных кодом. Т.е. фактически кэширование всех настроек в коде при генерации, а не при развертывании на хостинге.
В ней упоминались генераторы для генерации кода, которые служат для расширения базового функционала сайта. В примере представлены два из них:
- Shasoft\SamoyedCMG\Generator\Service\ServiceGeneratorPath — генерация сервиса для работы с путями сайта.
- Shasoft\STwig\Twig — генерация сервиса для работы с шаблонизатором Twig.
Рассмотрим генераторы более подробно для понимания их работы.
Введение
Сервис в понятии Samoyed CMG состоит из двух компонентов:
- Интерфейс с определением функций сервиса.
- Класс который реализует сервис.
В настройках генерации сайта происходит привязка интерфейса к конкретному сервису с помощью функции:
// Установить сервис public function setService(string $name, string $classname): bool
- $name — имя сервиса (имя интерфейса);
- $classname — имя класса реализации.
Такой подход позволяет устанавливать различные реализации сервиса не меняя при этом логику использования сервиса.
В простейшем случае класс реализации статический и его можно привязать непосредственно в настройках генерации. Однако я не ищу легкий путей если класс реализации генерируется по шаблону, то для этого необходимо использовать генератор.
Генератор
Генератор представляет собой обычный класс, который необходимо наследовать от класса CodeGenerator
// Генератор кода abstract class CodeGenerator extends ObjectGenerator { // Получить HTML код с информацией о генераторе abstract protected function getHtml(ApiHtml $api): string; // Генерация abstract protected function onGenerate(ApiGenerate $api): void; }
Метод getHtml возвращает html данные для страницы с технической информацией.
При генерации сайта выполняется проход по всему узлам дерева настроек и для каждого генератора вызывается метод onGenerate. Объект $api содержит функции для генерации кода. Основная функция для генерации кода:
// Добавить скрипт public function addScript(string $filepathTemplate, array $args = [], ?\Closure $cbTwigConfig = null): static;
Необходимо указать файл шаблона и его параметры и в текущий узел настроек будет добавлен сгенерированный скрипт. Вы можете указать замыкание в которое в качестве параметра передаётся объект Twig\Environment и есть возможность добавить свои функции и фильтры к шаблонизатору Twig. ВАЖНО(!): все добавленные функции и фильтры будут доступны только в указанном шаблоне.
Генератор сервиса работы с путями
Идея работы сервиса очень простая. Определяем интерфейс с функциями:
// Пути маршрута 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; // Директория www сервера public function wwwServer(?string $pathX = null): string; // Временная директория public function temp(?string $pathX = null): string; // Хранилище public function storage(?string $pathX = null): string; }
Функции site, vendor и package будут общими для всех маршрутов (Потому что кодовая база одна на все маршруты). Функция wwwServer зависит от домена маршрута. Функции temp и storage должны зависеть от входного параметра генератора. Это необходимо чтобы иметь возможность на разных доменах получать доступ к одним и тем же данным. Мы не можем привязываться к домену, так как при его изменении уже не сможем получить доступ к соответствующей папке.
// Генератор сервиса работы с путями class ServiceGeneratorPath extends CodeGenerator { // Конструктор public function __construct(protected string $dataKey) { parent::__construct(); } // Генерация protected function onGenerate(ApiGenerate $api): void { // Имя класса генерируем на основе имени сервиса с помощью замены $classname = str_replace('IPath', 'Path', IPath::class); // Установить класс в качестве сервиса и если произошло изменение сервисов if ($api->setService(IPath::class, $classname)) { // То сгенерировать класс по шаблону $api->addScript(__DIR__ . '/../../../twig/Service/Path.php.twig', [ 'classname' => $classname, 'dataKey' => $this->dataKey ]); } } // Получить HTML код с информацией о генераторе protected function getHtml(ApiHtml $api): string { return "<strong>dataKey</strong>: " . $this->dataKey; } }
<?php {{ classname | namespace }} use Shasoft\Support\File; use Shasoft\SamoyedCMG\Service\IPath; use Shasoft\SamoyedCMG\Service\IRoute; // Пути class {{ classname | class }} implements IPath { // Базовая директория protected string $basepath; // Конструктор public function __construct(protected IRoute $route) { // Базовая директория сайта $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; } // Директория www сервера public function wwwServer(?string $pathX = null): string { $ret = $this->site('www-server/@/'.$this->route->host()); 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 storage(?string $pathX = null): string { $ret = $this->site('storage/{{ dataKey }}'); if (!is_null($pathX)) { $ret .= '/' . $pathX; } return $ret; } }
Посмотреть техническую страницу

Результат генерации класса на станице с технической информацией. Сгенерированный сервис использует стандартный сервис IRoute который генерируется системой и содержит всю информацию по текущему маршруту. Для получения сервиса указываем его в конструкторе сервиса.
ВАЖНО(!): в $api генератора имеется функция setService для привязки сервиса. Эта функция возвращает TRUE если произошло изменение привязки. Т.е. если такой привязки не было или была привязка к другому классу. И только если было изменение, то генерируется код класса. Связано это с тем, что в случае если один из генераторов изменил состояние дерева настроек, то происходит перезапуск всех генераторов так как какой-то генератор может использовать результат работы другого генератора. Поэтому если изменения привязки не было, значит скрипт класса уже был сгенерирован ранее и нет смысла его ещё раз добавлять.
Генератор сервиса работы с шаблонизатором
Сначала определим общий сервис шаблонизатора
// Шаблонизатор interface ITemplate extends IService { // Сгенерировать public function render(string $templateName, array $args = []): string; }
Затем определим сервис работы с Twig просто унаследовав общий сервис шаблонизатора
// Шаблонизатор Twig interface ITwig extends ITemplate {}
Такие сложности нужны чтобы в случае необходимости можно было добавить в сервис уникальные для Twig функции не ломая при этом общий сервис шаблонов.
// Генератор сервиса работы с шаблонизатором Twig https://twig.symfony.com/ class ServiceGeneratorTwig extends CodeGenerator { // Все пространстава имён protected array $namespaces = []; // Конструктор public function __construct() { parent::__construct(); } // Получить HTML код с информацией о генераторе protected function getHtml(ApiHtml $api): string { $ret = ''; if (!empty($this->namespaces)) { $ret .= '<ul>'; foreach ($this->namespaces as $namespace => $filepath) { $ret .= '<li><strong>' . $namespace . '</strong>: ' . $api->relSite($filepath) . '</li>'; } $ret .= '</ul>'; } return $ret; } // Генерация protected function onGenerate(ApiGenerate $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; } }
<?php {{ 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); } }
Генератор шаблонизатора работает аналогично генератору путей, но есть отличие. Оно заключается в наличии методов настройки.
// Добавить пространство имён public function addNamespace(string $name, string $path): static; // Добавить пространства имён public function addNamespaces(array $namespaces): static;
С помощью этих методов в генератор добавляются пространства имен с шаблонами через функцию дерева настроек tune. Функция в качестве параметра принимает замыкание с параметром настраиваемого генератора:
//-- Добавить шаблоны $node->tune(function (ServiceGeneratorTwig $twig) { // Добавить пространство имён main И папку с шаблонами $twig->addNamespace('main', __DIR__ . '/../@twig'); });
ВАЖНО(!): в функциях настройки требуется обязательно вызывать функцию setModify для сообщения системе об изменениях. Как следствие: проверяйте, а было ли реальное изменение настроек или это просто повторный вызов функции с теми же параметрами.
ссылка на оригинал статьи https://habr.com/ru/post/720528/
Добавить комментарий