refactor(audit): decouple API audit classes from core via interfaces

Core code (lib/) no longer imports any MintyPHP\Module\Audit\* classes directly.
New ApiAuditServiceInterface and ApiSystemAuditReporterInterface follow the
existing pattern (interface + null implementation in core, concrete binding
from module registrar). DoctorRunner and helpers/app.php now resolve through
DI interfaces, ensuring the audit module remains fully optional.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 19:19:17 +02:00
parent 4967095bb8
commit a17c2c7cea
11 changed files with 101 additions and 16 deletions

View File

@@ -12,9 +12,13 @@ use MintyPHP\App\Container\Registrars\SettingsRegistrar;
use MintyPHP\App\Container\Registrars\UserRegistrar; use MintyPHP\App\Container\Registrars\UserRegistrar;
use MintyPHP\App\Module\ModuleEventDispatcher; use MintyPHP\App\Module\ModuleEventDispatcher;
use MintyPHP\App\Module\ModuleRegistry; use MintyPHP\App\Module\ModuleRegistry;
use MintyPHP\Service\Audit\ApiAuditServiceInterface;
use MintyPHP\Service\Audit\ApiSystemAuditReporterInterface;
use MintyPHP\Service\Audit\AuditMetadataEnricherInterface; use MintyPHP\Service\Audit\AuditMetadataEnricherInterface;
use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Audit\AuditRecorderInterface;
use MintyPHP\Service\Audit\ImportAuditInterface; use MintyPHP\Service\Audit\ImportAuditInterface;
use MintyPHP\Service\Audit\NullApiAuditService;
use MintyPHP\Service\Audit\NullApiSystemAuditReporter;
use MintyPHP\Service\Audit\NullAuditMetadataEnricher; use MintyPHP\Service\Audit\NullAuditMetadataEnricher;
use MintyPHP\Service\Audit\NullAuditRecorder; use MintyPHP\Service\Audit\NullAuditRecorder;
use MintyPHP\Service\Audit\NullImportAudit; use MintyPHP\Service\Audit\NullImportAudit;
@@ -87,5 +91,11 @@ if (!$container->has(ImportAuditInterface::class)) {
if (!$container->has(AuditMetadataEnricherInterface::class)) { if (!$container->has(AuditMetadataEnricherInterface::class)) {
$container->set(AuditMetadataEnricherInterface::class, static fn (): AuditMetadataEnricherInterface => new NullAuditMetadataEnricher()); $container->set(AuditMetadataEnricherInterface::class, static fn (): AuditMetadataEnricherInterface => new NullAuditMetadataEnricher());
} }
if (!$container->has(ApiAuditServiceInterface::class)) {
$container->set(ApiAuditServiceInterface::class, static fn (): ApiAuditServiceInterface => new NullApiAuditService());
}
if (!$container->has(ApiSystemAuditReporterInterface::class)) {
$container->set(ApiSystemAuditReporterInterface::class, static fn (): ApiSystemAuditReporterInterface => new NullApiSystemAuditReporter());
}
return $container; return $container;

View File

@@ -8,10 +8,10 @@ use MintyPHP\App\AppContainer;
use MintyPHP\App\Bootstrap\EnvValidator; use MintyPHP\App\Bootstrap\EnvValidator;
use MintyPHP\Console\Support\CliAppBootstrap; use MintyPHP\Console\Support\CliAppBootstrap;
use MintyPHP\DB; use MintyPHP\DB;
use MintyPHP\Module\Audit\Service\SystemAuditService;
use MintyPHP\Service\Access\AuthorizationService; use MintyPHP\Service\Access\AuthorizationService;
use MintyPHP\Service\Access\PermissionService; use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Access\UiAccessService; use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Service\Audit\AuditRecorderInterface;
use MintyPHP\Service\Auth\AuthService; use MintyPHP\Service\Auth\AuthService;
use Throwable; use Throwable;
@@ -283,7 +283,7 @@ final class DoctorRunner implements DoctorRunnerInterface
$durationMs = max(0, (int) round((microtime(true) - $startedAt) * 1000)); $durationMs = max(0, (int) round((microtime(true) - $startedAt) * 1000));
try { try {
app(SystemAuditService::class)->record( app(AuditRecorderInterface::class)->record(
'cli.command', 'cli.command',
$exitCode === 0 ? 'success' : 'failed', $exitCode === 0 ? 'success' : 'failed',
[ [

View File

@@ -0,0 +1,18 @@
<?php
namespace MintyPHP\Service\Audit;
/**
* Core-side contract for API request audit tracking.
*
* Implemented by the audit module's ApiAuditService when the module is active.
* Falls back to NullApiAuditService (no-op) when the audit module is disabled.
*/
interface ApiAuditServiceInterface
{
public function startRequestContext(): void;
public function finish(int $statusCode, ?string $errorCode = null): void;
public function currentRequestId(): ?string;
}

View File

@@ -0,0 +1,16 @@
<?php
namespace MintyPHP\Service\Audit;
/**
* Core-side contract for API system-level audit reporting.
*
* Implemented by the audit module's ApiSystemAuditReporter when the module is active.
* Falls back to NullApiSystemAuditReporter (no-op) when the audit module is disabled.
*/
interface ApiSystemAuditReporterInterface
{
public function start(): void;
public function finish(int $statusCode, ?string $errorCode = null): void;
}

View File

@@ -0,0 +1,22 @@
<?php
namespace MintyPHP\Service\Audit;
/**
* No-op API audit service used when the audit module is disabled.
*/
final class NullApiAuditService implements ApiAuditServiceInterface
{
public function startRequestContext(): void
{
}
public function finish(int $statusCode, ?string $errorCode = null): void
{
}
public function currentRequestId(): ?string
{
return null;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace MintyPHP\Service\Audit;
/**
* No-op API system audit reporter used when the audit module is disabled.
*/
final class NullApiSystemAuditReporter implements ApiSystemAuditReporterInterface
{
public function start(): void
{
}
public function finish(int $statusCode, ?string $errorCode = null): void
{
}
}

View File

@@ -41,24 +41,16 @@ function setAppContainer(\MintyPHP\App\AppContainer $container): void
); );
\MintyPHP\Http\ApiBootstrap::configure( \MintyPHP\Http\ApiBootstrap::configure(
$container->has(\MintyPHP\Module\Audit\Service\ApiAuditService::class) static fn (): \MintyPHP\Service\Audit\ApiAuditServiceInterface => $container->get(\MintyPHP\Service\Audit\ApiAuditServiceInterface::class),
? static fn () => $container->get(\MintyPHP\Module\Audit\Service\ApiAuditService::class)
: null,
static fn (): \MintyPHP\Service\Settings\SettingsApiPolicyGateway => $container->get(\MintyPHP\Service\Settings\SettingsApiPolicyGateway::class), static fn (): \MintyPHP\Service\Settings\SettingsApiPolicyGateway => $container->get(\MintyPHP\Service\Settings\SettingsApiPolicyGateway::class),
static fn (): \MintyPHP\Service\Security\RateLimiterService => $container->get(\MintyPHP\Service\Security\RateLimiterService::class), static fn (): \MintyPHP\Service\Security\RateLimiterService => $container->get(\MintyPHP\Service\Security\RateLimiterService::class),
$container->has(\MintyPHP\Module\Audit\Http\ApiSystemAuditReporter::class) static fn (): \MintyPHP\Service\Audit\ApiSystemAuditReporterInterface => $container->get(\MintyPHP\Service\Audit\ApiSystemAuditReporterInterface::class)
? static fn () => $container->get(\MintyPHP\Module\Audit\Http\ApiSystemAuditReporter::class)
: null
); );
\MintyPHP\Http\ApiResponse::configure( \MintyPHP\Http\ApiResponse::configure(
$container->has(\MintyPHP\Module\Audit\Service\ApiAuditService::class) static fn (): \MintyPHP\Service\Audit\ApiAuditServiceInterface => $container->get(\MintyPHP\Service\Audit\ApiAuditServiceInterface::class),
? static fn () => $container->get(\MintyPHP\Module\Audit\Service\ApiAuditService::class)
: null,
static fn (): \MintyPHP\Service\Access\AuthorizationService => $container->get(\MintyPHP\Service\Access\AuthorizationService::class), static fn (): \MintyPHP\Service\Access\AuthorizationService => $container->get(\MintyPHP\Service\Access\AuthorizationService::class),
$container->has(\MintyPHP\Module\Audit\Http\ApiSystemAuditReporter::class) static fn (): \MintyPHP\Service\Audit\ApiSystemAuditReporterInterface => $container->get(\MintyPHP\Service\Audit\ApiSystemAuditReporterInterface::class)
? static fn () => $container->get(\MintyPHP\Module\Audit\Http\ApiSystemAuditReporter::class)
: null
); );
\MintyPHP\Support\Guard::configure( \MintyPHP\Support\Guard::configure(

View File

@@ -11,6 +11,8 @@ use MintyPHP\Module\Audit\Handler\ImportAuditPurgeJobHandler;
use MintyPHP\Module\Audit\Handler\SystemAuditPurgeJobHandler; use MintyPHP\Module\Audit\Handler\SystemAuditPurgeJobHandler;
use MintyPHP\Module\Audit\Handler\UserLifecycleAuditPurgeJobHandler; use MintyPHP\Module\Audit\Handler\UserLifecycleAuditPurgeJobHandler;
use MintyPHP\Module\Audit\Http\ApiSystemAuditReporter; use MintyPHP\Module\Audit\Http\ApiSystemAuditReporter;
use MintyPHP\Service\Audit\ApiAuditServiceInterface;
use MintyPHP\Service\Audit\ApiSystemAuditReporterInterface;
use MintyPHP\Module\Audit\Providers\AuditLayoutProvider; use MintyPHP\Module\Audit\Providers\AuditLayoutProvider;
use MintyPHP\Module\Audit\Repository\ApiAuditLogRepository; use MintyPHP\Module\Audit\Repository\ApiAuditLogRepository;
use MintyPHP\Module\Audit\Repository\ImportAuditRunRepository; use MintyPHP\Module\Audit\Repository\ImportAuditRunRepository;
@@ -68,6 +70,8 @@ final class AuditContainerRegistrar implements ContainerRegistrar
$container->set(AuditRecorderInterface::class, static fn (AppContainer $c): AuditRecorderInterface => $c->get(SystemAuditService::class)); $container->set(AuditRecorderInterface::class, static fn (AppContainer $c): AuditRecorderInterface => $c->get(SystemAuditService::class));
$container->set(UserLifecycleAuditInterface::class, static fn (AppContainer $c): UserLifecycleAuditInterface => $c->get(UserLifecycleAuditService::class)); $container->set(UserLifecycleAuditInterface::class, static fn (AppContainer $c): UserLifecycleAuditInterface => $c->get(UserLifecycleAuditService::class));
$container->set(ImportAuditInterface::class, static fn (AppContainer $c): ImportAuditInterface => $c->get(ImportAuditService::class)); $container->set(ImportAuditInterface::class, static fn (AppContainer $c): ImportAuditInterface => $c->get(ImportAuditService::class));
$container->set(ApiAuditServiceInterface::class, static fn (AppContainer $c): ApiAuditServiceInterface => $c->get(ApiAuditService::class));
$container->set(ApiSystemAuditReporterInterface::class, static fn (AppContainer $c): ApiSystemAuditReporterInterface => $c->get(ApiSystemAuditReporter::class));
// Frontend telemetry // Frontend telemetry
$container->set(FrontendTelemetryIngestService::class, static fn (AppContainer $c): FrontendTelemetryIngestService => new FrontendTelemetryIngestService( $container->set(FrontendTelemetryIngestService::class, static fn (AppContainer $c): FrontendTelemetryIngestService => new FrontendTelemetryIngestService(

View File

@@ -6,8 +6,9 @@ use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\RequestContext; use MintyPHP\Http\RequestContext;
use MintyPHP\Module\Audit\Domain\SystemAuditOutcome; use MintyPHP\Module\Audit\Domain\SystemAuditOutcome;
use MintyPHP\Module\Audit\Service\SystemAuditService; use MintyPHP\Module\Audit\Service\SystemAuditService;
use MintyPHP\Service\Audit\ApiSystemAuditReporterInterface;
class ApiSystemAuditReporter class ApiSystemAuditReporter implements ApiSystemAuditReporterInterface
{ {
/** /**
* @var array{ * @var array{

View File

@@ -6,8 +6,9 @@ use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\RequestContext; use MintyPHP\Http\RequestContext;
use MintyPHP\Http\RequestRuntimeInterface; use MintyPHP\Http\RequestRuntimeInterface;
use MintyPHP\Module\Audit\Repository\ApiAuditLogRepository; use MintyPHP\Module\Audit\Repository\ApiAuditLogRepository;
use MintyPHP\Service\Audit\ApiAuditServiceInterface;
class ApiAuditService class ApiAuditService implements ApiAuditServiceInterface
{ {
private const RETENTION_DAYS = 90; private const RETENTION_DAYS = 90;
private const MAX_QUERY_KEYS = 30; private const MAX_QUERY_KEYS = 30;

View File

@@ -17,9 +17,13 @@ final class AuditModuleIsolationContractTest extends TestCase
)); ));
$allowed = [ $allowed = [
'ApiAuditServiceInterface.php',
'ApiSystemAuditReporterInterface.php',
'AuditMetadataEnricherInterface.php', 'AuditMetadataEnricherInterface.php',
'AuditRecorderInterface.php', 'AuditRecorderInterface.php',
'ImportAuditInterface.php', 'ImportAuditInterface.php',
'NullApiAuditService.php',
'NullApiSystemAuditReporter.php',
'NullAuditMetadataEnricher.php', 'NullAuditMetadataEnricher.php',
'NullAuditRecorder.php', 'NullAuditRecorder.php',
'NullImportAudit.php', 'NullImportAudit.php',