redis = $redis; // المسار بناء على بنية المشروع $this->serviceAccountFile = getenv('FIREBASE_SERVICE_ACCOUNT_PATH'); } // ── إرسال إشعار ──────────────────────────────────────── public function send( string $token, string $title, string $body, array $data = [], string $category = 'Order', string $tone = 'ding' ): array { $accessToken = $this->getAccessToken(); if (!$accessToken) { return ['status' => 'error', 'message' => 'No access token']; } if (!file_exists($this->serviceAccountFile)) { return ['status' => 'error', 'message' => 'Service account file missing']; } $creds = json_decode(file_get_contents($this->serviceAccountFile), true); $projectId = $creds['project_id']; $fcmUrl = "https://fcm.googleapis.com/v1/projects/$projectId/messages:send"; $finalData = array_merge($data, [ 'title' => $title, 'body' => $body, 'tone' => $tone, 'category' => $category, 'type' => $category, ]); // FCM يشترط أن تكون كل القيم strings $processedData = array_map( fn($v) => is_array($v) || is_object($v) ? json_encode($v, JSON_UNESCAPED_UNICODE) : (string)$v, $finalData ); $payload = [ 'message' => [ 'token' => $token, 'notification' => [ 'title' => $title, 'body' => $body, ], 'data' => $processedData, 'android' => ['priority' => 'HIGH'], 'apns' => [ 'headers' => ['apns-priority' => '10', 'apns-push-type' => 'background'], 'payload' => ['aps' => ['content-available' => 1]], ], ], ]; $ch = curl_init($fcmUrl); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer $accessToken", 'Content-Type: application/json; charset=UTF-8', ], CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_FRESH_CONNECT => false, // إعادة استخدام الاتصال CURLOPT_FORBID_REUSE => false, CURLOPT_TCP_KEEPALIVE => 1, ]); $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlErr = curl_errno($ch); curl_close($ch); if ($curlErr) { return ['status' => 'error', 'message' => 'CURL error']; } return $httpCode === 200 ? ['status' => 'success'] : ['status' => 'error', 'code' => $httpCode, 'response' => $result]; } // ── Access Token مع Redis Cache ───────────────────────── private function getAccessToken(): ?string { // 1. من Redis if ($this->redis) { $cached = $this->redis->get('google_fcm_access_token'); if ($cached) return $cached; } // 2. طلب جديد $token = $this->fetchGoogleToken(); if ($token && $this->redis) { $this->redis->setex('google_fcm_access_token', 3500, $token); } return $token; } private function fetchGoogleToken(): ?string { if (!file_exists($this->serviceAccountFile)) return null; $creds = json_decode(file_get_contents($this->serviceAccountFile), true); $clientEmail = $creds['client_email']; $privateKey = $creds['private_key']; $now = time(); $header = rtrim(strtr(base64_encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT'])), '+/', '-_'), '='); $claim = rtrim(strtr(base64_encode(json_encode([ 'iss' => $clientEmail, 'scope' => 'https://www.googleapis.com/auth/firebase.messaging', 'aud' => 'https://oauth2.googleapis.com/token', 'exp' => $now + 3600, 'iat' => $now, ])), '+/', '-_'), '='); $signature = ''; openssl_sign("$header.$claim", $signature, $privateKey, 'SHA256'); $jwt = "$header.$claim." . rtrim(strtr(base64_encode($signature), '+/', '-_'), '='); $ch = curl_init('https://oauth2.googleapis.com/token'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query([ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $jwt, ]), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, ]); $res = curl_exec($ch); curl_close($ch); return json_decode($res, true)['access_token'] ?? null; } }