feat(form-builder): add PurposeSubjectResolver per purpose (WS-6)

Parallel interface to PurposeGuardProvider for runtime subject
resolution. Seven concrete resolvers, one per v1.0 purpose. Wired
through purposes.php via subject_resolver_class key.

EventRegistration uses PersonProvisioner (may create). Other purposes
resolve from existing context (portal token, production request, auth).
IncidentReport is the only purpose allowed to return null (anonymous-
allowed configurations); the others return concrete model types
(narrowed via PHP covariance) for caller convenience.

Refs: RFC-WS-6.md §3 (Q9)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 12:57:21 +02:00
parent 47265e9d4f
commit 16a9265430
13 changed files with 606 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\FormBuilder;
use RuntimeException;
/**
* RFC-WS-6 §3 (Q9) failure during PurposeSubjectResolver operation.
*
* Reason codes (informal contract):
* - 'no_portal_token' artist_advance has no portal context
* - 'no_production_request' supplier_intake has no production_request link
* - 'no_auth' auth-based purpose has no authenticated user
* - 'no_person_for_user' user has no linked Person
* - 'subject_not_found' subject_type/subject_id set but record gone
*/
final class PurposeSubjectResolutionException extends RuntimeException
{
public function __construct(
public readonly string $purposeSlug,
public readonly string $reasonCode,
public readonly string $submissionId,
?string $message = null,
) {
parent::__construct($message ?? "Subject resolution failed for {$purposeSlug}: {$reasonCode} (submission {$submissionId})");
}
}