TenantAtlas/app/Services/Baselines/BaselineContentCapturePhase.php

130 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Baselines;
use App\Models\Policy;
use App\Models\Tenant;
use App\Services\Intune\PolicyCaptureOrchestrator;
use App\Support\Baselines\BaselineEvidenceResumeToken;
use App\Support\Baselines\PolicyVersionCapturePurpose;
final class BaselineContentCapturePhase
{
public function __construct(
private readonly PolicyCaptureOrchestrator $captureOrchestrator,
) {}
/**
* Capture baseline-purpose policy versions (content + assignments + scope tags) within a run budget.
*
* @param list<array{policy_type: string, subject_external_id: string}> $subjects
* @param array{max_items_per_run: int, max_concurrency: int, max_retries: int} $budgets
* @return array{
* stats: array{requested: int, succeeded: int, skipped: int, failed: int, throttled: int},
* gaps: array<string, int>,
* resume_token: ?string
* }
*/
public function capture(
Tenant $tenant,
array $subjects,
PolicyVersionCapturePurpose $purpose,
array $budgets,
?string $resumeToken = null,
?int $operationRunId = null,
?int $baselineProfileId = null,
?string $createdBy = null,
): array {
$maxItemsPerRun = max(0, (int) ($budgets['max_items_per_run'] ?? 0));
$offset = 0;
if (is_string($resumeToken) && $resumeToken !== '') {
$state = BaselineEvidenceResumeToken::decode($resumeToken) ?? [];
$offset = is_numeric($state['offset'] ?? null) ? max(0, (int) $state['offset']) : 0;
}
$remaining = array_slice($subjects, $offset);
$batch = $maxItemsPerRun > 0 ? array_slice($remaining, 0, $maxItemsPerRun) : [];
$stats = [
'requested' => count($batch),
'succeeded' => 0,
'skipped' => 0,
'failed' => 0,
'throttled' => 0,
];
/** @var array<string, int> $gaps */
$gaps = [];
foreach ($batch as $subject) {
$policyType = trim((string) ($subject['policy_type'] ?? ''));
$externalId = trim((string) ($subject['subject_external_id'] ?? ''));
if ($policyType === '' || $externalId === '') {
$gaps['invalid_subject'] = ($gaps['invalid_subject'] ?? 0) + 1;
$stats['failed']++;
continue;
}
$policy = Policy::query()
->where('tenant_id', (int) $tenant->getKey())
->where('policy_type', $policyType)
->where('external_id', $externalId)
->first();
if (! $policy instanceof Policy) {
$gaps['policy_not_found'] = ($gaps['policy_not_found'] ?? 0) + 1;
$stats['failed']++;
continue;
}
$result = $this->captureOrchestrator->capture(
policy: $policy,
tenant: $tenant,
includeAssignments: true,
includeScopeTags: true,
createdBy: $createdBy,
metadata: [
'capture_source' => 'baseline_evidence',
],
capturePurpose: $purpose,
operationRunId: $operationRunId,
baselineProfileId: $baselineProfileId,
);
if (is_array($result) && array_key_exists('failure', $result)) {
$gaps['capture_failed'] = ($gaps['capture_failed'] ?? 0) + 1;
$stats['failed']++;
continue;
}
$stats['succeeded']++;
}
$processed = $offset + count($batch);
$resumeTokenOut = null;
if ($processed < count($subjects)) {
$resumeTokenOut = BaselineEvidenceResumeToken::encode([
'offset' => $processed,
]);
}
ksort($gaps);
return [
'stats' => $stats,
'gaps' => $gaps,
'resume_token' => $resumeTokenOut,
];
}
}