1
0
Files
breadcrumb-the-shire/modules/audit/lib/Module/Audit/Service/ImportAuditService.php
fs 570e3923b7 refactor(audit): remove factory chain and repository interfaces
Remove AuditRepositoryFactory and AuditServicesFactory — two-layer
factory chain replaced by direct DI container wiring. Delete 4
single-consumer repository interfaces. Register all 4 repositories
and SystemAuditRedactionService directly in container. Update all
service constructors to type-hint concrete classes. Fix architecture
test and documentation references to deleted factories.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:33:59 +01:00

210 lines
6.5 KiB
PHP

<?php
namespace MintyPHP\Module\Audit\Service;
use MintyPHP\Module\Audit\Domain\ImportAuditStatus;
use MintyPHP\Module\Audit\Repository\ImportAuditRunRepository;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Service\Audit\ImportAuditInterface;
class ImportAuditService implements ImportAuditInterface
{
private const RETENTION_DAYS = 90;
/**
* @var array<int, float>
*/
private array $startedAtByRunId = [];
public function __construct(private readonly ImportAuditRunRepository $importAuditRunRepository)
{
}
public function startRun(
string $profileKey,
array $mappedTargets,
?string $sourceFilename,
int $userId,
?int $currentTenantId
): ?int {
$profileKey = trim($profileKey);
if ($profileKey === '') {
return null;
}
try {
$runId = $this->importAuditRunRepository->createRunning([
'run_uuid' => RepoQuery::uuidV4(),
'profile_key' => $profileKey,
'status' => ImportAuditStatus::Running->value,
'source_filename' => $this->normalizeSourceFilename($sourceFilename),
'mapped_targets_csv' => $this->normalizeMappedTargetsCsv($mappedTargets),
'user_id' => $userId > 0 ? $userId : null,
'current_tenant_id' => ($currentTenantId ?? 0) > 0 ? (int) $currentTenantId : null,
]);
} catch (\Throwable $exception) {
return null;
}
if (!$runId) {
return null;
}
$this->startedAtByRunId[(int) $runId] = microtime(true);
return (int) $runId;
}
public function finishRun(?int $runId, array $result, ?string $forcedStatus = null): void
{
$runId = (int) ($runId ?? 0);
if ($runId <= 0) {
return;
}
$rowsTotal = max(0, (int) ($result['processed'] ?? 0));
$createdCount = max(0, (int) ($result['created'] ?? 0));
$skippedCount = max(0, (int) ($result['skipped'] ?? 0));
$failedCount = max(0, (int) ($result['failed'] ?? 0));
$status = $this->normalizeStatus($forcedStatus);
if ($status === null) {
if (array_key_exists('ok', $result) && !($result['ok'] ?? false)) {
$status = ImportAuditStatus::Failed->value;
} elseif ($failedCount <= 0) {
$status = ImportAuditStatus::Success->value;
} elseif ($createdCount > 0 || $skippedCount > 0) {
$status = ImportAuditStatus::Partial->value;
} else {
$status = ImportAuditStatus::Failed->value;
}
}
$durationMs = null;
if (isset($this->startedAtByRunId[$runId])) {
$durationMs = (int) round((microtime(true) - $this->startedAtByRunId[$runId]) * 1000);
if ($durationMs < 0) {
$durationMs = 0;
}
unset($this->startedAtByRunId[$runId]);
}
$errorCodesJson = $this->encodeErrorCounts($result);
try {
$this->importAuditRunRepository->finishById($runId, [
'status' => $status,
'rows_total' => $rowsTotal,
'created_count' => $createdCount,
'skipped_count' => $skippedCount,
'failed_count' => $failedCount,
'error_codes_json' => $errorCodesJson,
'duration_ms' => $durationMs,
]);
} catch (\Throwable $exception) {
// Fail-open: import flow must not fail because audit logging fails.
}
}
public function listPaged(array $filters): array
{
return $this->importAuditRunRepository->listPaged($filters);
}
public function find(int $id): ?array
{
return $this->importAuditRunRepository->find($id);
}
public function filterOptions(int $limit = 200): array
{
return $this->importAuditRunRepository->listFilterOptions($limit);
}
public function purgeExpired(): int
{
return $this->importAuditRunRepository->purgeOlderThanDays(self::RETENTION_DAYS);
}
/**
* @param array<int, string> $mappedTargets
*/
private function normalizeMappedTargetsCsv(array $mappedTargets): ?string
{
$normalized = [];
foreach ($mappedTargets as $target) {
$value = trim((string) $target);
if ($value === '') {
continue;
}
$normalized[$value] = true;
}
if (!$normalized) {
return null;
}
$list = array_keys($normalized);
sort($list, SORT_STRING);
return implode(',', $list);
}
private function normalizeSourceFilename(?string $sourceFilename): ?string
{
$sourceFilename = trim((string) ($sourceFilename ?? ''));
if ($sourceFilename === '') {
return null;
}
$base = basename(str_replace("\0", '', $sourceFilename));
if ($base === '' || $base === '.' || $base === '..') {
return null;
}
return strlen($base) > 255 ? substr($base, 0, 255) : $base;
}
private function normalizeStatus(?string $status): ?string
{
return ImportAuditStatus::tryNormalize((string) ($status ?? ''))?->value;
}
private function encodeErrorCounts(array $result): ?string
{
$counts = [];
$fromResult = $result['error_counts'] ?? null;
if (is_array($fromResult)) {
foreach ($fromResult as $rawCode => $rawCount) {
$code = trim((string) $rawCode);
$count = (int) $rawCount;
if ($code === '' || $count <= 0) {
continue;
}
$counts[$code] = ($counts[$code] ?? 0) + $count;
}
}
if (!$counts) {
$errors = $result['errors'] ?? [];
if (is_array($errors)) {
foreach ($errors as $error) {
if (!is_array($error)) {
continue;
}
$code = trim((string) ($error['code'] ?? ''));
if ($code === '') {
continue;
}
$counts[$code] = ($counts[$code] ?? 0) + 1;
}
}
}
if (!$counts) {
return null;
}
ksort($counts, SORT_STRING);
$encoded = json_encode($counts, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return is_string($encoded) ? $encoded : null;
}
}