Files
crewli/api/tests/Feature/FormBuilder/Bindings/SchemaSnapshotEmbedsBindingFromRelationalTableTest.php
bert.hausmans 6933e6d700 feat(form-builder): FormFieldBindingService + library-to-field row copy + snapshot writer
WS-5a commit 2 of 4.

FormFieldBindingService owns all writes to the relational binding table.
Validation against config/form_binding.php entity-column registry lives here
(ARCH §6.2).

FormFieldService::insertFromLibrary now calls copyBindings instead of
hydrating JSON — the Q3 row-copy mandate. Library and field bindings share
the same table; insertion is a row-clone operation.

Snapshot writer (FormSubmissionService::buildSnapshot) serialises bindings
via toJsonShape so schema_snapshot JSON keeps its ARCH §4.6.1 / §6.3
contract. No snapshot format change.
API resources source binding output from the relational table via the same
serialiser — external shape preserved.

Tests: service transactional behaviour, copyBindings preservation,
snapshot parity, API resource parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 18:48:47 +02:00

120 lines
4.1 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\FormBuilder\Bindings;
use App\Enums\FormBuilder\FormFieldBindingMode;
use App\Enums\FormBuilder\FormFieldType;
use App\Enums\FormBuilder\FormPurpose;
use App\Enums\FormBuilder\FormSchemaSnapshotMode;
use App\Models\FormBuilder\FormField;
use App\Models\FormBuilder\FormFieldBinding;
use App\Models\FormBuilder\FormSchema;
use App\Models\FormBuilder\FormSubmission;
use App\Models\Organisation;
use App\Models\User;
use App\Services\FormBuilder\FormSubmissionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Asserts the `schema_snapshot` JSON keeps its ARCH §4.6.1 / §6.3 shape
* after WS-5a: the snapshot writer now serialises bindings from the
* relational table via FormFieldBindingService::toJsonShape, but the
* per-field JSON embedded in the snapshot is byte-for-byte the pre-WS-5a
* shape for entity-owned and mirrored patterns.
*/
final class SchemaSnapshotEmbedsBindingFromRelationalTableTest extends TestCase
{
use RefreshDatabase;
public function test_snapshot_embeds_entity_owned_binding_in_arch_shape(): void
{
$schema = $this->schemaWithSnapshot();
$field = FormField::factory()->create([
'form_schema_id' => $schema->id,
'field_type' => FormFieldType::EMAIL->value,
'slug' => 'email',
'label' => 'E-mail',
]);
FormFieldBinding::factory()->forField($field)->entityOwned('person', 'email')->create();
$submission = $this->submitFor($schema);
$snapshot = $submission->fresh()->schema_snapshot;
$this->assertIsArray($snapshot);
$embedded = collect($snapshot['fields'])->firstWhere('id', $field->id);
$this->assertNotNull($embedded);
$this->assertSame([
'mode' => 'entity_owned',
'entity' => 'person',
'column' => 'email',
], $embedded['binding']);
}
public function test_snapshot_embeds_mirrored_binding_with_sync_direction(): void
{
$schema = $this->schemaWithSnapshot();
$field = FormField::factory()->create([
'form_schema_id' => $schema->id,
'field_type' => FormFieldType::TEXT->value,
'slug' => 'noodcontact',
'label' => 'Noodcontact',
]);
FormFieldBinding::factory()->forField($field)->mirrored('user_profile', 'emergency_contact_name')->create();
$submission = $this->submitFor($schema);
$snapshot = $submission->fresh()->schema_snapshot;
$embedded = collect($snapshot['fields'])->firstWhere('id', $field->id);
$this->assertSame([
'mode' => 'mirrored',
'entity' => 'user_profile',
'column' => 'emergency_contact_name',
'sync_direction' => 'write_on_submit',
], $embedded['binding']);
}
public function test_snapshot_embeds_null_binding_for_form_owned_field(): void
{
$schema = $this->schemaWithSnapshot();
$field = FormField::factory()->create([
'form_schema_id' => $schema->id,
'field_type' => FormFieldType::TEXT->value,
'slug' => 'motivatie',
'label' => 'Motivatie',
]);
$submission = $this->submitFor($schema);
$snapshot = $submission->fresh()->schema_snapshot;
$embedded = collect($snapshot['fields'])->firstWhere('id', $field->id);
$this->assertNull($embedded['binding']);
}
private function schemaWithSnapshot(): FormSchema
{
$org = Organisation::factory()->create();
return FormSchema::factory()->create([
'organisation_id' => $org->id,
'purpose' => FormPurpose::USER_PROFILE->value,
'snapshot_mode' => FormSchemaSnapshotMode::ALWAYS->value,
]);
}
private function submitFor(FormSchema $schema): FormSubmission
{
$user = User::factory()->create();
$service = $this->app->make(FormSubmissionService::class);
$submission = $service->createDraft($schema, $user, $user);
return $service->submit($submission, $user);
}
}