Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 300
FirebaseCloudMessagingService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 13
2970.00
0.00% covered (danger)
0.00%
0 / 300
 __construct
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 2
 __clone
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 2
 getInstance
0.00% covered (danger)
0.00%
0 / 1
6.00
0.00% covered (danger)
0.00%
0 / 5
 saveFcmToken
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 15
 addNewFcmToken
0.00% covered (danger)
0.00%
0 / 1
30.00
0.00% covered (danger)
0.00%
0 / 31
 updateFcmToken
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 24
 checkIfTokenExist
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 17
 deleteFcmToken
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 15
 getUserFcmTokens
0.00% covered (danger)
0.00%
0 / 1
42.00
0.00% covered (danger)
0.00%
0 / 28
 sendPushNotification
0.00% covered (danger)
0.00%
0 / 1
156.00
0.00% covered (danger)
0.00%
0 / 84
 getUserFcmTokensUsingEmailIds
0.00% covered (danger)
0.00%
0 / 1
56.00
0.00% covered (danger)
0.00%
0 / 50
 deleteInvalidFcmTokens
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 8
 generateAccessToken
0.00% covered (danger)
0.00%
0 / 1
6.00
0.00% covered (danger)
0.00%
0 / 19
<?php
namespace com\linways\core\ams\professional\service\mobile;
use com\linways\base\constant\UserType;
use com\linways\core\ams\professional\constant\DevelopmentPlatform;
use com\linways\core\ams\professional\exception\mobile\MobileException;
use com\linways\core\ams\professional\request\GenerateFcmAccessTokenRequest;
use com\linways\core\ams\professional\request\mobile\GetUserFcmTokensUsingEmailIdsRequest;
use com\linways\core\ams\professional\request\mobile\PushNotificationRequest;
use com\linways\core\ams\professional\request\mobile\SaveFcmTokenRequest;
use com\linways\core\ams\professional\response\GenerateFcmAccessTokenResponse;
use com\linways\core\ams\professional\response\GetUserFcmTokensUsingEmailIdsResponse;
use com\linways\core\ams\professional\service\BaseService;
use Google\Auth\CredentialsLoader;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use stdClass;
/**
 * User: sarathks
 * Date: 20/05/22
 */
