Container.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\App;
  4. use App\Application\Admin\ConsumersController;
  5. use App\Application\Admin\MeController;
  6. use App\Application\Admin\ReportersController;
  7. use App\Application\Admin\TokensController;
  8. use App\Application\Auth\AuthController;
  9. use App\Application\Internal\JobsController;
  10. use App\Application\Jobs\CleanupAuditJob;
  11. use App\Application\Jobs\EnrichPendingJob;
  12. use App\Application\Jobs\RecomputeScoresJob;
  13. use App\Application\Jobs\TickJob;
  14. use App\Application\Public\ReportController;
  15. use App\Domain\Auth\Role;
  16. use App\Domain\Auth\TokenHasher;
  17. use App\Domain\Auth\TokenIssuer;
  18. use App\Domain\Reputation\PairScorer;
  19. use App\Domain\Time\Clock;
  20. use App\Domain\Time\SystemClock;
  21. use App\Infrastructure\Auth\RoleMappingRepository;
  22. use App\Infrastructure\Auth\ServiceTokenBootstrap;
  23. use App\Infrastructure\Auth\TokenRepository;
  24. use App\Infrastructure\Auth\UserRepository;
  25. use App\Infrastructure\Category\CategoryRepository;
  26. use App\Infrastructure\Consumer\ConsumerRepository;
  27. use App\Infrastructure\Db\ConnectionFactory;
  28. use App\Infrastructure\Http\JsonErrorHandler;
  29. use App\Infrastructure\Http\Middleware\ImpersonationMiddleware;
  30. use App\Infrastructure\Http\Middleware\InternalNetworkMiddleware;
  31. use App\Infrastructure\Http\Middleware\InternalTokenMiddleware;
  32. use App\Infrastructure\Http\Middleware\RateLimitMiddleware;
  33. use App\Infrastructure\Http\Middleware\TokenAuthenticationMiddleware;
  34. use App\Infrastructure\Http\RateLimiter;
  35. use App\Infrastructure\Jobs\JobLockRepository;
  36. use App\Infrastructure\Jobs\JobRegistry;
  37. use App\Infrastructure\Jobs\JobRunner;
  38. use App\Infrastructure\Jobs\JobRunRepository;
  39. use App\Infrastructure\Reporter\ReporterRepository;
  40. use App\Infrastructure\Reputation\IpScoreRepository;
  41. use App\Infrastructure\Reputation\ReportRepository;
  42. use function DI\autowire;
  43. use DI\ContainerBuilder;
  44. use function DI\factory;
  45. use Doctrine\DBAL\Connection;
  46. use Monolog\Formatter\JsonFormatter;
  47. use Monolog\Handler\StreamHandler;
  48. use Monolog\Logger;
  49. use Psr\Container\ContainerInterface;
  50. use Psr\Http\Message\ResponseFactoryInterface;
  51. use Psr\Log\LoggerInterface;
  52. use Slim\Psr7\Factory\ResponseFactory;
  53. /**
  54. * Builds the api's DI container.
  55. *
  56. * M04 additions: ReporterRepository / ConsumerRepository / CategoryRepository /
  57. * ReportRepository / IpScoreRepository, the PairScorer + Clock pair, and the
  58. * RateLimiter singleton plus its middleware. The rate-limit settings flow
  59. * through `settings` so tests can override capacity/refill cleanly.
  60. */
  61. final class Container
  62. {
  63. /**
  64. * @param array<string, mixed>|null $settings Optional override (tests pass in fixtures).
  65. */
  66. public static function build(?array $settings = null): ContainerInterface
  67. {
  68. $settings ??= require __DIR__ . '/../../config/settings.php';
  69. $builder = new ContainerBuilder();
  70. $builder->useAutowiring(true);
  71. $builder->addDefinitions([
  72. 'settings' => $settings,
  73. 'settings.db' => $settings['db'],
  74. 'settings.ui_service_token' => $settings['ui_service_token'] ?? '',
  75. 'settings.app_env' => $settings['app_env'] ?? 'production',
  76. 'settings.log_level' => $settings['log_level'] ?? \Monolog\Level::Info,
  77. 'settings.oidc_default_role' => $settings['oidc_default_role'] ?? Role::Viewer,
  78. 'settings.score_hard_cutoff_days' => (int) ($settings['score_hard_cutoff_days'] ?? 365),
  79. 'settings.rate_limit_per_second' => (int) ($settings['rate_limit_per_second'] ?? 60),
  80. 'settings.internal_job_token' => (string) ($settings['internal_job_token'] ?? ''),
  81. 'settings.job_recompute_max_runtime_seconds' => (int) ($settings['job_recompute_max_runtime_seconds'] ?? 240),
  82. 'settings.job_recompute_max_rows_per_tick' => (int) ($settings['job_recompute_max_rows_per_tick'] ?? 5000),
  83. 'settings.job_audit_retention_days' => (int) ($settings['job_audit_retention_days'] ?? 180),
  84. ConnectionFactory::class => factory(static function (ContainerInterface $c): ConnectionFactory {
  85. /** @var array{driver: string, sqlite_path: string, mysql_host: string, mysql_port: int, mysql_database: string, mysql_username: string, mysql_password: string} $db */
  86. $db = $c->get('settings.db');
  87. return new ConnectionFactory($db);
  88. }),
  89. Connection::class => factory(static function (ContainerInterface $c): Connection {
  90. /** @var ConnectionFactory $factory */
  91. $factory = $c->get(ConnectionFactory::class);
  92. return $factory->create();
  93. }),
  94. LoggerInterface::class => factory(static function (ContainerInterface $c): LoggerInterface {
  95. $logger = new Logger('api');
  96. /** @var \Monolog\Level $level */
  97. $level = $c->get('settings.log_level');
  98. $handler = new StreamHandler('php://stdout', $level);
  99. $handler->setFormatter(new JsonFormatter());
  100. $logger->pushHandler($handler);
  101. return $logger;
  102. }),
  103. ResponseFactoryInterface::class => autowire(ResponseFactory::class),
  104. Clock::class => autowire(SystemClock::class),
  105. TokenHasher::class => autowire(),
  106. TokenIssuer::class => autowire(),
  107. TokenRepository::class => autowire(),
  108. RoleMappingRepository::class => autowire(),
  109. UserRepository::class => autowire(),
  110. ReporterRepository::class => autowire(),
  111. ConsumerRepository::class => autowire(),
  112. CategoryRepository::class => autowire(),
  113. ReportRepository::class => autowire(),
  114. IpScoreRepository::class => autowire(),
  115. ServiceTokenBootstrap::class => autowire(),
  116. TokenAuthenticationMiddleware::class => autowire(),
  117. ImpersonationMiddleware::class => autowire(),
  118. PairScorer::class => factory(static function (ContainerInterface $c): PairScorer {
  119. /** @var ReportRepository $reports */
  120. $reports = $c->get(ReportRepository::class);
  121. /** @var CategoryRepository $categories */
  122. $categories = $c->get(CategoryRepository::class);
  123. /** @var Clock $clock */
  124. $clock = $c->get(Clock::class);
  125. /** @var int $cutoff */
  126. $cutoff = $c->get('settings.score_hard_cutoff_days');
  127. return new PairScorer($reports, $categories, $clock, $cutoff);
  128. }),
  129. RateLimiter::class => factory(static function (ContainerInterface $c): RateLimiter {
  130. /** @var Clock $clock */
  131. $clock = $c->get(Clock::class);
  132. /** @var int $perSecond */
  133. $perSecond = $c->get('settings.rate_limit_per_second');
  134. $perSecond = max(1, $perSecond);
  135. return new RateLimiter($clock, (float) $perSecond, (float) ($perSecond * 2));
  136. }),
  137. RateLimitMiddleware::class => autowire(),
  138. JobLockRepository::class => autowire(),
  139. JobRunRepository::class => autowire(),
  140. JobRunner::class => factory(static function (ContainerInterface $c): JobRunner {
  141. /** @var JobLockRepository $locks */
  142. $locks = $c->get(JobLockRepository::class);
  143. /** @var JobRunRepository $runs */
  144. $runs = $c->get(JobRunRepository::class);
  145. /** @var Clock $clock */
  146. $clock = $c->get(Clock::class);
  147. /** @var LoggerInterface $logger */
  148. $logger = $c->get(LoggerInterface::class);
  149. return new JobRunner($locks, $runs, $clock, $logger);
  150. }),
  151. RecomputeScoresJob::class => factory(static function (ContainerInterface $c): RecomputeScoresJob {
  152. /** @var ReportRepository $reports */
  153. $reports = $c->get(ReportRepository::class);
  154. /** @var IpScoreRepository $ipScores */
  155. $ipScores = $c->get(IpScoreRepository::class);
  156. /** @var PairScorer $scorer */
  157. $scorer = $c->get(PairScorer::class);
  158. /** @var int $maxRuntime */
  159. $maxRuntime = $c->get('settings.job_recompute_max_runtime_seconds');
  160. /** @var int $maxRows */
  161. $maxRows = $c->get('settings.job_recompute_max_rows_per_tick');
  162. return new RecomputeScoresJob($reports, $ipScores, $scorer, $maxRuntime, $maxRows);
  163. }),
  164. CleanupAuditJob::class => factory(static function (ContainerInterface $c): CleanupAuditJob {
  165. /** @var Connection $conn */
  166. $conn = $c->get(Connection::class);
  167. /** @var int $days */
  168. $days = $c->get('settings.job_audit_retention_days');
  169. return new CleanupAuditJob($conn, $days);
  170. }),
  171. EnrichPendingJob::class => autowire(),
  172. TickJob::class => factory(static function (ContainerInterface $c): TickJob {
  173. /** @var JobRunner $runner */
  174. $runner = $c->get(JobRunner::class);
  175. /** @var JobRunRepository $runs */
  176. $runs = $c->get(JobRunRepository::class);
  177. // Closure indirection: TickJob iterates JobRegistry at
  178. // run() time, after the registry is fully populated. A
  179. // direct JobRegistry dependency would create a build-time
  180. // cycle (registry → tick → registry).
  181. $resolver = static fn (): array => $c->get(JobRegistry::class)->all();
  182. return new TickJob($resolver, $runner, $runs);
  183. }),
  184. JobRegistry::class => factory(static function (ContainerInterface $c): JobRegistry {
  185. $registry = new JobRegistry();
  186. /** @var RecomputeScoresJob $recompute */
  187. $recompute = $c->get(RecomputeScoresJob::class);
  188. /** @var CleanupAuditJob $cleanup */
  189. $cleanup = $c->get(CleanupAuditJob::class);
  190. /** @var EnrichPendingJob $enrich */
  191. $enrich = $c->get(EnrichPendingJob::class);
  192. /** @var TickJob $tick */
  193. $tick = $c->get(TickJob::class);
  194. $registry->register($recompute);
  195. $registry->register($cleanup);
  196. $registry->register($enrich);
  197. $registry->register($tick);
  198. return $registry;
  199. }),
  200. JobsController::class => autowire(),
  201. InternalNetworkMiddleware::class => autowire(),
  202. InternalTokenMiddleware::class => factory(static function (ContainerInterface $c): InternalTokenMiddleware {
  203. /** @var ResponseFactoryInterface $rf */
  204. $rf = $c->get(ResponseFactoryInterface::class);
  205. /** @var string $token */
  206. $token = $c->get('settings.internal_job_token');
  207. return new InternalTokenMiddleware($rf, $token);
  208. }),
  209. JsonErrorHandler::class => factory(static function (ContainerInterface $c): JsonErrorHandler {
  210. /** @var ResponseFactoryInterface $factory */
  211. $factory = $c->get(ResponseFactoryInterface::class);
  212. /** @var LoggerInterface $logger */
  213. $logger = $c->get(LoggerInterface::class);
  214. return new JsonErrorHandler(
  215. $factory,
  216. $logger,
  217. $c->get('settings.app_env') === 'development',
  218. );
  219. }),
  220. AuthController::class => factory(static function (ContainerInterface $c): AuthController {
  221. /** @var Role|null $role */
  222. $role = $c->get('settings.oidc_default_role');
  223. /** @var UserRepository $users */
  224. $users = $c->get(UserRepository::class);
  225. return new AuthController($users, $role ?? Role::Viewer);
  226. }),
  227. MeController::class => autowire(),
  228. ReportersController::class => autowire(),
  229. ConsumersController::class => autowire(),
  230. TokensController::class => autowire(),
  231. ReportController::class => autowire(),
  232. ]);
  233. return $builder->build();
  234. }
  235. }