> $feeds */ public function __construct( private FeedRepository $repository, private FeedFetcher $fetcher, private array $feeds, private int $ttlSeconds, private int $errorRetryTtlSeconds = 300, private array $throttledHosts = [], private int $throttledHostMax = 1, ) { } /** * Refresh every feed whose cached copy is stale. Failures are recorded and * skipped so previously cached items remain available. * * @return array{attempted: int, succeeded: int, failed: int, new_items: int, errors: array} */ public function refreshStale(): array { $this->repository->syncFeeds($this->feeds); $report = [ 'attempted' => 0, 'succeeded' => 0, 'failed' => 0, 'new_items' => 0, 'errors' => [], ]; $hostCounts = []; $activeFeeds = $this->repository->enabledFeeds(); foreach ($this->repository->staleFeeds($activeFeeds, $this->ttlSeconds, $this->errorRetryTtlSeconds) as $feed) { $url = (string) $feed['url']; $host = (string) parse_url($url, PHP_URL_HOST); if (in_array($host, $this->throttledHosts, true)) { $seen = $hostCounts[$host] ?? 0; if ($seen >= $this->throttledHostMax) { // Leave it stale so a later request picks it up. continue; } $hostCounts[$host] = $seen + 1; } $report['attempted']++; try { $result = $this->fetcher->fetch($feed); $report['new_items'] += $this->repository->upsertItems($result['items']); $this->repository->recordAttempt($url, true); $report['succeeded']++; } catch (\Throwable $e) { $this->repository->recordAttempt($url, false, $e->getMessage()); $report['failed']++; $report['errors'][] = ($feed['label'] ?? $url) . ': ' . $e->getMessage(); } } return $report; } /** * @return array{items: array>, page: int, page_size: int, total: int, has_more: bool} */ public function page(?string $source, int $page, int $pageSize, ?string $since = null): array { $page = max(1, $page); $pageSize = max(1, $pageSize); $offset = ($page - 1) * $pageSize; $items = $this->repository->getItems($pageSize, $offset, $source, $since); $total = $this->repository->countItems($source, $since); return [ 'items' => $items, 'page' => $page, 'page_size' => $pageSize, 'total' => $total, 'has_more' => ($offset + count($items)) < $total, ]; } /** * @return array> */ public function feedsStatus(): array { return $this->repository->feedsStatus(); } /** * Sources that currently have at least one enabled feed. * * @return array */ public function sources(): array { $sources = []; foreach ($this->repository->enabledFeeds() as $feed) { $sources[(string) $feed['source']] = true; } return array_keys($sources); } }