class FirebaseCloudMessagingService extends BaseService
{
    private $mapper = [];
    private $commonApiUtil = null;
    private $logger = null;
    private $cache;
    /**
     * Presence of a static member variable
     *
     * @var null
     */
    private static $_instance = null;
    /**
     * Locked down the constructor or Prevent any outside instantiation of this class
     *
     * FirebaseCloudMessagingService constructor.
     */
    private function __construct()
    {
    }
    /**
     * Prevent any object or instance of that class to be cloned
     */
    private function __clone()
    {
    }
    /**
     * Have a single globally accessible static method
     *
     * @return FirebaseCloudMessagingService|null
     */
    public static function getInstance()
    {
        if (!is_object(self::$_instance))
            self::$_instance = new self();
        return self::$_instance;
    }
    /**
     * Save FCM Token
     * @param SaveFcmTokenRequest $request
     * @return String $newFcmToken
     * @throws MobileException
     */
    public function saveFcmToken(SaveFcmTokenRequest $request)
    {
        try {
            $isExist = $this->checkIfTokenExist($request->newFcmToken,$request->userId);
            if($isExist) {
                return;
            }
            if (empty($request->currentFcmToken)) {
                return $this->addNewFcmToken($request);
            } else {
                return $this->updateFcmToken($request);
            }
        } catch (\Exception $e) {
            throw $e;
        }
    }
    /**
     * Add new FCM Token
     * @param SaveFcmTokenRequest $request
     * @return String $newFcmToken
     * @throws MobileException
     */
    public function addNewFcmToken(SaveFcmTokenRequest $request)
    {
        if (empty($request->newFcmToken)) {
            throw new MobileException(MobileException::INVALID_FCM_TOKEN, "Invalid fcm token");
        }
        if (empty($request->userId)) {
            throw new MobileException(MobileException::INVALID_USER_ID, "Invalid user id");
        }
        if (empty($request->userType)) {
            throw new MobileException(MobileException::INVALID_USER_TYPE, "Invalid user type");
        }
        $sql="DELETE FROM v4_firebase_cloud_messaging_tokens where fcm_token = '$request->newFcmToken'";
        try {
            $this->executeQuery($sql);
        $sql = "INSERT
                    INTO
                    `v4_firebase_cloud_messaging_tokens` (`fcm_token`,
                    `user_id`,
                    `user_type`,
                    `platform`,
                    `is_valid`)
                VALUES ('$request->newFcmToken',
                '$request->userId',
                '$request->userType',
                '$request->platform',
                '$request->isValid')";
            $this->executeQuery($sql);
            return $request->newFcmToken;
        } catch (\Exception $e) {
            throw new MobileException($e->getCode(), $e->getMessage());
        }
    }
    /**
     * Update FCM Token
     * @param SaveFcmTokenRequest $request
     * @return String $newFcmToken
     * @throws MobileException
     */
    public function updateFcmToken(SaveFcmTokenRequest $request)
    {
        if (empty($request->currentFcmToken)) {
            throw new MobileException(MobileException::INVALID_FCM_TOKEN, "Invalid current fcm token");
        }
        if (empty($request->newFcmToken)) {
            throw new MobileException(MobileException::INVALID_FCM_TOKEN, "Invalid new fcm token");
        }
        $sql = "UPDATE
                    `v4_firebase_cloud_messaging_tokens`
                SET
                    fcm_token = '$request->newFcmToken',
                    created_date = UTC_TIMESTAMP()
                WHERE
                    fcm_token = '$request->currentFcmToken'
                    AND user_id = '$request->userId'
                    AND platform = '$request->platform'
                    AND user_type = '$request->userType'";
        try {
            $this->executeQuery($sql);
            return $request->newFcmToken;
        } catch (\Exception $e) {
            throw new MobileException($e->getCode(), $e->getMessage());
        }
    }
    /**
     * Check if same FCM Token exist for a user
     * @param $fcmToken
     * @return String $newFcmToken
     * @throws MobileException
     */
    public function checkIfTokenExist($token, $userId)
    {
        if (empty($token)) {
            throw new MobileException(MobileException::INVALID_FCM_TOKEN, "Invalid fcm token");
        }
        $sql = "SELECT
                    fcm_token
                FROM
                    `v4_firebase_cloud_messaging_tokens`
                WHERE
                    fcm_token = '$token' and user_id = '$userId'";
        try {
            $tokens = $this->executeQueryForList($sql);
            return count($tokens);
        } catch (\Exception $e) {
            throw new MobileException($e->getCode(), $e->getMessage());
        }
    }
    /**
     * Delete FCM Token
     * @param $request
     * @throws MobileException
     */
    public function deleteFcmToken($request)
    {
        // $request->fcmTokens is an array of tokens to be deleted;
        if (empty($request->fcmTokens)) {
            return;
        }
        $sql = "DELETE
                FROM
                    `v4_firebase_cloud_messaging_tokens`
                WHERE
                    fcm_token IN ('" . implode("','", $request->fcmTokens) . "')";
        try {
            $this->executeQuery($sql);
        } catch (\Exception $e) {
            throw new MobileException($e->getCode(), $e->getMessage());
        }
    }
    public function getUserFcmTokens($fcmTokenRequestObject, $platform = DevelopmentPlatform::MOBILE) {
        // fcmTokenRequestObject should contain the keys as usertype and values as array of ids of that type for eg:-
        // $userIdObject = new stdClass();
        // $userIdObject->STAFF = ['200'];
        // $userIdObject->STUDENT = ['3588'];
        $whereCondArr = [];
        $whereCond = "";
        foreach($fcmTokenRequestObject as $userType => $ids) {
            $whereCondArr []= " (user_id IN ('" . implode("','", $ids) . "') AND user_type = '$userType') ";
        }
        if(!empty($whereCondArr)) {
            $whereCond .= " AND (".implode(" OR ", $whereCondArr).")";
        }
        if($platform){
            $whereCond .= " AND platform = '$platform";
        }
        if (empty($whereCondArr)) {
            throw new MobileException(MobileException::INVALID_USER_ID, "Invalid user ids");
        }
        $sql = "SELECT
                    fcm_token
                FROM
                    `v4_firebase_cloud_messaging_tokens` WHERE 1=1
                $whereCond";
        try {
            $tokens = (array)$this->executeQueryForList($sql);
            $result = array_values(array_unique(array_map(function($token) {
                return $token->fcm_token;
            }, $tokens)));
            return $result;
        } catch (\Exception $e) {
            throw new MobileException($e->getCode(), $e->getMessage());
        }
    }
    public function sendPushNotification(PushNotificationRequest $request) {
        $url = 'https://fcm.googleapis.com/v1/projects/linways-b8708/messages:send';
        $accessTokenReq = new GenerateFcmAccessTokenRequest();
        $accessTokenData = $this->generateAccessToken($accessTokenReq);
        $accessToken = $accessTokenData->accessToken;
        // PREPARING THE FCM API REQUEST PARAM
        $notificationObject = array(
            'title' => $request->title ?? '',
            'body' => $request->body ?? ''
        );
        $messageObject = array(
            'notification' => $notificationObject
        );
        // PUSING DATA OBJECT IF PRESENT
        $dataObject = $request->data;
        if (!empty($dataObject)) {
            $messageObject['data'] = $dataObject;
        }
        // PUSHING WEBPUSH OBJECT IF PRESENT
        $webPushObject = array();
        if (!empty($request->webFcmOptionLink)) {
            $webPushObject['fcm_options'] = array('link' => $request->webFcmOptionLink);
        }
        
        if (!empty($webPushObject)) {
            $messageObject['webpush'] = $webPushObject;
        }
        $fields = array(
            'message' => $messageObject
        );
        $headers = array(
            'Content-Type:application/json',
            'Authorization: Bearer ' . $accessToken
        );
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        // EARLIER OUR REQUEST CONTAINED registration_ids KEY
        $registationIds = $request->fcmTokens;
        // FCM HTTP V1 API ONLY SUPPORT INDIVIDUAL TOKENS
        if(!empty($registationIds) && count($registationIds) > 1000) {
            // FOR NOW WE ARE SENDING TOKENS ONE BY ONE. IN FUTURE THIS CAN BE CHANGED TOPIC BASED. SUBSCRIBE ALL THESE TOKENS TO TOPIC AND THEN SEND TO TOPIC
            while(count($registationIds)) {
                $splicedRegistarationIds = array_splice($registationIds,0,1000);
                $unRegisteredTokens = [];
                foreach($splicedRegistarationIds as $token) {
                    $fields['message']['token'] = $token;
                    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
                    $result = curl_exec($ch);
                    $result = json_decode($result);
                    // ACCUMULATING UNREGISTERED TOKENS
                    $errorDetails = $result->error->details;
                    $errorCode = $errorDetails[0]->errorCode;
                    if ($errorCode == 'UNREGISTERED') {
                        $unRegisteredTokens[] = $token;
                    }
                }
                $deleteRequest = new \stdClass();
                $deleteRequest->fcmTokens = $unRegisteredTokens;
                $this->deleteFcmToken($deleteRequest);
                $unRegisteredTokens = [];
                sleep(2); // SLEEP FOR 2 SECONDS TO AVOID RATE LIMITING
            }
            return;
        }
        else if(!empty($registationIds)) {
            $unRegisteredTokens = [];
            foreach($registationIds as $token) {
                $fields['message']['token'] = $token;
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
                $result = curl_exec($ch);
                $result = json_decode($result);
                $errorDetails = $result->error->details;
                $errorCode = $errorDetails[0]->errorCode;
                if ($errorCode == 'UNREGISTERED') {
                    $unRegisteredTokens[] = $token;
                }
            }
            $deleteRequest = new \stdClass();
            $deleteRequest->fcmTokens = $unRegisteredTokens;
            $this->deleteFcmToken($deleteRequest);
            $unRegisteredTokens = [];
            return;
        }
        // OTHER CASES OF NOT USING REGISTRATION IDS LIKE TOPIC AND ALL
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
        $result = curl_exec($ch);
        $result = json_decode($result);
        $this->deleteInvalidFcmTokens($request->fields, $result);
    }
    /**
     * Get user fcm tokens using user email ids
     *
     * request->userTypeEmailMap should be a map as follows
     * $userIdObject = new stdClass();
     * $userIdObject->STAFF = ['staff1@email.com','staff2@email.com'];
     * $userIdObject->STUDENT = ['student1@email.com','student2@email.com'];
     *
     * @param GetUserFcmTokensUsingEmailIdsRequest $request
     * @return GetUserFcmTokensUsingEmailIdsResponse $responseData
     * @throws MobileException
     **/
    public function getUserFcmTokensUsingEmailIds(GetUserFcmTokensUsingEmailIdsRequest $request)
    {
        try {
            $fcmTokens = [];
            $userTypeEmailIdMap = $request->userTypeEmailMap;
            if (!empty($request->platform)) {
                $platformCond = " AND vfcmt.platform = '$request->platform";
            }
            foreach($userTypeEmailIdMap as $userType => $emailIds) {
                if (!empty($emailIds)) {
                    if ($userType === UserType::STAFF) {
                        $sql = "SELECT
                                vfcmt.fcm_token
                            FROM
                                staffaccounts s
                            INNER JOIN v4_firebase_cloud_messaging_tokens vfcmt ON
                                vfcmt.user_id = s.staffID
                                AND vfcmt.user_type = 'STAFF'
                            WHERE
                                s.staffEmail IN ('" . implode("','", $emailIds) . "') 
                                $platformCond ";
                        $tokenList = $this->executeQueryForList($sql);
                        $staffTokens = array_values(array_unique(array_map(function($token) {
                            return $token->fcm_token;
                        }, $tokenList)));
                        $fcmTokens = array_merge($fcmTokens, $staffTokens);
                    }
                    else if ($userType === UserType::STUDENT) {
                        $sql = "SELECT
                                vfcmt.fcm_token
                            FROM
                                studentaccount s
                            INNER JOIN v4_firebase_cloud_messaging_tokens vfcmt ON
                                vfcmt.user_id = s.studentID
                                AND vfcmt.user_type = 'STUDENT'
                            WHERE
                                s.studentEmail IN ('" . implode("','", $emailIds) . "') 
                                $platformCond ";
                        $tokenList = $this->executeQueryForList($sql);
                        $studentTokens = array_values(array_unique(array_map(function($token) {
                            return $token->fcm_token;
                        }, $tokenList)));
                        $fcmTokens = array_merge($fcmTokens, $studentTokens);
                    }
                }
            }
            $responseData = new GetUserFcmTokensUsingEmailIdsResponse();
            $responseData->fcmTokens = $fcmTokens;
            return $responseData;
        } catch (\Exception $e) {
            throw new MobileException($e->getCode(), $e->getMessage());
        }
    }
    /**
     * Delete FCM Tokens that are invalid
     * @param $fcmResult
     * @throws MobileException
     */
    protected function deleteInvalidFcmTokens($fcmRequestFields, $fcmResult) {
        if(!empty($fcmRequestFields['registration_ids']) && $fcmResult->failure > 0) {
            $failedTokens = array_filter($fcmRequestFields['registration_ids'], function($token, $key) use ($fcmResult) {
                return $fcmResult->results[$key]->error == 'NotRegistered' || $fcmResult->results[$key]->error == 'InvalidRegistration';
            }, ARRAY_FILTER_USE_BOTH);
            $deleteRequest = new \stdClass();
            $deleteRequest->fcmTokens = $failedTokens;
            $this->deleteFcmToken($deleteRequest);
        }
    }
    /**
     * Generate Access token for calling FCM api
     *
     * This method generates access token for calling fcm http v1 api
     *
     * @param GenerateFcmAccessTokenRequest $request
     * @return GenerateFcmAccessTokenResponse $response
     * @throws MobileException
     **/
    public function generateAccessToken(GenerateFcmAccessTokenRequest $request)
    {
        try {
            $credentialsPath = $GLOBALS['GOOGLE_FIREBASE_CREDENTIALS_PATH'];
            $credentialJsonContent = file_get_contents($credentialsPath);
            $credentialJson = json_decode($credentialJsonContent, true);
            $credentials = CredentialsLoader::makeCredentials(
                ['https://www.googleapis.com/auth/firebase.messaging'],
                $credentialJson
            );
            $httpHandler = HttpHandlerFactory::build(new Client(['handler' => HandlerStack::create()]));
            $credentials->fetchAuthToken($httpHandler);
            $accessToken = $credentials->getLastReceivedToken()['access_token'];
            $response = new GenerateFcmAccessTokenResponse();
            $response->accessToken = $accessToken;
            return $response;
        } catch (\Exception $e) {
            throw new MobileException($e->getCode(), $e->getMessage());
        }
    }
}