refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit, user lifecycle audit, frontend telemetry) from core into modules/audit/. Core decoupling via interface-based injection: - AuditRecorderInterface replaces SystemAuditService in 10+ core services - UserLifecycleAuditInterface / ImportAuditInterface for specialized flows - NullAuditRecorder fallback when audit module is disabled - ApiBootstrap/ApiResponse use null-safe callable resolvers Module structure (modules/audit/): - Manifest with routes, permissions, scheduler jobs, authorization policy - 9 services, 8 repositories, 6 domain enums, 4 job handlers - 33 page files, 4 JS files, 8 test files, migration scripts, i18n Core cleanup: - OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService surgically cleaned of audit-specific constants - Sidebar template cleared of hardcoded audit navigation - AuditModuleIsolationContractTest ensures no future core→module coupling All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5 clean, architecture contracts verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,21 +4,20 @@ namespace MintyPHP\Http;
|
||||
|
||||
use MintyPHP\Http\Input\FormErrors;
|
||||
use MintyPHP\Service\Access\AuthorizationService;
|
||||
use MintyPHP\Service\Audit\ApiAuditService;
|
||||
|
||||
class ApiResponse
|
||||
{
|
||||
/** @var (callable(): ApiAuditService)|null */
|
||||
/** @var (callable(): object)|null Resolver for API audit service (provided by audit module) */
|
||||
private static $apiAuditServiceResolver = null;
|
||||
/** @var (callable(): AuthorizationService)|null */
|
||||
private static $authorizationServiceResolver = null;
|
||||
/** @var (callable(): ApiSystemAuditReporter)|null */
|
||||
/** @var (callable(): object)|null Resolver for API system audit reporter (provided by audit module) */
|
||||
private static $apiSystemAuditReporterResolver = null;
|
||||
|
||||
public static function configure(
|
||||
callable $apiAuditServiceResolver,
|
||||
?callable $apiAuditServiceResolver,
|
||||
callable $authorizationServiceResolver,
|
||||
callable $apiSystemAuditReporterResolver
|
||||
?callable $apiSystemAuditReporterResolver
|
||||
): void {
|
||||
self::$apiAuditServiceResolver = $apiAuditServiceResolver;
|
||||
self::$authorizationServiceResolver = $authorizationServiceResolver;
|
||||
@@ -169,7 +168,7 @@ class ApiResponse
|
||||
|
||||
if ($body === null) {
|
||||
self::finishSystemAuditReporter($status, $errorCode);
|
||||
self::apiAuditService()->finish($status, $errorCode);
|
||||
self::tryFinishAudit($status, $errorCode);
|
||||
die();
|
||||
}
|
||||
|
||||
@@ -190,7 +189,7 @@ class ApiResponse
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
self::finishSystemAuditReporter($status, $errorCode);
|
||||
self::apiAuditService()->finish($status, $errorCode);
|
||||
self::tryFinishAudit($status, $errorCode);
|
||||
die($json);
|
||||
}
|
||||
|
||||
@@ -201,7 +200,7 @@ class ApiResponse
|
||||
return trim($requestId);
|
||||
}
|
||||
|
||||
$requestId = self::apiAuditService()->currentRequestId();
|
||||
$requestId = self::tryGetAuditRequestId();
|
||||
if (is_string($requestId) && trim($requestId) !== '') {
|
||||
return trim($requestId);
|
||||
}
|
||||
@@ -239,18 +238,33 @@ class ApiResponse
|
||||
return $details;
|
||||
}
|
||||
|
||||
private static function apiAuditService(): ApiAuditService
|
||||
private static function tryFinishAudit(int $statusCode, ?string $errorCode = null): void
|
||||
{
|
||||
if (!is_callable(self::$apiAuditServiceResolver)) {
|
||||
throw new \RuntimeException('ApiResponse is not configured for dependency: ' . ApiAuditService::class);
|
||||
try {
|
||||
if (is_callable(self::$apiAuditServiceResolver)) {
|
||||
$service = (self::$apiAuditServiceResolver)();
|
||||
if (is_object($service) && method_exists($service, 'finish')) {
|
||||
$service->finish($statusCode, $errorCode);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// fail-open
|
||||
}
|
||||
}
|
||||
|
||||
$service = (self::$apiAuditServiceResolver)();
|
||||
if (!$service instanceof ApiAuditService) {
|
||||
throw new \RuntimeException('ApiResponse resolver returned invalid dependency: ' . ApiAuditService::class);
|
||||
private static function tryGetAuditRequestId(): ?string
|
||||
{
|
||||
try {
|
||||
if (is_callable(self::$apiAuditServiceResolver)) {
|
||||
$service = (self::$apiAuditServiceResolver)();
|
||||
if (is_object($service) && method_exists($service, 'currentRequestId')) {
|
||||
return $service->currentRequestId();
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// fail-open
|
||||
}
|
||||
|
||||
return $service;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function authorizationService(): AuthorizationService
|
||||
@@ -267,24 +281,15 @@ class ApiResponse
|
||||
return $service;
|
||||
}
|
||||
|
||||
private static function systemAuditReporter(): ApiSystemAuditReporter
|
||||
{
|
||||
if (!is_callable(self::$apiSystemAuditReporterResolver)) {
|
||||
throw new \RuntimeException('ApiResponse is not configured for dependency: ' . ApiSystemAuditReporter::class);
|
||||
}
|
||||
|
||||
$service = (self::$apiSystemAuditReporterResolver)();
|
||||
if (!$service instanceof ApiSystemAuditReporter) {
|
||||
throw new \RuntimeException('ApiResponse resolver returned invalid dependency: ' . ApiSystemAuditReporter::class);
|
||||
}
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
private static function finishSystemAuditReporter(int $statusCode, ?string $errorCode = null): void
|
||||
{
|
||||
try {
|
||||
self::systemAuditReporter()->finish($statusCode, $errorCode);
|
||||
if (is_callable(self::$apiSystemAuditReporterResolver)) {
|
||||
$service = (self::$apiSystemAuditReporterResolver)();
|
||||
if (is_object($service) && method_exists($service, 'finish')) {
|
||||
$service->finish($statusCode, $errorCode);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// fail-open
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user