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>
231 lines
7.0 KiB
Markdown
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.
|