Skip to content

Actions

Action — PHP-класс, привязанный к блоку. Имеет две роли:

  1. Источник данных для блока. Action выполняется автоматически перед рендером блока, возвращает массив, который становится доступен в Blade-шаблоне как переменная $actions.
  2. HTTP-обработчик. Action можно вызвать через POST/GET /action/{slug} — для submit форм, webhook'ов, cross-domain интеграций.

Контракт BlockActionInterface

php
namespace Templite\Cms\Contracts;

interface BlockActionInterface
{
    public function params(): array;
    public function returns(): array;
    public function handle(array $params, ActionContext $context): array;
    public function csrfEnabled(): bool;
}

params(): array

Описание входных параметров. Используется для:

  • генерации UI настроек action в админке (при привязке к блоку);
  • валидации параметров;
  • описания в MCP-протоколе.

Формат:

php
public function params(): array
{
    return [
        'limit' => [
            'type' => 'number',
            'label' => 'Количество',
            'default' => 6,
        ],
        'page_type' => [
            'type' => 'select',
            'label' => 'Тип страницы',
            'options' => 'page_types',
            'required' => true,
        ],
    ];
}

returns(): array

Описание возвращаемых данных. Используется для:

  • подсказок доступных переменных в редакторе кода блока;
  • документации в MCP-протоколе;
  • автодополнения в CodeMirror.
php
public function returns(): array
{
    return [
        'pages' => [
            'type' => 'Collection<Page>',
            'description' => 'Коллекция страниц',
        ],
        'total' => [
            'type' => 'int',
            'description' => 'Общее количество',
        ],
    ];
}

handle(array $params, ActionContext $context): array

Выполнение action. Возвращает данные для шаблона блока.

$params — параметры, заданные при привязке к блоку (с учётом page-level override). $context — объект ActionContext с полным контекстом запроса.

csrfEnabled(): bool

Включает CSRF-проверку при HTTP-вызове через /action/{slug}. По умолчанию обычно true. Отключайте для webhook'ов и cross-domain fetch — при выключенном CSRF действует throttle + honeypot.


ActionContext

Объект-контейнер, передаваемый в handle() вторым аргументом:

php
namespace Templite\Cms\Contracts;

class ActionContext
{
    public function __construct(
        public Page $page,       // Текущая страница
        public Request $request, // HTTP-запрос
        public array $global,    // Глобальные настройки (key => value)
        public array $blockData, // Resolved-значения полей текущего блока
    ) {}
}

В $page — модель страницы со всеми связями. В $request — query/post-параметры (?page=2, данные формы). В $global — массив значений глобальных настроек. В $blockData — значения полей блока, уже прошедшие через BlockDataResolver.


Структура Action

В БД (таблица actions)

КолонкаНазначение
slugУникальный идентификатор
nameОтображаемое название
class_nameПолный namespace класса (для файловых actions из app/)
sourcedatabase / file / vendor
paramsJSON со схемой параметров (зеркало params())
returnsJSON со схемой возврата (зеркало returns())
descriptionОписание для UI
code_hashSHA-256 хеш файла кода — для контроля целостности
screenFK на files — скриншот для UI
allow_httpBoolean — разрешать HTTP-вызовы через /action/{slug}

На диске

Действие — это один PHP-файл с классом-имплементацией BlockActionInterface:

  • app/Actions/<ClassName>.php — с PHP namespace
  • storage/cms/actions/<ClassName>.php — без namespace (загружается как standalone)

Источники (ActionRegistry)

Реестр поддерживает три источника с приоритетом app > storage > vendor.

ИсточникПутьФормат
appapp/Actions/<ClassName>.phpPHP-класс с namespace App\Actions
storagestorage/cms/actions/<ClassName>.phpPHP-файл без namespace
vendorРегистрация через ServiceProvider пакетаЛюбой PHP-класс

Пример

PHP-класс app/Actions/LatestArticles.php:

php
<?php

namespace App\Actions;

use Templite\Cms\Contracts\ActionContext;
use Templite\Cms\Contracts\BlockActionInterface;
use Templite\Cms\Models\Page;

class LatestArticles implements BlockActionInterface
{
    public function params(): array
    {
        return [
            'limit' => [
                'type' => 'number',
                'label' => 'Количество статей',
                'default' => 6,
            ],
        ];
    }

    public function returns(): array
    {
        return [
            'articles' => [
                'type' => 'Collection<Page>',
                'description' => 'Список свежих статей',
            ],
        ];
    }

    public function handle(array $params, ActionContext $context): array
    {
        $limit = (int) ($params['limit'] ?? 6);

        return [
            'articles' => Page::published()
                ->whereHas('pageType', fn ($q) => $q->where('slug', 'article'))
                ->latest()
                ->take($limit)
                ->get(),
        ];
    }

    public function csrfEnabled(): bool
    {
        return true;
    }
}

В шаблоне блока (storage/cms/blocks/<slug>/template.blade.php):

blade
<section class="latest">
    @foreach ($actions['articles'] ?? [] as $article)
        <article>
            <a href="{{ $article->url }}">
                <h3>{{ $article->title }}</h3>
            </a>
        </article>
    @endforeach
</section>

После привязки action LatestArticles к блоку и установки limit = 6 через UI — переменная $actions['articles'] появится в шаблоне с коллекцией свежих статей.


Привязка action к блоку

Связь хранится в таблице block_actions:

КолонкаНазначение
block_idFK на блок
action_idFK на action
paramsJSON со значениями параметров (с учётом схемы из params())
orderПорядок выполнения

Дополнительно — page_blocks.action_params (JSON) для page-level override параметров: один и тот же блок на разных страницах может вызывать action с разными параметрами.

В UI: страница блока → вкладка «Actions» → выбрать action → задать параметры.


HTTP-вызовы

Templite регистрирует роут /action/{actionSlug} (GET и POST), который позволяет вызвать action напрямую — без рендера блока.

Используется для:

  • submit форм (через <form action="/action/send-contact-form" method="POST">);
  • webhook'ов от внешних сервисов;
  • AJAX-вызовов из JS на сайте.

Защита:

  • CSRF — управляется методом csrfEnabled(). По умолчанию обычно включён для POST.
  • Throttle — стандартный Laravel rate limiter.
  • Honeypot — встроенный антибот-механизм (есть компонент <x-cms::honeypot> для встраивания в формы).
  • allow_http — общая разрешённость HTTP-вызова на уровне записи actions. Если false/action/{slug} вернёт 404.

Также есть отдельный роут /cms/action/{blockSlug} для admin-операций со страницами блоков.


Защита целостности кода

ActionCodeValidator проверяет код при каждой загрузке:

  1. Хеш файла. Текущий SHA-256 файла сравнивается с actions.code_hash в БД. Если файл изменён вне CMS — ActionRegistry::resolve() возвращает null, нарушение логируется в security-канал.
  2. Токенизация. Запрещённые PHP-конструкции (например, eval, прямой system()/exec()) отлавливаются ещё до загрузки класса.

Это защита от подмены файлов на диске между деплоями.


Создание action

Через UI

  1. Меню ActionsСоздать.
  2. Указать slug, имя, описание.
  3. Описать params и returns через UI-редактор схемы.
  4. Написать PHP-код в встроенном редакторе кода (CodeMirror с PHP-подсветкой).
  5. Сохранить.

Файл попадает в storage/cms/actions/<ClassName>.php. code_hash рассчитывается автоматически.

Через cms:make-action

bash
docker exec templite-app php artisan cms:make-action LatestArticles --storage

С флагом --storage создаёт storage/cms/actions/LatestArticles.php — runtime найдёт.

Без --storage команда создаёт app/Cms/Actions/LatestArticles.phpActionRegistry сканирует app/Actions/, не app/Cms/Actions/. Файл нужно перенести либо использовать --storage.

Через файловую систему

Рекомендованный способ для кода под Git: положить app/Actions/<ClassName>.php (с namespace App\Actions) — ActionRegistry::scanAppActions() подхватит автоматически.

Дополнительно нужна запись в БД (через UI «Actions» или MCP create_action) — она сопоставляет slug со class_name и хранит params/returns/description.


Импорт и экспорт

Action экспортируется в ZIP вместе с PHP-файлом кода:

  • JSON-описание (slug, name, params, returns, description, allow_http)
  • <slug>.php — файл с PHP-классом

При импорте Action::fromImportArray() создаёт запись, importCodeFromZip() восстанавливает файл в storage/cms/actions/.


Подводные камни

Подводные камни

  • Action выполняется синхронно перед каждым рендером блока (с учётом кэша). Тяжёлые запросы/обращения к внешним API будут тормозить страницу. Для долгих задач — Laravel Queue: dispatch(new HeavyJob(...)) внутри handle(), а результат сохранять отдельно и читать из кэша/БД.
  • Результат action кэшируется вместе с HTML блока (если page_blocks.cache_enabled = true). Если данные часто меняются — отключите кэш блока на странице.
  • Файл вне CMS не загрузится, если code_hash в БД не совпадает с реальным. После правки файла через IDE — обновите хеш через UI («Сохранить» в админке) или MCP update_action.
  • cms:make-action без --storage создаёт app/Cms/Actions/<ClassName>.php — runtime не найдёт. Используйте --storage или кладите файл вручную в app/Actions/.
  • HTTP-вызов /action/{slug} требует allow_http = true в БД, иначе 404. Дополнительно метод csrfEnabled() контролирует CSRF-проверку.
  • Параметры action имеют три уровня переопределения: дефолты из params() → значения при привязке к блоку (block_actions.params) → page-level override (page_blocks.action_params). При выполнении они мержатся именно в этом порядке.
  • В шаблоне блока используйте $actions['key'] ?? default — если action не выполнился (например, ошибка в handle()), ключа в $actions может не быть. В отличие от $fields, переменная $actions — обычный массив, не FieldsBag.

Связанные разделы

  • Блоки — где $actions используется в шаблоне
  • Концепции — общий обзор архитектуры
  • REST API — другие HTTP-эндпоинты CMS
  • Поля$fields рядом с $actions в шаблоне блока

Распространяется под лицензией MIT.