EE_FrameWork использует гибкую контентную модель, где структура описывается не жёсткими колонками одной таблицы, а цепочкой типов, свойств, наборов и сущностей.
Этот раздел нужен, чтобы быстро понять:
- где хранится структура;
- где хранятся реальные значения;
- как связаны категории, страницы, пользователи и свойства;
- какие части модели обеспечиваются БД, а какие кодом.
Контентная ось проекта выглядит так:
property types
-> properties
-> property set to properties
-> property sets
-> category type to property set
-> categories types
-> categories
-> pages
property sets
-> user role to property set
-> user roles
-> users
property values хранят реальные значения для categories, pages и users
Если говорить языком таблиц:
ee_property_types
-> ee_properties
-> ee_property_set_to_properties
-> ee_property_sets
-> ee_category_type_to_property_set
-> ee_categories_types
-> ee_categories
-> ee_pages
ee_property_sets
-> ee_user_role_to_property_set
-> ee_user_roles
-> ee_users
ee_property_values
Это не полный шаблон свойства. Здесь хранится только структура полей типа свойства:
uidtype
Пример:
[
{ "uid": "pf_a1b2c3", "type": "text" },
{ "uid": "pf_d4e5f6", "type": "textarea" }
]Это уже прикладной шаблон конкретного свойства.
Здесь лежат:
uidtypelabeltitledefaultrequiredmultiple
То есть именно property определяет, как выглядит поле для конкретного сценария.
Это реальные данные конкретной сущности.
Здесь структура почти та же, но вместо default используется value.
Именно эта таблица хранит значения категории, страницы или пользователя.
Поле date-range хранит интервал дат как объект с двумя строковыми границами:
{
"uid": "pf_period",
"type": "date-range",
"value": {
"from": "01.06",
"to": "15.06"
}
}Для repeatable/composite-свойств value становится массивом таких объектов. Индекс элемента должен совпадать с индексами остальных fields этого же repeatable-свойства:
{
"uid": "pf_period",
"type": "date-range",
"value": [
{ "from": "01.06", "to": "15.06" },
{ "from": "16.06", "to": "30.06" }
]
}Ядро нормализует этот формат через единый property contract. Пустой интервал сохраняется как { "from": "", "to": "" } или отбрасывается там, где не требуется сохранять позицию repeatable-элемента.
date-range считается поисковым полем: from и to попадают в поисковый текст сущности. В фильтры этот тип не попадает автоматически, потому что корректный фильтр по датам должен проверять пересечение интервалов, а не сравнивать одно скалярное значение.
repeatable-group нужен для случаев, когда одно поле свойства должно хранить несколько строк, а каждая строка состоит из нескольких связанных значений. Типичный пример — набор периодов и цен, где один период связан ровно с одной ценой.
В ee_property_types.fields такой field описывается только верхним uid/type:
[
{ "uid": "pf_prices", "type": "repeatable-group" }
]Полная схема дочерних полей хранится в ee_properties.default_values:
{
"uid": "pf_prices",
"type": "repeatable-group",
"label": "Prices",
"fields": [
{
"uid": "period",
"type": "date-range",
"label": "Period",
"default": { "from": "", "to": "" }
},
{
"uid": "price",
"type": "number",
"label": "Price",
"default": ""
}
],
"default": []
}Живое значение не-repeatable свойства хранится как массив строк:
{
"uid": "pf_prices",
"type": "repeatable-group",
"value": [
{
"values": {
"period": { "from": "01.06", "to": "15.06" },
"price": "2500"
}
}
]
}Если repeatable-group находится внутри repeatable/composite-свойства, появляется два уровня массивов:
- внешний массив соответствует индексу основного повторяемого элемента;
- внутренний массив содержит строки группы для этого элемента.
{
"uid": "pf_prices",
"type": "repeatable-group",
"value": [
[
{
"values": {
"period": { "from": "01.06", "to": "15.06" },
"price": "2500"
}
}
],
[
{
"values": {
"period": { "from": "16.06", "to": "30.06" },
"price": "3000"
}
}
]
]
}Ограничения ядра:
- дочерние поля группы не могут быть
repeatable-group,fileилиimage; - дочерние choice-поля должны иметь собственный
options/defaultв схеме группы; - пустые строки группы отбрасываются при нормализации;
- индексы строк не считаются стабильными идентификаторами.
repeatable-group участвует в поисковом индексе через flatten вложенных значений. В ee_filters этот тип не попадает даже для выбора: обычные materialized filters работают только с плоскими field values. Фильтры по вложенным датам, ценам и пересечению периодов должны реализовываться отдельным nested-filter контрактом поверх property layer.
Ни ee_property_types, ни ee_properties, ни ee_property_sets не являются хранилищем живых значений.
Они описывают:
- структуру;
- шаблон;
- привязки.
Живые значения лежат только в ee_property_values.
Свойство имеет entity_type:
categorypageuserall
Правило такое:
categoryприменяется только к категориям;pageприменяется только к страницам;userприменяется только к пользователям;allприменяется ко всем поддерживаемым сущностям, для которых набор свойств назначен через соответствующий механизм.
Если lifecycle меняет entity_type, система должна:
- удалить лишние
property_values; - создать недостающие;
- нормализовать существующие.
Важно: all не означает, что свойство появится вообще у каждой записи в системе. Сначала набор должен быть назначен сущности:
- для категорий и страниц — через тип категории и наследование типов;
- для пользователей — через роль пользователя.
Для карточек объектов размещения можно использовать отдельный внутренний набор свойств, который не участвует в публичном контенте напрямую.
Текущий рабочий пример:
Статус карточкиРежим размещенияРазмещение активно доКонтакты провереныДата последней проверкиСледующая перепроверкаИсточник карточкиВнутренний комментарий менеджера
Эти свойства хранятся в обычных ee_properties и ee_property_values, но создаются со статусом hidden.
Это важный практический приём:
- свойства остаются доступны в админке;
- свойства не попадают в search/filter слой, который работает только с
activeсвойствами; - структура не требует отдельной таблицы под каждую бизнес-задачу.
Такой набор подходит для:
- публикационного статуса;
- коммерческого режима размещения;
- ручной верификации контактов;
- внутренних комментариев менеджера.
В контентной модели есть два дерева.
Дерево категорий строится через:
ee_categories.parent_id
Дерево страниц строится через:
ee_pages.parent_page_id
Если страница создаётся с parent_page_id, её category_id должен наследоваться от родительской страницы.
У типа категории есть прямые наборы свойств, но эффективный набор считается динамически:
- собственные наборы типа категории;
- плюс наборы всех родителей по
parent_type_id.
То есть унаследованные наборы не должны физически копироваться в таблицу связей.
Пользователи не расширяются новыми колонками под каждую проектную потребность. Для этого используется тот же property layer, что и для контента.
Схема такая:
ee_property_sets -> ee_user_role_to_property_set -> ee_user_roles -> ee_users
Практический workflow:
- Создайте тип свойства, свойство и набор свойств в разделе “Свойства”.
- Укажите у свойства
entity_type = userилиentity_type = all. - Откройте карточку роли пользователя.
- Назначьте роли нужные наборы свойств.
- Откройте карточку пользователя с этой ролью: вкладка “Свойства” покажет поля из назначенных наборов.
Значения хранятся в ee_property_values:
entity_type = user;entity_id = ee_users.user_id;set_idиproperty_idуказывают на набор и свойство;language_codeиспользует default content language, чтобы формат оставался совместим с общей property-моделью.
При полном удалении пользователя из архива его property_values с entity_type=user также удаляются.
При создании категории система должна собрать property_values для всех подходящих свойств её эффективных наборов.
При создании страницы система должна собрать property_values по эффективным наборам её категории.
Именно поэтому изменение:
- типа категории;
- состава набора;
entity_typeсвойства;- структуры типа свойства;
может запускать lifecycle-пересчёт.
Тяжёлые операции в этой модели включают:
- нормализацию
default_values; - нормализацию
property_values; - удаление устаревших значений;
- создание недостающих значений;
- пересборку после изменения состава набора;
- пересборку после изменения наборов типа категории.
Практический вывод:
- контентная модель очень гибкая;
- но за неё нужно платить дисциплиной lifecycle и понятной инвалидацией.
Поля description, short_description и текстовые значения свойств могут хранить rich text. Для них важно разделять две операции:
- нормализация приводит plain text или mixed HTML к ожидаемой структуре абзацев, переносов и ссылок;
- sanitization удаляет небезопасный HTML перед публичным выводом.
Нормализация не заменяет sanitization.
Публичные шаблоны, которые выводят rich text без htmlspecialchars, должны получать HTML уже после DOM allowlist sanitizer. Минимальная политика sanitizer:
- удалить
<script>и активные элементы вродеsvg,iframe,object,embed,form,video,audio; - удалить event-атрибуты вроде
onclick; - удалить URL со схемами
javascript:,data:,vbscript:; - оставить только разрешённые теги и атрибуты;
- применять одинаковую политику ко всем новым rich-text поверхностям.
Важная особенность EE_FrameWork: часть правил живёт физически в схеме БД, а часть обеспечивается приложением.
Поэтому при разработке важно разделять:
- физические
FK/UNIQUE/INDEX; - логические инварианты модели;
- lifecycle-правила, которые не видны из одной только схемы.
Идите сюда, если вы:
- проектируете новый тип контента;
- меняете состав свойств категории или страницы;
- разбираете, почему у сущности появились или не появились
property_values; - работаете с import/lifecycle/search/filter слоями.