refactor(audit): extract SystemAuditRowPresenter shared by data + export
Move the row formatting that was duplicated between system-audit's data() and export() endpoints into a single presenter. Both endpoints now call presentAll() on the same object, so enum label translation, actor resolution, and timestamp formatting cannot drift between the Grid.js UI and the CSV download. - SystemAuditRowPresenter::present()/presentAll(): canonical row shape with outcome + outcome_label + outcome_badge, channel + channel_label, actor_user_label with display-name-then-email fallback, and safe defaults for every missing field. - data().php shrinks from ~45 to ~15 lines; export().php drops its inline outcome/channel/actor resolvers and reads the already- resolved fields from the presenter output. - AuditContainerRegistrar registers the presenter. - tests/Module/Audit/Service/SystemAuditRowPresenterTest: 9 cases covering enum normalization, unknown-value fallback, actor label precedence (display name → email → "-"), whitespace trimming, safe defaults for missing keys, and iterable input. - StatusTaxonomyContractFiles: the taxonomy data contract now points at the presenter (the single source of truth for badge/label resolution) instead of the thin data endpoint, and the presenter is added to the literal-guard file list. Gates: PHPUnit 1889 OK, PHPStan 0 errors, module:sync ok. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ use MintyPHP\Module\Audit\Service\AuditMetadataEnricher;
|
||||
use MintyPHP\Module\Audit\Service\FrontendTelemetryIngestService;
|
||||
use MintyPHP\Module\Audit\Service\ImportAuditService;
|
||||
use MintyPHP\Module\Audit\Service\SystemAuditRedactionService;
|
||||
use MintyPHP\Module\Audit\Service\SystemAuditRowPresenter;
|
||||
use MintyPHP\Module\Audit\Service\SystemAuditService;
|
||||
use MintyPHP\Module\Audit\Service\UserLifecycleAuditService;
|
||||
use MintyPHP\Repository\User\UserReadRepository;
|
||||
@@ -47,6 +48,7 @@ final class AuditContainerRegistrar implements ContainerRegistrar
|
||||
|
||||
// Redaction
|
||||
$container->set(SystemAuditRedactionService::class, static fn (): SystemAuditRedactionService => new SystemAuditRedactionService());
|
||||
$container->set(SystemAuditRowPresenter::class, static fn (): SystemAuditRowPresenter => new SystemAuditRowPresenter());
|
||||
|
||||
// Concrete audit services
|
||||
$container->set(SystemAuditService::class, static fn (AppContainer $c): SystemAuditService => new SystemAuditService(
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Module\Audit\Service;
|
||||
|
||||
use MintyPHP\Module\Audit\Domain\SystemAuditChannel;
|
||||
use MintyPHP\Module\Audit\Domain\SystemAuditOutcome;
|
||||
|
||||
/**
|
||||
* Converts raw system audit log rows (as returned by the repository)
|
||||
* into a canonical presentation-ready row shape. One source of truth
|
||||
* shared by the Grid.js data endpoint and the CSV export endpoint, so
|
||||
* enum label translation, actor resolution, and timestamp formatting
|
||||
* can never drift between the UI and the download.
|
||||
*/
|
||||
final class SystemAuditRowPresenter
|
||||
{
|
||||
/**
|
||||
* @param array<string,mixed> $row
|
||||
* @return array{
|
||||
* id: int,
|
||||
* created_at: string,
|
||||
* event_type: string,
|
||||
* outcome: string,
|
||||
* outcome_badge: string,
|
||||
* outcome_label: string,
|
||||
* channel: string,
|
||||
* channel_label: string,
|
||||
* actor_user_id: int,
|
||||
* actor_user_uuid: string,
|
||||
* actor_user_label: string,
|
||||
* actor_user_email: string,
|
||||
* target_type: string,
|
||||
* target_uuid: string,
|
||||
* request_id: string,
|
||||
* error_code: string
|
||||
* }
|
||||
*/
|
||||
public function present(array $row): array
|
||||
{
|
||||
$outcome = SystemAuditOutcome::normalizeOr((string) ($row['outcome'] ?? ''), SystemAuditOutcome::Success);
|
||||
$channel = SystemAuditChannel::normalizeOr((string) ($row['channel'] ?? ''), SystemAuditChannel::Web);
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'created_at' => (string) dt((string) ($row['created_at'] ?? '')),
|
||||
'event_type' => (string) ($row['event_type'] ?? ''),
|
||||
'outcome' => $outcome->value,
|
||||
'outcome_badge' => $outcome->badgeVariant(),
|
||||
'outcome_label' => (string) t($outcome->labelToken()),
|
||||
'channel' => strtoupper($channel->labelToken()),
|
||||
'channel_label' => strtoupper($channel->labelToken()),
|
||||
'actor_user_id' => (int) ($row['actor_user_id'] ?? 0),
|
||||
'actor_user_uuid' => (string) ($row['actor_user_uuid'] ?? ''),
|
||||
'actor_user_label' => $this->resolveActorLabel($row),
|
||||
'actor_user_email' => (string) ($row['actor_user_email'] ?? ''),
|
||||
'target_type' => (string) ($row['target_type'] ?? ''),
|
||||
'target_uuid' => (string) ($row['target_uuid'] ?? ''),
|
||||
'request_id' => (string) ($row['request_id'] ?? ''),
|
||||
'error_code' => (string) ($row['error_code'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @api Called from data and export endpoints
|
||||
* @param iterable<array<string,mixed>> $rows
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
public function presentAll(iterable $rows): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($rows as $row) {
|
||||
$result[] = $this->present($row);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function resolveActorLabel(array $row): string
|
||||
{
|
||||
$name = trim((string) ($row['actor_user_display_name'] ?? ''));
|
||||
if ($name !== '') {
|
||||
return $name;
|
||||
}
|
||||
$email = trim((string) ($row['actor_user_email'] ?? ''));
|
||||
return $email !== '' ? $email : '-';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user