2
\$\begingroup\$

This is my first time using FCM, and I had a lot of trouble finding up to date examples on how to send notifications using Firebase Cloud Messaging Notifications via FCM HTTP v1 API with PHP. I'd appreciate any feedback; if none, I hope that others might find this useful.

require 'vendor/autoload.php';

use \Firebase\JWT\JWT;

function get_access_token($service_account_key_file) {
    $service_account = json_decode(file_get_contents($service_account_key_file), true);

    $now = time();
    $token = [
        'iss' => $service_account['client_email'],
        'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
        'aud' => 'https://oauth2.googleapis.com/token',
        'iat' => $now,
        'exp' => $now + 3600,
    ];

    $jwt = JWT::encode($token, $service_account['private_key'], 'RS256');

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, 'https://oauth2.googleapis.com/token');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
        'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        'assertion' => $jwt,
    ]));

    $response = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($response, true);
    return $data['access_token'];
}

function send_fcm_notification($server_key, $fcm_token, $title, $body) {
    // FCM API URL
    $fcm_url = 'https://fcm.googleapis.com/v1/projects/PROJECT-ID/messages:send';

    // The payload to send to FCM
    $payload = [
        'message' => [
            'token' => $fcm_token,
            'notification' => [
                'title' => $title,
                'body' => $body
            ]
        ]
    ];

    // Encode the payload as JSON
    $json_payload = json_encode($payload);

    // Prepare headers
    $headers = [
        'Authorization: Bearer ' . $server_key,
        'Content-Type: application/json'
    ];

    // Initialize cURL
    $ch = curl_init();

    // Set cURL options
    curl_setopt($ch, CURLOPT_URL, $fcm_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload);

    // Execute cURL request
    $response = curl_exec($ch);

    // Check for errors
    if ($response === FALSE) {
        die('FCM Send Error: ' . curl_error($ch));
    }

    // Close cURL
    curl_close($ch);

    // Return response
    return $response;
}

// Example usage
$title = 'Test Notification';
$body = 'This is a test notification sent using FCM HTTP v1';

$service_account_key_file = 'firebase-admin.json';
$access_token = get_access_token($service_account_key_file);
// Example usage
$fcm_token = 'DEVICE-TOKEN';

$response = send_fcm_notification($access_token, $fcm_token, $title, $body);
echo $response;
\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

I'm going to do this in steps so you can follow my train of thought:

1. Make it classy

First, I would wrap everything in a class. Functions that belong together go together in a class (unless you have a specific reason not to). This may feel a bit much when you're a beginner, but the more you do that, the more you understand why (its mostly in the details).

So lets make it a class:

class FirebaseExample{
    public function get_access_token ($service_account_key_file) {
        // your code
    }

    public function send_fcm_notification($server_key, $fcm_token, $title, $body) {
        // your code
    }
}

Typehinting

You should ALWAYS typehint. If you dont typehint, you can wiggle a bit with what you input, you get away with more sloppyness. But a lot of wiggle makes a bad construction. Also, php is camelCase, not snake_case. Lets apply that:

class FirebaseExample{
    public function getAccessToken (string $serviceAccountKeyFile): string
    {
        // your code
    }

    public function sendFcmNotification(
        string $serverKey, 
        string $fcmToken, 
        string $title, 
        string $body
    ): string {
        // your code
    }
}

2. Single Responsibility

A function should have one task. What 'a task' is is more of a feeling than an exact science, but I've done the first method for you as an example:

class FirebaseExample 
{
    // Because we have a class, we can define a const for it, no need to keep passing it
    // down to all functions.
    private const ACCESS_TOKEN_FILE_LOCATION = 'firebase-admin.json';

    public function getAccessToken() 
    {
        $serviceAccount = $this->getServiceAccount();
        $token = $this->getJwtData($serviceAccount['client_email']);
        $jwt = JWT::encode($token, $service_account['private_key'], 'RS256');

        $data = $this->getGoogleApiAccessTokenResult($jwt);

        return $data['access_token'];
    }

    private function getServiceAccount(): array
    {
        return json_decode(
            file_get_contents(self::ACCESS_TOKEN_FILE_LOCATION),
            true
        );
    }

    private function getJwtData(string $clientEmail): array
    {
        $now = time();

        return [
            'iss' => $clientEmail,
            'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
            'aud' => 'https://oauth2.googleapis.com/token',
            'iat' => $now,
            'exp' => $now + 3600,
        ]
    }

    private function getGoogleApiAccessTokenResult(string $jwt): array
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, 'https://oauth2.googleapis.com/token');
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'assertion' => $jwt,
        ]));

        $response = curl_exec($ch);
        curl_close($ch);

        return json_decode($response, true);
    }
}

Only the getAccessToken is public, other code may only call that method, not the others. Example:

$firebaseExample = new FirebaseExample();
$firebaseExample->getAccessToken(); // will give you a string, we know this because of the typehint
$firebaseExample->getServiceAccount(); // will error. The outside world has nothing to do with this function 'hide' it.

Each function now has a task. It handles on 'sub job' and only that. You try to keep them as pure as possible, eg not calling a function in a function in a function etc, but "do a trick, give back result".

And the getAccessToken also has one job: It delegates tasks to other functions (much a like a people manager). The 'higher' a function is in the chain, the more readable is should be, eg more english-y than code-y.


There is more room for improvement, also in my own examples, but the suggestions above would be a good step in the right direction. From there issues become less significant (IMO)

\$\endgroup\$
3
\$\begingroup\$

I would be curious about performance gain if reusing an existing curl handle rather than init/close every time. Especially when there is a TLS handshake involved. I have not been doing much PHP for a while but had a quick look at the doc for curl_close:

Note:

This function has no effect. Prior to PHP 8.0.0, this function was used to close the resource.

Interesting... you always learn something new when reviewing code and check the relevant documentation.

Minor point maybe: I haven't checked the Google doc but it could be desirable to add a request header such as: "Accept: application/json" to be explicit about your request, because an endpoint can return different types of responses like XML or plain text. Sometimes using that header with third party APIs is actually crucial.

Also, in addition to regular exception handling it is good practice to always check the status code returned for your requests. You normally expect 200 (201 sometimes). You will probably run into issues on the occasion, like 401 errors if your credentials are invalid etc. Then you should not even try to parse the response as JSON, unless the specs provide a format for error responses.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.