Create new FCM sender service to replace outdated library #799

This commit is contained in:
Ramon Gutierrez 2024-10-23 20:46:01 +08:00
parent b8666ff5e0
commit 33f48647b6
3 changed files with 260 additions and 36 deletions

View file

@ -33,6 +33,7 @@
"doctrine/doctrine-migrations-bundle": "^2",
"doctrine/orm": "^2",
"edwinhoksberg/php-fcm": "dev-notif-priority-hotfix",
"firebase/php-jwt": "^6.10",
"guzzlehttp/guzzle": "^6.3",
"hashids/hashids": "^4.1",
"jankstudio/catalyst-api-bundle": "dev-master",

65
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "653f8558c75614dd65421cb3eb48c29b",
"content-hash": "4676209ee947dbcf3cfcf937c838c6f2",
"packages": [
{
"name": "composer/package-versions-deprecated",
@ -1827,6 +1827,69 @@
},
"time": "2023-07-19T09:04:27+00:00"
},
{
"name": "firebase/php-jwt",
"version": "v6.10.1",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "500501c2ce893c824c801da135d02661199f60c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5",
"reference": "500501c2ce893c824c801da135d02661199f60c5",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"type": "library",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"jwt",
"php"
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.10.1"
},
"time": "2024-05-18T18:05:11+00:00"
},
{
"name": "friendsofphp/proxy-manager-lts",
"version": "v1.0.5",

View file

@ -5,55 +5,178 @@ namespace App\Service;
use App\Entity\Customer;
use App\Ramcar\FirebaseNotificationType;
use Symfony\Contracts\Translation\TranslatorInterface;
use Fcm\FcmClient;
use Fcm\Push\Notification;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Firebase\JWT\JWT;
use App\Entity\JobOrder;
use App\Entity\Subscription;
use Exception;
use RuntimeException;
class FCMSender
{
protected $client;
protected $translator;
protected $project_id;
protected $base_uri;
protected $credentials;
public function __construct(TranslatorInterface $translator, $server_key, $sender_id)
public function __construct(TranslatorInterface $translator, string $creds_file, string $project_id, string $base_uri)
{
$this->client = new FcmClient($server_key, $sender_id);
$this->translator = $translator;
$this->project_id = $project_id;
$this->base_uri = $base_uri;
// check credentials file
if (!file_exists($creds_file)) {
throw new RuntimeException("Service account JSON file not found: $creds_file");
}
// set credentials from file
$this->credentials = json_decode(file_get_contents($creds_file), true);
// instantiate client
$this->client = new Client([
'base_uri' => $base_uri . 'v1/',
'timeout' => 5.0,
]);
}
public function send($recipients, $title, $body, $data = [], $color = null, $sound = null, $badge = null)
private function generateAccessToken()
{
$notification = new Notification();
$notification->setTitle($title)
->setBody($body);
// build the access token parts
$private_key = $this->credentials['private_key'];
$now = time();
foreach ($recipients as $recipient) {
$notification->addRecipient($recipient);
}
$jwt_payload = [
'iss' => $this->credentials['client_email'],
'sub' => $this->credentials['client_email'],
'aud' => $this->base_uri,
'iat' => $now,
'exp' => $now + 3600,
];
if (!empty($color)) {
$notification->setColor($color);
}
if (!empty($sound)) {
$notification->setSound($sound);
}
if (!empty($color)) {
$notification->setColor($color);
}
if (!empty($badge)) {
$notification->setBadge($badge);
}
if (!empty($data)) {
$notification->addDataArray($data);
}
return $this->client->send($notification);
// encode into JWT
return JWT::encode($jwt_payload, $private_key, 'RS256');
}
public function sendJoEvent(JobOrder $job_order, $title, $body, $data = [], $title_params = [], $body_params = [])
public function send($recipients, $title, $body, $data = [], $color = null, $sound = null, $image = null)
{
// set URL for sending
$url = "projects/{$this->project_id}/messages:send";
$access_token = $this->generateAccessToken();
// build payload structure
$payload = [
'message' => [
'notification' => [
'title' => $title,
'body' => $body,
],
'android' => [
'priority' => 'high',
'notification' => [
'sound' => $sound ?? 'default',
],
],
'apns' => [
'headers' => [
'apns-priority' => '10',
],
'payload' => [
'aps' => [
'alert' => [
'title' => $title,
'body' => $body,
],
'sound' => $sound ?? 'default',
],
],
],
],
];
// if image is provided, apply params
if (!empty($image)) {
$payload['message']['notification']['image'] = $image;
$payload['message']['android']['notification']['image'] = $image;
$payload['message']['apns']['payload']['aps']['mutable-content'] = 1;
}
// if data is provided, attach it
if (!empty($data)) {
$payload['message']['data'] = $data;
}
// build headers for request
$headers = [
'Authorization' => 'Bearer ' . $access_token,
'Content-Type' => 'application/json',
];
// send the message to each recipient
foreach ($recipients as $recipient) {
$payload['message']['token'] = $recipient;
try {
$response = $this->client->post($url, [
'headers' => $headers,
'json' => $payload,
]);
$result = $response->getBody();
$json = json_decode($result, true);
// log response
$this->log(
$title,
$body,
'Success',
'success',
json_encode($payload),
$result,
);
// message sent!
return [
'success' => true,
'response' => $json,
];
} catch (RequestException $e) {
$error_msg = $e->hasResponse() ? $e->getResponse()->getBody()->getContents() : $e->getMessage();
// something went wrong
$this->log(
$title,
$body,
$error_msg,
'error',
json_encode($payload),
);
return [
'success' => false,
'error' => "Request error: " . $error_msg,
];
} catch (Exception $e) {
// something went wrong
$this->log(
$title,
$body,
$e->getMessage(),
'error',
json_encode($payload),
);
return [
'success' => false,
'error' => "Unexpected error: " . $e->getMessage(),
];
}
}
}
public function sendJoEvent(JobOrder $job_order, $title, $body, $data = [])
{
// get customer object
$cust = $job_order->getCustomer();
@ -64,7 +187,7 @@ class FCMSender
$data['notification_type'] = FirebaseNotificationType::JOB_ORDER;
// send the event
return $this->sendEvent($cust, $title, $body, $data, $title_params, $body_params);
return $this->sendEvent($cust, $title, $body, $data);
}
public function sendLoyaltyEvent(Customer $cust, $title, $body, $data = [], $title_params = [], $body_params = [])
@ -76,6 +199,18 @@ class FCMSender
return $this->sendEvent($cust, $title, $body, $data, $title_params, $body_params);
}
public function sendSubscriptionEvent(Subscription $sub, $title, $body, $data = [], $title_params = [], $body_params = [])
{
// get customer object
$cust = $sub->getCustomer();
// attach type info
$data['notification_type'] = FirebaseNotificationType::SUBSCRIPTION;
// send the event
return $this->sendEvent($cust, $title, $body, $data, $title_params, $body_params);
}
public function sendEvent(Customer $cust, $title, $body, $data = [], $title_params = [], $body_params = [])
{
// get all v2 devices
@ -86,7 +221,12 @@ class FCMSender
}
// send fcm notification
$result = $this->send(array_keys($devices), $this->translator->trans($title), $this->translator->trans($body), $data);
$result = $this->send(
array_keys($devices),
$this->translator->trans($title, $title_params),
$this->translator->trans($body, $body_params),
$data,
);
return $result;
}
@ -123,4 +263,24 @@ class FCMSender
return $device_ids;
}
// TODO: make this more elegant
public function log($title, $body, $message, $type, $data = "[]", $result = "[]")
{
$filename = '/../../var/log/fcm_' . $type . '.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("\r\n", array_filter([
$date,
$title,
$body,
$message,
"DATA:\r\n" . $data,
"RESULT:\r\n" . $result,
"\r\n----------------------------------------\r\n\r\n",
]));
file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
}
}