Skip to content

Commit e94a62e

Browse files
authored
Remove Multiline "__mlid" early from row data (#2301)
1 parent 6396d5c commit e94a62e

File tree

3 files changed

+49
-66
lines changed

3 files changed

+49
-66
lines changed

demos/form-control/multiline.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@
4444
$controlTotal = $column->addControl('total', ['readOnly' => true])->set($total);
4545

4646
// update total when qty and box value in any row has changed
47-
$multiline->onLineChange(static function (array $rows, Form $form) use ($controlTotal) {
47+
$multiline->onLineChange(static function (array $rows, array $mlids, Form $form) use ($controlTotal) {
4848
$total = 0;
49-
foreach ($rows as $row => $cols) {
49+
foreach ($rows as $cols) {
5050
$total += $cols[MultilineItem::hinting()->fieldName()->qty] * $cols[MultilineItem::hinting()->fieldName()->box];
5151
}
5252

docs/multiline.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,9 @@ You can return a single JsExpressionable or an array of JsExpressionables which
177177
In this case we display a message when any of the control value for 'qty' and 'box' are changed:
178178

179179
```
180-
$multiline->onLineChange(function (array $rows, Form $form) {
180+
$multiline->onLineChange(function (array $rows, array $mlids, Form $form) {
181181
$total = 0;
182-
foreach ($rows as $row => $cols) {
182+
foreach ($rows as $cols) {
183183
$qty = $cols['qty'] ?? 0;
184184
$box = $cols['box'] ?? 0;
185185
$total += $qty * $box;

src/Form/Control/Multiline.php

Lines changed: 45 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Atk4\Data\Field\SqlExpressionField;
1111
use Atk4\Data\Model;
1212
use Atk4\Data\Persistence;
13-
use Atk4\Data\Reference\ContainsOne;
1413
use Atk4\Data\ValidationException;
1514
use Atk4\Ui\Form;
1615
use Atk4\Ui\HtmlTemplate;
@@ -150,7 +149,7 @@ class Multiline extends Form\Control
150149
/** @var JsCallback */
151150
private $renderCallback;
152151

153-
/** @var \Closure(mixed, Form): (JsExpressionable|View|string|void)|null Function to execute when field change or row is delete. */
152+
/** @var \Closure(list<array<string, mixed>>, list<string>, Form): (JsExpressionable|View|string|void)|null Function to execute when field change or row is delete. */
154153
protected $onChangeFunction;
155154

156155
/** @var list<string> Set fields that will trigger onChange function. */
@@ -163,7 +162,7 @@ class Multiline extends Form\Control
163162
public $rowFields;
164163

165164
/** @var list<array<string, mixed>> The data sent for each row. */
166-
public $rowData;
165+
protected $rowData;
167166

168167
/** @var int The max number of records (rows) that can be added to Multiline. 0 means no limit. */
169168
public $rowLimit = 0;
@@ -227,7 +226,7 @@ protected function init(): void
227226
return $jsError;
228227
});
229228

230-
if ($this->isContainsOne()) {
229+
if ($this->isOneToOne()) {
231230
$this->rowLimit = 1;
232231
}
233232
}
@@ -252,32 +251,26 @@ private function typecastUiSaveValues(array $values): array
252251
}
253252

254253
/**
255-
* @param array<mixed, array<string, string|null>> $values
254+
* @param array<string, string|null> $row
256255
*
257-
* @return array<mixed, array<string, mixed>>
256+
* @return array<string, mixed>
258257
*/
259-
private function typecastUiLoadValues(array $values): array
258+
private function typecastUiLoadRow(array $row): array
260259
{
261260
$res = [];
262-
foreach ($values as $k => $row) {
263-
foreach ($row as $fieldName => $value) {
264-
if ($fieldName === '__atkml') {
265-
$res[$k][$fieldName] = $value;
266-
} else {
267-
$res[$k][$fieldName] = $fieldName === $this->model->idField
268-
? $this->getApp()->uiPersistence->typecastAttributeLoadField($this->model->getField($fieldName), $value)
269-
: $this->getApp()->uiPersistence->typecastLoadField($this->model->getField($fieldName), $value);
270-
}
271-
}
261+
foreach ($row as $fieldName => $value) {
262+
$res[$fieldName] = $fieldName === $this->model->idField
263+
? $this->getApp()->uiPersistence->typecastAttributeLoadField($this->model->getField($fieldName), $value)
264+
: $this->getApp()->uiPersistence->typecastLoadField($this->model->getField($fieldName), $value);
272265
}
273266

274267
return $res;
275268
}
276269

277-
private function isContainsOne(): bool
270+
private function isOneToOne(): bool
278271
{
279272
return $this->entityField->getField()->hasReference()
280-
&& $this->entityField->getField()->getReference() instanceof ContainsOne;
273+
&& $this->entityField->getField()->getReference()->isOneToOne();
281274
}
282275

283276
#[\Override]
@@ -349,31 +342,45 @@ private function invokeWithContainsXxxNormalizeHookIgnored(\Closure $fx): void
349342
}, null, Model::class)();
350343
}
351344

345+
/**
346+
* @return array{list<array<string, mixed>>, list<string>}
347+
*/
348+
private function decodeInput(string $json): array
349+
{
350+
$rowDataWithMlid = $this->getApp()->decodeJson($json);
351+
$rowData = [];
352+
$mlids = [];
353+
foreach ($rowDataWithMlid as $row) {
354+
$mlids[] = $row['__atkml'];
355+
unset($row['__atkml']);
356+
$rowData[] = $this->typecastUiLoadRow($row);
357+
}
358+
359+
return [$rowData, $mlids];
360+
}
361+
352362
#[\Override]
353363
public function setInputValue(string $value): void
354364
{
355-
$this->rowData = $this->typecastUiLoadValues($this->getApp()->decodeJson($value));
365+
[$rowData, $mlids] = $this->decodeInput($value);
366+
367+
$this->rowData = $rowData;
356368
if ($this->rowData !== []) {
357-
$this->rowErrors = $this->validate($this->rowData);
369+
$this->rowErrors = $this->validate($this->rowData, $mlids);
358370
if ($this->rowErrors !== []) {
359371
throw new ValidationException([$this->shortName => 'Multiline error']);
360372
}
361373
}
362374

363-
$rowsRaw = [];
364-
foreach ($this->rowData as $k => $v) {
365-
unset($v['__atkml']);
366-
367-
$rowsRaw[$k] = $this->typecastContainedSaveRow($v);
368-
}
375+
$rowsRaw = array_map(fn ($v) => $this->typecastContainedSaveRow($v), $this->rowData);
369376

370377
// mimic ContainsOne save format
371378
// https://github.com/atk4/data/blob/6.0.0/src/Reference/ContainsOne.php#L37
372379
// TODO replace with something like "schedule model save task" and then drop self::saveRows() method
373380
if ($rowsRaw === []) {
374381
$value = '';
375382
} else {
376-
foreach ($rowsRaw as $k => $rowRaw) { // @phpstan-ignore foreach.keyOverwrite (https://github.com/phpstan/phpstan-strict-rules/issues/194)
383+
foreach ($rowsRaw as $k => $rowRaw) {
377384
$idFieldRawName = $this->model->getIdField()->getPersistenceName();
378385
if ($rowRaw[$idFieldRawName] === null) {
379386
$refModel = $this->entityField->getField()->hasReference()
@@ -396,7 +403,7 @@ public function setInputValue(string $value): void
396403
}
397404
}
398405

399-
if ($this->isContainsOne()) {
406+
if ($this->isOneToOne()) {
400407
assert(count($rowsRaw) === 1);
401408
$rowsRaw = array_first($rowsRaw);
402409
}
@@ -413,8 +420,8 @@ public function setInputValue(string $value): void
413420
* Add a callback when fields are changed. You must supply array of fields
414421
* that will trigger the callback when changed.
415422
*
416-
* @param \Closure(mixed, Form): (JsExpressionable|View|string|void) $fx
417-
* @param list<string> $fields
423+
* @param \Closure(list<array<string, mixed>>, list<string>, Form): (JsExpressionable|View|string|void) $fx
424+
* @param list<string> $fields
418425
*/
419426
public function onLineChange(\Closure $fx, array $fields): void
420427
{
@@ -427,18 +434,19 @@ public function onLineChange(\Closure $fx, array $fields): void
427434
* Validate each row and return errors if found.
428435
*
429436
* @param list<array<string, mixed>> $rows
437+
* @param list<string> $mlids
430438
*
431439
* @return array<string, list<array{name: string, msg: string}>>
432440
*/
433-
public function validate(array $rows): array
441+
public function validate(array $rows, array $mlids): array
434442
{
435443
$rowErrors = [];
436444
$entity = $this->model->createEntity();
437445

438-
foreach ($rows as $cols) {
439-
$rowId = $this->getMlRowId($cols);
446+
foreach ($rows as $i => $cols) {
447+
$rowId = $mlids[$i];
440448
foreach ($cols as $fieldName => $value) {
441-
if ($fieldName === '__atkml' || $fieldName === $entity->idField) {
449+
if ($fieldName === $entity->idField) {
442450
continue;
443451
}
444452

@@ -477,10 +485,6 @@ public function saveRows(): self
477485
? $model->load($row[$model->idField])
478486
: $model->createEntity();
479487
foreach ($row as $fieldName => $value) {
480-
if ($fieldName === '__atkml') {
481-
continue;
482-
}
483-
484488
if ($model->getField($fieldName)->isEditable()) {
485489
$entity->set($fieldName, $value);
486490
}
@@ -513,25 +517,6 @@ protected function addModelValidateErrors(array $errors, string $rowId, Model $e
513517
return $errors;
514518
}
515519

516-
/**
517-
* Finds and returns Multiline row ID.
518-
*
519-
* @param array<string, string> $row
520-
*/
521-
private function getMlRowId(array $row): ?string
522-
{
523-
$rowId = null;
524-
foreach ($row as $col => $value) {
525-
if ($col === '__atkml') {
526-
$rowId = $value;
527-
528-
break;
529-
}
530-
}
531-
532-
return $rowId;
533-
}
534-
535520
/**
536521
* @param list<string>|null $fields
537522
*/
@@ -831,10 +816,8 @@ private function outputJson(): void
831816
$this->getApp()->terminateJson(['success' => true, 'expressions' => $expressionValues]);
832817
// no break - expression above always terminate
833818
case 'on-change':
834-
$rowsRaw = $this->getApp()->decodeJson($this->getApp()->getRequestPostParam('rows'));
835-
$this->renderCallback->set(function () use ($rowsRaw) {
836-
return ($this->onChangeFunction)($this->typecastUiLoadValues($rowsRaw), $this->form);
837-
});
819+
[$rows, $mlids] = $this->decodeInput($this->getApp()->getRequestPostParam('rows'));
820+
$this->renderCallback->set(fn () => ($this->onChangeFunction)($rows, $mlids, $this->form));
838821
}
839822
}
840823

0 commit comments

Comments
 (0)