Skip to content

ТЗ — Свойства/характеристики (cms/commerce-attributes)

Слой: 🟡 функц-модуль · Зрелость доноров: ★★ · Донор: catalog (имя проекта, путь не выдан) Статус: ТЗ к разработке

Назначение и возможности

Типизированные свойства товаров и торговых предложений с наборами per-категория и флагами фильтруемости/сравнимости. Разделение важно для доменной модели: свойства товара (материал, бренд) — на Product, свойства-опции ТП (размер, цвет) — на Variant (см. /cms-v2/commerce-model).

  • Типы свойств: строка, число, список (select/multiselect), булево, единицы измерения
  • Наборы свойств per-категория (набор атрибутов зависит от категории товара)
  • Разделение: свойства товара vs свойства-опции ТП (размер/цвет — на ТП, не на товаре)
  • Флаг «фильтруемо» — участвует в cms/commerce-facets
  • Флаг «сравнимо» — участвует в таблице сравнения cms/commerce-wishlist
  • JSONB-хранение значений на товаре/ТП с GIN-индексом под фильтрацию
  • Единицы измерения (кг, см, л) со справочником групп и конвертацией отображения

Зависимости и выключение

requires: cms/commerce-catalog · suggests: cms/commerce-facets, cms/commerce-wishlist

Поведение при выключении: карточка и листинг показывают только базовые поля товара без дополнительных характеристик — деградация витрины без поломки каталога.

Модель данных

ТаблицаКлючевые поляПримечание
commerce_attributesid, code, type, unit_id (nullable), is_filterable, is_comparable, lock_versionсправочник свойств; type — PHP Enum (string|number|select|multiselect|bool); тип неизменяем после первого записанного значения (см. «Крайние случаи»); lock_version — optimistic lock
commerce_measurement_unitsid, code, title, unit_group, ratio_to_baseсправочник единиц измерения; unit_group — PHP Enum (weight|length|volume|area|…); конвертация отображения — по ratio_to_base только внутри одной группы
commerce_attribute_setsid, category_id, attribute_id, sort, lock_versionнабор свойств per-категория; FK category_id/attribute_idconstrained()->index(); lock_version — optimistic lock на конкурентную правку набора
commerce_attribute_optionsid, attribute_id, value, sortсправочник значений для type ∈ {select,multiselect}; FK attribute_idconstrained()->index()
commerce_product_attribute_valuesproduct_id, attribute_id, value (jsonb)значения свойств товара, GIN по value; для select/multiselect — ссылка на commerce_attribute_options.id внутри value
commerce_variant_attribute_valuesvariant_id, attribute_id, value (jsonb)значения свойств-опций ТП, GIN по value

ПДн-паспорт. Модуль не хранит персональных данных — атрибуты и их значения описывают товар/ТП, не субъекта; участие в «выгрузить/забыть по субъекту» не требуется.

Входные и выходные данные

Входы:

ИсточникПоляЧем валидируется
Форма атрибута (POST/PUT /api/v1/admin/commerce-attributes)code, type, unit_id?, is_filterable, is_comparable, options[]?FormRequest whitelist; type неизменяем при наличии значений (422 attribute_type_locked); options[] допустимы только при type ∈ {select,multiselect}; unit_id допустим только при type = number
Набор атрибутов категории (POST/PUT .../commerce-attributes/sets)category_id, attribute_id, sortFormRequest whitelist; лимит max_per_category; конфликт правки — lock_version
Значение атрибута товара/ТП (пишется сервисом cms/commerce-catalog при сохранении товара/ТП, отдельного публичного эндпоинта записи нет)attribute_id, valueвалидация по type атрибута: string — экранирование/санитизация без сырого HTML (см. «Безопасность»), number — числовой формат + единица группы, select/multiselect — существующий commerce_attribute_options.id, booltrue/false
Импорт значений (cms:commerce-attributes:import-legacy --source=<профиль>)построчный маппинг «атрибут → товар/ТП» из донорасхема профиля; батчевый upsert; --dry-run с отчётом расхождений

Слушаемых событий нет — модуль не подписывается на чужую шину (см. «События и обмен»). Всё, что не перечислено выше, отвергается (whitelist-принцип §11 стандарта): неизвестные поля атрибута, значение не по type, option_id вне справочника атрибута.

Выходы:

ПотребительДанныеФормат
Публичный набор атрибутов категории (GET /api/v1/catalog/categories/{slug}/attributes)атрибуты набора + справочник единицREST-конверт {data:[{attribute, options?, unit}], meta}
cms/commerce-facetsфлаг is_filterable, значения фильтруемых атрибутовсобытия AttributeFilterableChanged, AttributeDeleted, AttributeValueChanged
cms/commerce-wishlistфлаг is_comparable, значения сравнимых атрибутовсобытие AttributeDeleted (убрать колонку), прямое чтение значений при построении таблицы сравнения
Filament (админка)справочник атрибутов, конструктор набора per-категория, справочник единицadmin-view
cms/commerce-catalogподтверждение записи значения (успех/ошибка валидации по типу)ответ сервис-вызова

