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>
64 lines
2.5 KiB
PHP
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');
|
|
}
|
|
}
|
|
}
|