Sections 1-5, 10, 11 written in full. Sections 6-9 stubbed with session-2/3 markers and RFC references. Out-of-scope items §10 explicit. Refs: RFC-WS-6.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.5 KiB
ARCH-BINDINGS.md — Form Builder Binding Pipeline
Status
- v0.1 (skeleton) — 2026-04-25
- Owner: Bert
- Authoritative for the binding pipeline architecture, complementing ARCH-FORM-BUILDER.md §17 and §31.
- Decisions originate from RFC-WS-6.md.
1. Scope
The binding pipeline turns a submitted form (form_submissions) into
durable writes against domain entities — Person, Artist, Company, User
— via per-field bindings configured in form_field_bindings (WS-5a).
This document describes the end-to-end flow from
FormSubmissionSubmitted event to the post-pipeline state where the
domain has been updated, identity-match has been triggered, queued
listeners (tag sync, shifts, webhooks, mail) have fired, and any
binding-level failures have been recorded as
form_submission_action_failures rows.
The library is a template-source, not a live link. When a
form_field is created from a FormFieldLibrary entry with bindings,
the library's bindings are copied into form_field_bindings rows
with owner_type = 'form_field'. Library updates do not propagate to
existing field instances (RFC §3 Q11). Future re-sync admin action
tracked in BACKLOG: FORM-LIBRARY-RESYNC.
2. Schema
2.1 form_field_bindings (WS-5a)
Polymorphic owner (owner_type ∈ {form_field, form_field_library},
owner_id ULID), target_entity × target_attribute columns,
merge_strategy (Overwrite/Append/Replace/FirstWriteWins), trust_level
(0-100, default 50), is_identity_key bool. UNIQUE on
(owner_type, owner_id, target_entity, target_attribute). See
SCHEMA.md §3.5.12.
2.2 form_submissions.apply_status / apply_completed_at (WS-6)
Two new columns added in 2026_04_25_140000_extend_form_submissions_with_apply_status:
apply_status— string(20) nullable, no default. Values:pending|completed|partial|failed. Drives admin "open work" filtering. NULL legacy rows are excluded from open-work views by design (RFC O1).apply_completed_at— timestamp nullable, set when applicator finishes (success or fail).
Indexed on (form_schema_id, apply_status) and
(organisation_id, apply_status) for dashboard queries.
2.3 form_submission_action_failures (WS-6)
Audit table for binding-pipeline failures (RFC §3 Q5). One row per
listener invocation that failed, with retry / resolve / dismiss state
fields. No organisation_id column — tenant scope flows via
form_submission_id → form_submissions.organisation_id. Cascade-delete
through the parent submission. Resolve and Dismiss are mutually
exclusive workflows (RFC V2): a failure has at most one of
resolved_at or dismissed_at.
Full DDL in SCHEMA.md §3.5.12.
3. Value objects and enums
App\FormBuilder\Bindings\ (final readonly classes):
ResolvedBinding— output of session 2'sBindingConflictResolver. One winning binding per(target_entity, target_attribute)group. CarriesvalueIsExplicitto distinguish "user explicitly cleared" from "field skipped by conditional logic" (RFC §3 Q7).BindingApplicationResult— result of applying one resolved binding. Sealed viasucceeded()/failed()named constructors so consumers cannot synthesise impossible states.BindingPassResult— aggregate result of one applicator pass.applyStatus()maps toApplyStatusenum per RFC §3 Q4 rules: empty applications →COMPLETED, all OK →COMPLETED, all failed →FAILED, mixed →PARTIAL.BindingTargetMeta— single config row fromconfig/form_builder/binding_targets.php.
Enums in App\Enums\FormBuilder\:
ApplyStatus(RFC §3 Q4) —PENDING|COMPLETED|PARTIAL|FAILED. Helpers:isTerminal(),isOpen(),label().DismissalReasonType(RFC §4 V2) — six cases.manually_resolvedis intentionally absent: Resolve and Dismiss are different workflows.BindingTargetType(RFC §4 V1) —SCALAR|COLLECTION|RELATION. Storage shape only; PHP type and identity-key eligibility live onBindingTargetMeta.FormFieldBindingMergeStrategy(existing, WS-5a) — extended in WS-6 withnullWinnerBehaviour()andisValidForScalarTargets().
4. BindingTypeRegistry
App\FormBuilder\Bindings\BindingTypeRegistry is the single source of
truth for the storage shape of binding-target attributes. Config-driven
(config/form_builder/binding_targets.php), singleton-bound in
AppServiceProvider.
Public surface:
resolve(entity, attribute): BindingTargetMetaisKnown(entity, attribute): boolisIdentityKeyEligible(entity, attribute): boolentities(): list<string>attributesFor(entity): list<string>validateAppendStrategy(entity, attribute, strategy)— throwsInvalidBindingTargetExceptionwhenstrategy=Appendis paired with a non-COLLECTION target.
The registry is not name-suffix matching (e.g. "ends with _tags").
Convention-not-contract is rejected because it silently misclassifies
attributes that don't follow the convention or accidentally match it.
5. PublishGuard framework
Per-purpose schema validation. FormSchemaService::publish() calls,
in order:
assertRequiredBindingsPresent()(existing) — every required binding path on the purpose'srequired_bindingslist is bound. ThrowsPurposeRequirementsNotMetException.assertPublishGuardsSatisfied()(new) — every guard returned by the purpose'sPurposeGuardProviderevaluates to passed. ThrowsPublishGuardViolationExceptioncarrying ALL violations sorted lexicographically bycode().
Guard catalog
| Class | code() | Universal? |
|---|---|---|
RequiresIdentityKeyBinding(entity, attribute) |
requires_identity_key_binding:{entity}:{attribute} |
no |
MaxOneIdentityKeyPerTargetEntity |
max_one_identity_key_per_target_entity |
yes |
RequiresFieldType(type, minCount) |
requires_field_type:{type} |
no |
SchemaHasLinkedEvent |
schema_has_linked_event |
sub-guard |
TagCategoriesConfiguredOnAllPickers |
tag_categories_configured_on_all_pickers |
sub-guard |
IdentityKeyBindingsOnlyInFirstSection |
identity_key_bindings_only_in_first_section |
yes |
AppendStrategyRequiresCollectionTarget |
append_strategy_requires_collection_target |
yes |
NoAmbiguousTrustLevels |
no_ambiguous_trust_levels |
yes |
ConditionalRequirement(predicate, sub, code) |
conditional:{caller-supplied} |
composer |
Per-purpose providers
| Purpose | Provider | Purpose-specific guards |
|---|---|---|
event_registration |
EventRegistrationGuards |
RequiresIdentityKeyBinding(person, email), RequiresFieldType(EMAIL,1), Conditional(AVAILABILITY_PICKER → SchemaHasLinkedEvent), Conditional(TAG_PICKER → TagCategoriesConfiguredOnAllPickers) |
artist_advance |
ArtistAdvanceGuards |
(universal only — artist resolved via portal token) |
supplier_intake |
SupplierIntakeGuards |
(universal only — company via production_request) |
post_event_evaluation |
PostEventEvaluationGuards |
(universal only — person via auth) |
incident_report |
IncidentReportGuards |
(universal only — anonymous-allowed) |
signature_contract |
SignatureContractGuards |
(universal only — user via auth) |
user_profile |
UserProfileGuards |
(universal only — user via auth) |
Universal guards (MaxOneIdentityKeyPerTargetEntity,
AppendStrategyRequiresCollectionTarget, NoAmbiguousTrustLevels,
IdentityKeyBindingsOnlyInFirstSection) wire into every purpose. The
section guard is a cheap no-op when section_level_submit=false, but
remains active at publish time for schemas that flip it on later.
i18n message keys live in
api/lang/nl/form_builder_publish_guards.php. Dutch only for v1.
6. Apply pipeline
6.1 Snapshot vs. live (RFC Q6)
Populated in session 2 — see RFC-WS-6.md §3 Q6.
6.2 Conflict resolution (RFC Q7)
Populated in session 2 — see RFC-WS-6.md §3 Q7.
6.3 Apply algorithm and merge-strategy null matrix (RFC V1, Q7)
Populated in session 2 — see RFC-WS-6.md §4 V1 + §3 Q7.
6.4 Person provisioning (RFC Q8)
Populated in session 2 — see RFC-WS-6.md §3 Q8.
6.5 Per-purpose subject resolution (RFC Q9)
Populated in session 2 — see RFC-WS-6.md §3 Q9.
6.6 Section-level apply stub (RFC Q10)
Populated in session 2 — see RFC-WS-6.md §3 Q10.
6.7 Activity log granularity (RFC Q12)
Populated in session 2 — see RFC-WS-6.md §3 Q12.
7. Failures and retry
7.1 Two-transaction pattern (RFC Q4)
Populated in session 2 — see RFC-WS-6.md §3 Q4.
7.2 Retry, resolve, dismiss flows (RFC V2)
Populated in session 2 — see RFC-WS-6.md §4 V2.
8. Multi-tenancy and security
8.1 FK-chain tenant resolution (RFC V3)
Populated in session 2 — see RFC-WS-6.md §4 V3.
8.2 IDOR class tests
Populated in session 3 — see RFC-WS-6.md §4 V3.
9. Listener chain
Populated in session 2 — see RFC-WS-6.md §3 Q1, Q2, Q3.
10. Out of scope (v1)
- Composite identity-key resolution (single-key only) — BACKLOG:
FORM-BINDING-COMPOSITE-IDENTITY - Library-binding runtime cascade — BACKLOG:
FORM-LIBRARY-RESYNC - Append on scalar targets — collection-only by design (RFC V1)
- Active section-level apply — stub only, activated when
ARTIST_ADVANCE feature work begins (BACKLOG:
ARTIST-ADV-SECTION-APPLY) - Daily failure digest mailable — depends on notification framework
- Wall-clock concurrent load testing — BACKLOG:
LOAD-TEST-FOUNDATION
11. Related docs
RFC-WS-6.md— design decisionsARCH-FORM-BUILDER.md§17, §31ARCH-CONSOLIDATION-2026-04.md§6.1, §6.2SCHEMA.md§3.5.12