Настройки (группа commerce-attributes)

КлючТипДефолтaffectsPageCacheОписание
commerce-attributes.max_per_categoryint50нетЛимит атрибутов в наборе категории
commerce-attributes.cache_ttlint3600даTTL кеша справочника атрибутов и наборов
commerce-attributes.max_options_per_attributeint200нетЛимит значений справочника для select/multiselect
commerce-attributes.max_string_value_lengthint500нетЛимит длины строкового значения атрибута
commerce-attributes.default_unit_systemstringmetricдаСистема единиц отображения (metric/imperial)
commerce-attributes.facet_sync_enabledbooltrueнетKill-switch: аварийная остановка событийной синхронизации с cms/commerce-facets без выключения модуля

Достижение лимитов (max_per_category, max_options_per_attribute, max_string_value_length) отдаёт понятную ошибку 422 с кодом, не тихое обрезание списка/строки.

API

МетодПутьДоступНазначение
GET/api/v1/catalog/categories/{slug}/attributespublicНабор свойств категории для карточки/фильтра
GET/api/v1/admin/commerce-attributesadmin (commerce-attributes.view)Справочник свойств
POST/api/v1/admin/commerce-attributesadmin (commerce-attributes.manage)Создание/правка свойства и набора
DELETE/api/v1/admin/commerce-attributes/{id}admin (commerce-attributes.manage)Удаление атрибута из справочника (эмитит AttributeDeleted)
GET/api/v1/admin/commerce-attributes/unitsadmin (commerce-attributes.view)Справочник единиц измерения и групп

Компоненты

Filament: справочник свойств с типами и единицами измерения, конструктор набора per-категория (drag-n-drop сортировки), редактор справочника единиц измерения. Команды: cms:commerce-attributes:sync-facets --json, cms:commerce-attributes:doctor --json.

Демо-контент: сидер демо-категории с полным набором типов атрибутов (строка, число с единицей, select, multiselect, bool) — галерея /_gallery показывает карточку с характеристиками без ручного ввода. Фронтенд-бюджет: таблица характеристик на карточке рендерится без дополнительного JS-чанка (SSR/Blade-фрагмент), фасетный виджет — на стороне cms/commerce-facets (lazy-load там же); таблица характеристик доступна для скринридеров (семантическая <table> или <dl>, не div-сетка).

События и обмен

СобытиеКогдаPayload
AttributeValueChangedизменено значение свойства товара/ТПentity_type, entity_id, attribute_id
AttributeSetChangedизменён набор свойств категорииcategory_id
AttributeFilterableChangedизменён флаг is_filterable атрибутаattribute_id, is_filterable
AttributeDeletedатрибут удалён из справочникаattribute_id, code

Слушает: — (модуль не подписан на чужие события).

Provides-контракты: не предоставляет. FilterBus: не использует напрямую; AttributeValueChanged, AttributeFilterableChanged и AttributeDeleted триггерят пересчёт/удаление фасетов cms/commerce-facets событийно, без прямого вызова.

Таблица взаимодействий:

Сущность/модульКаналНаправлениеЧто происходит
cms/commerce-catalogпрямой сервис-вызов по requiresattributes → cms/commerce-catalogпроверка существования товара/ТП перед записью значения через публичный сервис каталога
cms/commerce-facetsсобытие (шина)attributes → cms/commerce-facetsAttributeValueChanged/AttributeFilterableChanged/AttributeDeleted триггерят пересчёт или удаление фасетной конфигурации
cms/commerce-wishlistсобытие (шина)attributes → cms/commerce-wishlistAttributeDeleted убирает колонку сравнения; таблица сравнения читает значения is_comparable-атрибутов напрямую
cms/search (косвенно, через cms/commerce-catalog)событие (шина)attributes → cms/searchзначения фильтруемых атрибутов участвуют в поисковом индексе через переиндексацию каталога, не напрямую
Filament (админка)внутренний рендерконструктор набора атрибутов per-категория, справочник единиц измерения

Фоновая работа

Значения атрибутов пишутся синхронно в рамках сохранения товара/ТП в cms/commerce-catalog. Фоновая работа — только у синхронизации и импорта:

  • sync-facets — джоба по событиям AttributeFilterableChanged/AttributeDeleted/ AttributeValueChanged, не синхронно в запросе (не блокирует сохранение атрибута); пропускается целиком при facet_sync_enabled=false;
  • import-legacy — батчевый upsert значений через очередь commerce-attributes, идемпотентен по external_id на связке «атрибут + сущность».

