refactor(form-field): drop form_fields.options + form_field_library.options
Final WS-5d cleanup. The JSON columns that have been unread since
commit 3 are now physically dropped on both source tables. Their
canonical rich-shape lives in form_field_options, accessed
exclusively through the morphMany relation.
Defensive sweep: any lingering translations.{locale}.options key in
either source table's translations bag is stripped. Commit 2's
backfill should already have done so exhaustively; this is
belt-and-braces.
Rollback re-creates the columns as nullable JSON but leaves them
empty. Pair with commit 2's rollback to restore the pre-WS-5d data
shape on every owner row.
The commit-3 getOptionsAttribute accessor-bridge on FormField +
FormFieldLibrary is removed — Eloquent's getAttribute() resolution
now naturally falls through to the morphMany relation since there's
no underlying column to shadow it. New regression test
FormFieldOptionsAccessTest asserts $field->options resolves to an
Eloquent Collection of FormFieldOption instances and lazy-loads in
exactly 2 queries (1 parent + 1 lazy-load options) on a fresh fetch
without with() preload. Same trio for FormFieldLibrary.
Migration step-count tests in WS-5a/b/c bumped by 1 to account for
the new drop_form_field_options_json_columns migration on the
rollback stack.
Documentation:
- SCHEMA.md v2.6: form_field_options table documented; options row
removed from form_fields and form_field_library; morphMany
relations updated; cross-references to ARCH-FORM-BUILDER §17.6
and addendum §Q3 WS-5d Uitvoering added on both source-table
docblocks.
- ARCH-FORM-BUILDER.md v1.8: new §17.6 "Field options (relational)"
mirrors the §17.4 / §17.5 relational-sibling structure with
sub-sections 17.6.1 rationale, 17.6.2 table + catalogue, 17.6.3
service / scope / cascade / activity log, 17.6.4 snapshot
embedding, 17.6.5 external API contract. Existing Webhooks
section renumbered from §17.6 to §17.7.
- ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md: "Uitvoering — WS-5d
(2026-04-27)" section added. Eight paragraphs covering the
snapshot atomic rewrite, strict-fail backfill dispatch, dual
activity-log emit, four-sibling base-class extraction warrant,
commit 0 dead-code precondition, the temporary getOptionsAttribute
accessor-bridge pattern (with reusability note for future
JSON→relational refactors), the dev-seeder vergoedingstype RADIO
normalisation (drift correction explicitly distinguished from the
parallel apps/app RegistrationFieldTemplate description domain),
and the WS-5 family completion note.
- BACKLOG.md: FORM-BUILDER-LIBRARY-AUDIT-LOG entry extended to four
services (adds library.options_replaced); new
FORM-BUILDER-MORPH-SCOPE-BASE-CLASS entry added as the WS-5d
follow-up now that all four concrete morph-scope siblings exist.
Tests: 1193 → 1208 green (+15 across commits 3+4+5; this commit alone:
+2 from the regression test).
This completes the WS-5 family.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* WS-5d commit 5 — final cleanup. Drops the form_fields.options and
|
||||
* form_field_library.options JSON columns now that every read path
|
||||
* (resources, snapshot writer, FormRequests, validators, filter
|
||||
* registry, rule builder) sources options from form_field_options
|
||||
* since commit 3.
|
||||
*
|
||||
* Also strips any residual translations.{locale}.options key that
|
||||
* somehow persisted. Commit 2's backfill should already have done so
|
||||
* exhaustively; this is belt-and-braces.
|
||||
*
|
||||
* down() recreates the columns as nullable JSON but leaves them empty.
|
||||
* Pair with commit 2's rollback to restore full pre-WS-5d data shape;
|
||||
* partial rollback unsupported.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->stripResidualTranslationsOptionsKeys('form_fields');
|
||||
$this->stripResidualTranslationsOptionsKeys('form_field_library');
|
||||
|
||||
Schema::table('form_fields', function (Blueprint $table): void {
|
||||
$table->dropColumn('options');
|
||||
});
|
||||
Schema::table('form_field_library', function (Blueprint $table): void {
|
||||
$table->dropColumn('options');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('form_fields', function (Blueprint $table): void {
|
||||
$table->json('options')->nullable()->after('section');
|
||||
});
|
||||
Schema::table('form_field_library', function (Blueprint $table): void {
|
||||
$table->json('options')->nullable()->after('help_text');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Defensive sweep — walk every row's translations bag and strip
|
||||
* `{locale}.options` keys. After WS-5d commit 2 this should be a
|
||||
* no-op on every row.
|
||||
*/
|
||||
private function stripResidualTranslationsOptionsKeys(string $table): void
|
||||
{
|
||||
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, 'translations')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = DB::table($table)
|
||||
->whereNotNull('translations')
|
||||
->get(['id', 'translations']);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$bag = json_decode((string) $row->translations, true);
|
||||
if (! is_array($bag)) {
|
||||
continue;
|
||||
}
|
||||
$changed = false;
|
||||
foreach ($bag as $locale => $localeBag) {
|
||||
if (is_array($localeBag) && array_key_exists('options', $localeBag)) {
|
||||
unset($bag[$locale]['options']);
|
||||
if ($bag[$locale] === []) {
|
||||
unset($bag[$locale]);
|
||||
}
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
DB::table($table)->where('id', $row->id)->update([
|
||||
'translations' => $bag === [] ? null : json_encode($bag),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user