Files
crewli/api/app/FormBuilder/Bindings/BindingActivityLogger.php
bert.hausmans 9f98a4fe1b 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>
2026-04-26 13:06:45 +02:00

64 lines
2.5 KiB
PHP

<?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');
}
}
}