FcmService.php 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. <?php
  2. namespace App\Services\Fcm;
  3. use Illuminate\Support\Facades\Http;
  4. use Illuminate\Support\Facades\Log;
  5. use Throwable;
  6. class FcmService
  7. {
  8. protected string $projectId;
  9. protected string $accessToken;
  10. public function __construct()
  11. {
  12. $this->projectId = config('fcm.project_id');
  13. $this->accessToken = $this->getAccessToken();
  14. }
  15. public function send(string $token, string $title, string $body, ?string $image = null): ?array
  16. {
  17. $url = "https://fcm.googleapis.com/v1/projects/{$this->projectId}/messages:send";
  18. $message = [
  19. 'message' => [
  20. 'token' => $token,
  21. 'notification' => [
  22. 'title' => $title,
  23. 'body' => $body,
  24. 'image' => $image,
  25. ],
  26. 'android' => [
  27. 'priority' => 'high',
  28. ],
  29. 'apns' => [
  30. 'headers' => [
  31. 'apns-priority' => '10',
  32. ],
  33. 'payload' => [
  34. 'aps' => [
  35. 'sound' => 'default',
  36. 'badge' => 1,
  37. ],
  38. ],
  39. ],
  40. ],
  41. ];
  42. try {
  43. $response = Http::withToken($this->accessToken)->post($url, $message);
  44. $result = $response->json();
  45. Log::channel('fcm')->info('FCM send', [
  46. 'token' => substr($token, 0, 20) . '...',
  47. 'title' => $title,
  48. 'status' => $response->status(),
  49. 'response' => $result,
  50. ]);
  51. if ($response->failed()) {
  52. Log::channel('fcm')->error('FCM send failed', [
  53. 'token' => substr($token, 0, 20) . '...',
  54. 'status' => $response->status(),
  55. 'error' => $result,
  56. ]);
  57. }
  58. return $result;
  59. } catch (Throwable $e) {
  60. Log::channel('fcm')->error('FCM exception', [
  61. 'token' => substr($token, 0, 20) . '...',
  62. 'message' => $e->getMessage(),
  63. ]);
  64. return null;
  65. }
  66. }
  67. protected function getAccessToken(): string
  68. {
  69. $clientEmail = config('fcm.client_email');
  70. $privateKey = config('fcm.private_key');
  71. $jwt = $this->createJwt($clientEmail, $privateKey);
  72. return $this->exchangeJwtForAccessToken($jwt);
  73. }
  74. protected function createJwt(string $clientEmail, string $privateKey): string
  75. {
  76. $header = json_encode(['alg' => 'RS256', 'typ' => 'JWT']);
  77. $now = time();
  78. $payload = json_encode([
  79. 'iss' => $clientEmail,
  80. 'sub' => $clientEmail,
  81. 'aud' => 'https://oauth2.googleapis.com/token',
  82. 'iat' => $now,
  83. 'exp' => $now + 3600,
  84. 'scope' => 'https://www.googleapis.com/auth/cloud-platform',
  85. ]);
  86. $base64Header = $this->base64UrlEncode($header);
  87. $base64Payload = $this->base64UrlEncode($payload);
  88. $signature = '';
  89. openssl_sign("{$base64Header}.{$base64Payload}", $signature, $privateKey, 'sha256');
  90. return "{$base64Header}.{$base64Payload}." . $this->base64UrlEncode($signature);
  91. }
  92. protected function exchangeJwtForAccessToken(string $jwt): string
  93. {
  94. $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [
  95. 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
  96. 'assertion' => $jwt,
  97. ]);
  98. $data = $response->json();
  99. if (isset($data['access_token'])) {
  100. return $data['access_token'];
  101. }
  102. throw new \RuntimeException('FCM: Failed to obtain access token: ' . $response->body());
  103. }
  104. protected function base64UrlEncode(string $data): string
  105. {
  106. return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
  107. }
  108. }