$slugToValue */ public function upsertMany(FormSubmission $submission, array $slugToValue, ?User $actor): void { $schema = $submission->schema; $fields = FormField::query() ->where('form_schema_id', $schema->id) ->whereIn('slug', array_keys($slugToValue)) ->get() ->keyBy('slug'); DB::transaction(function () use ($slugToValue, $fields, $submission, $actor): void { foreach ($slugToValue as $slug => $raw) { $field = $fields->get($slug); if ($field === null) { continue; } if ($actor === null) { // Public submission path: portal-visible non-admin fields only. if (! (bool) $field->is_portal_visible || (bool) $field->is_admin_only) { throw new AuthorizationException(sprintf('Not allowed to write field "%s" on public submission.', $slug)); } } elseif (! $this->fieldAccess->canWrite($actor, $field, $submission)) { throw new AuthorizationException(sprintf('Not allowed to write field "%s".', $slug)); } $this->writeValue($submission, $field, $raw); $this->writeEntityMirror($submission, $field, $raw); } }); } private function writeValue(FormSubmission $submission, FormField $field, mixed $raw): void { $payload = $this->normalisePayload($field, $raw); /** @var FormValue|null $value */ $value = FormValue::query() ->where('form_submission_id', $submission->id) ->where('form_field_id', $field->id) ->first(); if ($value === null) { $value = new FormValue; $value->form_submission_id = $submission->id; $value->form_field_id = $field->id; } $value->setRelation('field', $field); $value->value = $payload; $value->value_anonymised = false; $value->save(); } private function normalisePayload(FormField $field, mixed $raw): mixed { $multi = in_array($field->field_type, [ FormFieldType::MULTISELECT->value, FormFieldType::CHECKBOX_LIST->value, FormFieldType::TAG_PICKER->value, FormFieldType::AVAILABILITY_PICKER->value, FormFieldType::SECTION_PRIORITY->value, FormFieldType::TABLE_ROWS->value, ], true); if ($multi) { return is_array($raw) ? array_values($raw) : []; } return $raw; } private function writeEntityMirror(FormSubmission $submission, FormField $field, mixed $raw): void { $binding = $field->binding; if (! is_array($binding) || ($binding['mode'] ?? null) === null) { return; } $mode = (string) $binding['mode']; if (! in_array($mode, ['entity_owned', 'mirrored'], true)) { return; } $entity = (string) ($binding['entity'] ?? ''); $column = (string) ($binding['column'] ?? ''); if ($entity === '' || $column === '') { return; } $registry = config('form_binding.'.$entity); if (! is_array($registry) || ! isset($registry[$column]) || ! ($registry[$column]['writable'] ?? false)) { return; } $target = $this->resolveEntityTarget($submission, $entity); if ($target === null) { // Cross-entity Pattern C (person → user_profile) may have null user_id. Log::info('form-builder.mirror.skipped', [ 'submission_id' => $submission->id, 'field_id' => $field->id, 'entity' => $entity, 'column' => $column, 'reason' => 'target_not_resolvable', ]); return; } $scalar = is_scalar($raw) ? $raw : null; $target->{$column} = $scalar; $target->save(); } private function resolveEntityTarget(FormSubmission $submission, string $entity): ?\Illuminate\Database\Eloquent\Model { $subjectType = $submission->subject_type; $subjectId = $submission->subject_id; if ($subjectId === null) { return null; } if ($subjectType === $entity) { $map = config('form_subjects'); $model = $map[$entity]['model'] ?? null; if ($model === null) { return null; } return $model::withoutGlobalScopes()->find($subjectId); } // Cross-entity: person → user_profile via person.user_id if ($entity === 'user_profile' && $subjectType === 'person') { $person = \App\Models\Person::withoutGlobalScopes()->find($subjectId); if ($person === null || $person->user_id === null) { return null; } return \App\Models\UserProfile::firstOrCreate(['user_id' => $person->user_id]); } if ($entity === 'user_profile' && $subjectType === 'user') { return \App\Models\UserProfile::firstOrCreate(['user_id' => $subjectId]); } return null; } }