feat: add app and database modules
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\DeviceToken;
|
||||
use App\Services\MobileConfig\MobileConfigService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FcmService
|
||||
{
|
||||
private string $serverKey;
|
||||
|
||||
private string $endpoint = 'https://fcm.googleapis.com/fcm/send';
|
||||
|
||||
protected $mobileConfig;
|
||||
|
||||
public function __construct(MobileConfigService $mobileConfig)
|
||||
{
|
||||
$this->serverKey = config('services.fcm.server_key', '');
|
||||
$this->mobileConfig = $mobileConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if notifications are enabled globally in settings.
|
||||
*/
|
||||
private function isEnabled(): bool
|
||||
{
|
||||
$config = $this->mobileConfig->all();
|
||||
|
||||
return filter_var($config['notifications']['enable_push_notifications'] ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send push notification to a single user (all their devices).
|
||||
*/
|
||||
public function sendToUser(int $userId, string $title, string $body, array $data = []): void
|
||||
{
|
||||
if (! $this->isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tokens = DeviceToken::where('user_id', $userId)->pluck('token')->toArray();
|
||||
|
||||
if (empty($tokens)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendToTokens($tokens, $title, $body, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send to a list of device tokens.
|
||||
*/
|
||||
public function sendToTokens(array $tokens, string $title, string $body, array $data = []): void
|
||||
{
|
||||
if (! $this->isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($this->serverKey)) {
|
||||
Log::warning('FCM server key not configured — push notification skipped');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// FCM supports max 1000 tokens per request
|
||||
foreach (array_chunk($tokens, 1000) as $chunk) {
|
||||
$this->dispatch($chunk, $title, $body, $data);
|
||||
}
|
||||
}
|
||||
|
||||
private function dispatch(array $tokens, string $title, string $body, array $data): void
|
||||
{
|
||||
$payload = [
|
||||
'registration_ids' => $tokens,
|
||||
'notification' => ['title' => $title, 'body' => $body],
|
||||
'data' => $data,
|
||||
'priority' => 'high',
|
||||
];
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'key='.$this->serverKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->post($this->endpoint, $payload);
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::error('FCM dispatch failed', ['status' => $response->status(), 'body' => $response->body()]);
|
||||
} else {
|
||||
$this->handleFcmResponse($response->json(), $tokens);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('FCM HTTP error', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleFcmResponse(array $result, array $tokens): void
|
||||
{
|
||||
if (empty($result['results'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$invalidTokens = [];
|
||||
foreach ($result['results'] as $index => $res) {
|
||||
if (isset($res['error']) && in_array($res['error'], ['NotRegistered', 'InvalidRegistration'])) {
|
||||
$invalidTokens[] = $tokens[$index];
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($invalidTokens)) {
|
||||
DeviceToken::whereIn('token', $invalidTokens)->delete();
|
||||
Log::info('FCM removed stale tokens', ['count' => count($invalidTokens)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user