# Form settings extension hooks — implementation plan

This document describes how to simplify **form-level data stored in `forms.settings` JSON** (e.g. conversational welcome page) so Pro extends Lite through **three hooks** plus a small Lite **extension data** passthrough—without hooking every layer (sanitizer, factory, VO, entity, repository).

**Audience:** IvyForms Lite + IvyForms Pro developers.  
**Related:** [extension-api.md](./extension-api.md), Pro [02-LITE-EXTENSION-HOOKS.md](../../ivyforms-pro/docs/02-LITE-EXTENSION-HOOKS.md).

---

## Goals

1. **Lite stays feature-agnostic** — no `welcomePage` or other Pro key names in Lite persistence code.
2. **Pro owns feature logic** — sanitize, decode, persist—via a small, documented hook set.
3. **Avoid duplicate wiring** — no parallel hooks on VO + entity + `additionalProperties` bags for JSON-backed form settings.
4. **Keep field extensions unchanged** — fields continue to use the full field hook pattern (`ivyforms/field/*`, `ivyforms/repository/field/settings`).

---

## Target architecture

### Mental model

| Layer | Responsibility |
|-------|----------------|
| **REST / Sanitizer** | Accept and validate top-level keys (e.g. `welcomePage`) on save |
| **FormFactory** | On load, lift keys from decoded `settings` JSON into `$data`; stash unknown top-level keys on the entity |
| **Entity `toArray()`** | Expose extension keys to API and persistence |
| **FormPersistenceHelper** | Build core `settings` blob; Pro merges extension keys into the blob |

### Data flow

**Save**

```text
REST body
  → ivyforms/form/sanitized_form_data (Pro: sanitize welcomePage)
  → FormService / Factory
  → extensionData on entity (Lite whitelist from $data)
  → entity->toArray() (includes welcomePage)
  → ivyforms/form/persistence/settings_blob (Pro: merge into JSON)
  → DB forms.settings
```

**Load**

```text
DB forms.settings
  → FormFactory json_decode
  → ivyforms/form/factory/extract_settings (Pro: lift welcomePage into $data)
  → extensionData on entity
  → GET entity->toArray()
  → frontend receives welcomePage
```

---

## Hook inventory

### Pro-facing hooks (register in Pro only)

| Hook | Type | Lite location | Purpose |
|------|------|---------------|---------|
| `ivyforms/form/sanitized_form_data` | filter | `Common/Sanitizer/Sanitizer.php` | Inbound save: add, sanitize, or strip extension keys (e.g. `welcomePage` for non-conversational forms) |
| `ivyforms/form/factory/extract_settings` | filter | `Factory/Form/FormFactory.php` | On load: merge keys from decoded `settings` JSON into `$data` |
| `ivyforms/form/persistence/settings_blob` | filter | `Common/Helpers/FormPersistenceHelper.php` | Before DB write: merge extension keys into the `settings` JSON blob |

### Lite internal mechanism (not Pro-specific hooks)

| Mechanism | Location | Purpose |
|-----------|----------|---------|
| **`extensionData` on `Entity\Form\Form`** | Entity + Factory | Hold top-level extension keys for the request lifecycle |
| **Whitelist diff in `FormFactory`** | `FormFactory::create()` | After `extract_settings`, copy keys from `$data` that are not known Lite fields into `extensionData` |

### Remove from Lite (IVY-1240 form extension additions)

| Item | File(s) |
|------|---------|
| Hardcoded `welcomePage` in settings blob | `Common/Helpers/FormPersistenceHelper.php` |
| `apply_filters('ivyforms/form/value_object/set_properties', …)` | `Factory/Form/FormFactory.php` |
| `apply_filters('ivyforms/form/entity/set_properties', …)` | `Factory/Form/FormFactory.php` |
| `additionalProperties` + `init` / `set` / `get` / `getAll` | `Entity/Form/Form.php` |
| `additionalProperties` + same methods | `ValueObjects/Form/Form.php` |
| `ivyforms/form/entity/init_additional_properties` | Entity constructor |
| `ivyforms/form/value_object/init_additional_properties` | VO constructor |
| `ivyforms/form/entity/add_to_array_additional_properties` | `Entity::toArray()` |
| `ivyforms/form/value_object/add_to_array_additional_properties` | `ValueObjects\Form\Form::toArray()` |

### Keep unchanged (out of scope)

- `ivyforms/form/can_use_form_type_conversational`
- `ivyforms/form/allowed_form_types`
- `ivyforms/form/before_submission` / `after_submission`
- `ivyforms/form/import_export/*`
- All **`ivyforms/field/*`** and **`ivyforms/repository/field/settings`** hooks

