В прошлой статье был описан процесс установки и запуска 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
для сообщения системе об изменениях. Как следствие: проверяйте, а было ли реальное изменение настроек или это просто повторный вызов функции с теми же параметрами.