Skip to content

fix(product): сохранение Data и extra fields в процессорах Create/Update#298

Open
Ibochkarev wants to merge 3 commits into
betafrom
fix/297-product-data-payload
Open

fix(product): сохранение Data и extra fields в процессорах Create/Update#298
Ibochkarev wants to merge 3 commits into
betafrom
fix/297-product-data-payload

Conversation

@Ibochkarev

@Ibochkarev Ibochkarev commented May 24, 2026

Copy link
Copy Markdown
Member

Описание

Исправляет #297: поля msProductData (включая Object Extension / extra fields, например ext_id) не сохранялись при вызове runProcessor с блоком Data или плоскими ключами (price, …).

Причина (две части):

  1. Resource\Create|Update вызывают $object->fromArray($properties) без ignoreInvalid — колонки msProductData не попадают на объект.
  2. На Create composite Data требует id ресурса; запись в beforeSave() не гарантирует persist price и extra fields.

Решение:

  • Trait ProductDataPayloadTrait: захват Data в beforeSet(), whitelist через getDataFieldsNames(), loadMap() для extra fields.
  • UpdateapplyProductDataPayload() в beforeSave().
  • CreatepersistProductDataPayload() в afterSave() после insert ресурса.
  • Alias-классы msProductCreateProcessor / msProductUpdateProcessor для резолвера Resource\Create|Update::getInstance() (импорт CSV, интеграции).

Поддерживаются плоские ключи в корне payload и явный блок Data (при конфликте побеждает Data).

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

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

Связанные Issues

Closes #297

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

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

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

  • MiniShop3: dev / beta
  • MODX: 3.x
  • PHP: php -l для изменённых PHP-файлов (OK)

Сценарии для ревьюера:

  1. MiniShop3\Processors\Product\Create + Data => ['price' => 5200, 'ext_id' => '…'] — значения в ms3_products.
  2. MODX\Revolution\Processors\Resource\Create + class_key = msProduct — тот же результат (alias-процессор).
  3. Update с блоком Data — поля обновляются.
  4. Плоские price / ext_id в корне payload.
  5. Регрессия: создание/редактирование товара в менеджере (price, options-*).

Пример runProcessor:

$response = $modx->runProcessor('MiniShop3\\Processors\\Product\\Create', [
    'pagetitle' => 'Товар',
    'parent' => 42,
    'class_key' => \MiniShop3\Model\msProduct::class,
    'Data' => [
        'price' => 1990.00,
        'ext_id' => 'SKU-123',
    ],
]);

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

До После
price / extra fields не сохранялись через runProcessor на Create сохраняются через Data и плоские ключи после afterSave()

Чеклист

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

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

Code review (clean-code / pre-merge)

Сильные стороны:

  • Trait с одной ответственностью, методы короткие, имена раскрывают intent (capture / apply / persist).
  • Whitelist полей через getDataFieldsNames() + array_intersect_key — безопаснее произвольного merge.
  • fromArray(..., true, true) с ignoreInvalid — корректный контракт для extra fields.
  • Разделение Create (afterSave + explicit save) / Update (beforeSave) задокументировано в docblock trait.
  • Alias-процессоры в Model\ — минимальный diff, без дублирования логики.

Замечания (не блокеры, follow-up):

  • persistProductDataPayload() возвращает false и при «нет полей для записи», и при ошибке $productData->save() — в Create::afterSave() результат не проверяется; при сбое save клиент может получить success. Имеет смысл различать noop / failure или проверять return при непустом payload.
  • Невалидная JSON-строка в Data из connector молча игнорируется — для отладки можно log или validation error (низкий приоритет).
  • Автотестов на trait нет — regression только ручной (типично для MODX processors).

Затронутые файлы:

  • Processors/Product/ProductDataPayloadTrait.php (новый)
  • Processors/Product/Create.php, Update.php
  • Model/msProductCreateProcessor.php, msProductUpdateProcessor.php (новые)

@Ibochkarev Ibochkarev requested a review from biz87 May 24, 2026 11:11
@Ibochkarev Ibochkarev changed the title fix(product): поддержка Data и extra fields в процессорах Create/Update fix(product): сохранение Data и extra fields в процессорах Create/Update May 24, 2026
@biz87

biz87 commented Jun 14, 2026

Copy link
Copy Markdown
Member

Спасибо за PR — закрывает реальный гэп в processor-флоу для msProductData и сделано архитектурно правильно (trait, whitelist, разделение Create/Update, alias-классы). Прошёлся по коду, PHPStan прогнал — есть три вещи, которые лучше доделать до мержа.

Корректность диагноза

Подтверждаю: modResource::fromArray() без ignoreInvalid отбрасывает колонки msProductData; на Create composite Data требует id ресурса, которого нет в beforeSave(), → нужен afterSave(). На Update — beforeSave() достаточно. Закрывает гэп для runProcessor (CSV-импорт, интеграции, Resource\Create с class_key=msProduct).

Архитектура

Делю на сильные стороны и проблемы.

