docs(schema,arch-form-builder): reflect purpose registry v1.0
- SCHEMA.md §3.5.12 header rewritten for the 7-purpose vocabulary and `PurposeRegistry`. The `custom_purpose_slug` column is dropped from the `form_schemas` table and removed from the index list. The `form_submissions.subject_type` note cites `PurposeRegistry::allSubjectTypes()` instead of the deleted `config/form_subjects.php`. - ARCH-FORM-BUILDER.md TL;DR updated: goal bullet cites 7 purposes (v1.0); §3.2 bullet notes the legacy 22-variant vocabulary is retired. §17.3 replaced: the "Custom purposes per organisation" section is gone; the new "Purpose registry" section documents the seven-slug table, PurposeDefinition shape, PurposeRegistry API, MorphMapAlignmentTest guard, the pre-publish binding check, and a step-by-step "adding a new purpose" checklist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,8 +24,12 @@
|
||||
Ten bullets every Claude Code session reads before starting:
|
||||
|
||||
1. **Goal:** replace the event-scoped `registration_form_fields` with a
|
||||
polymorphic, universal form builder serving 22 distinct purposes from
|
||||
event registration to incident reports to contract signatures.
|
||||
polymorphic, universal form builder serving 7 distinct purposes
|
||||
(v1.0) from event registration to incident reports to contract
|
||||
signatures. Purposes are registered in
|
||||
`config/form_builder/purposes.php` via `PurposeRegistry`; adding a
|
||||
new purpose requires a code change (config + typically a listener)
|
||||
— per ARCH-CONSOLIDATION §3 besluit 4.
|
||||
2. **Four patterns:** entity-bound, submission-bound, event-registration,
|
||||
public. Every schema matches one.
|
||||
3. **Three tables are core:** `form_schemas` (definitions), `form_fields`
|
||||
@@ -64,9 +68,12 @@ Ten bullets every Claude Code session reads before starting:
|
||||
12. **Integration contracts are explicit.** The form builder's interaction
|
||||
with identity matching, crowd lists, shifts, and email notifications
|
||||
is contract-defined (§31). No ad-hoc cross-module coupling.
|
||||
13. **Per-purpose lifecycles are documented.** Each of the 22 FormPurpose
|
||||
values has a concrete lifecycle paragraph (§3.2) covering subject
|
||||
handling, submission flow, integrations, and sample fields.
|
||||
13. **Per-purpose lifecycles are documented.** Each of the 7 FormPurpose
|
||||
values (v1.0) has a concrete lifecycle paragraph (§3.2) covering
|
||||
subject handling, submission flow, integrations, and sample fields.
|
||||
The wider vocabulary that once counted 22 variants is intentionally
|
||||
retired in v1.0 — purposes outside the registered seven are not
|
||||
part of the physical schema nor the behaviour spec.
|
||||
|
||||
---
|
||||
|
||||
@@ -1976,19 +1983,72 @@ FormSchemaService uses the registry when validating field_type on create/
|
||||
update. Built-in types are registered by default. Custom types extend the
|
||||
list without code changes to core.
|
||||
|
||||
### 17.3 Custom purposes per organisation
|
||||
### 17.3 Purpose registry
|
||||
|
||||
`FormPurpose::CUSTOM` + `form_schemas.custom_purpose_slug` string.
|
||||
Organisations define their own purposes (e.g.,
|
||||
`brandweertraining_certificering`).
|
||||
The set of purposes served by the form builder is a closed, code-defined
|
||||
vocabulary. There is no "custom purpose" escape. Organisations cannot
|
||||
invent purposes at runtime. This is deliberate — purpose drives subject
|
||||
handling, submission mode, public-access rules, pre-publish required
|
||||
bindings, and downstream listeners; each of those needs explicit code
|
||||
support.
|
||||
|
||||
Rules:
|
||||
- `custom_purpose_slug` required when purpose=custom
|
||||
- Unique(organisation_id, custom_purpose_slug) when purpose=custom
|
||||
- Subject type and submission_mode must be explicitly set (no purpose-
|
||||
derived defaults)
|
||||
- Public token not allowed for custom purposes in v1 (future: per-slug
|
||||
allowlist)
|
||||
**v1.0 vocabulary** (seven purposes, defined in
|
||||
`config/form_builder/purposes.php`):
|
||||
|
||||
| Slug | Label (NL) | Subject type | Default submission mode | Public access | Required bindings |
|
||||
|------|-----------|--------------|-------------------------|---------------|-------------------|
|
||||
| `event_registration` | Aanmelding vrijwilligers/crew | person | single | yes | `person.email`, `person.first_name`, `person.last_name` |
|
||||
| `artist_advance` | Artiest advance | artist | draft_single | no | — |
|
||||
| `supplier_intake` | Leverancier intake | company | single | no | `company.name` |
|
||||
| `post_event_evaluation` | Evaluatie na afloop | person | single | no | — |
|
||||
| `incident_report` | Incident-melding | person | multiple | no | — |
|
||||
| `signature_contract` | Contract-ondertekening | user | single | no | — |
|
||||
| `user_profile` | Profiel-update | user | single | no | — |
|
||||
|
||||
`allows_public_access` is the schema-level public-submission flag.
|
||||
Portal-token-based flows (artists, suppliers, press) are a different
|
||||
mechanism and do not consume this flag.
|
||||
|
||||
**PurposeDefinition value object** (`app/FormBuilder/Purposes/
|
||||
PurposeDefinition.php`) holds the five properties above plus the slug.
|
||||
It is immutable (`final readonly`) and `subjectType` uses the morph
|
||||
alias, not the FQCN.
|
||||
|
||||
**PurposeRegistry service** (`app/FormBuilder/Purposes/
|
||||
PurposeRegistry.php`) reads the config file, memoises the parsed
|
||||
definitions per instance, and exposes:
|
||||
|
||||
- `all()` — `array<slug, PurposeDefinition>`
|
||||
- `get(string $slug)` — throws `PurposeNotFoundException` on miss
|
||||
- `has(string $slug)` — bool
|
||||
- `allSubjectTypes()` — sorted, unique list of subject-type aliases;
|
||||
consumed by `AppServiceProvider::registerMorphMap()` (domain-subject
|
||||
block) and by `StoreFormSubmissionRequest` validation
|
||||
- `publicAccessibleSlugs()` — slugs whose schemas permit public
|
||||
submission
|
||||
|
||||
`MorphMapAlignmentTest` guards the invariant that every subject_type
|
||||
returned by `PurposeRegistry::allSubjectTypes()` is registered as a key
|
||||
in `Relation::morphMap()`.
|
||||
|
||||
**Required-bindings pre-publish check.**
|
||||
`FormSchemaService::publish()` fails with
|
||||
`PurposeRequirementsNotMetException` (structured; `purposeSlug` +
|
||||
`missingBindings[]`) if any binding path in the schema's
|
||||
`PurposeDefinition::requiredBindings` is not present on at least one
|
||||
`form_fields.binding` JSON of the schema. In WS-5a this check switches
|
||||
to the relational `form_field_bindings` table.
|
||||
|
||||
**Adding a new purpose.** In scope only via an architect-level decision:
|
||||
|
||||
1. Add a migration if existing schemas carry the new slug via data.
|
||||
2. Add the new entry to `config/form_builder/purposes.php`.
|
||||
3. If new subject type: register the FQCN in
|
||||
`AppServiceProvider::PURPOSE_SUBJECT_FQCN`; `MorphMapAlignmentTest`
|
||||
enforces this step.
|
||||
4. Add listeners wired to `FormSubmissionSubmitted` as needed
|
||||
(identity-match, tag sync, entity creation, etc.).
|
||||
5. Add a lifecycle paragraph under §3.2 and a row to the table above.
|
||||
|
||||
### 17.4 Custom validation callbacks
|
||||
|
||||
|
||||
Reference in New Issue
Block a user