Skip to content

[Feature] Тип поля key=value (ms3-key-value) для extra fields #300

Description

@Ibochkarev

Статус (актуализация кодовой базы, 2026-06-22)

Компонент Статус
ms3-repeater (#299, #301) Реализован — эталон для #300
ms3-key-value (#300) Не реализован
dbtype=json + textarea в Extra Fields Можно создать колонку, UI пар ключ/значение нет
Сохранение JSON-объекта через prepareObject() Ломается для не-repeater JSON (см. ниже)

Связанные issues: #299 (repeater, закрыт), #297/#298 (ProductDataPayloadTrait), #301 (whitelist repeater в ProductDataService::updateProductData()).


Описание

Тип extra field ms3-key-value: UI для плоского ассоциативного массива (JSON object / map), пара «ключ → значение».

  • редактирование в менеджере: список пар «Параметр / Значение» (добавить, удалить);
  • хранение: одна JSON-колонка на поле (объект, не массив строк);
  • доступ через «Дополнительные поля» (ms3_extra_fields) и формы товара/заказа;
  • API и сниппеты отдают ассоциативный массив, не JSON-строку.

Проблема

Для flat map (КБЖУ, meta, flat config) repeater из #299 лишний: там схема колонок и JSON массив [{...}, {...}].

Сейчас обходные пути:

  1. json + textarea — колонку создать можно (ExtraFieldsManager: dbtype=json, phptype=json), но нет UI пар, нет валидации ключей, легко сломать JSON.
  2. MODX TV + произвольный JSON — вне extra fields MS3, слабая связь с Manager API.
  3. ms3-repeater — UX тяжелее; модель [{name, value}], не {"calories": "250"}.

Ловушка текущего кода (важно для реализации)

msProductData::getArraysValues() отдаёт все поля с phptype=json в ProductDataService::prepareObject(). Repeater обрабатывается отдельно через RepeaterFieldService::processValue(). Остальные JSON-поля проходят prepareOptionValues() — логика для списков (tags, color, size): array_keys(array_flip(...)) уничтожает ключи объекта.

// msProductData.php — все json-поля попадают сюда
foreach ($this->_fieldMeta as $name => $field) {
    if (strtolower($field['phptype']) === 'json') {
        $arrays[$name] = parent::get($name);
    }
}

Вывод: key-value нельзя выпускать как «просто json + textarea». Нужен отдельный xtype и ветка нормализации, как у repeater.


Сравнение с repeater (#299)

Repeater (ms3-repeater) Key=value (ms3-key-value)
Форма в БД JSON массив JSON объект
UI Таблица строк + схема колонок Пары ключ/значение
Конфиг в ms3_extra_fields repeater_config key_value_config (новая колонка)
Типичный кейс Характеристики, комплектация КБЖУ, meta, flat config
Сложность UI Выше Ниже

Repeater уже задаёт паттерн полного стека — #300 повторяет его, не изобретая новую архитектуру.


Предлагаемое решение

1. Контракт типа

Уровень Значение
dbtype json
phptype json
xtype ms3-key-value

key_value_config (JSON в ms3_extra_fields, аналог repeater_config):

{
  "mode": "fixed",
  "keys": [
    { "key": "calories", "label": "Калории", "valueType": "string" },
    { "key": "protein", "label": "Белки", "valueType": "string" }
  ]
}

mode: "free" — произвольные ключи (Filament KeyValue). MVP: оба режима.

2. Бэкенд (следовать паттерну RepeaterFieldService)

Новый класс MiniShop3\Services\ExtraFields\KeyValueFieldService:

  • XTYPE = 'ms3-key-value';
  • parseConfig / validateConfigSchema / encodeConfig;
  • decodeValue → ассоциативный массив (отклонять JSON-массив);
  • normalizeMap — trim, удаление пар с пустым ключом;
  • validateMap — уникальность ключей, обязательные ключи в fixed, numeric check по valueType;
  • processValue — decode → normalize → validate;
  • getKeyValueFieldsForClass(string $modelClass) — кэш по классу.

Интеграция (те же точки, что у repeater):

Файл Изменение
ServiceRegistry.php регистрация сервиса
ExtraFieldsService.php create/update, msProductField config, очистка key_value_config при смене xtype
ProductDataService.php ветка в prepareObject(); whitelist + normalizeKeyValueFieldsInPayload() в updateProductData() (урок #301)
OrdersController.php нормализация extra fields заказа/адреса
ProductDataPayloadTrait.php без изменений, если payload уже object/array
Phinx migration колонка key_value_config TEXT NULL в ms3_extra_fields
msExtraField mysql model + schema XML поле key_value_config
`lexicon/en ru`

3. Фронт (Vue Manager)

Файл Изменение
vueManager/src/utils/keyValueField.js константа xtype, parse/default config, serialize для POST
vueManager/src/components/KeyValueField.vue UI пар (PrimeVue DataTable или двухколоночный список)
vueManager/src/components/KeyValueSchemaEditor.vue режим fixed/free, список ключей с labels
ExtraFieldsManager.vue xtype в списке, auto dbtype=json, schema editor
DynamicField.vue ветка ms3-key-value + hidden input для legacy MODX form (как repeater, #298/#301)
OrderExtraFieldsSection.vue col-12 для key-value (как repeater)

4. Code-judo (structural)

Не плодить if (xtype === …) в десяти файлах без модели:

  1. MVP: KeyValueFieldService зеркалит RepeaterFieldService — предсказуемо для контрибьюторов MS3.
  2. Follow-up (не MVP): общий trait/helper StructuredJsonFieldServiceTrait для parseConfig/encodeConfig/malformed JSON log — только если после [Feature] Тип поля key=value (ms3-key-value) для extra fields #300 появится третий structured JSON xtype.
  3. Не обобщать repeater + key-value в один «универсальный JSON editor» — разные инварианты (array vs object), общий UI создаст spaghetti.

MVP

  • Extra fields: msProductData, msOrder, msOrderAddress (заказы уже через OrderExtraFieldsSection).
  • Режимы fixed keys (КБЖУ) и free keys.
  • Vue-менеджер, REST API, документация.

Вне MVP: i18n labels из схемы на витрине, generated column + index на JSON path, sort/filter в Grid API.


Примеры

КБЖУ (nutrition)

{
  "calories": "250",
  "protein": "12",
  "fat": "8",
  "carbs": "30"
}

Save через processor (#298)

$response = $modx->runProcessor('MiniShop3\\Processors\\Product\\Create', [
    'pagetitle' => 'Бокс «Весенний каприз» XL',
    'class_key' => \MiniShop3\Model\msProduct::class,
    'parent' => 409,
    'Data' => [
        'price' => 5200,
        'nutrition' => [
            'calories' => '250',
            'protein' => '12',
            'fat' => '8',
            'carbs' => '30',
        ],
    ],
]);

Fenom

<dl class="nutrition">
  <dt>{$_('ms3_nutrition_calories')}</dt><dd>{$nutrition.calories}</dd>
  <dt>{$_('ms3_nutrition_protein')}</dt><dd>{$nutrition.protein} г</dd>
</dl>

SQL (MySQL 5.7+ / MariaDB 10.3+)

WHERE JSON_EXTRACT(ms3_products.nutrition, '$.calories') IS NOT NULL
ORDER BY CAST(JSON_UNQUOTE(JSON_EXTRACT(ms3_products.nutrition, '$.calories')) AS UNSIGNED) ASC

Критерии приёмки

  • В Extra Fields можно создать поле ms3-key-value (fixed и free keys).
  • Phinx-миграция: key_value_config в ms3_extra_fields.
  • KeyValueFieldService: decode/normalize/validate; ошибки с lexicon.
  • prepareObject() не прогоняет key-value через prepareOptionValues().
  • ProductDataService::updateProductData() включает key-value keys в whitelist (regression guard как feat(extra-fields): repeater field type (ms3-repeater) #301).
  • DynamicField.vue: hidden input для legacy form POST.
  • Карточка товара и заказа: UI пар; save в JSON-объект.
  • API возвращает ассоциативный массив (xPDO phptype=json).
  • Документация: КБЖУ, Data save, Fenom, JSON_EXTRACT.

Эталон в коде (repeater, уже merged)

core/components/minishop3/src/Services/ExtraFields/RepeaterFieldService.php
core/components/minishop3/migrations/20260524120000_add_repeater_config_to_extra_fields.php
vueManager/src/utils/repeaterField.js
vueManager/src/components/RepeaterField.vue
vueManager/src/components/RepeaterSchemaEditor.vue
vueManager/src/components/ExtraFieldsManager.vue
vueManager/src/components/DynamicField.vue
core/components/minishop3/src/Services/Product/ProductDataService.php
core/components/minishop3/src/Controllers/Api/Manager/OrdersController.php

Альтернативы

Вариант Плюсы Минусы
Repeater (#299) Уже есть Массив вместо object, лишний UX
json + textarea Колонка без кода Нет UX; prepareObject() ломает object
Filament KeyValue Проверенный UX Своя PrimeVue-реализация
Отдельная таблица EAV Нормализация Overkill для MVP

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions