Как стать автором
Обновить

CMS, CMF … CMG (Content management generator)

Уровень сложностиПростой
Время на прочтение27 мин
Количество просмотров1.8K

Основная идея 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;
    }
}

Заключение


Данная статья написана для структурирования своих мыслей. Зафиксировать уже реализованное и попытаться понять что я упустил. Следующий этап — довести разработку до состояния когда можно будет сгенерировать сайт и выложить его на хостинге.


Добавление по результатам комментариев


В комментариях очень точно описали процесс работы — генерирование кода сайта на основе PHP кода конфига. На мой взгляд это удобнее, чем конфиг в виде PHP массива (как минимум есть подсказки в виде типов данных для классов настройки).
Также указали на то что современные фреймворки генерируют в КЕШе код для исключения вызовов Reflection API. Т.е. тут я иду в "ногу со всеми современными фреймворками". Однако у меня упор делается на то, что можно не просто закешировать процесс создания сервиса, а можно сам сервис сгенерировать динамически. В качестве примера (он есть выше) я привожу сервис шаблонизатора ITwig. В настройках задаётся список пространств имен и папки шаблонов, после чего генерируется процесс создания сервиса + сам класс реализации сервиса в который прописываются эти самые шаблоны. Т.е. список шаблонов хранится не где-то в настройках, а создаётся статический класс со ссылками на эти шаблоны.

Теги:
Хабы:
Всего голосов 6: ↑3 и ↓30
Комментарии7

Публикации

Истории

Работа

PHP программист
110 вакансий

Ближайшие события

19 сентября
CDI Conf 2024
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн