Поля
Поле — типизированный элемент данных блока. Поля описывают, что редактор контента сможет заполнить в админке, и какие значения попадут в Blade-шаблон при рендере.
Где живут поля
Структура полей (тип, имя, key, опции) хранится в таблице block_fields. Конкретные значения, заполненные на странице — в таблице page_blocks.data (JSON). При рендере значения проходят через BlockDataResolver, который подставляет модели вместо ID и оборачивает результат в FieldsBag.
Колонки block_fields
| Поле | Назначение |
|---|---|
key | Идентификатор поля (snake_case) — используется в $fields['key'] |
name | Отображаемое название в админке |
type | Тип поля (один из перечня ниже) |
default_value | Значение по умолчанию (хранится как строка, приводится к типу через FieldValueCaster) |
data | JSON-конфиг тип-специфичных опций (список для select, и т.п.) |
hint | Подсказка для контент-менеджера |
parent_id | Для вложенности (поле внутри array-repeater'а) |
block_tab_id, block_section_id | Привязка к табу и секции в UI |
order | Порядок |
Зарезервированные ключи
Эти имена нельзя использовать как key поля — они конфликтуют с переменными в шаблоне блока или системными именами Blade:
id, type, block, page, data, fields, global, actions,
request, slot, attributes, errorsЗащита проверяется на уровне BlockField::RESERVED_KEYS — попытка создать поле с таким ключом будет отклонена валидатором.
Доступ к значению в Blade
В шаблоне блока значения доступны через переменную $fields, обёрнутую в Templite\Cms\Support\FieldsBag:
{{ $fields['title'] }}Поведение FieldsBag
- Поддерживает
ArrayAccess,IteratorAggregate,Countable— можно использовать как массив. - Для несуществующего ключа возвращает не
null, а пустойFieldsBag([]). Поэтому:{{ $fields['missing'] }}→ пустая строка (через__toString).@foreach ($fields['missing'] as $item)→ 0 итераций без ошибки.count($fields['missing'])→ 0.
- Вложенные массивы (например, элементы repeater'а или структура поля
link) автоматически оборачиваются вFieldsBagпри доступе.
Значения по умолчанию
Если поле пустое и default_value не задан, BlockDataResolver::getDefaultForType() подставляет типизированный дефолт. Шаблон всегда получает корректный тип данных:
| Тип | Дефолт |
|---|---|
array | [] |
checkbox | false |
number | 0 |
link | ['url' => '', 'text' => '', 'target' => '_self'] |
img, file, page, user, category, product, product_option | null |
text, textfield, editor, tiptap, html, color, date, datetime, select, radio | '' |
Если default_value задан в БД — он приводится к типу через Templite\Cms\Support\FieldValueCaster::cast().
Справочник типов
Реальный список — BlockField::FIELD_TYPES. Восемнадцать базовых типов плюс три типа от модуля шопа.
text — Однострочный текст
- Назначение: короткие строки (заголовки, имена, telephone).
- Хранение: строка в
page_blocks.data. - В шаблоне: строка.
<h2>{{ $fields['title'] }}</h2>textfield — Многострочный текст
- Назначение: описания, адреса, длинные тексты без форматирования.
- Хранение: строка с переводами строк.
- В шаблоне: строка. Для сохранения переносов —
nl2br():
{!! nl2br(e($fields['description'])) !!}number — Число
- Назначение: цены, количество, индексы.
- Хранение: строка (приводится к int/float).
- В шаблоне: число (или строка с числом).
{{ number_format($fields['price'], 0, '.', ' ') }}img — Изображение
- Хранение: ID файла в таблице
files. - В шаблоне: модель
Templite\Cms\Models\File(илиnull). - Резолвер
BlockDataResolverзагружает модель файла за один SQL-запрос на всю страницу (eager-load — нет N+1).
<x-cms::image :file="$fields['hero']" />
<a href="{{ $fields['hero']?->url }}">Прямой URL</a>file — Файл
- Идентично
imgпо структуре хранения и резолва. - Назначение: PDF, DOCX, ZIP — любые файлы, не предназначенные для рендера через
<img>.
@if ($fields['document'])
<a href="{{ $fields['document']->url }}" download>
Скачать {{ $fields['document']->name }}
</a>
@endifeditor — Форматированный текст
- Назначение: статьи, контент с форматированием (исторически — CKEditor 5 в админке).
- Хранение: HTML-строка.
- В шаблоне: HTML — выводить через
{!! !!}, не{{ }}.
{!! $fields['article_body'] !!}tiptap — Tiptap-редактор
- Назначение: альтернативный rich-editor (Tiptap), используется для более структурированного контента и встраивания медиа.
- Хранение: контент в формате Tiptap (HTML или JSON — зависит от настроек поля в
data). - В шаблоне: удобнее всего использовать встроенный компонент:
<x-cms::tiptap :content="$fields['body']" />html — Произвольный HTML
- Назначение: вставка готовых HTML-фрагментов, embed-кодов (видео, iframe).
- Хранение: строка.
- В шаблоне: через
{!! !!}.
{!! $fields['embed_code'] !!}WARNING
Содержимое html не санитизируется. Доверяйте только редакторам с правами blocks.edit. Для пользовательского ввода — отдельная очистка через Purifier или аналог.
select — Выбор из списка
- Назначение: один вариант из заданного множества.
- Опции: в
block_fields.data.options— массив[{value, label}]. - Хранение: строка (выбранное
value). - В шаблоне: строка.
<section class="hero hero--{{ $fields['align'] }}">...</section>checkbox — Чекбокс
- Хранение: boolean.
- В шаблоне: boolean. Дефолт —
false.
@if ($fields['show_cta'])
<x-cms::link :value="$fields['cta_link']">{{ $fields['cta_text'] }}</x-cms::link>
@endifradio — Радио-кнопки
- Идентично
selectпо хранению и резолву, отличается только UI в админке.
link — Ссылка
- Хранение: массив
{url, text, target}(либо строка — нормализуется в массив автоматически). - В шаблоне: массив. Поля доступны как
$fields['cta']['url'],$fields['cta']['text'],$fields['cta']['target']. - Дефолт:
['url' => '', 'text' => '', 'target' => '_self'].
<a href="{{ $fields['cta']['url'] }}" target="{{ $fields['cta']['target'] }}">
{{ $fields['cta']['text'] }}
</a>Или через встроенный компонент:
<x-cms::link :value="$fields['cta']" />date — Дата
- Хранение: строка в формате ISO (YYYY-MM-DD).
- В шаблоне: строка. Форматирование — через Carbon:
{{ \Carbon\Carbon::parse($fields['event_date'])->format('d.m.Y') }}datetime — Дата и время
- Идентично
date, формат включает время.
{{ \Carbon\Carbon::parse($fields['published_at'])->format('d.m.Y H:i') }}array — Повторитель (repeater)
- Назначение: список однотипных элементов (отзывы, преимущества, FAQ-пары).
- Структура: подполя определяются как обычные
BlockFieldсparent_id = <array field id>— вложенные записи в той же таблице. - Хранение: массив массивов.
- В шаблоне: массив, итерируется через
@foreach. Вложенные значения —FieldsBag.
@foreach ($fields['features'] as $feature)
<div class="card">
<x-cms::image :file="$feature['icon']" />
<h3>{{ $feature['title'] }}</h3>
<p>{{ $feature['description'] }}</p>
</div>
@endforeachВнутри array могут быть любые типы (включая ссылки на файлы, страницы, других repeater'ов).
color — Цвет
- Хранение: строка с hex-кодом (
#4480F4) или другим форматом цвета. - В шаблоне: строка.
<div style="background: {{ $fields['accent_color'] }};">...</div>page — Ссылка на страницу CMS
- Хранение: ID страницы из
pages. - В шаблоне: модель
Templite\Cms\Models\Page(илиnull). Резолвится eager-load'ом.
@if ($fields['parent_page'])
<a href="{{ $fields['parent_page']->url }}">
← {{ $fields['parent_page']->title }}
</a>
@endifuser — Ссылка на пользователя
- Хранение: ID пользователя сайта.
- В шаблоне: модель пользователя (или
null). Резолвер регистрируется черезBlockDataResolver::registerType('user', ...)в Service Provider CMS.
Шопные типы (модуль templite/shop)
Если установлен модуль шопа, появляются три дополнительных типа:
| Тип | Назначение |
|---|---|
category | Ссылка на категорию товаров |
product | Ссылка на товар |
product_option | Ссылка на опцию товара |
Все три работают по той же схеме, что и page / user: хранится ID, в шаблон попадает модель.
Без установленного модуля шопа эти типы остаются в списке BlockField::FIELD_TYPES, но резолвер для них не зарегистрирован — значение в шаблоне будет null.
Кастомные типы
Расширить набор типов можно через BlockDataResolver::registerType() — обычно из ServiceProvider пакета или из app/Providers/AppServiceProvider.php.
use Templite\Cms\Services\BlockDataResolver;
use App\Models\Article;
public function boot(BlockDataResolver $resolver): void
{
$resolver->registerType('article', fn ($id) => Article::find($id));
}После регистрации $fields['related_article'] в шаблоне вернёт модель Article или null. Для отображения нового типа в UI добавления полей в админке также нужен соответствующий Vue-компонент-редактор.
Подводные камни
Подводные камни
- Зарезервированные ключи (
id,type,block,page,data,fields,global,actions,request,slot,attributes,errors) — использовать какkeyполя нельзя. imgиfileвозвращают модельFile, не URL. Для прямого URL —$fields['hero']?->url. Чаще удобнее использовать<x-cms::image>.linkвсегда нормализуется в массив{url, text, target}, даже если в БД лежит строка. Не выводите{{ $fields['cta'] }}напрямую —FieldsBag::__toString()вернёт пустую строку. Используйте либо обращение по ключу ($fields['cta']['url']), либо<x-cms::link>.array(repeater) хранит вложенные поля как отдельные записи вblock_fieldsсparent_id. При резолве файлы/страницы/пользователи внутри repeater'а тоже подгружаются eager-load'ом.default_valueв БД — всегда строка. Для типовcheckbox,number,arrayкорректное значение получается черезFieldValueCaster::cast()при загрузке. Не пытайтесь хранить структурированный JSON вdefault_value— для опций типа используйте колонкуdata.FieldsBagдля несуществующего ключа возвращает пустой Bag, а неnull. Конструкция$fields['x'] ?? 'fallback'не сработает —??смотрит наnull, а тут всегда объект. Для проверки наличия —isset($fields['x'])или$fields->offsetExists('x').- Эффективность: все ссылки на файлы, страницы и кастомные сущности резолвятся одним SQL-запросом на тип сразу по всей странице. N+1 не возникает.