2025-07-05 18:26:04 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Jobs;
|
|
|
|
|
|
2026-08-10 01:02:16 +02:00
|
|
|
use App\Enums\NotificationSeverityEnum;
|
|
|
|
|
use App\Enums\NotificationTypeEnum;
|
2025-07-05 18:26:04 +02:00
|
|
|
use App\Models\Feed;
|
2026-08-10 01:02:16 +02:00
|
|
|
use App\Models\Notification;
|
2025-07-05 18:26:04 +02:00
|
|
|
use App\Services\Article\ArticleFetcher;
|
|
|
|
|
use App\Services\Log\LogSaver;
|
2026-08-10 01:02:16 +02:00
|
|
|
use App\Services\Notification\NotificationService;
|
2025-07-05 18:26:04 +02:00
|
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
|
|
|
use Illuminate\Foundation\Queue\Queueable;
|
|
|
|
|
|
|
|
|
|
class ArticleDiscoveryForFeedJob implements ShouldQueue
|
|
|
|
|
{
|
|
|
|
|
use Queueable;
|
|
|
|
|
|
|
|
|
|
private const FEED_DISCOVERY_DELAY_MINUTES = 5;
|
|
|
|
|
|
|
|
|
|
public function __construct(
|
|
|
|
|
private readonly Feed $feed
|
|
|
|
|
) {
|
|
|
|
|
$this->onQueue('feed-discovery');
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 01:02:16 +02:00
|
|
|
public function handle(LogSaver $logSaver, ArticleFetcher $articleFetcher, NotificationService $notificationService): void
|
2025-07-05 18:26:04 +02:00
|
|
|
{
|
2025-08-15 02:50:42 +02:00
|
|
|
$logSaver->info('Starting feed article fetch', null, [
|
2025-07-05 18:26:04 +02:00
|
|
|
'feed_id' => $this->feed->id,
|
|
|
|
|
'feed_name' => $this->feed->name,
|
2026-03-08 14:17:55 +01:00
|
|
|
'feed_url' => $this->feed->url,
|
2025-07-05 18:26:04 +02:00
|
|
|
]);
|
|
|
|
|
|
2025-08-15 02:50:42 +02:00
|
|
|
$articles = $articleFetcher->getArticlesFromFeed($this->feed);
|
2025-07-05 18:26:04 +02:00
|
|
|
|
2025-08-15 02:50:42 +02:00
|
|
|
$logSaver->info('Feed article fetch completed', null, [
|
2025-07-05 18:26:04 +02:00
|
|
|
'feed_id' => $this->feed->id,
|
|
|
|
|
'feed_name' => $this->feed->name,
|
2026-03-08 14:17:55 +01:00
|
|
|
'articles_count' => $articles->count(),
|
2025-07-05 18:26:04 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->feed->update(['last_fetched_at' => now()]);
|
2026-08-10 01:02:16 +02:00
|
|
|
|
|
|
|
|
if ($articles->isEmpty()) {
|
|
|
|
|
$this->warnFeedReturnedNothing($notificationService);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function warnFeedReturnedNothing(NotificationService $notificationService): void
|
|
|
|
|
{
|
|
|
|
|
$alreadyNotified = Notification::query()
|
|
|
|
|
->where('type', NotificationTypeEnum::FEED_EMPTY)
|
|
|
|
|
->where('notifiable_type', $this->feed->getMorphClass())
|
|
|
|
|
->where('notifiable_id', $this->feed->getKey())
|
|
|
|
|
->unread()
|
|
|
|
|
->exists();
|
|
|
|
|
|
|
|
|
|
if ($alreadyNotified) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$notificationService->send(
|
|
|
|
|
type: NotificationTypeEnum::FEED_EMPTY,
|
|
|
|
|
severity: NotificationSeverityEnum::WARNING,
|
|
|
|
|
title: "Feed \"{$this->feed->name}\" returned no articles",
|
|
|
|
|
message: "The fetch completed but produced nothing. Check that {$this->feed->url} is still a valid feed.",
|
|
|
|
|
notifiable: $this->feed,
|
|
|
|
|
);
|
2025-07-05 18:26:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function dispatchForAllActiveFeeds(): void
|
|
|
|
|
{
|
2025-08-15 02:50:42 +02:00
|
|
|
$logSaver = app(LogSaver::class);
|
2026-03-08 14:17:55 +01:00
|
|
|
|
2025-07-05 18:26:04 +02:00
|
|
|
Feed::where('is_active', true)
|
|
|
|
|
->get()
|
2025-08-15 02:50:42 +02:00
|
|
|
->each(function (Feed $feed, $index) use ($logSaver) {
|
2025-07-05 18:26:04 +02:00
|
|
|
// Space jobs apart to avoid overwhelming feeds
|
|
|
|
|
$delayMinutes = $index * self::FEED_DISCOVERY_DELAY_MINUTES;
|
|
|
|
|
|
|
|
|
|
self::dispatch($feed)
|
|
|
|
|
->delay(now()->addMinutes($delayMinutes))
|
|
|
|
|
->onQueue('feed-discovery');
|
|
|
|
|
|
2025-08-15 02:50:42 +02:00
|
|
|
$logSaver->info('Dispatched feed discovery job', null, [
|
2025-07-05 18:26:04 +02:00
|
|
|
'feed_id' => $feed->id,
|
|
|
|
|
'feed_name' => $feed->name,
|
2026-03-08 14:17:55 +01:00
|
|
|
'delay_minutes' => $delayMinutes,
|
2025-07-05 18:26:04 +02:00
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|