Files
crewli/dev-docs/form-builder-getting-started.md
bert.hausmans cfc7610497 docs(forms): SCHEMA crosswalk, foundation concept page, getting-started + migration playbook, copy catalogue init
SCHEMA.md
- New §3.5.12 "Form Builder" with the legacy-tables-retained note
  placed prominently directly under the section header (per S1 wrap-up
  Path 3 decision: Phase 8 deferred to S2).
- Crosswalk: every legacy volunteer_profiles column → its new home
  (user_profiles columns vs form_fields vs person_tags).
- Summary table for the 13 new tables with one-line purpose + ARCH §
  pointer each.
- Activity log strategy and multi-tenancy discipline noted.
- §3.5.4 marked SUPERSEDED with a pointer to the new section.

/dev-docs/form-builder-migration-playbook.md (new)
- Operator runbook for forms:migrate-legacy-data on real legacy data.
- Pre-flight audit, dry-run, migrate, verify, spot-check, rollback
  paths spelled out. Same legacy-tables-retained note prominently.

/dev-docs/form-builder-getting-started.md (new)
- Developer onboarding. Mental model, code samples for creating a
  schema/field/submission/value, adding a new subject type, registering
  a custom field type, suppressing activity log via
  App\Support\ActivityLog::suppressed.

/dev-docs/COPY_CATALOGUE.md (new)
- Seeded verbatim from ARCH §30 (naming conventions, tooltip catalogue,
  warning catalogue) with a header explaining purpose, growth strategy,
  and the per-PR update workflow.

/docs/organizer/forms/concepts/wat-is-een-formulier.md (new VitePress)
- Dutch, informal je/jij. Follows /docs/.templates/concept-page.md.
- Three example use-cases: vrijwilligersregistratie, artist advance,
  incidentrapportage. Light foundation; depth arrives in S2-S5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:06:53 +02:00

231 lines
7.0 KiB
Markdown

# 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.