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 @@ +captureProductDataPayload(); + $properties = $this->getProperties(); $options = []; $hadOptionFieldsInRequest = false; @@ -91,6 +95,7 @@ public function beforeSet() public function beforeSave() { $this->object->set('isfolder', false); + return parent::beforeSave(); } @@ -114,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 new file mode 100644 index 00000000..9edcbba3 --- /dev/null +++ b/core/components/minishop3/src/Processors/Product/ProductDataPayloadTrait.php @@ -0,0 +1,160 @@ +fromArray($properties)` without ignoreInvalid, + * 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 + */ +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_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 false; + } + + if ($persist && (int) $this->object->get('id') <= 0) { + return false; + } + + $this->ensureProductDataFieldMapLoaded(); + + $allowedFields = $this->getAllowedProductDataFieldNames(); + $flatFields = $this->collectFlatProductDataFields($allowedFields); + $nestedFields = $this->collectNestedProductDataFields($allowedFields); + + if ($flatFields === [] && $nestedFields === []) { + 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 + { + 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(); }