From d140624e66dd500ff5c1b5eb0d5adc8915b1d4b0 Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Sun, 24 May 2026 17:09:26 +0600 Subject: [PATCH 1/2] fix(product): apply msProductData fields in Create/Update processors 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 --- .../src/Processors/Product/Create.php | 6 + .../Product/ProductDataPayloadTrait.php | 123 ++++++++++++++++++ .../src/Processors/Product/Update.php | 6 + 3 files changed, 135 insertions(+) create mode 100644 core/components/minishop3/src/Processors/Product/ProductDataPayloadTrait.php diff --git a/core/components/minishop3/src/Processors/Product/Create.php b/core/components/minishop3/src/Processors/Product/Create.php index 41a3bd0e..1bef5dff 100644 --- a/core/components/minishop3/src/Processors/Product/Create.php +++ b/core/components/minishop3/src/Processors/Product/Create.php @@ -8,6 +8,8 @@ class Create extends CreateProcessor { + use ProductDataPayloadTrait; + public $classKey = msProduct::class; public $languageTopics = ['resource', 'minishop3:default']; public $permission = 'msproduct_save'; @@ -57,6 +59,8 @@ public function beforeSet() ), ]); + $this->captureProductDataPayload(); + $properties = $this->getProperties(); $options = []; $hadOptionFieldsInRequest = false; @@ -91,6 +95,8 @@ public function beforeSet() public function beforeSave() { $this->object->set('isfolder', false); + $this->applyProductDataPayload(); + return parent::beforeSave(); } diff --git a/core/components/minishop3/src/Processors/Product/ProductDataPayloadTrait.php b/core/components/minishop3/src/Processors/Product/ProductDataPayloadTrait.php new file mode 100644 index 00000000..45989ea3 --- /dev/null +++ b/core/components/minishop3/src/Processors/Product/ProductDataPayloadTrait.php @@ -0,0 +1,123 @@ +fromArray($properties)` without ignoreInvalid, + * so msProductData columns must be applied in beforeSave(). + * + * @property \MODX\Revolution\modX $modx + * @property msProduct $object + */ +trait ProductDataPayloadTrait +{ + private const PRODUCT_DATA_PROPERTY = 'Data'; + + /** @var array|null */ + protected ?array $ms3ProductDataPayload = null; + + protected function captureProductDataPayload(): void + { + $this->ms3ProductDataPayload = null; + + $payload = $this->getProperty(self::PRODUCT_DATA_PROPERTY); + if ($payload === null || $payload === '') { + return; + } + + $this->unsetProperty(self::PRODUCT_DATA_PROPERTY); + + if (is_array($payload)) { + $this->ms3ProductDataPayload = $payload; + } + } + + protected function applyProductDataPayload(): void + { + if (!$this->object instanceof msProduct) { + return; + } + + $this->ensureProductDataFieldMapLoaded(); + + $allowedFields = $this->getAllowedProductDataFieldNames(); + $flatFields = $this->collectFlatProductDataFields($allowedFields); + $nestedFields = $this->collectNestedProductDataFields($allowedFields); + + if ($flatFields === [] && $nestedFields === []) { + return; + } + + $productData = $this->object->loadData(); + $this->assignProductDataFields($productData, $flatFields); + $this->assignProductDataFields($productData, $nestedFields); + } + + private function ensureProductDataFieldMapLoaded(): void + { + if (!$this->modx->services->has('ms3')) { + return; + } + + $this->modx->services->get('ms3')->loadMap(); + } + + /** + * @return array + */ + private function getAllowedProductDataFieldNames(): array + { + $fields = array_flip($this->object->getDataFieldsNames()); + unset($fields['id']); + + return $fields; + } + + /** + * @param array $allowedFields + * @return array + */ + private function collectFlatProductDataFields(array $allowedFields): array + { + $fields = []; + + foreach ($this->getProperties() as $key => $value) { + if (!isset($allowedFields[$key])) { + continue; + } + $fields[$key] = $value; + } + + return $fields; + } + + /** + * @param array $allowedFields + * @return array + */ + private function collectNestedProductDataFields(array $allowedFields): array + { + if ($this->ms3ProductDataPayload === null) { + return []; + } + + return array_intersect_key($this->ms3ProductDataPayload, $allowedFields); + } + + /** + * @param array $fields + */ + private function assignProductDataFields(msProductData $productData, array $fields): void + { + if ($fields === []) { + return; + } + + $productData->fromArray($fields, '', true, true); + } +} diff --git a/core/components/minishop3/src/Processors/Product/Update.php b/core/components/minishop3/src/Processors/Product/Update.php index fdefec18..4bc07eb3 100644 --- a/core/components/minishop3/src/Processors/Product/Update.php +++ b/core/components/minishop3/src/Processors/Product/Update.php @@ -11,6 +11,8 @@ class Update extends UpdateProcessor { + use ProductDataPayloadTrait; + public $classKey = msProduct::class; public $languageTopics = ['resource', 'minishop3:default']; public $permission = 'msproduct_save'; @@ -48,6 +50,9 @@ public static function getInstance(modX $modx, $className, $properties = []) public function beforeSet() { $this->ms3ProductFormOptions = null; + + $this->captureProductDataPayload(); + $properties = $this->getProperties(); $options = []; $hadOptionFieldsInRequest = false; @@ -109,6 +114,7 @@ public function checkFriendlyAlias() public function beforeSave() { $this->object->set('isfolder', false); + $this->applyProductDataPayload(); return parent::beforeSave(); } From 78f1724b0d1e355913a177708564c917b27eebee Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Sun, 24 May 2026 19:39:08 +0600 Subject: [PATCH 2/2] fix(product): persist msProductData on Create afterSave (#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. --- .../src/Model/msProductCreateProcessor.php | 12 ++++++ .../src/Model/msProductUpdateProcessor.php | 12 ++++++ .../src/Processors/Product/Create.php | 4 +- .../Product/ProductDataPayloadTrait.php | 43 +++++++++++++++++-- 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 core/components/minishop3/src/Model/msProductCreateProcessor.php create mode 100644 core/components/minishop3/src/Model/msProductUpdateProcessor.php diff --git a/core/components/minishop3/src/Model/msProductCreateProcessor.php b/core/components/minishop3/src/Model/msProductCreateProcessor.php new file mode 100644 index 00000000..734a2c43 --- /dev/null +++ b/core/components/minishop3/src/Model/msProductCreateProcessor.php @@ -0,0 +1,12 @@ +object->set('isfolder', false); - $this->applyProductDataPayload(); return parent::beforeSave(); } @@ -120,6 +119,9 @@ public function afterSave() $result = parent::afterSave(); + // msProductData needs resource id; composite may not persist payload fields on insert (#297). + $this->persistProductDataPayload(); + // Same contract as Update::afterSave (#199): only sync when the request contained options-* keys (#257). if ($this->ms3ProductFormOptions !== null) { /** @var \MiniShop3\Model\msProductData $productData */ diff --git a/core/components/minishop3/src/Processors/Product/ProductDataPayloadTrait.php b/core/components/minishop3/src/Processors/Product/ProductDataPayloadTrait.php index 45989ea3..9edcbba3 100644 --- a/core/components/minishop3/src/Processors/Product/ProductDataPayloadTrait.php +++ b/core/components/minishop3/src/Processors/Product/ProductDataPayloadTrait.php @@ -9,7 +9,8 @@ * Applies msProductData fields from the `Data` block and flat request keys (#297). * * Resource Create/Update call `$object->fromArray($properties)` without ignoreInvalid, - * so msProductData columns must be applied in beforeSave(). + * so msProductData columns must be applied explicitly. On Create the msProductData row + * needs a resource id — persist in afterSave(); on Update beforeSave() is enough. * * @property \MODX\Revolution\modX $modx * @property msProduct $object @@ -32,15 +33,44 @@ protected function captureProductDataPayload(): void $this->unsetProperty(self::PRODUCT_DATA_PROPERTY); + if (is_string($payload)) { + $decoded = json_decode($payload, true); + if (is_array($decoded)) { + $payload = $decoded; + } else { + return; + } + } + if (is_array($payload)) { $this->ms3ProductDataPayload = $payload; } } + /** + * Assign msProductData fields on the in-memory composite (Update / pre-save). + */ protected function applyProductDataPayload(): void + { + $this->assignProductDataPayload(false); + } + + /** + * Assign and save msProductData after the resource id exists (Create). + */ + protected function persistProductDataPayload(): bool + { + return $this->assignProductDataPayload(true); + } + + private function assignProductDataPayload(bool $persist): bool { if (!$this->object instanceof msProduct) { - return; + return false; + } + + if ($persist && (int) $this->object->get('id') <= 0) { + return false; } $this->ensureProductDataFieldMapLoaded(); @@ -50,12 +80,19 @@ protected function applyProductDataPayload(): void $nestedFields = $this->collectNestedProductDataFields($allowedFields); if ($flatFields === [] && $nestedFields === []) { - return; + return false; } $productData = $this->object->loadData(); + + if ($persist) { + $productData->set('id', (int) $this->object->get('id')); + } + $this->assignProductDataFields($productData, $flatFields); $this->assignProductDataFields($productData, $nestedFields); + + return $persist ? (bool) $productData->save() : true; } private function ensureProductDataFieldMapLoaded(): void