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:
110
api/tests/Feature/FormBuilder/FormFieldOptionsAccessTest.php
Normal file
110
api/tests/Feature/FormBuilder/FormFieldOptionsAccessTest.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\FormBuilder;
|
||||
|
||||
use App\Enums\FormBuilder\FormFieldType;
|
||||
use App\Models\FormBuilder\FormField;
|
||||
use App\Models\FormBuilder\FormFieldLibrary;
|
||||
use App\Models\FormBuilder\FormFieldOption;
|
||||
use App\Models\FormBuilder\FormSchema;
|
||||
use App\Models\Organisation;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression test for the post-WS-5d-commit-5 access pattern. With the
|
||||
* form_fields.options / form_field_library.options JSON columns dropped
|
||||
* (and the temporary getOptionsAttribute accessor-bridge removed),
|
||||
* Eloquent's getAttribute('options') falls through to the morphMany
|
||||
* relation naturally. `$model->options` (no parens) must:
|
||||
*
|
||||
* - return an Eloquent Collection
|
||||
* - whose entries are FormFieldOption instances
|
||||
* - lazily lazy-load on first access (1 query for the parent fetch
|
||||
* plus 1 query for the lazy-load — no surprise extra reads)
|
||||
*
|
||||
* Tested for both polymorphic owners — FormField and FormFieldLibrary.
|
||||
*/
|
||||
final class FormFieldOptionsAccessTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_form_field_options_resolves_to_morph_many_collection_with_lazy_load(): void
|
||||
{
|
||||
$org = Organisation::factory()->create();
|
||||
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
|
||||
FormField::factory()
|
||||
->withOptions(['XS', 'S', 'M'])
|
||||
->create([
|
||||
'form_schema_id' => $schema->id,
|
||||
'field_type' => FormFieldType::SELECT->value,
|
||||
'slug' => 'shirtmaat',
|
||||
]);
|
||||
|
||||
DB::flushQueryLog();
|
||||
DB::enableQueryLog();
|
||||
|
||||
// Fresh fetch — no with('options') eager-load. The lazy-load
|
||||
// happens on first $field->options access.
|
||||
$field = FormField::query()->where('slug', 'shirtmaat')->first();
|
||||
$options = $field->options;
|
||||
|
||||
DB::disableQueryLog();
|
||||
$queries = DB::getQueryLog();
|
||||
|
||||
$this->assertInstanceOf(Collection::class, $options);
|
||||
$this->assertNotEmpty($options);
|
||||
$this->assertInstanceOf(FormFieldOption::class, $options->first());
|
||||
$this->assertSame(['XS', 'S', 'M'], $options->pluck('value')->all());
|
||||
|
||||
// Exactly two queries: 1× FormField fetch + 1× lazy-load options.
|
||||
$this->assertCount(
|
||||
2,
|
||||
$queries,
|
||||
sprintf(
|
||||
'Expected exactly 2 queries (parent fetch + lazy-load options); got %d. Queries: %s',
|
||||
count($queries),
|
||||
json_encode(array_column($queries, 'query')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_form_field_library_options_resolves_to_morph_many_collection_with_lazy_load(): void
|
||||
{
|
||||
$org = Organisation::factory()->create();
|
||||
FormFieldLibrary::factory()
|
||||
->withOptions(['a', 'b'])
|
||||
->create([
|
||||
'organisation_id' => $org->id,
|
||||
'slug' => 'lib-select',
|
||||
]);
|
||||
|
||||
DB::flushQueryLog();
|
||||
DB::enableQueryLog();
|
||||
|
||||
$library = FormFieldLibrary::query()->where('slug', 'lib-select')->first();
|
||||
$options = $library->options;
|
||||
|
||||
DB::disableQueryLog();
|
||||
$queries = DB::getQueryLog();
|
||||
|
||||
$this->assertInstanceOf(Collection::class, $options);
|
||||
$this->assertNotEmpty($options);
|
||||
$this->assertInstanceOf(FormFieldOption::class, $options->first());
|
||||
$this->assertSame(['a', 'b'], $options->pluck('value')->all());
|
||||
|
||||
$this->assertCount(
|
||||
2,
|
||||
$queries,
|
||||
sprintf(
|
||||
'Expected exactly 2 queries (parent fetch + lazy-load options); got %d. Queries: %s',
|
||||
count($queries),
|
||||
json_encode(array_column($queries, 'query')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user