Atomic data migration. Every options datum in the database — in
form_fields and form_field_library, their translations bags, and the
form_submissions.schema_snapshot + form_templates.schema_snapshot JSON
blobs — is converted to the new relational rich-shape representation.
Strict dispatch per §17.4.4 / §8.7 convention:
- Fail on field_type ∉ {RADIO, SELECT, MULTISELECT, CHECKBOX_LIST}
carrying non-null options (post-WS-5b TAG_PICKER seed-bug indicator)
- Fail on non-flat-string-array options shape
- Fail on translations.{locale}.options[] length mismatch
- Fail on non-string / >255-char translated labels
- Fail on any residual translations.{locale}.options key after
step C migration
Snapshot rewrite in-place: both form_submissions.schema_snapshot and
form_templates.schema_snapshot walk fields[*] and rewrite options to
the new rich-shape, strip per-locale options[] from the parallel
translations bag. Zero-compromise directive — no reader tolerance for
pre-WS-5d shape in commit 3 onwards.
Rollback reconstructs JSON column shapes plus translations bags.
Forward+back pair safe as a unit; partial rollback unsupported.
FormFieldService::insertFromLibrary switches from JSON-copy to
FormFieldOptionService::copyOptions row-clone per addendum Q3 row-copy
mandate. The field's own translations bag no longer carries
{locale}.options keys — those live on option rows now.
Seeders and factories switch to service-level option creation:
- FormBuilderDevSeeder.canonicalFields keeps flat-string options as
its data shape; FormField::create no longer receives an options
key, the post-create FormFieldOptionService::replaceOptions call
inserts the rich rows. The same applies to
seedEventRegistrationShowcaseSchema. The vergoedingstype field's
legacy {label, description} object shape (a pre-WS-5d seed-bug
that the strict backfill would reject) is normalised to flat
strings; the descriptions are dropped.
- seedSystemTemplates embeds rich-shape options in the template
snapshot — no flat-array snapshot data remains in newly-seeded
rows.
- FormFieldFactory + FormFieldLibraryFactory drop the options
default; new ::withOptions() helper accepts either flat strings
(each becomes value+label) or full spec arrays and routes through
the service.
JSON columns (form_fields.options, form_field_library.options) remain
present and writable via fillable; column-drop lands in commit 5.
Reads from the JSON column still exist in resources, snapshot writer,
FormRequests, FormValueService, and FilterRegistryController — commit
3 switches those all atomically.
Migration step-count tests in WS-5a/b/c bumped by 1 to account for
the new backfill_form_field_options migration on the migration stack.
Tests: 1182 → 1193 green (+11 tests / +56 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
80 lines
3.2 KiB
PHP
80 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\FormBuilder\Options;
|
|
|
|
use App\Models\FormBuilder\FormFieldLibrary;
|
|
use App\Models\FormBuilder\FormFieldOption;
|
|
use App\Models\FormBuilder\FormSchema;
|
|
use App\Models\Organisation;
|
|
use App\Services\FormBuilder\FormFieldOptionService;
|
|
use App\Services\FormBuilder\FormFieldService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* WS-5d commit 2 — confirms FormFieldService::insertFromLibrary copies
|
|
* options via FormFieldOptionService::copyOptions instead of JSON-copy.
|
|
* Each library option row is cloned with new ID, owner pointed at the
|
|
* new field; translations and sort_order preserved row-for-row.
|
|
*/
|
|
final class FormFieldServiceInsertFromLibraryOptionsTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_insert_from_library_clones_options_via_service_path(): void
|
|
{
|
|
$org = Organisation::factory()->create();
|
|
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
|
|
$library = FormFieldLibrary::factory()
|
|
->withOptions([
|
|
['value' => 'red', 'label' => 'Red', 'sort_order' => 0, 'translations' => ['nl' => 'Rood']],
|
|
['value' => 'green', 'label' => 'Green', 'sort_order' => 1],
|
|
])
|
|
->create(['organisation_id' => $org->id]);
|
|
|
|
$field = app(FormFieldService::class)->insertFromLibrary($schema, $library);
|
|
|
|
$optionService = app(FormFieldOptionService::class);
|
|
$copied = $optionService->optionsFor($field);
|
|
$libraryOptions = $optionService->optionsFor($library);
|
|
|
|
$this->assertCount(2, $copied);
|
|
$this->assertSame(['red', 'green'], $copied->pluck('value')->all());
|
|
$this->assertSame([0, 1], $copied->pluck('sort_order')->all());
|
|
$this->assertSame(['nl' => 'Rood'], $copied->firstWhere('value', 'red')->translations);
|
|
$this->assertNull($copied->firstWhere('value', 'green')->translations);
|
|
|
|
// Distinct row IDs — options are CLONED, not shared.
|
|
$this->assertNotSame(
|
|
$libraryOptions->pluck('id')->sort()->values()->all(),
|
|
$copied->pluck('id')->sort()->values()->all(),
|
|
);
|
|
}
|
|
|
|
public function test_insert_from_library_does_not_carry_legacy_options_translations_key(): void
|
|
{
|
|
$org = Organisation::factory()->create();
|
|
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
|
|
$library = FormFieldLibrary::factory()
|
|
->withOptions(['a', 'b'])
|
|
->create([
|
|
'organisation_id' => $org->id,
|
|
// Library carries label/help_text translations only — no
|
|
// {locale}.options[] parallel array. WS-5d strips that key
|
|
// from the FormField's translations bag too.
|
|
'translations' => ['nl' => ['label' => 'Bibliotheek']],
|
|
]);
|
|
|
|
$field = app(FormFieldService::class)->insertFromLibrary($schema, $library);
|
|
|
|
$bag = $field->fresh()->translations ?? [];
|
|
if (is_array($bag)) {
|
|
foreach ($bag as $localeBag) {
|
|
$this->assertArrayNotHasKey('options', is_array($localeBag) ? $localeBag : []);
|
|
}
|
|
}
|
|
}
|
|
}
|