Skip to content

feat(options): per-category caption and description for category-option links#203

Closed
Ibochkarev wants to merge 2 commits intomodx-pro:betafrom
Ibochkarev:feat/issue-200-category-option-caption-description
Closed

feat(options): per-category caption and description for category-option links#203
Ibochkarev wants to merge 2 commits intomodx-pro:betafrom
Ibochkarev:feat/issue-200-category-option-caption-description

Conversation

@Ibochkarev
Copy link
Copy Markdown
Member

Описание

Реализовано переопределение подписи (caption) и описания (description) опции на связи «опция–категория» (ms3_category_options): значения из связи имеют приоритет над глобальными полями ms3_options; пустое значение означает наследование с глобальной опции. Поддержаны витрина (OptionLoaderService, в том числе пакетная подгрузка для loadForProducts), менеджер MODX (ExtJS: грид и окно добавления опции к категории), процессоры и копирование при дублировании категории.

Тип изменений

  • Исправление бага (non-breaking change)
  • Новая функциональность (non-breaking change)
  • Breaking change (изменение, ломающее обратную совместимость)
  • Рефакторинг (без изменения функциональности)
  • Документация
  • Другое (опишите):

Связанные Issues

Closes #200

Как это было протестировано?

Добавлена миграция Phinx и обновлены слой загрузки опций и формы менеджера. Рекомендуется прогнать установку/обновление компонента, применить миграцию и проверить сценарии: категория → опции, отображение подписей на витрине, дублирование категории.

  • Ручное тестирование
  • Автоматические тесты (PHPStan, ESLint)
  • Тестирование на разных версиях PHP/MODX

Конфигурация тестирования:

  • MiniShop3: ветка PR
  • MODX: 3.x
  • PHP: 8.2+

Скриншоты (если применимо)

До После

Чеклист

  • Код соответствует стилю проекта
  • Добавлены/обновлены комментарии в сложных местах
  • Изменения не ломают существующую функциональность
  • Лексиконы добавлены на двух языках (ru/en)
  • PHPStan проходит без новых ошибок
  • ESLint проходит без ошибок (для JS/Vue изменений)
  • Обновлён CHANGELOG.md (для значимых изменений)

Дополнительные заметки

При нескольких категориях для одного товара выбор строки для оверлея: родительская категория товара, затем минимальный position, затем минимальный category_id (см. OptionLoaderService::pickWinningCategoryOptionLink).

