From 5e01c5be565989182a876a38d5780b236700e90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 17 Oct 2025 14:24:03 +0200 Subject: [PATCH 1/3] use Reference::isOneToOne --- src/Form/Control/Multiline.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Form/Control/Multiline.php b/src/Form/Control/Multiline.php index 62832e97c2..6f935f1559 100644 --- a/src/Form/Control/Multiline.php +++ b/src/Form/Control/Multiline.php @@ -10,7 +10,6 @@ use Atk4\Data\Field\SqlExpressionField; use Atk4\Data\Model; use Atk4\Data\Persistence; -use Atk4\Data\Reference\ContainsOne; use Atk4\Data\ValidationException; use Atk4\Ui\Form; use Atk4\Ui\HtmlTemplate; @@ -227,7 +226,7 @@ protected function init(): void return $jsError; }); - if ($this->isContainsOne()) { + if ($this->isOneToOne()) { $this->rowLimit = 1; } } @@ -274,10 +273,10 @@ private function typecastUiLoadValues(array $values): array return $res; } - private function isContainsOne(): bool + private function isOneToOne(): bool { return $this->entityField->getField()->hasReference() - && $this->entityField->getField()->getReference() instanceof ContainsOne; + && $this->entityField->getField()->getReference()->isOneToOne(); } #[\Override] @@ -396,7 +395,7 @@ public function setInputValue(string $value): void } } - if ($this->isContainsOne()) { + if ($this->isOneToOne()) { assert(count($rowsRaw) === 1); $rowsRaw = array_first($rowsRaw); } From d85debb2be55207aa6fab9dce3950e81e7d3470e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 17 Oct 2025 14:55:01 +0200 Subject: [PATCH 2/3] refactor - simplify getMlRowId() --- src/Form/Control/Multiline.php | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/Form/Control/Multiline.php b/src/Form/Control/Multiline.php index 6f935f1559..f7c412cf84 100644 --- a/src/Form/Control/Multiline.php +++ b/src/Form/Control/Multiline.php @@ -435,7 +435,7 @@ public function validate(array $rows): array $entity = $this->model->createEntity(); foreach ($rows as $cols) { - $rowId = $this->getMlRowId($cols); + $rowId = $cols['__atkml'] ?? null; foreach ($cols as $fieldName => $value) { if ($fieldName === '__atkml' || $fieldName === $entity->idField) { continue; @@ -512,25 +512,6 @@ protected function addModelValidateErrors(array $errors, string $rowId, Model $e return $errors; } - /** - * Finds and returns Multiline row ID. - * - * @param array $row - */ - private function getMlRowId(array $row): ?string - { - $rowId = null; - foreach ($row as $col => $value) { - if ($col === '__atkml') { - $rowId = $value; - - break; - } - } - - return $rowId; - } - /** * @param list|null $fields */ From f2f6461a85edc9363e064ee378a2e050d7c4c704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 17 Oct 2025 15:20:47 +0200 Subject: [PATCH 3/3] refactor - remove mlid early from row data --- demos/form-control/multiline.php | 4 +- docs/multiline.md | 4 +- src/Form/Control/Multiline.php | 79 +++++++++++++++++--------------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/demos/form-control/multiline.php b/demos/form-control/multiline.php index e0d4b5cb7f..8de33913a5 100644 --- a/demos/form-control/multiline.php +++ b/demos/form-control/multiline.php @@ -44,9 +44,9 @@ $controlTotal = $column->addControl('total', ['readOnly' => true])->set($total); // update total when qty and box value in any row has changed -$multiline->onLineChange(static function (array $rows, Form $form) use ($controlTotal) { +$multiline->onLineChange(static function (array $rows, array $mlids, Form $form) use ($controlTotal) { $total = 0; - foreach ($rows as $row => $cols) { + foreach ($rows as $cols) { $total += $cols[MultilineItem::hinting()->fieldName()->qty] * $cols[MultilineItem::hinting()->fieldName()->box]; } diff --git a/docs/multiline.md b/docs/multiline.md index c6b992885e..123a6e0f34 100644 --- a/docs/multiline.md +++ b/docs/multiline.md @@ -177,9 +177,9 @@ You can return a single JsExpressionable or an array of JsExpressionables which In this case we display a message when any of the control value for 'qty' and 'box' are changed: ``` -$multiline->onLineChange(function (array $rows, Form $form) { +$multiline->onLineChange(function (array $rows, array $mlids, Form $form) { $total = 0; - foreach ($rows as $row => $cols) { + foreach ($rows as $cols) { $qty = $cols['qty'] ?? 0; $box = $cols['box'] ?? 0; $total += $qty * $box; diff --git a/src/Form/Control/Multiline.php b/src/Form/Control/Multiline.php index f7c412cf84..a64d284235 100644 --- a/src/Form/Control/Multiline.php +++ b/src/Form/Control/Multiline.php @@ -149,7 +149,7 @@ class Multiline extends Form\Control /** @var JsCallback */ private $renderCallback; - /** @var \Closure(mixed, Form): (JsExpressionable|View|string|void)|null Function to execute when field change or row is delete. */ + /** @var \Closure(list>, list, Form): (JsExpressionable|View|string|void)|null Function to execute when field change or row is delete. */ protected $onChangeFunction; /** @var list Set fields that will trigger onChange function. */ @@ -162,7 +162,7 @@ class Multiline extends Form\Control public $rowFields; /** @var list> The data sent for each row. */ - public $rowData; + protected $rowData; /** @var int The max number of records (rows) that can be added to Multiline. 0 means no limit. */ public $rowLimit = 0; @@ -251,23 +251,17 @@ private function typecastUiSaveValues(array $values): array } /** - * @param array> $values + * @param array $row * - * @return array> + * @return array */ - private function typecastUiLoadValues(array $values): array + private function typecastUiLoadRow(array $row): array { $res = []; - foreach ($values as $k => $row) { - foreach ($row as $fieldName => $value) { - if ($fieldName === '__atkml') { - $res[$k][$fieldName] = $value; - } else { - $res[$k][$fieldName] = $fieldName === $this->model->idField - ? $this->getApp()->uiPersistence->typecastAttributeLoadField($this->model->getField($fieldName), $value) - : $this->getApp()->uiPersistence->typecastLoadField($this->model->getField($fieldName), $value); - } - } + foreach ($row as $fieldName => $value) { + $res[$fieldName] = $fieldName === $this->model->idField + ? $this->getApp()->uiPersistence->typecastAttributeLoadField($this->model->getField($fieldName), $value) + : $this->getApp()->uiPersistence->typecastLoadField($this->model->getField($fieldName), $value); } return $res; @@ -348,23 +342,37 @@ private function invokeWithContainsXxxNormalizeHookIgnored(\Closure $fx): void }, null, Model::class)(); } + /** + * @return array{list>, list} + */ + private function decodeInput(string $json): array + { + $rowDataWithMlid = $this->getApp()->decodeJson($json); + $rowData = []; + $mlids = []; + foreach ($rowDataWithMlid as $row) { + $mlids[] = $row['__atkml']; + unset($row['__atkml']); + $rowData[] = $this->typecastUiLoadRow($row); + } + + return [$rowData, $mlids]; + } + #[\Override] public function setInputValue(string $value): void { - $this->rowData = $this->typecastUiLoadValues($this->getApp()->decodeJson($value)); + [$rowData, $mlids] = $this->decodeInput($value); + + $this->rowData = $rowData; if ($this->rowData !== []) { - $this->rowErrors = $this->validate($this->rowData); + $this->rowErrors = $this->validate($this->rowData, $mlids); if ($this->rowErrors !== []) { throw new ValidationException([$this->shortName => 'Multiline error']); } } - $rowsRaw = []; - foreach ($this->rowData as $k => $v) { - unset($v['__atkml']); - - $rowsRaw[$k] = $this->typecastContainedSaveRow($v); - } + $rowsRaw = array_map(fn ($v) => $this->typecastContainedSaveRow($v), $this->rowData); // mimic ContainsOne save format // https://github.com/atk4/data/blob/6.0.0/src/Reference/ContainsOne.php#L37 @@ -372,7 +380,7 @@ public function setInputValue(string $value): void if ($rowsRaw === []) { $value = ''; } else { - foreach ($rowsRaw as $k => $rowRaw) { // @phpstan-ignore foreach.keyOverwrite (https://github.com/phpstan/phpstan-strict-rules/issues/194) + foreach ($rowsRaw as $k => $rowRaw) { $idFieldRawName = $this->model->getIdField()->getPersistenceName(); if ($rowRaw[$idFieldRawName] === null) { $refModel = $this->entityField->getField()->hasReference() @@ -412,8 +420,8 @@ public function setInputValue(string $value): void * Add a callback when fields are changed. You must supply array of fields * that will trigger the callback when changed. * - * @param \Closure(mixed, Form): (JsExpressionable|View|string|void) $fx - * @param list $fields + * @param \Closure(list>, list, Form): (JsExpressionable|View|string|void) $fx + * @param list $fields */ public function onLineChange(\Closure $fx, array $fields): void { @@ -426,18 +434,19 @@ public function onLineChange(\Closure $fx, array $fields): void * Validate each row and return errors if found. * * @param list> $rows + * @param list $mlids * * @return array> */ - public function validate(array $rows): array + public function validate(array $rows, array $mlids): array { $rowErrors = []; $entity = $this->model->createEntity(); - foreach ($rows as $cols) { - $rowId = $cols['__atkml'] ?? null; + foreach ($rows as $i => $cols) { + $rowId = $mlids[$i]; foreach ($cols as $fieldName => $value) { - if ($fieldName === '__atkml' || $fieldName === $entity->idField) { + if ($fieldName === $entity->idField) { continue; } @@ -476,10 +485,6 @@ public function saveRows(): self ? $model->load($row[$model->idField]) : $model->createEntity(); foreach ($row as $fieldName => $value) { - if ($fieldName === '__atkml') { - continue; - } - if ($model->getField($fieldName)->isEditable()) { $entity->set($fieldName, $value); } @@ -811,10 +816,8 @@ private function outputJson(): void $this->getApp()->terminateJson(['success' => true, 'expressions' => $expressionValues]); // no break - expression above always terminate case 'on-change': - $rowsRaw = $this->getApp()->decodeJson($this->getApp()->getRequestPostParam('rows')); - $this->renderCallback->set(function () use ($rowsRaw) { - return ($this->onChangeFunction)($this->typecastUiLoadValues($rowsRaw), $this->form); - }); + [$rows, $mlids] = $this->decodeInput($this->getApp()->getRequestPostParam('rows')); + $this->renderCallback->set(fn () => ($this->onChangeFunction)($rows, $mlids, $this->form)); } }