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>
53 lines
1.7 KiB
PHP
53 lines
1.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Exceptions\FormBuilder;
|
|
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Base for all FormBindingApplicator-pipeline exceptions.
|
|
*
|
|
* Subclasses provide a `reasonCode()` that maps to:
|
|
* - failure_response_code on form_submissions (response-shape driver)
|
|
* - HTTP status code (422 / 503 / 500)
|
|
* - user-facing copy class (rendered by frontend)
|
|
*
|
|
* Per RFC-WS-6 §Q3 v1.3 addition 2.
|
|
*
|
|
* Concrete subclasses:
|
|
* - FormBindingSchemaConfigException — schema misconfiguration (422, schema_config_error)
|
|
* - FormBindingInfraException — infra issue, retryable (503, temporary_error)
|
|
* - FormBindingApplicatorTimeoutException — deadline-wrapper exceeded (extends Infra)
|
|
* - FormBindingDataIntegrityException — data shape violation (422, data_integrity_error)
|
|
*
|
|
* The classifier (FormBindingExceptionClassifier) maps unknown Throwables
|
|
* to 'unknown_error' — that is the fallback for anything not in this
|
|
* hierarchy.
|
|
*
|
|
* `submissionId` is preserved as a public readonly property so D2's
|
|
* outer-transaction handler can structurally read it when writing the
|
|
* `form_submission_action_failures.context` JSON, instead of regex-parsing
|
|
* the message string.
|
|
*/
|
|
abstract class FormBindingApplicatorException extends RuntimeException
|
|
{
|
|
public function __construct(
|
|
public readonly string $submissionId,
|
|
string $message,
|
|
?Throwable $previous = null,
|
|
) {
|
|
parent::__construct($message, 0, $previous);
|
|
}
|
|
|
|
/**
|
|
* Response-shape classification token. One of:
|
|
* - 'schema_config_error'
|
|
* - 'temporary_error'
|
|
* - 'data_integrity_error'
|
|
*/
|
|
abstract public function reasonCode(): string;
|
|
}
|