*/ public function bindingsFor(FormField|FormFieldLibrary $owner): Collection { $type = $this->ownerTypeFor($owner); return FormFieldBinding::query() ->where('owner_type', $type) ->where('owner_id', $owner->getKey()) ->get(); } /** * Replace the set of bindings on an owner transactionally. Callers pass * an array of binding specs; each spec is validated against the entity- * column registry before anything is written. An empty array clears all * bindings for the owner (Pattern B). * * @param list $bindingData */ public function replaceBindings(FormField|FormFieldLibrary $owner, array $bindingData): void { foreach ($bindingData as $spec) { $this->assertSpecValid($spec); } $ownerType = $this->ownerTypeFor($owner); DB::transaction(function () use ($owner, $ownerType, $bindingData): void { FormFieldBinding::query() ->withoutGlobalScopes() ->where('owner_type', $ownerType) ->where('owner_id', $owner->getKey()) ->delete(); foreach ($bindingData as $spec) { FormFieldBinding::query()->withoutGlobalScopes()->create([ 'owner_type' => $ownerType, 'owner_id' => $owner->getKey(), 'target_entity' => $spec['target_entity'], 'target_attribute' => $spec['target_attribute'], 'mode' => $spec['mode'], 'sync_direction' => $spec['sync_direction'] ?? null, 'merge_strategy' => $spec['merge_strategy'] ?? FormFieldBindingMergeStrategy::Overwrite->value, 'trust_level' => $spec['trust_level'] ?? 50, 'is_identity_key' => $spec['is_identity_key'] ?? false, ]); } if ($owner instanceof FormField) { $owner->logFieldChange('field.bindings_replaced', [ 'count' => count($bindingData), ]); } }); } /** * Row-copy from a library entry to a freshly-inserted field (ARCH §6.7, * addendum Q3). Preserves every binding column; only owner_type / * owner_id are rewritten. */ public function copyBindings(FormFieldLibrary $from, FormField $to): void { $bindings = $this->bindingsFor($from); if ($bindings->isEmpty()) { return; } DB::transaction(function () use ($bindings, $to): void { foreach ($bindings as $binding) { FormFieldBinding::query()->withoutGlobalScopes()->create([ 'owner_type' => 'form_field', 'owner_id' => $to->id, 'target_entity' => $binding->target_entity, 'target_attribute' => $binding->target_attribute, 'mode' => $binding->mode instanceof FormFieldBindingMode ? $binding->mode->value : (string) $binding->mode, 'sync_direction' => $binding->sync_direction, 'merge_strategy' => $binding->merge_strategy instanceof FormFieldBindingMergeStrategy ? $binding->merge_strategy->value : (string) $binding->merge_strategy, 'trust_level' => (int) $binding->trust_level, 'is_identity_key' => (bool) $binding->is_identity_key, ]); } }); } /** * Serialise a binding row into the ARCH §6.3 JSON shape. Returned null * if no binding is given — callers can pipe directly into snapshot / * resource output (Pattern B = null). * * @return array{mode:string,entity:string,column:string,sync_direction?:string}|null */ public function toJsonShape(?FormFieldBinding $binding): ?array { if ($binding === null) { return null; } $mode = $binding->mode instanceof FormFieldBindingMode ? $binding->mode->value : (string) $binding->mode; $shape = [ 'mode' => $mode, 'entity' => $binding->target_entity, 'column' => $binding->target_attribute, ]; if ($binding->sync_direction !== null && $binding->sync_direction !== '') { $shape['sync_direction'] = $binding->sync_direction; } return $shape; } private function ownerTypeFor(FormField|FormFieldLibrary $owner): string { return $owner instanceof FormField ? 'form_field' : 'form_field_library'; } /** @param array $spec */ private function assertSpecValid(array $spec): void { $entity = (string) ($spec['target_entity'] ?? ''); $attribute = (string) ($spec['target_attribute'] ?? ''); $mode = (string) ($spec['mode'] ?? ''); if ($entity === '' || $attribute === '') { throw new \InvalidArgumentException( 'Binding spec requires target_entity and target_attribute.', ); } if (FormFieldBindingMode::tryFrom($mode) === null) { throw new \InvalidArgumentException( "Binding spec mode '{$mode}' is not a valid FormFieldBindingMode.", ); } if (array_key_exists('merge_strategy', $spec)) { $strategy = (string) $spec['merge_strategy']; if (FormFieldBindingMergeStrategy::tryFrom($strategy) === null) { throw new \InvalidArgumentException( "Binding spec merge_strategy '{$strategy}' is not a valid FormFieldBindingMergeStrategy.", ); } } $registry = (array) config('form_binding.'.$entity); if ($registry === []) { throw new \InvalidArgumentException( "Binding target_entity '{$entity}' is not registered in config/form_binding.php.", ); } if (! array_key_exists($attribute, $registry)) { throw new \InvalidArgumentException( "Binding target_attribute '{$entity}.{$attribute}' is not registered in config/form_binding.php.", ); } } }