### Optional deprecation (one release)

If external code might listen on removed hooks, register no-op deprecated filters in Lite and document removal in the next minor release. **Current Pro usage is only `WelcomePageFormHooks`**—safe to remove in a coordinated Lite + Pro release.

---

## Lite implementation steps

### 1. `FormPersistenceHelper::buildSettingsBlob()`

Build the core Lite blob, then delegate extensions to Pro:

```php
public static function buildSettingsBlob(array $data): array
{
    $settingsBlob = [
        'showTitle'         => (int) $data['showTitle'],
        'showDescription'   => (int) $data['showDescription'],
        'storeEntries'      => (int) $data['storeEntries'],
        'formActionButtons' => $data['formActionButtons'] ?? self::defaultFormActionButtons(),
    ];

    /**
     * Merge form-level extension data into the settings JSON column.
     *
     * @param array<string, mixed> $settingsBlob Core keys written by Lite.
     * @param array<string, mixed> $data           Full form array from entity->toArray().
     */
    return apply_filters('ivyforms/form/persistence/settings_blob', $settingsBlob, $data);
}
```

- Remove any `if (isset($data['welcomePage']))` (or other Pro key) branches.
- Optional: use `wp_json_encode()` instead of `json_encode()` in insert/update rows.

### 2. `Entity\Form\Form` — extension data bag

Add a simple associative store (not the list-of-single-key-maps `additionalProperties` pattern):

```php
/** @var array<string, mixed> */
private array $extensionData = [];

public function setExtensionData(array $data): void
{
    $this->extensionData = $data;
}

/** @return array<string, mixed> */
public function getExtensionData(): array
{
    return $this->extensionData;
}
```

In `toArray()`, merge after the base payload and integration settings:

```php
$data = array_merge($base, $integrationPayload);
return array_merge($data, $this->extensionData);
```

Only add `ivyforms/form/entity/to_array` if a later feature needs a final filter pass; welcome page does not require it.

### 3. `FormFactory::create()`

1. Decode `settings` (keep invalid JSON guard: `if (!is_array($settings)) { $settings = []; }`).
2. `apply_filters('ivyforms/form/factory/extract_settings', $data, $settings)`.
3. Map known Lite keys (`showTitle`, `formActionButtons`, etc.) as today.
4. Build VO + entity.
5. `$form->setExtensionData(self::extractExtensionData($data));`
6. Return entity **without** `value_object/set_properties` or `entity/set_properties` filters.

`define extractExtensionData(array $data): array`

- `array_diff_key($data, array_flip(self::KNOWN_FORM_DATA_KEYS))`
- Maintain `KNOWN_FORM_DATA_KEYS` in one place (id, name, author, fields, description, formType, starred, published, dates, showTitle, showDescription, storeEntries, integrationSettings, styleSettings, formActionButtons, settings, etc.).
- **Do not** include `welcomePage` or other Pro keys in the whitelist.

### 4. Repository

No signature changes:

```php
FormPersistenceHelper::buildInsertRow($entity->toArray());
FormPersistenceHelper::buildUpdateRow($entity->toArray());
```

`welcomePage` must appear in `toArray()` via `extensionData`.

### 5. `ValueObjects\Form\Form`

Revert form VO `additionalProperties` and related hooks added for welcome page. Fields keep their VO/entity extension pattern.

### 6. Documentation

- Update Lite and Pro relevant docs files
- Note: entity/VO `additional_properties` hooks remain documented for **fields** and for future **non-JSON** form features only.

---

## Pro implementation (`WelcomePageFormHooks`)

### Before (6 filter registrations)

| Hook |
|------|
| `ivyforms/form/sanitized_form_data` |
| `ivyforms/form/factory/extract_settings` |
| `ivyforms/form/value_object/set_properties` |
| `ivyforms/form/entity/set_properties` |
| `ivyforms/form/value_object/add_to_array_additional_properties` |
| `ivyforms/form/entity/add_to_array_additional_properties` |

### After (3 filter registrations)

```php
public function register(): void
{
    add_filter('ivyforms/form/sanitized_form_data', [$this, 'filterSanitizedFormData'], 10, 2);
    add_filter('ivyforms/form/factory/extract_settings', [$this, 'extractWelcomePageFromSettings'], 10, 2);
    add_filter('ivyforms/form/persistence/settings_blob', [$this, 'mergeWelcomePageIntoSettingsBlob'], 10, 2);
}
```

