| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 |
- <?php
- declare(strict_types=1);
- namespace App\Infrastructure\Reputation;
- use App\Domain\Policy\Policy;
- use App\Domain\Reputation\Blocklist;
- use App\Domain\Reputation\BlocklistBuilder;
- use App\Domain\Time\Clock;
- /**
- * Per-policy in-process cache of `Blocklist` results.
- *
- * SPEC §M07.3: 30-second TTL by default, keyed on `policy_id`. Cache stores
- * the domain `Blocklist` object — text and JSON renderings happen
- * downstream and share the same cached build (the build is the expensive
- * part).
- *
- * Invalidation: mutation paths call `invalidate($policyId)` for policy-
- * scoped changes and `invalidateAll()` for global changes (manual blocks,
- * allowlist, "rebuild scores"). Tests can pass `ttlSeconds=0` to disable
- * caching entirely.
- *
- * Multi-replica caveat: the cache is per-process. The cidr-evaluator
- * factory has the same caveat (see PROGRESS.md M06).
- */
- class BlocklistCache
- {
- /**
- * @var array<int, array{blocklist: Blocklist, expires_at: float}>
- */
- private array $cache = [];
- public function __construct(
- private readonly BlocklistBuilder $builder,
- private readonly Clock $clock,
- private readonly int $ttlSeconds = 30,
- ) {
- }
- public function getOrBuild(Policy $policy): Blocklist
- {
- $now = (float) $this->clock->now()->getTimestamp();
- $entry = $this->cache[$policy->id] ?? null;
- if ($entry !== null && $now < $entry['expires_at']) {
- return $entry['blocklist'];
- }
- $blocklist = $this->builder->build($policy);
- if ($this->ttlSeconds > 0) {
- $this->cache[$policy->id] = [
- 'blocklist' => $blocklist,
- 'expires_at' => $now + $this->ttlSeconds,
- ];
- }
- return $blocklist;
- }
- public function invalidate(int $policyId): void
- {
- unset($this->cache[$policyId]);
- }
- public function invalidateAll(): void
- {
- $this->cache = [];
- }
- }
|