@@ -161,8 +161,8 @@ class Multiline extends Form\Control
161161 /** @var list<string> The fields names used in each row. */
162162 public $ rowFields ;
163163
164- /** @var list<array<string, mixed>> The data sent for each row . */
165- protected $ rowData ;
164+ /** The changes set by self::setInputValue() . */
165+ public TheirChanges $ changes ;
166166
167167 /** @var int The max number of records (rows) that can be added to Multiline. 0 means no limit. */
168168 public $ rowLimit = 0 ;
@@ -276,17 +276,17 @@ private function isOneToOne(): bool
276276 #[\Override]
277277 public function getInputValue (): string
278278 {
279- $ refModelOrEntity = $ this ->entityField ->getField ()->hasReference ()
279+ $ theirModelOrEntity = $ this ->entityField ->getField ()->hasReference ()
280280 ? $ this ->entityField ->getField ()->getReference ()->ref ($ this ->entityField ->getEntity ())
281281 : $ this ->model ;
282- if ($ refModelOrEntity ->isEntity ()) {
283- $ refModelOrEntity = $ refModelOrEntity ->isLoaded ()
284- ? [$ refModelOrEntity ]
282+ if ($ theirModelOrEntity ->isEntity ()) {
283+ $ theirModelOrEntity = $ theirModelOrEntity ->isLoaded ()
284+ ? [$ theirModelOrEntity ]
285285 : [];
286286 }
287287
288288 $ rows = [];
289- foreach ($ refModelOrEntity as $ row ) {
289+ foreach ($ theirModelOrEntity as $ row ) {
290290 $ rows [] = $ row ->get ();
291291 }
292292
@@ -295,53 +295,6 @@ public function getInputValue(): string
295295 return $ this ->getApp ()->encodeJson ($ rowsUi );
296296 }
297297
298- /**
299- * Same as Persistence::typecastSaveRow() but allow null in not-nullable fields.
300- *
301- * @param array<string, mixed> $row
302- *
303- * @return array<string, scalar|null>
304- */
305- private function typecastContainedSaveRow (array $ row ): array
306- {
307- $ res = [];
308- foreach ($ row as $ fieldName => $ value ) {
309- $ field = $ this ->model ->getField ($ fieldName );
310-
311- $ res [$ field ->getPersistenceName ()] = $ value === null
312- ? null
313- : $ this ->model ->getPersistence ()->typecastSaveField ($ field , $ value );
314- }
315-
316- return $ res ;
317- }
318-
319- /**
320- * @param \Closure(): void $fx
321- */
322- private function invokeWithContainsXxxNormalizeHookIgnored (\Closure $ fx ): void
323- {
324- // TOD hack to supress "Contained model data cannot be modified directly" exception
325- // https://github.com/atk4/data/blob/fca1cd55b0/src/Reference/ContainsBase.php#L58-L81
326- $ ourModel = $ this ->entityField ->getModel ();
327- \Closure::bind (static function () use ($ fx , $ ourModel ) {
328- $ spot = Model::HOOK_NORMALIZE ;
329- $ priority = \PHP_INT_MIN ;
330- $ normalizeHooks = $ ourModel ->hooks [$ spot ][$ priority ] ?? [];
331- foreach (array_keys ($ normalizeHooks ) as $ hookIndex ) {
332- $ ourModel ->hooks [$ spot ][$ priority ][$ hookIndex ][0 ] = static fn () => null ;
333- }
334-
335- try {
336- $ fx ();
337- } finally {
338- foreach (array_keys ($ normalizeHooks ) as $ hookIndex ) {
339- $ ourModel ->hooks [$ spot ][$ priority ][$ hookIndex ][0 ] = $ normalizeHooks [$ hookIndex ][0 ];
340- }
341- }
342- }, null , Model::class)();
343- }
344-
345298 /**
346299 * @return array{list<array<string, mixed>>, list<string>}
347300 */
@@ -353,6 +306,13 @@ private function decodeInput(string $json): array
353306 foreach ($ rowDataWithMlid as $ row ) {
354307 $ mlids [] = $ row ['__atkml ' ];
355308 unset($ row ['__atkml ' ]);
309+
310+ foreach ($ row as $ k => $ v ) {
311+ if ($ this ->model ->getField ($ k )->readOnly ) {
312+ unset($ row [$ k ]);
313+ }
314+ }
315+
356316 $ rowData [] = $ this ->typecastUiLoadRow ($ row );
357317 }
358318
@@ -364,56 +324,41 @@ public function setInputValue(string $value): void
364324 {
365325 [$ rowData , $ mlids ] = $ this ->decodeInput ($ value );
366326
367- $ this ->rowData = $ rowData ;
368- if ($ this ->rowData !== []) {
369- $ this ->rowErrors = $ this ->validate ($ this ->rowData , $ mlids );
327+ if ($ rowData !== []) {
328+ $ this ->rowErrors = $ this ->validate ($ rowData , $ mlids );
370329 if ($ this ->rowErrors !== []) {
371330 throw new ValidationException ([$ this ->shortName => 'Multiline error ' ]);
372331 }
373332 }
374333
375- $ rowsRaw = array_map (fn ($ v ) => $ this ->typecastContainedSaveRow ($ v ), $ this ->rowData );
334+ $ theirModelOrEntity = $ this ->entityField ->getField ()->hasReference ()
335+ ? $ this ->entityField ->getField ()->getReference ()->ref ($ this ->entityField ->getEntity ())
336+ : $ this ->model ;
376337
377- // mimic ContainsOne save format
378- // https://github.com/atk4/data/blob/6.0.0/src/Reference/ContainsOne.php#L37
379- // TODO replace with something like "schedule model save task" and then drop self::saveRows() method
380- if ($ rowsRaw === []) {
381- $ value = '' ;
382- } else {
383- foreach ($ rowsRaw as $ k => $ rowRaw ) {
384- $ idFieldRawName = $ this ->model ->getIdField ()->getPersistenceName ();
385- if ($ rowRaw [$ idFieldRawName ] === null ) {
386- $ refModel = $ this ->entityField ->getField ()->hasReference ()
387- ? $ this ->entityField ->getField ()->getReference ()->ref ($ this ->entityField ->getEntity ())->getModel (true )
388- : $ this ->model ;
389-
390- // TODO to pass Behat tests purely
391- if (!$ this ->entityField ->getField ()->hasReference () && $ this ->entityField ->getField ()->type !== 'json ' && !$ refModel ->getPersistence () instanceof Persistence \Array_) {
392- continue ;
393- }
338+ $ changes = new TheirChanges ();
394339
395- $ idField = $ refModel ->getIdField ();
396- $ origIdFieldType = $ idField ->type ;
397- $ idField ->type = 'bigint ' ;
398- try {
399- $ rowsRaw [$ k ][$ idFieldRawName ] = Persistence \Array_::assertInstanceOf ($ refModel ->getPersistence ())->generateNewId ($ refModel );
400- } finally {
401- $ idField ->type = $ origIdFieldType ;
402- }
403- }
404- }
340+ // TODO this is dangerous, deleted row IDs should be passed from UI
341+ $ idsToDelete = array_filter (array_column ($ rowData , $ theirModelOrEntity ->idField ), static fn ($ v ) => $ v !== null );
342+ foreach ($ theirModelOrEntity ->getModel (true )->createIteratorBy ($ theirModelOrEntity ->idField , 'not in ' , $ idsToDelete ) as $ entity ) {
343+ $ changes ->deletes [] = [$ theirModelOrEntity ->idField => $ entity ->getId ()];
344+ }
405345
406- if ($ this ->isOneToOne ()) {
407- assert (count ($ rowsRaw ) === 1 );
408- $ rowsRaw = array_first ($ rowsRaw );
346+ foreach ($ rowData as $ row ) {
347+ if ($ row [$ theirModelOrEntity ->idField ] === null ) {
348+ $ changes ->inserts [] = $ row ;
349+ } else {
350+ $ changes ->updates [] = [[$ theirModelOrEntity ->idField => $ row [$ theirModelOrEntity ->idField ]], $ row ];
409351 }
410-
411- $ value = $ this ->getApp ()->encodeJson ($ rowsRaw );
412352 }
413353
414- $ this ->invokeWithContainsXxxNormalizeHookIgnored (function () use ($ value ) {
415- parent ::setInputValue ($ value );
416- });
354+ $ this ->changes = $ changes ;
355+
356+ if ($ this ->entityField ->getField ()->hasReference ()) {
357+ $ changes ->saveOnSave (
358+ $ this ->entityField ->getEntity (),
359+ $ this ->entityField ->getField ()->getReference ()
360+ );
361+ }
417362 }
418363
419364 /**
@@ -466,36 +411,11 @@ public function validate(array $rows, array $mlids): array
466411 return $ rowErrors ;
467412 }
468413
469- /**
470- * @return $this
471- */
472- public function saveRows (): self
414+ public function saveRows (): void
473415 {
474- $ model = $ this ->model ;
475-
476- // delete removed rows
477- // TODO this is dangerous, deleted row IDs should be passed from UI
478- $ idsToDelete = array_filter (array_column ($ this ->rowData , $ model ->idField ), static fn ($ v ) => $ v !== null );
479- foreach ($ model ->createIteratorBy ($ model ->idField , 'not in ' , $ idsToDelete ) as $ entity ) {
480- $ entity ->delete ();
481- }
482-
483- foreach ($ this ->rowData as $ row ) {
484- $ entity = $ row [$ model ->idField ] !== null
485- ? $ model ->load ($ row [$ model ->idField ])
486- : $ model ->createEntity ();
487- foreach ($ row as $ fieldName => $ value ) {
488- if ($ model ->getField ($ fieldName )->isEditable ()) {
489- $ entity ->set ($ fieldName , $ value );
490- }
491- }
492-
493- if (!$ entity ->isLoaded () || $ entity ->getDirtyRef () !== []) {
494- $ entity ->save ();
495- }
496- }
416+ assert (!$ this ->entityField ->getField ()->hasReference ());
497417
498- return $ this ;
418+ $ this -> changes -> saveTo ( $ this -> model ) ;
499419 }
500420
501421 /**
0 commit comments