155 lines
5.2 KiB
PHP
155 lines
5.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Collection;
|
|
use App\Models\CollectionHistory;
|
|
use App\Models\User;
|
|
use App\Services\CollectionHealthService;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class CollectionHistoryService
|
|
{
|
|
/**
|
|
* @var array<string, array<int, string>>
|
|
*/
|
|
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<string, mixed>
|
|
*/
|
|
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;
|
|
}
|
|
}
|