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

Комментарии (0)