Эксплуатация (ранбук). Метрики: темп записи значений атрибутов, отставание очереди sync-facets, число значений, ссылающихся на несуществующий commerce_attribute_options.id (orphaned reference). Алерты: отставание sync-facets выше порога, max_options_per_attribute близок к исчерпанию у активно используемого атрибута.

СимптомЧто проверитьКоманда
Фасет не обновился после смены атрибутаочередь commerce-attributes отстаёт или facet_sync_enabled=falsecms:commerce-attributes:sync-facets --json
Значения атрибута расходятся со справочником опцийorphaned-ссылки на удалённые commerce_attribute_optionscms:commerce-attributes:doctor --json
Импорт значений завис/частично применилсяпострочные ошибки джобы импортаcms:commerce-attributes:import-legacy --source=<профиль> --dry-run

Бэкап/рестор: таблицы модуля (commerce_attributes, commerce_measurement_units, commerce_attribute_sets, commerce_attribute_options, значения) — целиком в бэкап. После рестора ссылки значений на commerce_attribute_options проверяются cms:commerce-attributes:doctor --json; денормализованные фасетные агрегации cms/commerce-facets пересчитываются отдельно на стороне того модуля, не восстанавливаются этим бэкапом.

Производительность и кеш

Ожидаемые объёмы: миллионы строк в commerce_product_attribute_values/ commerce_variant_attribute_values (по числу атрибутов на товар/ТП — см. commerce-model, «Highload-требования»). Горячий путь — сборка набора атрибутов категории и значений текущего товара для карточки/фильтра, бюджет ≤ 2 запроса (кешированный набор категории + значения товара/ТП одним select). Настройки читаются из кеша группы — 0 запросов на горячем пути.

Индексы: GIN по value в обеих value-таблицах (обязателен под фильтрацию JSONB); уникальный составной (product_id, attribute_id) и (variant_id, attribute_id) — не дублируем значение атрибута на сущности; индекс (category_id, attribute_id) в commerce_attribute_sets.

Тег commerce-attributes:category:{id} на набор атрибутов категории. Инвалидация — событием AttributeSetChanged, TTL справочника — commerce-attributes.cache_ttl. Массовая правка/импорт значений — батчевая инвалидация по категории чанками (принцип import_batch_sizecms/commerce-catalog), не поштучно на товар.

Безопасность

Вход — FormRequest-whitelist на создание/правку свойства и набора; значения JSONB валидируются по типу атрибута до записи, а не постфактум. Строковые значения по умолчанию — plain text: любой HTML экранируется на вывод (); если инсталляция допускает форматированный текст в атрибуте (например, описание материала с разметкой), запись обязана пройти двойной барьер санитизации rich-text ядра (§11 стандарта: санитайзер на входе + автоэскейп на выходе) — сырой непровалидированный HTML в значении атрибута запрещён категорически (XSS-риск при выводе на публичной карточке). Rate-limit — на публичный GET набора атрибутов категории. Конкурентная правка набора атрибутов категории — lock_version (optimistic lock), конфликт — 409 version_conflict.

ПДн-паспорт — см. «Модель данных» (модуль не хранит персональных данных).

Матрица ролей:

ДействиеРедакторМенеджер (commerce-attributes.manage)Админstudio
Просмотр справочника атрибутов и наборов
Создание/правка атрибута (без смены типа при значениях)
Смена типа атрибута до появления значений
Удаление атрибута из справочника
Правка набора атрибутов категории
Запуск import-legacy
Отключение kill-switch facet_sync_enabled

Права: commerce-attributes.view, commerce-attributes.manage.

UX-требования

Для админа: пустая категория без набора атрибутов — подсказка «настроить характеристики», не пустая таблица; конструктор набора — drag-n-drop с подтверждением при удалении атрибута из набора категории (данные значений на товарах не теряются, просто перестают показываться); попытка сменить тип атрибута при существующих значениях — ошибка на человеческом языке («нельзя изменить тип: у атрибута N уже есть M значений — создайте новый атрибут»), не код исключения; удаление атрибута из справочника — отдельное необратимо звучащее действие с явным подтверждением («удалить атрибут и все его значения на N товарах?»).

Для покупателя/посетителя: таблица характеристик на карточке рендерится без задержки (бюджет запросов выше — часть общего рендера карточки, не отдельный AJAX); единицы измерения показываются в системе, привычной аудитории (default_unit_system); таблица характеристик доступна для скринридеров.

