projectId = config('fcm.project_id'); $this->accessToken = $this->getAccessToken(); } public function send(string $token, string $title, string $body, ?string $image = null): ?array { $url = "https://fcm.googleapis.com/v1/projects/{$this->projectId}/messages:send"; $message = [ 'message' => [ 'token' => $token, 'notification' => [ 'title' => $title, 'body' => $body, 'image' => $image, ], 'android' => [ 'priority' => 'high', ], 'apns' => [ 'headers' => [ 'apns-priority' => '10', ], 'payload' => [ 'aps' => [ 'sound' => 'default', 'badge' => 1, ], ], ], ], ]; try { $response = Http::withToken($this->accessToken)->post($url, $message); $result = $response->json(); Log::channel('fcm')->info('FCM send', [ 'token' => substr($token, 0, 20) . '...', 'title' => $title, 'status' => $response->status(), 'response' => $result, ]); if ($response->failed()) { Log::channel('fcm')->error('FCM send failed', [ 'token' => substr($token, 0, 20) . '...', 'status' => $response->status(), 'error' => $result, ]); } return $result; } catch (Throwable $e) { Log::channel('fcm')->error('FCM exception', [ 'token' => substr($token, 0, 20) . '...', 'message' => $e->getMessage(), ]); return null; } } protected function getAccessToken(): string { $clientEmail = config('fcm.client_email'); $privateKey = config('fcm.private_key'); $jwt = $this->createJwt($clientEmail, $privateKey); return $this->exchangeJwtForAccessToken($jwt); } protected function createJwt(string $clientEmail, string $privateKey): string { $header = json_encode(['alg' => 'RS256', 'typ' => 'JWT']); $now = time(); $payload = json_encode([ 'iss' => $clientEmail, 'sub' => $clientEmail, 'aud' => 'https://oauth2.googleapis.com/token', 'iat' => $now, 'exp' => $now + 3600, 'scope' => 'https://www.googleapis.com/auth/cloud-platform', ]); $base64Header = $this->base64UrlEncode($header); $base64Payload = $this->base64UrlEncode($payload); $signature = ''; openssl_sign("{$base64Header}.{$base64Payload}", $signature, $privateKey, 'sha256'); return "{$base64Header}.{$base64Payload}." . $this->base64UrlEncode($signature); } protected function exchangeJwtForAccessToken(string $jwt): string { $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $jwt, ]); $data = $response->json(); if (isset($data['access_token'])) { return $data['access_token']; } throw new \RuntimeException('FCM: Failed to obtain access token: ' . $response->body()); } protected function base64UrlEncode(string $data): string { return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data)); } }