<?php
declare(strict_types=1);
namespace Machweis\Backend;
use PDO;
final readonly class AuditService
{
public function __construct(private PDO $pdo) {}
/** @param array<string, mixed>|null $before @param array<string, mixed>|null $after */
public function record(string $tenant, ?string $actor, string $eventType, string $subjectType, ?string $subjectId, ?array $before, ?array $after): void
{
$statement = $this->pdo->prepare('INSERT INTO tenant_audit_events (id, tenant_id, actor_user_id, event_type, subject_type, subject_id, before_json, after_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())');
$statement->execute([$this->uuid(), $tenant, $actor, $eventType, $subjectType, $subjectId, $before === null ? null : json_encode($before, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), $after === null ? null : json_encode($after, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)]);
}
/** @return list<array<string, mixed>> */
public function list(string $tenant, int $limit = 100): array
{
$limit = min(200, max(1, $limit));
$statement = $this->pdo->prepare("SELECT id, actor_user_id, event_type, subject_type, subject_id, before_json, after_json, created_at FROM tenant_audit_events WHERE tenant_id = ? ORDER BY created_at DESC, id DESC LIMIT $limit");
$statement->execute([$tenant]);
return array_map(static function (array $row): array {
foreach (['before_json' => 'before', 'after_json' => 'after'] as $source => $target) {
$value = $row[$source] ?? null;
$row[$target] = is_string($value) && $value !== '' ? json_decode($value, true, 512, JSON_THROW_ON_ERROR) : null;
unset($row[$source]);
}
return $row;
}, $statement->fetchAll());
}
private function uuid(): string
{
$bytes = random_bytes(16); $bytes[6] = chr((ord($bytes[6]) & 15) | 64); $bytes[8] = chr((ord($bytes[8]) & 63) | 128); $hex = bin2hex($bytes);
return substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-' . substr($hex, 12, 4) . '-' . substr($hex, 16, 4) . '-' . substr($hex, 20);
}
}