Крайние случаи и типовые баги

  • Смена типа атрибута при существующих значениях → запрещена (422 attribute_type_locked); чтобы сменить тип, нужно создать новый атрибут и явно смигрировать/перезаполнить значения — тихого автопереноса между несовместимыми типами (строка → число) нет.
  • Атрибут удалён или помечён нефильтруемымAttributeDeleted/AttributeFilterableChanged уходят на шину; cms/commerce-facets реагирует событийно (пересчитывает/убирает конфиг и агрегации), без polling и без прямого вызова из этого модуля.
  • Единицы измерения разных групп → атрибуту с уже выбранной unit_id из группы weight нельзя присвоить единицу из группы length — конвертация ratio_to_base имеет смысл только внутри одной unit_group, попытка отклоняется валидацией.
  • HTML/произвольный текст в строковом значении → по умолчанию plain text с автоэскейпом на выходе; форматированный текст — только через двойной барьер санитизации rich-text ядра (§11); попытка ввести неразрешённую разметку — понятная ошибка, не тихое обрезание тегов.
  • Массовый импорт значений → батчевый upsert, инвалидация кеша набора — по категориям чанками, не по каждому изменённому значению (лавина инвалидации на миллион строк недопустима).
  • Конкурентная правка набора атрибутов категории двумя админамиlock_version, конфликт — 409 version_conflict с понятным сообщением, не «последний победил» молча.
  • Значение ссылается на удалённую опцию справочника (select/multiselect) → удаление commerce_attribute_options, на которую есть ссылки в значениях, запрещено напрямую — сначала нужно очистить/перенести значения (аналогично запрету удаления категории с товарами в cms/commerce-catalog).
  • cms/commerce-facets выключенAttributeFilterableChanged/AttributeDeleted публикуются как обычно (шина не проверяет наличие подписчиков, fire-and-forget) — это не ошибка, просто событие временно без потребителя.
  • Достижение лимитов (max_per_category, max_options_per_attribute, max_string_value_length) → понятная ошибка 422 с кодом при попытке превысить, не тихое обрезание списка/строки.
  • ⚠️ Противоречие: commerce-model.md декларирует опции ТП (цвет/размер, генерирующие вариант) через движок полей на типе товара, а этот модуль хранит «свойства-опции ТП» в commerce_variant_attribute_values как отдельный механизм — не зафиксировано, дублируют ли они друг друга (одно и то же значение «цвет: красный» одновременно вариант-генерирующая опция и фильтруемый/сравнимый атрибут) или это разные сущности с похожими именами. Разрешение: commerce_variant_options (опции движка полей, порождают ТП) и commerce_variant_attribute_values (атрибуты для фасетов/сравнения) — разные по назначению таблицы; атрибут, дублирующий опцию-генератор (например «цвет»), создаётся с полем, ссылающимся на тот же option_id движка полей, — одно введённое значение обслуживает и генерацию варианта, и фильтр/сравнение, без повторного ввода администратором.

Донорский код

Что взятьПуть
Модель типизированных свойств и наборов per-категорияcatalog (имя проекта, путь не выдан)

Миграция legacy: cms:commerce-attributes:import-legacy --source=<профиль> (донор catalog) — маппинг старых характеристик товаров на новую схему атрибутов/наборов; идемпотентна по external_id на связке «атрибут + сущность», --dry-run с отчётом расхождений (создано/обновлено/пропущено и почему).

Тесты и приёмка

  • [ ] Контрактный тест: свойства-опции ТП (размер/цвет) хранятся отдельно от свойств товара
  • [ ] Смена типа атрибута при существующих значениях отклоняется с кодом attribute_type_locked
  • [ ] AttributeFilterableChanged/AttributeDeleted триггерят пересчёт/удаление фасетов cms/commerce-facets без прямой связки в коде
  • [ ] Единицы измерения конвертируются только внутри одной unit_group, кросс-групповая единица отклоняется
  • [ ] Строковое значение с сырым HTML не проходит без двойного барьера санитизации (contract-тест на XSS-пейлоад)
  • [ ] Удаление опции справочника, на которую есть ссылки в значениях, блокируется
  • [ ] Конкурентная правка набора атрибутов категории отдаёт 409 version_conflict
  • [ ] GIN-индекс на value используется при фильтрации по JSONB (EXPLAIN подтверждает Index Scan)
  • [ ] Набор атрибутов категории кешируется, инвалидация — по тегу категории, батчево при импорте
  • [ ] При выключении модуля карточка/листинг не падают, показывают базовые поля без атрибутов
  • [ ] Достижение лимитов (max_per_category, max_options_per_attribute, max_string_value_length) отдаёт 422 с понятным кодом
  • [ ] Права commerce-attributes.view/.manage и матрица ролей разграничивают чтение, правку и удаление справочника
  • [ ] Контрактный набор cms-testing и testbench-изоляция зелёные, feature-тест на каждый роут
  • [ ] Тестовая БД только commerce-attributes_test; migrate:fresh/refresh/reset, db:wipe запрещены

Внутренняя база знаний студии