Create new FCM sender service to replace outdated library #799
This commit is contained in:
parent
b8666ff5e0
commit
33f48647b6
3 changed files with 260 additions and 36 deletions
|
|
@ -33,6 +33,7 @@
|
||||||
"doctrine/doctrine-migrations-bundle": "^2",
|
"doctrine/doctrine-migrations-bundle": "^2",
|
||||||
"doctrine/orm": "^2",
|
"doctrine/orm": "^2",
|
||||||
"edwinhoksberg/php-fcm": "dev-notif-priority-hotfix",
|
"edwinhoksberg/php-fcm": "dev-notif-priority-hotfix",
|
||||||
|
"firebase/php-jwt": "^6.10",
|
||||||
"guzzlehttp/guzzle": "^6.3",
|
"guzzlehttp/guzzle": "^6.3",
|
||||||
"hashids/hashids": "^4.1",
|
"hashids/hashids": "^4.1",
|
||||||
"jankstudio/catalyst-api-bundle": "dev-master",
|
"jankstudio/catalyst-api-bundle": "dev-master",
|
||||||
|
|
|
||||||
65
composer.lock
generated
65
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "653f8558c75614dd65421cb3eb48c29b",
|
"content-hash": "4676209ee947dbcf3cfcf937c838c6f2",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "composer/package-versions-deprecated",
|
"name": "composer/package-versions-deprecated",
|
||||||
|
|
@ -1827,6 +1827,69 @@
|
||||||
},
|
},
|
||||||
"time": "2023-07-19T09:04:27+00:00"
|
"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",
|
"name": "friendsofphp/proxy-manager-lts",
|
||||||
"version": "v1.0.5",
|
"version": "v1.0.5",
|
||||||
|
|
|
||||||
|
|
@ -5,55 +5,178 @@ namespace App\Service;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Ramcar\FirebaseNotificationType;
|
use App\Ramcar\FirebaseNotificationType;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
use Fcm\FcmClient;
|
use GuzzleHttp\Client;
|
||||||
use Fcm\Push\Notification;
|
use GuzzleHttp\Exception\RequestException;
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
use App\Entity\JobOrder;
|
use App\Entity\JobOrder;
|
||||||
|
use App\Entity\Subscription;
|
||||||
|
use Exception;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
class FCMSender
|
class FCMSender
|
||||||
{
|
{
|
||||||
protected $client;
|
protected $client;
|
||||||
protected $translator;
|
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->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();
|
// build the access token parts
|
||||||
$notification->setTitle($title)
|
$private_key = $this->credentials['private_key'];
|
||||||
->setBody($body);
|
$now = time();
|
||||||
|
|
||||||
foreach ($recipients as $recipient) {
|
$jwt_payload = [
|
||||||
$notification->addRecipient($recipient);
|
'iss' => $this->credentials['client_email'],
|
||||||
}
|
'sub' => $this->credentials['client_email'],
|
||||||
|
'aud' => $this->base_uri,
|
||||||
|
'iat' => $now,
|
||||||
|
'exp' => $now + 3600,
|
||||||
|
];
|
||||||
|
|
||||||
if (!empty($color)) {
|
// encode into JWT
|
||||||
$notification->setColor($color);
|
return JWT::encode($jwt_payload, $private_key, 'RS256');
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// get customer object
|
||||||
$cust = $job_order->getCustomer();
|
$cust = $job_order->getCustomer();
|
||||||
|
|
@ -64,7 +187,7 @@ class FCMSender
|
||||||
$data['notification_type'] = FirebaseNotificationType::JOB_ORDER;
|
$data['notification_type'] = FirebaseNotificationType::JOB_ORDER;
|
||||||
|
|
||||||
// send the event
|
// 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 = [])
|
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);
|
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 = [])
|
public function sendEvent(Customer $cust, $title, $body, $data = [], $title_params = [], $body_params = [])
|
||||||
{
|
{
|
||||||
// get all v2 devices
|
// get all v2 devices
|
||||||
|
|
@ -86,7 +221,12 @@ class FCMSender
|
||||||
}
|
}
|
||||||
|
|
||||||
// send fcm notification
|
// 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;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
@ -123,4 +263,24 @@ class FCMSender
|
||||||
|
|
||||||
return $device_ids;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue