Samoyed CMG — пользовательские генераторы кодовой базы

от автора

В прошлой статье был описан процесс установки и запуска Samoyed CMG (Content Management Generator). Основная идея — генерация кода сайта на основе настроек заданных кодом. Т.е. фактически кэширование всех настроек в коде при генерации, а не при развертывании на хостинге.

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

Рассмотрим генераторы более подробно для понимания их работы.

Введение

Сервис в понятии Samoyed CMG состоит из двух компонентов:

  1. Интерфейс с определением функций сервиса.
  2. Класс который реализует сервис.

В настройках генерации сайта происходит привязка интерфейса к конкретному сервису с помощью функции:

// Установить сервис 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;     } }
Информация которая возвращается getHtml на технической странице

Результат генерации класса на станице с технической информацией. Сгенерированный сервис использует стандартный сервис 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/


Комментарии

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

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