Files
filament-short-url/src/Services/ShortUrlSettingsManager.php
Bartłomiej Janczak 65a1b126df feat: global overview widget, optimized cache, CSS asset docs
- Add ShortUrlGlobalOverview StatsOverviewWidget with split cache strategy:
  link counts cached forever (invalidated on create/delete), click stats
  with short TTL (geo_ip.stats_cache_ttl config)
- Fix visited_at column in ShortUrlVisit queries (was created_at)
- Disable Livewire polling (pollingInterval = null)
- Add cache invalidation for global overview in ShortUrl::booted()
- Rewrite README asset section: user-facing install instructions clarified,
  post-autoload-dump tip added, developer recompile notes separated
2026-06-01 18:59:17 +02:00

186 lines
7.9 KiB
PHP

<?php
namespace Bjanczak\FilamentShortUrl\Services;
use Illuminate\Support\Facades\File;
class ShortUrlSettingsManager
{
private ?array $cache = null;
public function getSettingsPath(): string
{
return storage_path('app/filament-short-url-settings.json');
}
/**
* Get all settings merged with configuration defaults.
*
* @return array<string, mixed>
*/
public function all(): array
{
if ($this->cache !== null) {
return $this->cache;
}
$path = $this->getSettingsPath();
$stored = [];
if (File::exists($path)) {
try {
$stored = json_decode(File::get($path), true) ?: [];
} catch (\Throwable) {
// Fallback to empty if json is corrupt
}
}
// Merge stored settings with default values from config()
$this->cache = array_merge([
'route_prefix' => config('filament-short-url.route_prefix', 's'),
'redirect_status_code' => config('filament-short-url.redirect_status_code', 302),
'key_length' => config('filament-short-url.key_length', 6),
'geo_ip_enabled' => config('filament-short-url.geo_ip.enabled', true),
'geo_ip_driver' => config('filament-short-url.geo_ip.driver', 'headers'),
'geo_ip_cache_ttl' => config('filament-short-url.geo_ip.cache_ttl', 86400),
'geo_ip_timeout' => config('filament-short-url.geo_ip.timeout', 3),
'maxmind_database_path' => config('filament-short-url.geo_ip.maxmind.database_path', storage_path('geoip/GeoLite2-Country.mmdb')),
'ga4_api_secret' => config('filament-short-url.ga4.api_secret'),
'ga4_firebase_app_id' => config('filament-short-url.ga4.firebase_app_id'),
'queue_connection' => config('filament-short-url.queue_connection', 'sync'),
'cache_ttl' => config('filament-short-url.cache_ttl', 3600),
'counter_buffering_enabled' => config('filament-short-url.counter_buffering.enabled', false),
'trust_cdn_headers' => config('filament-short-url.trust_cdn_headers', false),
'pruning_enabled' => config('filament-short-url.pruning.enabled', true),
'pruning_retention_days' => config('filament-short-url.pruning.retention_days', 90),
'rate_limiting_enabled' => config('filament-short-url.rate_limiting.enabled', false),
'rate_limiting_max_attempts' => config('filament-short-url.rate_limiting.max_attempts', 60),
'rate_limiting_decay_seconds' => config('filament-short-url.rate_limiting.decay_seconds', 60),
], $stored);
return $this->cache;
}
public function get(string $key, mixed $default = null): mixed
{
return $this->all()[$key] ?? $default;
}
/**
* Persist settings to JSON file.
*
* @param array<string, mixed> $data
*/
public function set(array $data): void
{
$path = $this->getSettingsPath();
$dir = dirname($path);
if (! File::isDirectory($dir)) {
File::makeDirectory($dir, 0755, true);
}
// Keep only supported settings keys to prevent bloat
$keys = [
'route_prefix',
'redirect_status_code',
'key_length',
'geo_ip_enabled',
'geo_ip_driver',
'geo_ip_cache_ttl',
'geo_ip_timeout',
'maxmind_database_path',
'ga4_api_secret',
'ga4_firebase_app_id',
'queue_connection',
'cache_ttl',
'counter_buffering_enabled',
'trust_cdn_headers',
'pruning_enabled',
'pruning_retention_days',
'rate_limiting_enabled',
'rate_limiting_max_attempts',
'rate_limiting_decay_seconds',
];
$filtered = array_intersect_key($data, array_flip($keys));
// Format datatypes properly
if (isset($filtered['redirect_status_code'])) {
$filtered['redirect_status_code'] = (int) $filtered['redirect_status_code'];
}
if (isset($filtered['key_length'])) {
$filtered['key_length'] = (int) $filtered['key_length'];
}
if (isset($filtered['geo_ip_enabled'])) {
$filtered['geo_ip_enabled'] = (bool) $filtered['geo_ip_enabled'];
}
if (isset($filtered['geo_ip_cache_ttl'])) {
$filtered['geo_ip_cache_ttl'] = (int) $filtered['geo_ip_cache_ttl'];
}
if (isset($filtered['geo_ip_timeout'])) {
$filtered['geo_ip_timeout'] = (int) $filtered['geo_ip_timeout'];
}
if (isset($filtered['cache_ttl'])) {
$filtered['cache_ttl'] = (int) $filtered['cache_ttl'];
}
if (isset($filtered['counter_buffering_enabled'])) {
$filtered['counter_buffering_enabled'] = (bool) $filtered['counter_buffering_enabled'];
}
if (isset($filtered['trust_cdn_headers'])) {
$filtered['trust_cdn_headers'] = (bool) $filtered['trust_cdn_headers'];
}
if (isset($filtered['pruning_enabled'])) {
$filtered['pruning_enabled'] = (bool) $filtered['pruning_enabled'];
}
if (isset($filtered['pruning_retention_days'])) {
$filtered['pruning_retention_days'] = (int) $filtered['pruning_retention_days'];
}
if (isset($filtered['rate_limiting_enabled'])) {
$filtered['rate_limiting_enabled'] = (bool) $filtered['rate_limiting_enabled'];
}
if (isset($filtered['rate_limiting_max_attempts'])) {
$filtered['rate_limiting_max_attempts'] = (int) $filtered['rate_limiting_max_attempts'];
}
if (isset($filtered['rate_limiting_decay_seconds'])) {
$filtered['rate_limiting_decay_seconds'] = (int) $filtered['rate_limiting_decay_seconds'];
}
File::put($path, json_encode($filtered, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
$this->cache = null;
// Apply immediately to current request config
$this->applyConfigOverrides();
}
/**
* Override standard Laravel config with user-configured settings.
*/
public function applyConfigOverrides(): void
{
$settings = $this->all();
config([
'filament-short-url.route_prefix' => $settings['route_prefix'],
'filament-short-url.redirect_status_code' => $settings['redirect_status_code'],
'filament-short-url.key_length' => $settings['key_length'],
'filament-short-url.geo_ip.enabled' => $settings['geo_ip_enabled'],
'filament-short-url.geo_ip.driver' => $settings['geo_ip_driver'],
'filament-short-url.geo_ip.cache_ttl' => $settings['geo_ip_cache_ttl'],
'filament-short-url.geo_ip.timeout' => $settings['geo_ip_timeout'],
'filament-short-url.geo_ip.maxmind.database_path' => $settings['maxmind_database_path'],
'filament-short-url.ga4.api_secret' => $settings['ga4_api_secret'],
'filament-short-url.ga4.firebase_app_id' => $settings['ga4_firebase_app_id'],
'filament-short-url.queue_connection' => $settings['queue_connection'],
'filament-short-url.cache_ttl' => $settings['cache_ttl'],
'filament-short-url.counter_buffering.enabled' => $settings['counter_buffering_enabled'],
'filament-short-url.trust_cdn_headers' => $settings['trust_cdn_headers'],
'filament-short-url.pruning.enabled' => $settings['pruning_enabled'],
'filament-short-url.pruning.retention_days' => $settings['pruning_retention_days'],
'filament-short-url.rate_limiting.enabled' => $settings['rate_limiting_enabled'],
'filament-short-url.rate_limiting.max_attempts' => $settings['rate_limiting_max_attempts'],
'filament-short-url.rate_limiting.decay_seconds' => $settings['rate_limiting_decay_seconds'],
]);
}
}