# Form Builder — Developer Getting Started > Onboarding for developers writing code against the new form-builder > tables. Companion to `/dev-docs/ARCH-FORM-BUILDER.md` v1.2 (the > authoritative spec) and `/dev-docs/SCHEMA.md` §3.5.12. --- ## Prerequisites - Laravel 12 / PHP 8.2+. - Familiarity with Eloquent, ULIDs, polymorphic relationships. - Read ARCH §0 (TL;DR) and §3 (FormPurpose catalogue). --- ## The mental model in one paragraph A `FormSchema` is a form definition. `FormFields` belong to it. A `FormSubmission` is one filled-in instance of a schema, identified by its polymorphic subject (a Person, User, Company, Organisation, Event, or nothing for public forms). `FormValues` are the EAV row per `(submission, field)` pair, JSON payload + typed columns populated by an observer based on the field's storage hint and filterability. Every schema declares its purpose (`event_registration`, `incident_report`, …) which constrains lifecycle and integrations. --- ## Creating a new schema with two fields ```php use App\Enums\FormBuilder\FormFieldType; use App\Enums\FormBuilder\FormPurpose; use App\Enums\FormBuilder\FormSubmissionMode; use App\Models\Event; use App\Models\FormBuilder\FormField; use App\Models\FormBuilder\FormSchema; $event = Event::find($eventId); $schema = FormSchema::create([ 'organisation_id' => $event->organisation_id, 'owner_type' => 'event', 'owner_id' => $event->id, 'name' => "{$event->name} — registratie", 'slug' => "{$event->slug}-registratie", 'purpose' => FormPurpose::EVENT_REGISTRATION, 'submission_mode' => FormSubmissionMode::DRAFT_SINGLE, 'is_published' => false, 'locale' => 'nl', ]); FormField::create([ 'form_schema_id' => $schema->id, 'field_type' => FormFieldType::SELECT->value, 'slug' => 'shirtmaat', 'label' => 'Shirtmaat', 'options' => ['XS', 'S', 'M', 'L', 'XL', 'XXL'], 'is_required' => true, 'is_filterable' => true, 'value_storage_hint' => FormFieldType::SELECT->recommendedValueStorageHint(), 'sort_order' => 1, ]); FormField::create([ 'form_schema_id' => $schema->id, 'field_type' => FormFieldType::TEXTAREA->value, 'slug' => 'opmerkingen', 'label' => 'Opmerkingen', 'value_storage_hint' => FormFieldType::TEXTAREA->recommendedValueStorageHint(), 'sort_order' => 2, ]); ``` Then to log a meaningful change: ```php $schema->is_published = true; $schema->save(); $schema->logSchemaChange('schema.published'); ``` Trivial label/help_text edits should NOT call `logSchemaChange` — see ARCH §17.1 for the curated event list. --- ## Creating a submission and its values ```php use App\Enums\FormBuilder\FormSubmissionStatus; use App\Models\FormBuilder\FormSubmission; use App\Models\FormBuilder\FormValue; $person = \App\Models\Person::find($personId); $submission = FormSubmission::create([ 'form_schema_id' => $schema->id, 'subject_type' => 'person', 'subject_id' => $person->id, 'submitted_by_user_id' => $person->user_id, 'status' => FormSubmissionStatus::SUBMITTED, 'submitted_at' => now(), 'is_test' => false, ]); // One value per field. The shape of `value` is JSON-wrapped per field type. FormValue::create([ 'form_submission_id' => $submission->id, 'form_field_id' => $shirtmaatField->id, 'value' => ['value' => 'L'], ]); FormValue::create([ 'form_submission_id' => $submission->id, 'form_field_id' => $opmerkingenField->id, 'value' => ['value' => 'Graag een nachtshift'], ]); ``` `FormValueObserver` automatically populates `value_indexed` for filterable single-value fields, `value_number` for NUMBER hint, `value_date` for DATE hint, `value_bool` for BOOL hint, and the `form_value_options` pivot for multi-value filterable fields (MULTISELECT / CHECKBOX_LIST / TAG_PICKER). You don't write to those columns directly. --- ## Adding a new subject type Three places to update — keep them in sync: 1. **Morph map** (`app/Providers/AppServiceProvider.php`): ```php Relation::enforceMorphMap([ // …existing entries… 'artist' => \App\Models\Artist::class, ]); ``` 2. **Subject registry** (`config/form_subjects.php`): ```php 'artist' => [ 'model' => \App\Models\Artist::class, 'display_attribute' => 'name', 'permission_check' => \App\Policies\ArtistPolicy::class.'@view', ], ``` 3. **Reverse relation on the model** itself: ```php public function formSubmissions(): MorphMany { return $this->morphMany(\App\Models\FormBuilder\FormSubmission::class, 'subject'); } ``` The verifier (`forms:verify-data-integrity`) cross-checks subject_type against the `config/form_subjects.php` keys; an unknown subject_type will fail the submission-coherence check. --- ## Registering a custom field type The `field_type` column on `form_fields` is a string, not a DB enum, so custom types can be added at runtime via configuration. Under the hood this is the `CustomFieldTypeRegistry` planned for ARCH §17.2 — for now, register the type's value in `config/form_builder.php`: ```php 'custom_field_types' => [ 'COLOR_PICKER' => [ 'label' => 'Kleurkeuze', 'storage_hint' => 'string', 'filterable' => true, ], ], ``` The verifier accepts any field_type that's either in `FormFieldType::values()` or in `config('form_builder.custom_field_types')`. Frontend rendering and validation handlers come in S6 when the registry interface lands. --- ## Suppressing activity log during bulk operations `logSchemaChange` and `logFieldChange` produce one activity-log row per call. In bulk fixture runs (DevSeeder, the data-migration command, import scripts) this floods `activity_log` with hundreds of rows that provide no audit value. Wrap the bulk operation: ```php use App\Support\ActivityLog; ActivityLog::suppressed(function () use ($schemas): void { foreach ($schemas as $schemaData) { $schema = FormSchema::create($schemaData); // logSchemaChange calls inside this closure are silent no-ops. } }); ``` The helper flips `config('activitylog.enabled')` for the duration of the callback and restores it in `finally`. Both our explicit calls AND the spatie `LogsActivity` trait (used on `Organisation` elsewhere) respect the flag via `ActivityLogger::log()`. Do NOT use this in regular request paths — the activity log is the audit trail; suppressing it silently is an antipattern outside of fixtures and one-shot commands. --- ## Where to look next - **ARCH-FORM-BUILDER.md §4** — full column specs for every table. - **ARCH-FORM-BUILDER.md §6** — binding patterns (entity-owned / form-owned / mirrored). - **ARCH-FORM-BUILDER.md §7** — filter architecture and the `FilterQueryBuilder` interface (S4). - **`config/form_binding.php`** — Entity Column Registry (Pattern A/C binding targets). - **`config/form_subjects.php`** — Subject Type Registry. - **`config/form_builder.php`** — limits, webhook policy, captcha, retention, feature flags. - **`forms:migrate-legacy-data`** + **`forms:verify-data-integrity`** — see the migration playbook in this directory for the operator's view.