Модели
Третья версия работает на Eloquent. Модели живут в PageBlocks\App\Models\ и достаются по короткому имени хелпером model():
model('PbResource')->published()->limit(10)->get();Почти всё здесь — обычный Eloquent. Кроме одной вещи, и на ней держится весь компонент.
Поля, которых нет в колонках
Конструктор позволяет менеджеру добавить блоку или таблице поле без миграции. Колонки у такого поля нет — оно живёт внутри JSON-колонки. AbstractDataModel делает это незаметным:
$block->subtitle = 'Привет'; // колонки `subtitle` не существует
$block->save();
echo $block->subtitle; // ПриветРаботают два хука:
| Хук | Что происходит |
|---|---|
retrieved | Ключи JSON-колонки, не являющиеся настоящими колонками, разворачиваются в extra-атрибуты |
saving | Extra-атрибуты сворачиваются обратно в JSON-колонку |
Чтение идёт через __get, поэтому extra-атрибут неотличим от колонки и в шаблонах, и в коде. Аксессоры для полей конструктора писать не нужно.
В какую именно JSON-колонку
Зависит от семейства модели:
| Семейство | Базовый класс | JSON-колонка |
|---|---|---|
| Данные — блоки, строки таблиц, ресурсы | AbstractDataModel | data |
| Конструктор — поля, колонки | ConstructorModel | properties |
| Конструктор — блоки, таблицы | ConstructorModel | permissions |
Не присваивайте JSON-колонку целиком
$field->properties = ['foo' => 1] выбрасывает всё, что хук собирался положить обратно, и поле теряет настройки. Задавайте отдельные ключи: $field->foo = 1.
Extra-атрибуты фильтруются конструктором — если он есть
При сохранении extra-атрибуты сверяются с именами полей конструктора, и всё незнакомое отбрасывается. Именно это держит data в чистоте.
Но модель без конструктора сохраняет все свои extra:
$fields = $model->fields()->get()->toArray();
$data = $fields ? array_filter($extra, /* по именам полей */) : $extra;Эта ветка появилась из-за настоящего бага: безусловная фильтрация стирала data при каждом сохранении руками написанной модели — pb_companies терял почту, телефон и адрес на каждом обновлении, потому что сверяться ему не с чем.
Fillable следует за реальной таблицей
Сгенерированные таблицы данных несут только часть базовых колонок. getFillable() пересекает базовый список с колонками, которые у таблицы действительно есть, — схема читается один раз и кэшируется на таблицу.
Без этого пришедший из формы parent_type или context_key дошёл бы до UPDATE и упал с Unknown column.
Мягкое удаление везде
AbstractDataModel и ConstructorModel наследуют SoftModel с трейтом SoftDeletes. Удаление проставляет deleted_at, выборки такие строки не видят. Из этого и восстанавливает корзина.
Окончательное удаление конструктора уносит и его поля, вкладки и колонки: дети привязаны парой model_type + model_id, а не внешним ключом, поэтому на стороне базы каскада нет.
Шаблоны видят extra-атрибуты — благодаря __isset
Fenom компилирует {$obj->prop} в проверку isset(). До __get дело бы не дошло, и каждое поле конструктора выводилось бы пустым. Поэтому AbstractDataModel реализует __isset и сообщает про extra-атрибуты, что они установлены.
Пишете свою модель с магическими свойствами — держите это в голове: баг выглядит как «в PHP значение есть, а в шаблоне пусто».
Морф-классы
Строки привязаны к владельцу парой model_type + model_id. Два семейства пишут model_type по-разному:
| Семейство | model_type |
|---|---|
AbstractDataModel | полное имя класса, например PageBlocks\App\Models\PbBlockData |
ConstructorModel | короткий литерал, например pbBlock, pbTable |
Оба написания встречаются в живых данных по историческим причинам, поэтому код, который сопоставляет model_type, обычно вынужден принимать несколько вариантов. Не «упрощайте» BaseModel::getMorphClass() — на нём держится весь конструктор.