TenantAtlas/app/Services/Intune/RestoreDiffGenerator.php
ahmido b048131f81 feat/011-restore-run-wizard (#17)
Wichtige Änderungen:
   - Eine neue "Restore via Wizard"-Aktion wurde der PolicyVersion-Tabelle hinzugefügt.
   - Diese Aktion ermöglicht die Erstellung eines Einzelposten-BackupSets aus dem ausgewählten
     Policy-Version-Snapshot.
   - Der CreateRestoreRun Wizard unterstützt nun das Vorbefüllen seiner Formularfelder basierend auf
     Abfrageparametern, was eine nahtlose Übergabe von der PolicyVersion-Aktion ermöglicht.
   - Umfassende Feature-Tests wurden hinzugefügt, um die korrekte Funktionalität und Integration dieses
     neuen Workflows sicherzustellen.
   - Die specs/011-restore-run-wizard/tasks.md wurde aktualisiert, um den Abschluss von Aufgabe T023
     widerzuspiegeln.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #17
2025-12-31 19:14:59 +00:00

249 lines
8.7 KiB
PHP

<?php
namespace App\Services\Intune;
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\PolicyVersion;
use App\Models\Tenant;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
class RestoreDiffGenerator
{
public function __construct(
private readonly PolicyNormalizer $policyNormalizer,
private readonly VersionDiff $versionDiff,
) {}
/**
* @param array<int>|null $selectedItemIds
* @return array{summary: array<string, mixed>, diffs: array<int, array<string, mixed>>}
*/
public function generate(Tenant $tenant, BackupSet $backupSet, ?array $selectedItemIds = null): array
{
if ($backupSet->tenant_id !== $tenant->id) {
throw new \InvalidArgumentException('Backup set does not belong to the provided tenant.');
}
if ($selectedItemIds === []) {
$selectedItemIds = null;
}
$items = $this->loadItems($backupSet, $selectedItemIds);
$policyItems = $items
->reject(fn (BackupItem $item): bool => $item->isFoundation())
->values();
$policyIds = $policyItems
->pluck('policy_id')
->filter()
->unique()
->values()
->all();
$latestVersions = $this->latestVersionsByPolicyId($tenant, $policyIds);
$maxDetailedDiffs = 25;
$maxEntriesPerSection = 200;
$policiesChanged = 0;
$assignmentsChanged = 0;
$scopeTagsChanged = 0;
$diffs = [];
$diffsOmitted = 0;
foreach ($policyItems as $index => $item) {
$policyId = $item->policy_id ? (int) $item->policy_id : null;
$currentVersion = $policyId ? ($latestVersions[$policyId] ?? null) : null;
$currentSnapshot = is_array($currentVersion?->snapshot) ? $currentVersion->snapshot : [];
$backupSnapshot = is_array($item->payload) ? $item->payload : [];
$policyType = (string) ($item->policy_type ?? '');
$platform = $item->platform;
$from = $this->policyNormalizer->flattenForDiff($currentSnapshot, $policyType, $platform);
$to = $this->policyNormalizer->flattenForDiff($backupSnapshot, $policyType, $platform);
$diff = $this->versionDiff->compare($from, $to);
$summary = $diff['summary'] ?? ['added' => 0, 'removed' => 0, 'changed' => 0];
$hasPolicyChanges = ((int) ($summary['added'] ?? 0) + (int) ($summary['removed'] ?? 0) + (int) ($summary['changed'] ?? 0)) > 0;
if ($hasPolicyChanges) {
$policiesChanged++;
}
$assignmentDiff = $this->assignmentsChanged($item->assignments, $currentVersion?->assignments);
if ($assignmentDiff) {
$assignmentsChanged++;
}
$scopeTagDiff = $this->scopeTagsChanged($item, $currentVersion);
if ($scopeTagDiff) {
$scopeTagsChanged++;
}
$diffEntry = [
'backup_item_id' => $item->id,
'display_name' => $item->resolvedDisplayName(),
'policy_identifier' => $item->policy_identifier,
'policy_type' => $policyType,
'platform' => $platform,
'action' => $currentVersion ? 'update' : 'create',
'diff' => [
'summary' => $summary,
'added' => [],
'removed' => [],
'changed' => [],
],
'assignments_changed' => $assignmentDiff,
'scope_tags_changed' => $scopeTagDiff,
'diff_omitted' => false,
'diff_truncated' => false,
];
if ($index >= $maxDetailedDiffs) {
$diffEntry['diff_omitted'] = true;
$diffEntry['diff_truncated'] = true;
$diffEntry['diff'] = [
'summary' => $summary,
];
$diffsOmitted++;
$diffs[] = $diffEntry;
continue;
}
$added = is_array($diff['added'] ?? null) ? $diff['added'] : [];
$removed = is_array($diff['removed'] ?? null) ? $diff['removed'] : [];
$changed = is_array($diff['changed'] ?? null) ? $diff['changed'] : [];
$diffEntry['diff_truncated'] = count($added) > $maxEntriesPerSection
|| count($removed) > $maxEntriesPerSection
|| count($changed) > $maxEntriesPerSection;
$diffEntry['diff'] = [
'summary' => $summary,
'added' => array_slice($added, 0, $maxEntriesPerSection, true),
'removed' => array_slice($removed, 0, $maxEntriesPerSection, true),
'changed' => array_slice($changed, 0, $maxEntriesPerSection, true),
];
$diffs[] = $diffEntry;
}
return [
'summary' => [
'generated_at' => CarbonImmutable::now()->toIso8601String(),
'policies_total' => $policyItems->count(),
'policies_changed' => $policiesChanged,
'assignments_changed' => $assignmentsChanged,
'scope_tags_changed' => $scopeTagsChanged,
'diffs_detailed' => min($policyItems->count(), $maxDetailedDiffs),
'diffs_omitted' => $diffsOmitted,
'limits' => [
'max_detailed_diffs' => $maxDetailedDiffs,
'max_entries_per_section' => $maxEntriesPerSection,
],
],
'diffs' => $diffs,
];
}
/**
* @param array<int>|null $selectedItemIds
* @return Collection<int, BackupItem>
*/
private function loadItems(BackupSet $backupSet, ?array $selectedItemIds): Collection
{
$query = $backupSet->items()->getQuery();
if ($selectedItemIds !== null) {
$query->whereIn('id', $selectedItemIds);
}
return $query->orderBy('id')->get();
}
/**
* @param array<int, int> $policyIds
* @return array<int, PolicyVersion>
*/
private function latestVersionsByPolicyId(Tenant $tenant, array $policyIds): array
{
if ($policyIds === []) {
return [];
}
$latestVersionsQuery = PolicyVersion::query()
->where('tenant_id', $tenant->id)
->whereIn('policy_id', $policyIds)
->selectRaw('policy_id, max(version_number) as version_number')
->groupBy('policy_id');
return PolicyVersion::query()
->where('tenant_id', $tenant->id)
->joinSub($latestVersionsQuery, 'latest_versions', function ($join): void {
$join->on('policy_versions.policy_id', '=', 'latest_versions.policy_id')
->on('policy_versions.version_number', '=', 'latest_versions.version_number');
})
->get()
->keyBy('policy_id')
->all();
}
private function assignmentsChanged(?array $backupAssignments, ?array $currentAssignments): bool
{
$backup = $this->normalizeAssignments($backupAssignments);
$current = $this->normalizeAssignments($currentAssignments);
return $backup !== $current;
}
private function scopeTagsChanged(BackupItem $backupItem, ?PolicyVersion $currentVersion): bool
{
$backupIds = $backupItem->scope_tag_ids;
$backupIds = is_array($backupIds) ? $backupIds : [];
$backupIds = array_values(array_filter($backupIds, fn (mixed $id): bool => is_string($id) && $id !== '' && $id !== '0'));
sort($backupIds);
$scopeTags = $currentVersion?->scope_tags;
$currentIds = is_array($scopeTags) ? ($scopeTags['ids'] ?? []) : [];
$currentIds = is_array($currentIds) ? $currentIds : [];
$currentIds = array_values(array_filter($currentIds, fn (mixed $id): bool => is_string($id) && $id !== '' && $id !== '0'));
sort($currentIds);
return $backupIds !== $currentIds;
}
/**
* @return array<int, array<string, mixed>>
*/
private function normalizeAssignments(?array $assignments): array
{
$assignments = is_array($assignments) ? $assignments : [];
$normalized = [];
foreach ($assignments as $assignment) {
if (! is_array($assignment)) {
continue;
}
$normalized[] = $assignment;
}
usort($normalized, function (array $a, array $b): int {
$left = json_encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '';
$right = json_encode($b, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '';
return $left <=> $right;
});
return $normalized;
}
}