1010use Atk4 \Data \Field \SqlExpressionField ;
1111use Atk4 \Data \Model ;
1212use Atk4 \Data \Persistence ;
13- use Atk4 \Data \Reference \ContainsOne ;
1413use Atk4 \Data \ValidationException ;
1514use Atk4 \Ui \Form ;
1615use 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