@Ibochkarev Ibochkarev changed the title feat(options): per-category caption and description for category-option links (#200) feat(options): per-category caption and description for category-option links Apr 17, 2026
Copy link
Copy Markdown
Member

@biz87 biz87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ревью

Фича полезная, миграция корректная, batch-загрузка для loadForProducts продумана (2 запроса вместо 2N). Но OptionLoaderService вырос на 414 строк — есть вопросы по производительности и дублированию.


1. getCollection вместо getIterator / raw PDO

// loadCategoryOptionLinksGrouped()
$links = $this->xpdo->getCollection(msCategoryOption::class, $c);

getCollection загружает все объекты в память. Для batch-операций с десятками категорий × опций — может быть тяжело. Весь остальной OptionLoaderService использует raw PDO ($c->stmt->fetch). Здесь тоже лучше:

if ($c->prepare() && $c->stmt->execute()) {
    while ($row = $c->stmt->fetch(\PDO::FETCH_ASSOC)) {
        $oid = (int)$row['option_id'];
        $linksByOptionId[$oid][] = $row;
    }
}

2. Service lookup на каждой строке в GetList::prepareRow()

$optSvc = $this->modx->services->get('ms3_option_service');
$loader = $optSvc->getLoader();

Вызывается на каждой строке результата. Сервис-контейнер дёшев, но getLoader() — нет факт. Лучше кэшировать в свойстве процессора (в initialize() или prepareQueryBeforeCount()).

3. Дублирование кода: buildCaptionDescriptionOverlayForProduct и buildCaptionDescriptionOverlayFromRowsAndLinks

Оба метода содержат идентичный блок:

$globalsByOptionId = [];
$keyByOptionId = [];
foreach ($lastRowByOptionKey as $key => $row) {
    $oid = (int)($row['id'] ?? 0);
    if ($oid < 1) continue;
    $globalsByOptionId[$oid] = [
        'caption' => (string)($row['caption'] ?? ''),
        'description' => (string)($row['description'] ?? ''),
    ];
    $keyByOptionId[$oid] = $key;
}

Стоит вынести в приватный хелпер extractGlobalsAndKeyMap($lastRowByOptionKey).

4. getProductCategoriesContext грузит getObject(msProduct) — лишний запрос?

Для loadForProduct (single) вызывается getProductCategoriesContext, который делает getObject(msProduct). Но loadForProduct уже работает в контексте конкретного товара — parent_id можно передать параметром (он известен из getFieldsForProduct).

5. getIteratorgetCollection в getFieldsForProduct

$options = [];
foreach ($this->xpdo->getIterator(msOption::class, $c) as $option) {
    $options[] = $option;
}

Собирать iterator в массив — то же самое что getCollection. Но здесь это нужно для двух проходов (сначала сбор globals, потом обход). Оправдано, просто стоит добавить комментарий почему.

6. Лексиконы дублируются

Одни и те же ключи (ms3_category_option_caption_override и др.) добавлены в оба файла: default.inc.php и manager.inc.php. Нужно только в одном — в manager.inc.php (они используются только в админке ExtJS).

7. Settings/Option/Update::updateAssignedCategory — хороший фикс

$allowed = ['value', 'active', 'required', 'position', 'caption', 'description'];
$props = array_intersect_key($this->getProperties(), array_flip($allowed));
$ftCat->fromArray($props);

Раньше fromArray($this->getProperties()) мог перезаписать что угодно. Allowlist — правильно. Это улучшение безопасности за рамками фичи.


Итого

Фича работоспособная, миграция идемпотентная, batch-загрузка продумана. Основные замечания:

  • Блокер: пункт 1 (getCollection в batch) — может создать проблему на больших каталогах
  • Желательно: пункты 2-3 (кэширование сервиса, вынос дублей)
  • Мелочи: пункты 5-6 (комментарий, лексиконы)

…on links (modx-pro#200)

- Add caption/description columns on ms3_category_options with migration
- OptionLoaderService: overlay on storefront and batch path for loadForProducts
- Processors, OptionCategoryService, OptionService: read/write and duplicate
- ExtJS: category option grid and add window; lexicons ru/en
- CHANGELOG: April 2026 entry
@Ibochkarev Ibochkarev force-pushed the feat/issue-200-category-option-caption-description branch from 76928ec to 0eb07ed Compare April 19, 2026 04:59
- loadCategoryOptionLinksGrouped: PDO fetch вместо getCollection
- кэш OptionLoaderService в Category/Option/GetList::initialize
- extractGlobalsAndKeyMap для overlay caption/description
- getProductCategoriesContext: известный parent без getObject(msProduct)
- лексиконы admin-only только в manager; GetList подключает minishop3:manager
- комментарий к materialize iterator в getFieldsForProduct
@Ibochkarev Ibochkarev requested a review from biz87 April 19, 2026 05:01
biz87 pushed a commit that referenced this pull request Apr 20, 2026
…onsController

Replaces legacy Processors/Settings/Option/* and Processors/Category/Option/* with
two REST controllers that delegate business logic to existing services
(OptionService, OptionCategoryService, OptionSyncService). Processors stay in place
for now — will be deleted in the cleanup stage.

OptionsController (Settings → Options):
- getList, get, create, update, delete
- bulkDelete (mass deletion)
- bulkAssign (assign options[] to categories[])
- getTypes (discover available option types + lexicon captions)
- getTree (MODX resource tree with checkbox state for option's categories)
- getModcategories (flat modCategory list for grouping dropdown)

CategoryOptionsController (Category → Options tab):
- getList (options linked to category with global caption/description)
- create, update (partial: value/active/required/position), delete
- sort (reorder positions atomically)
- bulk (activate/deactivate/require/unrequire/remove for many)
- duplicate (copy all links from another category, skip existing)

Per-link caption/description override is intentionally out of scope here
(lands after PR #203 which adds columns to ms3_category_options).
@biz87
Copy link
Copy Markdown
Member

biz87 commented Apr 20, 2026

PR закрыт в связи с глобальным рефакторингом системы опций. Выброшен ExtJS - изменения здесь более не актуальны

@biz87 biz87 closed this Apr 20, 2026
biz87 pushed a commit that referenced this pull request Apr 20, 2026
…e into Vue refactor

PR #203 (#200) adds per-link caption/description override on msCategoryOption:
non-empty override wins over msOption.caption/description, empty inherits. That PR
predates the ExtJS→Vue options refactor in this branch and targets processors/ExtJS
that no longer exist here, so we cherry-pick the schema + service + overlay changes
and re-implement the UI portion on top of the new REST + Vue stack.

Backend (from PR #203, applied as-is):
- Migration 20260417120000_add_caption_description_to_category_options.php —
  adds ms3_category_options.caption (varchar 191, null) and .description (text, null).
- schema/minishop3.mysql.schema.xml + src/Model/msCategoryOption.php +
  src/Model/mysql/msCategoryOption.php — new fields in xPDO meta.
- OptionCategoryService::addToCategory() and OptionService::addOptionToCategory()
  accept optional caption/description override.
- OptionLoaderService — caption/description overlay for loadForProduct,
  getFieldsForProduct, loadForProducts (batched, no N+1 per product). Non-empty
  override wins; tie-breaking across multiple product categories: parent category
  first, then lowest position, then lowest category_id.
- ru/en lexicons: ms3_category_option_caption_override, *_description_override,
  ms3_global_caption, ms3_effective_caption.

Frontend (new work on top of PR #203):
- CategoryOptionsController formats rows with global_caption / global_description /
  category_caption / category_description and the effective caption; list search now
  also matches the per-link override columns. create() accepts caption/description;
  update() allows partial edit of both (empty string → null, so clearing the field
  restores inheritance).
- CategoryOptionsTab.vue — new columns 'Глобально' (global_caption, read-only) and
  'Название (для категории)' (category_caption, inline-editable). Inline cell save
  maps category_caption → caption on the wire. Add-option dialog gained caption +
  description override inputs.

Kept the case-insensitive convertPreloadedValue fix from earlier in this branch
(multiTypes includes comboMultiple, comboColors, comboOptions).
@biz87
Copy link
Copy Markdown
Member

biz87 commented Apr 20, 2026

Функционал этого PR полностью перенесён в #205 — там же сделан полный рефакторинг управления опциями на Vue, поэтому таргет-файлы (ExtJS) больше не существуют и этот PR технически не применим. Сам функционал (миграция + overlay в OptionLoaderService + поля в msCategoryOption + CategoryOptionsController + inline-editable колонка в Vue-гриде + поля в диалоге добавления) уже доступен в #205.

Предлагаю закрыть этот PR (функционал не теряется).

biz87 pushed a commit that referenced this pull request Apr 20, 2026
…203 cherry-pick

Pulling lexicon files from the PR #203 branch overwrote the ms3_option_create,
ms3_categories, ms3_modcategory_filter, ms3_options_assign_hint, etc. keys that
were added earlier in this branch for the new Vue UI. Merge both sets together.
biz87 pushed a commit that referenced this pull request Apr 20, 2026
…ption on hydration

After PR #203 added caption/description columns to ms3_category_options, the
getFieldsForProduct() query selects those columns alongside msOption.caption /
description. xPDO hydration maps columns by name — the second caption/description
overwrote the global one, so every option came out with an empty label (user saw
option keys instead of captions).

Exclude caption/description from the msCategoryOption select. The per-category
override is already layered on top by the overlay logic below — that path still
queries msCategoryOption directly for its values.
@Ibochkarev Ibochkarev deleted the feat/issue-200-category-option-caption-description branch April 20, 2026 13:13
@biz87 biz87 mentioned this pull request Apr 20, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Переопределение названий опций при привязке к категории

2 participants