| Method | Action |
|--------|--------|
| `filterSanitizedFormData` | **Keep** — strip `welcomePage` when `formType !== conversational`; sanitize when conversational |
| `extractWelcomePageFromSettings` | **Keep** — copy `settings['welcomePage']` into `$data['welcomePage']` when missing |
| `mergeWelcomePageIntoSettingsBlob` | **Add** — persist into settings JSON |

Example persistence callback:

```php
/**
 * @param array<string, mixed> $blob
 * @param array<string, mixed> $data
 * @return array<string, mixed>
 */
public function mergeWelcomePageIntoSettingsBlob(array $blob, array $data): array
{
    if (isset($data['welcomePage']) && is_array($data['welcomePage']) && $data['welcomePage'] !== []) {
        $blob['welcomePage'] = $data['welcomePage'];
    }

    return $blob;
}
```

### Delete from Pro

- `setWelcomePageProperty`
- `setWelcomePageOnEntity`
- `addWelcomePageToArray`
- Unused `FormValueObject` import if applicable

### Sanitization reference (unchanged behavior)

`welcomePage` shape after sanitize:

- `enabled` (bool)
- `headerLogoUrl` (esc_url_raw)
- `title` (sanitize_text_field)
- `description` (sanitize_textarea_field)
- `startButtonLabel` (sanitize_text_field)

---

## Adding a new Pro form setting in `forms.settings` JSON

1. **Sanitize** on `ivyforms/form/sanitized_form_data`.
2. **Extract** on `ivyforms/form/factory/extract_settings` (lift from `$settings` into `$data`).
3. **Persist** on `ivyforms/form/persistence/settings_blob` (merge into blob), **or** rely on Lite `extensionData` + `toArray()` if the key is already in `$data` and you only need it in the JSON column (persistence filter is explicit and recommended).
4. Ensure the key is **not** in Lite `KNOWN_FORM_DATA_KEYS` so it flows into `extensionData` automatically.
5. Frontend: use form builder extension registry (`storageKey`) per [extension-api.md](./extension-api.md).

Do **not** add Lite branches for Pro key names in `FormPersistenceHelper`.

---

## When to use entity/VO hooks instead

Re-introduce form entity/VO lifecycle hooks only when a feature:

- Stores data **outside** `forms.settings` JSON (custom table, separate column), or
- Requires typed methods on the entity (domain logic, not plain arrays).

Welcome page and similar JSON blobs use the **three-hook + extensionData** model only.

---

## Execution order

| Step | Repository | Work |
|------|------------|------|
| 1 | **ivyforms** | `settings_blob` filter, entity `extensionData`, factory whitelist, remove form VO/entity additional-property hooks and hardcoded `welcomePage` |
| 2 | **ivyforms-pro** | Slim `WelcomePageFormHooks` to three filters |
| 3 | **both** | Update extension API docs |
| 4 | **QA** | Manual tests below |

---

## Test checklist

| # | Scenario | Expected |
|---|----------|----------|
| 1 | Conversational form: enable welcome page, save, reload builder | `welcomePage` in API response and DB `settings` JSON |
| 2 | Classic form: POST with `welcomePage` in raw body | Stripped on sanitize; not in DB |
| 3 | Update starred/published only | No regression |
| 4 | Duplicate form | Welcome page copied when present on source |
| 5 | Lite only (Pro deactivated) | No `welcomePage` persisted from Lite code paths |
| 6 | Import/export (if used for conversational templates) | `welcomePage` round-trips via `toArray()` / settings blob |

---

## Hook rename (optional)

| Current | Suggested | Recommendation |
|---------|-----------|----------------|
| `ivyforms/form/factory/extract_settings` | `ivyforms/form/factory/decoded_settings` | **Keep current name** — matches field factory naming and existing docs |

---

## Summary

| Action | Item |
|--------|------|
| **Add** | `ivyforms/form/persistence/settings_blob` |
| **Add** | Entity `extensionData` + `FormFactory::extractExtensionData()` |
| **Keep** | `ivyforms/form/sanitized_form_data`, `ivyforms/form/factory/extract_settings` |
| **Remove** | Form entity/VO `additionalProperties` and related factory/constructor/`toArray` filters |
| **Remove** | Lite hardcoded `welcomePage` in `FormPersistenceHelper` |
| **Pro** | Three filters in `WelcomePageFormHooks`; delete VO/entity set/add_to_array handlers |

---

## References

- IVY-1240: welcome page settings during form creation (motivation for this refactor)
- Field persistence pattern: `ivyforms/repository/field/settings` in `Repository/Field/FieldRepository.php`
- Pro hook class: `ivyforms-pro/.../ConversationalForm/Hooks/WelcomePageFormHooks.php`
