> */ private const RESTORABLE_FIELDS = [ 'updated' => ['title', 'visibility', 'lifecycle_state'], 'workflow_updated' => ['workflow_state', 'program_key', 'partner_key', 'experiment_key', 'placement_eligibility'], 'partner_program_metadata_updated' => ['partner_key', 'trust_tier', 'promotion_tier', 'sponsorship_state', 'ownership_domain', 'commercial_review_state', 'legal_review_state'], ]; public function __construct( private readonly CollectionHealthService $health, ) { } public function record(Collection $collection, ?User $actor, string $actionType, ?string $summary = null, ?array $before = null, ?array $after = null): void { CollectionHistory::query()->create([ 'collection_id' => $collection->id, 'actor_user_id' => $actor?->id, 'action_type' => $actionType, 'summary' => $summary, 'before_json' => $before, 'after_json' => $after, 'created_at' => now(), ]); $collection->forceFill([ 'history_count' => (int) $collection->history_count + 1, ])->save(); } public function historyFor(Collection $collection, int $perPage = 40): LengthAwarePaginator { return CollectionHistory::query() ->with('actor:id,username,name') ->where('collection_id', $collection->id) ->orderByDesc('created_at') ->paginate(max(10, min($perPage, 80))); } public function mapPaginator(LengthAwarePaginator $paginator): array { return [ 'data' => collect($paginator->items())->map(function (CollectionHistory $entry): array { $restorableFields = $this->restorablePayload($entry); return [ 'id' => (int) $entry->id, 'action_type' => $entry->action_type, 'summary' => $entry->summary, 'before' => $entry->before_json, 'after' => $entry->after_json, 'can_restore' => $restorableFields !== [], 'restore_fields' => array_keys($restorableFields), 'created_at' => $entry->created_at?->toISOString(), 'actor' => $entry->actor ? [ 'id' => (int) $entry->actor->id, 'username' => $entry->actor->username, 'name' => $entry->actor->name, ] : null, ]; })->values()->all(), 'meta' => [ 'current_page' => $paginator->currentPage(), 'last_page' => $paginator->lastPage(), 'per_page' => $paginator->perPage(), 'total' => $paginator->total(), ], ]; } public function canRestore(CollectionHistory $entry): bool { return $this->restorablePayload($entry) !== []; } public function restore(Collection $collection, CollectionHistory $entry, ?User $actor = null): Collection { if ((int) $entry->collection_id !== (int) $collection->id) { throw ValidationException::withMessages([ 'history' => 'This history entry does not belong to the selected collection.', ]); } $payload = $this->restorablePayload($entry); if ($payload === []) { throw ValidationException::withMessages([ 'history' => 'This history entry cannot be restored safely.', ]); } $working = $collection->fresh(); $before = []; foreach (array_keys($payload) as $field) { $before[$field] = $working->{$field}; } $working->forceFill($payload); $healthPayload = $this->health->evaluate($working); $working->forceFill(array_merge($healthPayload, $payload, [ 'last_activity_at' => now(), ]))->save(); $fresh = $working->fresh(); $this->record( $fresh, $actor, 'history_restored', sprintf('Collection restored from history entry #%d.', $entry->id), array_merge(['restored_history_id' => (int) $entry->id], $before), array_merge(['restored_history_id' => (int) $entry->id], $payload), ); return $fresh; } /** * @return array */ private function restorablePayload(CollectionHistory $entry): array { $before = is_array($entry->before_json) ? $entry->before_json : []; $fields = self::RESTORABLE_FIELDS[$entry->action_type] ?? []; $payload = []; foreach ($fields as $field) { if (array_key_exists($field, $before)) { $payload[$field] = $before[$field]; } } return $payload; } }