feat(form-builder): exception hierarchy for binding-apply pipeline
Per RFC-WS-6 §Q3 v1.3 addition 2 (binding hierarchy) + §Q2 (invariant exception).
- Refactored FormBindingApplicatorException from concrete final to abstract
base. Constructor (submissionId, message, previous?) preserves submissionId
as a public readonly property so D2's outer-transaction handler can write
it structurally to form_submission_action_failures.context JSON without
regex-parsing the message. Replaced public-readonly reasonCode property
with abstract reasonCode(): string method.
- Added 3 reason-coded subclasses:
- FormBindingSchemaConfigException -> 'schema_config_error' (422)
- FormBindingInfraException -> 'temporary_error' (503, NOT final because
Timeout extends it)
- FormBindingDataIntegrityException -> 'data_integrity_error' (422)
- Added FormBindingApplicatorTimeoutException extending FormBindingInfraException
(timeout = temporary infra issue from user perspective; reasonCode inherited).
- Added IdentityMatchInvariantViolation as a sibling DomainException — NOT
in the FormBindingApplicatorException hierarchy because it's thrown
outside the binding-applicator pipeline.
- Migrated 3 existing throw sites in FormBindingApplicator::apply():
- 'no_transaction' -> FormBindingInfraException (developer-error wants
infra-triage workflow: GlitchTip alert + retry-after)
- 'no_schema' -> FormBindingSchemaConfigException
- 'unknown_purpose' -> FormBindingSchemaConfigException
- Updated FormBindingApplicatorIntegrationTest::test_no_transaction_guard_present
to assert against the new throw shape (FormBindingInfraException + new
message string) while preserving the test's intent (guard exists in source).
Wiring (deadline wrapper, classifier integration in listener catch +
retry-service recordFailure) lands in D2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -65,11 +65,15 @@ final class FormBindingApplicatorIntegrationTest extends TestCase
|
||||
// RefreshDatabase wraps every PHPUnit test in a transaction; the
|
||||
// guard is exercised via the listener path (ApplyBindingsOnFormSubmit)
|
||||
// which opens its own transaction explicitly. Verify the guard
|
||||
// exists in the source.
|
||||
// exists in the source by checking for the throw of
|
||||
// FormBindingInfraException (per RFC-WS-6 §Q3 v1.3 addition 2 — the
|
||||
// 'no_transaction' developer-error maps onto temporary_error so the
|
||||
// GlitchTip alert + retry-after workflow fires).
|
||||
$reflection = new \ReflectionClass(FormBindingApplicator::class);
|
||||
$source = file_get_contents($reflection->getFileName());
|
||||
$this->assertStringContainsString('no_transaction', $source);
|
||||
$this->assertStringContainsString('FormBindingInfraException', $source);
|
||||
$this->assertStringContainsString('DB::transactionLevel()', $source);
|
||||
$this->assertStringContainsString('must be invoked inside DB::transaction', $source);
|
||||
}
|
||||
|
||||
private function applicator(): FormBindingApplicator
|
||||
@@ -151,7 +155,7 @@ final class FormBindingApplicatorIntegrationTest extends TestCase
|
||||
|
||||
private function writeValue(string $submissionId, string $fieldId, mixed $value): void
|
||||
{
|
||||
$row = new FormValue();
|
||||
$row = new FormValue;
|
||||
$row->form_submission_id = $submissionId;
|
||||
$row->form_field_id = $fieldId;
|
||||
$row->setAttribute('value', $value);
|
||||
|
||||
Reference in New Issue
Block a user