2026-06-01 13:05:48 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Bjanczak\FilamentShortUrl\Console\Commands;
|
|
|
|
|
|
|
|
|
|
use Bjanczak\FilamentShortUrl\Models\ShortUrl;
|
|
|
|
|
use Illuminate\Console\Command;
|
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
2026-06-01 13:24:02 +02:00
|
|
|
use Illuminate\Support\Facades\Redis;
|
2026-06-01 13:05:48 +02:00
|
|
|
|
|
|
|
|
class SyncBufferedCountersCommand extends Command
|
|
|
|
|
{
|
|
|
|
|
/** @var string */
|
|
|
|
|
protected $signature = 'short-url:sync-counters';
|
|
|
|
|
|
|
|
|
|
/** @var string */
|
|
|
|
|
protected $description = 'Sync buffered short URL visit counters from cache to the database';
|
|
|
|
|
|
|
|
|
|
public function handle(): int
|
|
|
|
|
{
|
|
|
|
|
$prefix = config('filament-short-url.counter_buffering.cache_key_prefix', 'filament-short-url:buffer:');
|
|
|
|
|
$dirtyKey = "{$prefix}dirty_ids";
|
|
|
|
|
|
|
|
|
|
// Pull the list atomically to avoid race conditions with incoming clicks
|
2026-06-01 13:24:02 +02:00
|
|
|
if (Cache::getDefaultDriver() === 'redis' && class_exists(Redis::class)) {
|
|
|
|
|
$tempKey = "{$dirtyKey}:temp:".time();
|
2026-06-01 13:05:48 +02:00
|
|
|
try {
|
2026-06-01 13:24:02 +02:00
|
|
|
Redis::rename($dirtyKey, $tempKey);
|
|
|
|
|
$dirtyIds = Redis::smembers($tempKey);
|
|
|
|
|
Redis::del($tempKey);
|
2026-06-01 13:05:48 +02:00
|
|
|
} catch (\Throwable) {
|
|
|
|
|
// If key does not exist or rename fails, fallback
|
|
|
|
|
$dirtyIds = [];
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$dirtyIds = Cache::pull($dirtyKey, []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (empty($dirtyIds)) {
|
|
|
|
|
$this->info('No buffered counters to synchronize.');
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$dirtyIds = array_unique(array_filter($dirtyIds));
|
|
|
|
|
$processed = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($dirtyIds as $id) {
|
|
|
|
|
$totalKey = "{$prefix}total:{$id}";
|
|
|
|
|
$uniqueKey = "{$prefix}unique:{$id}";
|
|
|
|
|
|
|
|
|
|
$totalDelta = (int) Cache::pull($totalKey, 0);
|
|
|
|
|
$uniqueDelta = (int) Cache::pull($uniqueKey, 0);
|
|
|
|
|
|
|
|
|
|
if ($totalDelta > 0 || $uniqueDelta > 0) {
|
|
|
|
|
// Perform a single atomic update query for this URL
|
|
|
|
|
ShortUrl::where('id', $id)->update([
|
|
|
|
|
'total_visits' => DB::raw("total_visits + {$totalDelta}"),
|
|
|
|
|
'unique_visits' => DB::raw("unique_visits + {$uniqueDelta}"),
|
|
|
|
|
]);
|
|
|
|
|
$processed++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->info("Successfully synchronized counters for {$processed} short URLs.");
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|