|null $settings Optional override (tests pass in fixtures). */ public static function build(?array $settings = null): ContainerInterface { $settings ??= require __DIR__ . '/../../config/settings.php'; $builder = new ContainerBuilder(); $builder->useAutowiring(true); $builder->addDefinitions([ 'settings' => $settings, 'settings.db' => $settings['db'], 'settings.ui_service_token' => $settings['ui_service_token'] ?? '', 'settings.app_env' => $settings['app_env'] ?? 'production', 'settings.log_level' => $settings['log_level'] ?? \Monolog\Level::Info, 'settings.oidc_default_role' => $settings['oidc_default_role'] ?? Role::Viewer, 'settings.score_hard_cutoff_days' => (int) ($settings['score_hard_cutoff_days'] ?? 365), 'settings.rate_limit_per_second' => (int) ($settings['rate_limit_per_second'] ?? 60), 'settings.internal_job_token' => (string) ($settings['internal_job_token'] ?? ''), 'settings.job_recompute_max_runtime_seconds' => (int) ($settings['job_recompute_max_runtime_seconds'] ?? 240), 'settings.job_recompute_max_rows_per_tick' => (int) ($settings['job_recompute_max_rows_per_tick'] ?? 5000), 'settings.job_audit_retention_days' => (int) ($settings['job_audit_retention_days'] ?? 180), ConnectionFactory::class => factory(static function (ContainerInterface $c): ConnectionFactory { /** @var array{driver: string, sqlite_path: string, mysql_host: string, mysql_port: int, mysql_database: string, mysql_username: string, mysql_password: string} $db */ $db = $c->get('settings.db'); return new ConnectionFactory($db); }), Connection::class => factory(static function (ContainerInterface $c): Connection { /** @var ConnectionFactory $factory */ $factory = $c->get(ConnectionFactory::class); return $factory->create(); }), LoggerInterface::class => factory(static function (ContainerInterface $c): LoggerInterface { $logger = new Logger('api'); /** @var \Monolog\Level $level */ $level = $c->get('settings.log_level'); $handler = new StreamHandler('php://stdout', $level); $handler->setFormatter(new JsonFormatter()); $logger->pushHandler($handler); return $logger; }), ResponseFactoryInterface::class => autowire(ResponseFactory::class), Clock::class => autowire(SystemClock::class), TokenHasher::class => autowire(), TokenIssuer::class => autowire(), TokenRepository::class => autowire(), RoleMappingRepository::class => autowire(), UserRepository::class => autowire(), ReporterRepository::class => autowire(), ConsumerRepository::class => autowire(), CategoryRepository::class => autowire(), ReportRepository::class => autowire(), IpScoreRepository::class => autowire(), ServiceTokenBootstrap::class => autowire(), TokenAuthenticationMiddleware::class => autowire(), ImpersonationMiddleware::class => autowire(), PairScorer::class => factory(static function (ContainerInterface $c): PairScorer { /** @var ReportRepository $reports */ $reports = $c->get(ReportRepository::class); /** @var CategoryRepository $categories */ $categories = $c->get(CategoryRepository::class); /** @var Clock $clock */ $clock = $c->get(Clock::class); /** @var int $cutoff */ $cutoff = $c->get('settings.score_hard_cutoff_days'); return new PairScorer($reports, $categories, $clock, $cutoff); }), RateLimiter::class => factory(static function (ContainerInterface $c): RateLimiter { /** @var Clock $clock */ $clock = $c->get(Clock::class); /** @var int $perSecond */ $perSecond = $c->get('settings.rate_limit_per_second'); $perSecond = max(1, $perSecond); return new RateLimiter($clock, (float) $perSecond, (float) ($perSecond * 2)); }), RateLimitMiddleware::class => autowire(), JobLockRepository::class => autowire(), JobRunRepository::class => autowire(), JobRunner::class => factory(static function (ContainerInterface $c): JobRunner { /** @var JobLockRepository $locks */ $locks = $c->get(JobLockRepository::class); /** @var JobRunRepository $runs */ $runs = $c->get(JobRunRepository::class); /** @var Clock $clock */ $clock = $c->get(Clock::class); /** @var LoggerInterface $logger */ $logger = $c->get(LoggerInterface::class); return new JobRunner($locks, $runs, $clock, $logger); }), RecomputeScoresJob::class => factory(static function (ContainerInterface $c): RecomputeScoresJob { /** @var ReportRepository $reports */ $reports = $c->get(ReportRepository::class); /** @var IpScoreRepository $ipScores */ $ipScores = $c->get(IpScoreRepository::class); /** @var PairScorer $scorer */ $scorer = $c->get(PairScorer::class); /** @var int $maxRuntime */ $maxRuntime = $c->get('settings.job_recompute_max_runtime_seconds'); /** @var int $maxRows */ $maxRows = $c->get('settings.job_recompute_max_rows_per_tick'); return new RecomputeScoresJob($reports, $ipScores, $scorer, $maxRuntime, $maxRows); }), CleanupAuditJob::class => factory(static function (ContainerInterface $c): CleanupAuditJob { /** @var Connection $conn */ $conn = $c->get(Connection::class); /** @var int $days */ $days = $c->get('settings.job_audit_retention_days'); return new CleanupAuditJob($conn, $days); }), EnrichPendingJob::class => autowire(), TickJob::class => factory(static function (ContainerInterface $c): TickJob { /** @var JobRunner $runner */ $runner = $c->get(JobRunner::class); /** @var JobRunRepository $runs */ $runs = $c->get(JobRunRepository::class); // Closure indirection: TickJob iterates JobRegistry at // run() time, after the registry is fully populated. A // direct JobRegistry dependency would create a build-time // cycle (registry → tick → registry). $resolver = static fn (): array => $c->get(JobRegistry::class)->all(); return new TickJob($resolver, $runner, $runs); }), JobRegistry::class => factory(static function (ContainerInterface $c): JobRegistry { $registry = new JobRegistry(); /** @var RecomputeScoresJob $recompute */ $recompute = $c->get(RecomputeScoresJob::class); /** @var CleanupAuditJob $cleanup */ $cleanup = $c->get(CleanupAuditJob::class); /** @var EnrichPendingJob $enrich */ $enrich = $c->get(EnrichPendingJob::class); /** @var TickJob $tick */ $tick = $c->get(TickJob::class); $registry->register($recompute); $registry->register($cleanup); $registry->register($enrich); $registry->register($tick); return $registry; }), JobsController::class => autowire(), InternalNetworkMiddleware::class => autowire(), InternalTokenMiddleware::class => factory(static function (ContainerInterface $c): InternalTokenMiddleware { /** @var ResponseFactoryInterface $rf */ $rf = $c->get(ResponseFactoryInterface::class); /** @var string $token */ $token = $c->get('settings.internal_job_token'); return new InternalTokenMiddleware($rf, $token); }), JsonErrorHandler::class => factory(static function (ContainerInterface $c): JsonErrorHandler { /** @var ResponseFactoryInterface $factory */ $factory = $c->get(ResponseFactoryInterface::class); /** @var LoggerInterface $logger */ $logger = $c->get(LoggerInterface::class); return new JsonErrorHandler( $factory, $logger, $c->get('settings.app_env') === 'development', ); }), AuthController::class => factory(static function (ContainerInterface $c): AuthController { /** @var Role|null $role */ $role = $c->get('settings.oidc_default_role'); /** @var UserRepository $users */ $users = $c->get(UserRepository::class); return new AuthController($users, $role ?? Role::Viewer); }), MeController::class => autowire(), ReportersController::class => autowire(), ConsumersController::class => autowire(), TokensController::class => autowire(), ReportController::class => autowire(), ]); return $builder->build(); } }