Skip to content

Commit 8348fe2

Browse files
authored
fix(product): сохранение Data и extra fields в процессорах Create/Update (#298)
* 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 * 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. * fix(product): address PR #298 review (PHPStan, persist errors, JSON log) - 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 * fix(product): use array_fill_keys for msProductData whitelist (#298)
1 parent c29b4d6 commit 8348fe2

6 files changed

Lines changed: 215 additions & 1 deletion

File tree

core/components/minishop3/src/Model/msProduct.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,10 @@ public function addMany(&$obj, $alias = '')
352352
*
353353
* @return array
354354
*/
355-
public function getDataFieldsNames()
355+
/**
356+
* @return list<string>
357+
*/
358+
public function getDataFieldsNames(): array
356359
{
357360
return array_keys($this->loadData()->_fieldMeta);
358361
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace MiniShop3\Model;
4+
5+
/**
6+
* Resolver target for MODX Resource\Create::getInstance() when class_key is msProduct.
7+
*
8+
* @see \MODX\Revolution\Processors\Resource\Create::getInstance()
9+
*/
10+
class msProductCreateProcessor extends \MiniShop3\Processors\Product\Create
11+
{
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace MiniShop3\Model;
4+
5+
/**
6+
* Resolver target for MODX Resource\Update::getInstance() when class_key is msProduct.
7+
*
8+
* @see \MODX\Revolution\Processors\Resource\Update::getInstance()
9+
*/
10+
class msProductUpdateProcessor extends \MiniShop3\Processors\Product\Update
11+
{
12+
}

core/components/minishop3/src/Processors/Product/Create.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
use MiniShop3\Model\msProduct;
66
use MiniShop3\Utils\Utils;
77
use MODX\Revolution\modDocument;
8+
use MODX\Revolution\modX;
89
use MODX\Revolution\Processors\Resource\Create as CreateProcessor;
910

1011
class Create extends CreateProcessor
1112
{
13+
use ProductDataPayloadTrait;
14+
1215
public $classKey = msProduct::class;
1316
public $languageTopics = ['resource', 'minishop3:default'];
1417
public $permission = 'msproduct_save';
@@ -71,6 +74,8 @@ public function beforeSet()
7174
),
7275
]);
7376

77+
$this->captureProductDataPayload();
78+
7479
$properties = $this->getProperties();
7580
$options = [];
7681
$hadOptionFieldsInRequest = false;
@@ -105,6 +110,7 @@ public function beforeSet()
105110
public function beforeSave()
106111
{
107112
$this->object->set('isfolder', false);
113+
108114
return parent::beforeSave();
109115
}
110116

@@ -128,6 +134,15 @@ public function afterSave()
128134

129135
$result = parent::afterSave();
130136

137+
// msProductData needs resource id; composite may not persist payload fields on insert (#297).
138+
if (!$this->persistProductDataPayload()) {
139+
$this->modx->log(
140+
modX::LOG_LEVEL_ERROR,
141+
'[msProduct/Create] failed to persist msProductData for resource id '
142+
. $this->object->get('id')
143+
);
144+
}
145+
131146
// Same contract as Update::afterSave (#199): only sync when the request contained options-* keys (#257).
132147
if ($this->ms3ProductFormOptions !== null) {
133148
/** @var \MiniShop3\Model\msProductData $productData */
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
3+
namespace MiniShop3\Processors\Product;
4+
5+
use MiniShop3\Model\msProduct;
6+
use MiniShop3\Model\msProductData;
7+
use MODX\Revolution\modX;
8+
9+
/**
10+
* Applies msProductData fields from the `Data` block and flat request keys (#297).
11+
*
12+
* Resource Create/Update call `$object->fromArray($properties)` without ignoreInvalid,
13+
* so msProductData columns must be applied explicitly. On Create the msProductData row
14+
* needs a resource id — persist in afterSave(); on Update beforeSave() is enough.
15+
*
16+
* @property \MODX\Revolution\modX $modx
17+
* @property msProduct $object
18+
*/
19+
trait ProductDataPayloadTrait
20+
{
21+
private const PRODUCT_DATA_PROPERTY = 'Data';
22+
23+
/** @var array<string, mixed>|null */
24+
protected ?array $ms3ProductDataPayload = null;
25+
26+
protected function captureProductDataPayload(): void
27+
{
28+
$this->ms3ProductDataPayload = null;
29+
30+
$payload = $this->getProperty(self::PRODUCT_DATA_PROPERTY);
31+
if ($payload === null || $payload === '') {
32+
return;
33+
}
34+
35+
$this->unsetProperty(self::PRODUCT_DATA_PROPERTY);
36+
37+
if (is_string($payload)) {
38+
$decoded = json_decode($payload, true);
39+
if (is_array($decoded)) {
40+
$payload = $decoded;
41+
} else {
42+
$this->modx->log(
43+
modX::LOG_LEVEL_WARN,
44+
'[msProduct] malformed Data JSON payload: ' . json_last_error_msg()
45+
);
46+
47+
return;
48+
}
49+
}
50+
51+
if (is_array($payload)) {
52+
$this->ms3ProductDataPayload = $payload;
53+
}
54+
}
55+
56+
/**
57+
* Assign msProductData fields on the in-memory composite (Update / pre-save).
58+
*/
59+
protected function applyProductDataPayload(): void
60+
{
61+
$this->assignProductDataPayload(false);
62+
}
63+
64+
/**
65+
* Assign and save msProductData after the resource id exists (Create).
66+
*/
67+
protected function persistProductDataPayload(): bool
68+
{
69+
return $this->assignProductDataPayload(true);
70+
}
71+
72+
private function assignProductDataPayload(bool $persist): bool
73+
{
74+
if (!$this->object instanceof msProduct) {
75+
return false;
76+
}
77+
78+
if ($persist && (int) $this->object->get('id') <= 0) {
79+
return false;
80+
}
81+
82+
$this->ensureProductDataFieldMapLoaded();
83+
84+
$allowedFields = $this->getAllowedProductDataFieldNames();
85+
$flatFields = $this->collectFlatProductDataFields($allowedFields);
86+
$nestedFields = $this->collectNestedProductDataFields($allowedFields);
87+
88+
if ($flatFields === [] && $nestedFields === []) {
89+
return true;
90+
}
91+
92+
$productData = $this->object->loadData();
93+
94+
if ($persist) {
95+
$productData->set('id', (int) $this->object->get('id'));
96+
}
97+
98+
$this->assignProductDataFields($productData, $flatFields);
99+
$this->assignProductDataFields($productData, $nestedFields);
100+
101+
return $persist ? (bool) $productData->save() : true;
102+
}
103+
104+
private function ensureProductDataFieldMapLoaded(): void
105+
{
106+
if (!$this->modx->services->has('ms3')) {
107+
return;
108+
}
109+
110+
$this->modx->services->get('ms3')->loadMap();
111+
}
112+
113+
/**
114+
* @return array<string, true>
115+
*/
116+
private function getAllowedProductDataFieldNames(): array
117+
{
118+
$fields = array_fill_keys($this->object->getDataFieldsNames(), true);
119+
unset($fields['id']);
120+
121+
return $fields;
122+
}
123+
124+
/**
125+
* @param array<string, true> $allowedFields
126+
* @return array<string, mixed>
127+
*/
128+
private function collectFlatProductDataFields(array $allowedFields): array
129+
{
130+
$fields = [];
131+
132+
foreach ($this->getProperties() as $key => $value) {
133+
if (!isset($allowedFields[$key])) {
134+
continue;
135+
}
136+
$fields[$key] = $value;
137+
}
138+
139+
return $fields;
140+
}
141+
142+
/**
143+
* @param array<string, true> $allowedFields
144+
* @return array<string, mixed>
145+
*/
146+
private function collectNestedProductDataFields(array $allowedFields): array
147+
{
148+
if ($this->ms3ProductDataPayload === null) {
149+
return [];
150+
}
151+
152+
return array_intersect_key($this->ms3ProductDataPayload, $allowedFields);
153+
}
154+
155+
/**
156+
* @param array<string, mixed> $fields
157+
*/
158+
private function assignProductDataFields(msProductData $productData, array $fields): void
159+
{
160+
if ($fields === []) {
161+
return;
162+
}
163+
164+
$productData->fromArray($fields, '', true, true);
165+
}
166+
}

core/components/minishop3/src/Processors/Product/Update.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
class Update extends UpdateProcessor
1313
{
14+
use ProductDataPayloadTrait;
15+
1416
public $classKey = msProduct::class;
1517
public $languageTopics = ['resource', 'minishop3:default'];
1618
public $permission = 'msproduct_save';
@@ -48,6 +50,9 @@ public static function getInstance(modX $modx, $className, $properties = [])
4850
public function beforeSet()
4951
{
5052
$this->ms3ProductFormOptions = null;
53+
54+
$this->captureProductDataPayload();
55+
5156
$properties = $this->getProperties();
5257
$options = [];
5358
$hadOptionFieldsInRequest = false;
@@ -109,6 +114,7 @@ public function checkFriendlyAlias()
109114
public function beforeSave()
110115
{
111116
$this->object->set('isfolder', false);
117+
$this->applyProductDataPayload();
112118

113119
return parent::beforeSave();
114120
}

0 commit comments

Comments
 (0)