Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@

#### 🐛 Исправлено

**Опции товара — удаление не применялось после сохранения (дополнение к #199 / #202):**
- В `Processors\Product\Update` после вызова родительского `afterSave()` свойство процессора `options` в MODX 3 часто пустое, из‑за чего не выполнялся `ProductDataService::saveOptions(..., removeOther: true)` и строки в `ms3_product_options` не синхронизировались с формой. Массив из полей `options-*` сохраняется в `beforeSet` в `$ms3ProductFormOptions` и передаётся в сервис после сохранения ресурса.

**Manager API — события жизненного цикла позиции заказа (#208, closes #207):**
- Vue-админка дёргает `msOnBefore/Create/Update/Remove OrderProduct` через `Utils::invokeEvent` при добавлении/изменении/удалении позиций заказа — раньше события были зарегистрированы в `events.php`, но `OrdersController` их не вызывал, и сторонние подписчики (ms3PromoCode и т.п.) не срабатывали.
- Before-hooks могут заблокировать операцию через `Response::error(400)`. After-hooks логируются на WARN-уровне с маркером `(persistence already done)` — ошибка плагина после `save()`/`remove()` не возвращается клиенту как 4xx, потому что persistence уже произошёл.
Expand Down
25 changes: 17 additions & 8 deletions core/components/minishop3/src/Processors/Product/Update.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ class Update extends UpdateProcessor
/** @var msProduct $object */
public $object;

/**
* Values parsed from options-* request fields in beforeSet(). On MODX 3, getProperty('options') is
* often empty after the parent afterSave() call, so this copy is used for
* ProductDataService::saveOptions(..., removeOther: true) — #199. Null if the request had no options-*.
*
* @var array<string, mixed>|null
*/
protected $ms3ProductFormOptions = null;

/**
* Allow for Resources to use derivative classes for their processors
*
Expand All @@ -38,15 +47,21 @@ public static function getInstance(modX $modx, $className, $properties = [])
*/
public function beforeSet()
{
$this->ms3ProductFormOptions = null;
$properties = $this->getProperties();
$options = [];
$hadOptionFieldsInRequest = false;
foreach ($properties as $key => $value) {
$optionKey = Utils::extractOptionKey($key);
if ($optionKey !== null) {
$hadOptionFieldsInRequest = true;
$options[$optionKey] = Utils::decodeOptionValue($value);
$this->unsetProperty($key);
}
}
if ($hadOptionFieldsInRequest) {
$this->ms3ProductFormOptions = $options;
}
if (!empty($options)) {
$this->setProperty('options', $options);
}
Expand Down Expand Up @@ -107,18 +122,12 @@ public function afterSave()
{
$result = parent::afterSave();

// Save product options from options-* form fields (parsed in beforeSet)
// Only runs when form actually contained options-* fields
// removeOther=true: POST contains the full set of options shown on the form — keys missing after
// user removal must be deleted from DB (see #199). JSON-only sync still uses saveOptions(null)
// in msProductData::save(), which forces removeOther=false (#153, #158).
$options = $this->getProperty('options');
if (!empty($options) && is_array($options)) {
if ($this->ms3ProductFormOptions !== null) {
/** @var \MiniShop3\Model\msProductData $productData */
$productData = $this->object->loadData();
if ($productData) {
$service = $this->modx->services->get('ms3_product_data_service');
$service->saveOptions($productData, $options, true);
$service->saveOptions($productData, $this->ms3ProductFormOptions, true);
}
}

Expand Down