Files
crewli/dev-docs/form-builder-migration-playbook.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

7.0 KiB
Raw Permalink Blame History

Form Builder — Migration Playbook

Operator runbook for migrating legacy registration_form_fields / person_field_values / registration_field_templates data into the new form_* structure. Companion to /dev-docs/ARCH-FORM-BUILDER.md §11.


Status (post-S1)

Legacy tables retained intentionally. The tables registration_form_fields, person_field_values, and registration_field_templates remain in the schema through end of S1. They are dropped atomically in the first S2 commit together with the removal of legacy controllers, services, requests, resources, policies, and routes. DevSeeder and FormBuilderDevSeeder no longer write to them; they hold zero rows in dev but the schema is preserved for environments with real legacy data that will be migrated via forms:migrate-legacy-data.

Until S2 lands, the legacy registration UI remains functional for any environment that depends on it.


When to use this playbook

Run this when promoting an environment that holds real legacy registration data into the new form-builder schema. For development machines starting from migrate:fresh --seed, no migration is needed — DevSeeder produces new-structure data directly.


Pre-flight audit

Before touching any data, confirm the working state matches expectations.

# 1. Working tree clean?
git status

# 2. On the expected branch?
git log -1 --oneline

# 3. Database backup exists? (production / staging only — your call for dev)

# 4. Verify the new tables are present and the legacy tables still hold data
php artisan db:show | grep -E "form_|registration_form_fields|person_field_values|registration_field_templates"

If the legacy tables are absent, the migration command will detect this and exit cleanly without doing work — there is nothing to migrate.


Step 1 — Dry run

Always start with a dry run. It writes nothing and prints the same summary table the real run would produce.

php artisan forms:migrate-legacy-data --dry-run

What to look for:

  • The per-organisation log lines list every event that will get a new schema and the number of fields/submissions per event.
  • The summary table shows projected counts for form_schemas, form_fields, form_submissions, form_values, form_templates.
  • Exit code is 0.

If anything looks surprising — wildly off counts, organisations missing, templates duplicated — STOP and investigate. Do not proceed to step 2 until the dry-run output is what you expect.


Step 2 — Run the migration

php artisan forms:migrate-legacy-data

What happens:

  • Per organisation, inside a transaction (one bad org rolls back only itself).
  • Per event in that org: one form_schemas row + N form_fields rows.
  • Per person with person_field_values for that event: one form_submissions row (status = submitted if person.status ∈ applied/approved/no_show else draft) + one form_values row per legacy value.
  • FormValueObserver populates typed columns and the form_value_options pivot during the inserts (per ARCH §7.2).
  • All registration_field_templates rows are wrapped in the ARCH §4.6.1 schema_snapshot shape and inserted as form_templates.
  • The whole run is wrapped in App\Support\ActivityLog::suppressed(...) so the activity log isn't flooded by hundreds of logSchemaChange / logFieldChange entries from the bulk inserts.
  • After migration completes, verification runs automatically. Final exit code is whatever forms:verify-data-integrity returns.

Idempotent: if a form_schemas row already exists for (org, event, event_registration), that event is skipped silently. Safe to re-run.


Step 3 — Verify

Verification runs automatically at the end of step 2. To re-run it standalone (or to verify state without touching data):

php artisan forms:verify-data-integrity

Or, equivalently:

php artisan forms:migrate-legacy-data --verify-only

Nine checks:

  1. Schema coherence (purpose enum, custom_purpose_slug ⇄ purpose=custom, public_token uniqueness)
  2. Field coherence (FK valid, field_type ∈ FormFieldType custom registry, is_filterable matches type, slug unique within schema, binding JSON valid against config/form_binding.php)
  3. Submission coherence (subject_type/subject_id consistency, subject_type ∈ config/form_subjects.php, status ⇄ submitted_at)
  4. Value coherence (orphans, unique pair, length, multi-value sanity, value_indexed only set when field.is_filterable)
  5. User profile coherence (every user has one profile; reliability_score ∈ [0, 5])
  6. Data migration counts (skipped when legacy tables absent)
  7. Orphan records (pivot rows, section statuses)
  8. Section/schema relation consistency
  9. (--strict) Strict reachability: value.field.schema must equal value.submission.schema

Exit code 0 means all checks passed. Anything else is a real failure worth investigating before proceeding.


Step 4 — Spot-check a sample

After verification is green, eyeball a sample submission to confirm the shape is right.

php artisan tinker
>>> use App\Models\FormBuilder\FormSubmission;
>>> $sub = FormSubmission::with(['schema', 'values.field'])->latest()->first();
>>> $sub->schema->name;
>>> $sub->values->map(fn($v) => [$v->field->slug => $v->value]);

Compare against the legacy view of the same person's data in person_field_values to confirm nothing was silently lost.


Rollback

The data-migration command does not have a down() step. If a migration went wrong, the recovery path is:

  1. Restore from backup (production / staging).
  2. For dev: php artisan migrate:fresh --seed rebuilds everything from scratch.

The legacy tables are preserved during the migration — they are not modified. So even after running forms:migrate-legacy-data, the legacy data is still there (until S2 drops the tables). This means a production data-migration error can be recovered by truncating the new form_* tables and re-running with the underlying legacy data still intact.


Common gotchas

  • registration_form_fields table absent: the command detects this and exits 0 silently. Expected behaviour for fresh environments.
  • PII heuristic miss: the heuristic in §11.2.1 of the ARCH covers obvious patterns (email, phone, geboort, allergie, …) but is not exhaustive. After migration, organisers can fine-tune is_pii per field via the form-builder UI (S5).
  • is_published derived from event status: the command considers an event "published" when its status is one of registration_open / buildup / showday / teardown / closed. If the legacy registration was open on events with other statuses, adjust is_published manually after migration.
  • Custom field types: the command maps the 10 legacy field types (text/textarea/select/multiselect/checkbox/radio/boolean/number/tag_picker/heading) into the new uppercase enum values. If your legacy data has values outside this set, the command will set them to TEXT and the verifier will flag them as invalid field_type.