Хорошо

  • Trait с одной ответственностью, методы short + self-describing (capture / apply / persist).
  • Whitelist через $this->object->getDataFieldsNames() + array_intersect_key — безопаснее raw merge.
  • loadMap() через services->get('ms3') гарантирует регистрацию extra fields к моменту whitelist.
  • Alias-классы в MiniShop3\Model\msProduct{Create,Update}Processor — корректное место для резолвера Resource\Create::getInstance(), минимальный diff.
  • Конфликт Data vs flat keys разрешён в пользу Data через порядок assign(flat) → assign(nested). Документировано.
  • unsetProperty('Data') — parent fromArray не споткнётся на лишнем ключе.
  • Защита от {"Data": {"id": 999999}} через unset($fields['id']) в getAllowedProductDataFieldNames().

🔴 Блокер 1 — PHPStan

Прогнал по файлам PR (phpstan.neon):

ProductDataPayloadTrait.php:115
Method getAllowedProductDataFieldNames() should return array<string, true>
but returns array<mixed, (int|string)>.

array_flip($this->object->getDataFieldsNames()) — PHPStan не может сузить тип к array<string, true>, потому что docblock getDataFieldsNames() объявляет просто array. Чек-бокс «PHPStan проходит» в PR не отмечен — подтверждается.

Решение: либо уточнить тип в msProduct::getDataFieldsNames() (@return list<string>), либо явный assert/cast в trait. Первый вариант чище, заодно улучшит inference в других вызовах.

🔴 Блокер 2 — discarded return value в Create::afterSave()

В описании PR отмечено как follow-up. Не согласен — это блокирующий класс багов для критического пути (Create продукта).

Сейчас:

$result = parent::afterSave();
$this->persistProductDataPayload();  // bool отброшен

Если $productData->save() упадёт (DB error, валидация, FK constraint) — ресурс создан, запись в ms3_products отсутствует, клиент получает success. Silent partial-state, найти потом по жалобе «у товаров нет цен» — недёшево.

Плюс persistProductDataPayload() возвращает false и для «нет полей для записи», и для «save упал» — два разных исхода под одним boolean. Нужно различать.

Минимальный фикс:

private function assignProductDataPayload(bool $persist): bool
{
    // ...
    if ($flatFields === [] && $nestedFields === []) {
        return true;  // noop = success, нет работы
    }
    // ...
    return $persist ? (bool) $productData->save() : true;
}

И в Create::afterSave():

if (!$this->persistProductDataPayload()) {
    $this->modx->log(
        \MODX\Revolution\modX::LOG_LEVEL_ERROR,
        '[msProduct/Create] failed to persist msProductData for resource id '
        . $this->object->get('id')
    );
}

Решать — ронять процессор через addFieldError или продолжать с логом — на твоё усмотрение.

🟡 Замечание 1 — невалидный JSON в Data молча игнорируется

В captureProductDataPayload():

if (is_string($payload)) {
    $decoded = json_decode($payload, true);
    if (is_array($decoded)) {
        $payload = $decoded;
    } else {
        return;  // payload потерян без следа
    }
}

Connector с опечаткой в JSON ('{"price":1990, malformed}') → блок молча выкинут, success возвращается. Debug-кошмар: пользователь жалуется «не сохранилось», ищется часами, проблема в одной запятой.

Цена фикса — одна строка перед return:

$this->modx->log(
    \MODX\Revolution\modX::LOG_LEVEL_WARN,
    '[msProduct] malformed Data JSON payload: ' . json_last_error_msg()
);

Рекомендую.

🟡 Замечание 2 — Update полагается на каскад

applyProductDataPayload() в Update::beforeSave() модифицирует $productData в памяти, рассчитывает что parent::save() его сохранит каскадно. Если по любой причине каскад не отработает (xPDO config, события прерывают, гонка) — изменения не уйдут в БД.

Не блокер, но единая стратегия (explicit save и для Create, и для Update в afterSave()) убрала бы скрытую зависимость от каскада composite. На усмотрение.

Резюме

Закрыть до мержа:

  1. PHPStan типы (5 минут).
  2. Discarded return value + лог ошибки на Create (15 минут).
  3. Лог при невалидном JSON (1 строка).

После этого — мержим без оговорок.

MODX Resource processors call fromArray() without ignoreInvalid, so extra
fields and flat msProductData keys were dropped. Add Data payload support
and apply allowed fields in beforeSave() after loadMap().

Closes #297
Move product data payload application to afterSave on Create so price and
extra fields save once the resource id exists; add MODX resolver aliases for
Resource Create/Update and JSON-decoded Data payloads.
@Ibochkarev Ibochkarev force-pushed the fix/297-product-data-payload branch from 78f1724 to 0edef3c Compare June 14, 2026 10:14
- Annotate msProduct::getDataFieldsNames() as list<string> for PHPStan
- Treat empty msProductData payload as noop success in trait
- Log ERROR when Create fails to persist msProductData after resource insert
- Log WARN when Data JSON payload is malformed
@Ibochkarev

Copy link
Copy Markdown
Member Author

@biz87 можно смотреть повторно

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] кастомные поля msProductData в процессор create product

2 participants