feat(options): per-category caption and description for category-option links#203
Conversation
biz87
left a comment
There was a problem hiding this comment.
Ревью
Фича полезная, миграция корректная, 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. getIterator → getCollection в 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
76928ec to
0eb07ed
Compare
- 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
…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).
|
PR закрыт в связи с глобальным рефакторингом системы опций. Выброшен ExtJS - изменения здесь более не актуальны |
…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).
|
Функционал этого PR полностью перенесён в #205 — там же сделан полный рефакторинг управления опциями на Vue, поэтому таргет-файлы (ExtJS) больше не существуют и этот PR технически не применим. Сам функционал (миграция + overlay в OptionLoaderService + поля в msCategoryOption + CategoryOptionsController + inline-editable колонка в Vue-гриде + поля в диалоге добавления) уже доступен в #205. Предлагаю закрыть этот PR (функционал не теряется). |
…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.
Описание
Реализовано переопределение подписи (caption) и описания (description) опции на связи «опция–категория» (
ms3_category_options): значения из связи имеют приоритет над глобальными полямиms3_options; пустое значение означает наследование с глобальной опции. Поддержаны витрина (OptionLoaderService, в том числе пакетная подгрузка дляloadForProducts), менеджер MODX (ExtJS: грид и окно добавления опции к категории), процессоры и копирование при дублировании категории.Тип изменений
Связанные Issues
Closes #200
Как это было протестировано?
Добавлена миграция Phinx и обновлены слой загрузки опций и формы менеджера. Рекомендуется прогнать установку/обновление компонента, применить миграцию и проверить сценарии: категория → опции, отображение подписей на витрине, дублирование категории.
Конфигурация тестирования:
Скриншоты (если применимо)
Чеклист
Дополнительные заметки
При нескольких категориях для одного товара выбор строки для оверлея: родительская категория товара, затем минимальный
position, затем минимальныйcategory_id(см.OptionLoaderService::pickWinningCategoryOptionLink).