feat(form-builder): FormBindingApplicator + BindingActivityLogger (WS-6)
Orchestrates per-purpose subject resolution + binding conflict resolution + per-binding writes per RFC Q4/Q7/Q9. Per-binding failures captured in BindingPassResult, not thrown — partial failures are expected and recoverable. Catastrophic failures (no transaction, unknown purpose, missing schema) throw FormBindingApplicatorException and bubble. Per-strategy null-winner matrix implemented via a NO_OP sentinel: overwrite=write null, append=noop, replace=conditional, first_write_wins= write only into null target. Append is collection-only with set-merge semantics (deduplicated array_merge). Identity-key bindings are skipped during apply — the subject resolver already used them for lookup/provisioning; re-writing is a no-op or a clobber. Activity log hierarchical: one bindings_pass_completed parent + N binding_applied children with parent_activity_id linkage (RFC Q12). Failed bindings get error_class/error_message in their activity entry in addition to their FormSubmissionActionFailure row (deliberate dual source of truth). Refs: RFC-WS-6.md §3 (Q4, Q7, Q9, Q12) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
63
api/app/FormBuilder/Bindings/BindingActivityLogger.php
Normal file
63
api/app/FormBuilder/Bindings/BindingActivityLogger.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\FormBuilder\Bindings;
|
||||
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
/**
|
||||
* RFC-WS-6 §3 (Q12) — hierarchical activity log for the binding
|
||||
* pipeline. One pass-level activity (form_submission.bindings_pass_completed)
|
||||
* with N child activities (form_submission.binding_applied), linked via
|
||||
* properties.parent_activity_id.
|
||||
*
|
||||
* Failed bindings get their own binding_applied activity entry too,
|
||||
* with `error_class` / `error_message` in properties — in addition to
|
||||
* their FormSubmissionActionFailure row (deliberate dual source of
|
||||
* truth: activity_log is the human timeline, action_failures is the
|
||||
* machine-replayable workflow).
|
||||
*/
|
||||
final class BindingActivityLogger
|
||||
{
|
||||
public function logPass(FormSubmission $submission, BindingPassResult $result): void
|
||||
{
|
||||
$passActivity = activity()
|
||||
->performedOn($submission)
|
||||
->withProperties([
|
||||
'binding_count' => count($result->applications),
|
||||
'succeeded' => $result->successCount(),
|
||||
'failed' => $result->failureCount(),
|
||||
'apply_status' => $result->applyStatus()->value,
|
||||
'person_provisioned' => $result->provisionedSubjectType === 'person',
|
||||
'subject_type' => $result->provisionedSubjectType,
|
||||
'subject_id' => $result->provisionedSubjectId,
|
||||
])
|
||||
->log('form_submission.bindings_pass_completed');
|
||||
|
||||
$parentActivityId = $passActivity instanceof Activity ? (string) $passActivity->id : null;
|
||||
|
||||
foreach ($result->applications as $application) {
|
||||
$properties = [
|
||||
'parent_activity_id' => $parentActivityId,
|
||||
'binding_id' => $application->bindingId,
|
||||
'target_entity' => $application->targetEntity,
|
||||
'target_attribute' => $application->targetAttribute,
|
||||
'success' => $application->success,
|
||||
'old_value' => $application->oldValue,
|
||||
'new_value' => $application->newValue,
|
||||
'source_submission_id' => (string) $submission->id,
|
||||
];
|
||||
if (! $application->success) {
|
||||
$properties['error_class'] = $application->exceptionClass;
|
||||
$properties['error_message'] = $application->exceptionMessage;
|
||||
}
|
||||
|
||||
activity()
|
||||
->performedOn($submission)
|
||||
->withProperties($properties)
|
||||
->log('form_submission.binding_applied');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user