PHP实现FCM消息推送

吐槽下官方的文档写的真烂~~~

官方文档地址(中文)

功能说明

FCM是google提供的一个消息推送服务,支持IOS, ANDROID, WEB浏览器等。
推送功能:

  • 单设备推送
  • 主题推送(合适多设备, 好像最多1000个设备)
  • 组推送(适合某个人的多台设备, 最多20个)

说明下:因为是google服务, 部分功能需要VPN才能达到效果

WEB端实现订阅

web端主要就是为了拿到用户注册的registration_id, 然后传给服务端, 服务端拿到registration_id来实现不同的推送效果

Server端实现推送

有两种方式可以实现:

  1. 官方提供的sdk, 目前仅支持 Node、Java、Python、C# 和 Go。
  2. 基于FCM 服务器协议 实现推送效果 (其他语言就用这个来实现推送效果,比如php)
    2.1 最新协议 FCM HTTP v1 API
    说明:用这个的可以实现推送消息的点击跳转到指定url, 旧版的http协议也许也有该功能,只是没有找到对应的方法
    2.2 旧版 HTTP 协议
    2.3 XMPP 服务器协议(没有实现该协议)

准备工作:

  • 注册一个firebase账号
  • 在 Firebase 控制台中,打开项目设置->云消息传递, 找到
    1. 旧版服务器密钥
    2. 发送者 ID
      PHP实现FCM消息推送_第1张图片
    3. 下载service-account-file.json
      点击项目设置->其他服务帐号->Google Cloud Platform, 如图:
      PHP实现FCM消息推送_第2张图片
      在跳转的页面,点击创建密匙即可得到对应的服务账号密匙,并命名为service-account-file.json:
      PHP实现FCM消息推送_第3张图片

最新协议 FCM HTTP v1 API

说明:该协议走的是动态授权, 分OAuth2和服务账号授权, 这边用的是服务账号授权 
流程说明:
1. 通过google-api-php-client 和 service-account-file.json 获取access_token(临时授权, 有过期时间)
2. 前端代码通过注册获取registration_id, 并上报服务端
3. 如果需要订阅功能,则通过旧版服务器密钥请求google接口实现设备的订阅
4. 使用access_token调用google接口来实现具体推送功能
  1. 安装google-api-php-client拓展, 用于授权请求github地址
  2. 授权获取access_token, 代码如下:
 namespace App\Util\FCM;
use Illuminate\Support\Facades\Redis;                               //Redis 工具类

/**
 1. Google FCM 工具类
 */
class AccessToken
{
    /**
     * 获取access_token的方法,并对access_token做了缓存处理
     * @param string $config_path require 下载的service-account-file.json文件存放路径
     * @date 2019-11-22
     * @return string
     * @demo AccessToken::getAccessToken('/path/to/service-account-file.json')
     * @throws \Google_Exception
     */
    public static function getAccessToken($config_path)
    {
        $cacheKey = 'lh:google:fcm:accesstoken';
        $temp = Redis::get($cacheKey);
        if (empty($temp)) {
            $temp = self::requestAccessToken($config_path);
            Redis::set($cacheKey, $temp['access_token']);
            Redis::expire($cacheKey, $temp['expires_in']);
            return $temp['access_token'];
        }
        return $temp;
    }

    /**
     * 调用google google-api-php-client 获取access_token 这个是通过google的服务账号授权(用于server端) 不是页面的OAuth授权
     * @date 2019-11-22
     * @param string $config_path option 配置文件路径
     * @throws \Google_Exception
     * @return [
     *      'access_token' => 'ya29.*****',             //访问令牌
     *      'expires_in' => 3600,                       //访问令牌过期时间
     *      'token_type' => 'Bearer',                   //token_type
     *      'created' => 1574401624,                    //token 创建时间
     * ]
     */
    protected static function requestAccessToken($config_path)
    {
        $client = new \Google_Client();
        $client->useApplicationDefaultCredentials();
        $client->setAuthConfig($config_path);
        $client->setScopes(['https://www.googleapis.com/auth/firebase.messaging']);     # 授予访问 FCM 的权限
        return $client->fetchAccessTokenWithAssertion();
    }
}
  1. 订阅主题:
 namespace App\Util\FCM;

/**
 * Google FCM 工具类
 */
class Subscribe
{

    /**
     * 将设备添加到主题
     * @author jeanku
     * @date 2019-11-22
     * @param string $topic_name require 根据业务自定义的主题名称
     * @param string $register_token require 前端授权得到的REGISTRATION_TOKEN
     * @return array
     * @throws \Exception
     */
    public static function addTopic($topic_name, $register_token)
    {
        $url = sprintf('https://iid.googleapis.com/iid/v1/%s/rel/topics/%s', $register_token, $topic_name);
        return Curl::setHeader(self::getCommonHeader())->post($url);
    }

    /**
     * 非推送消息的请求header
     * @date 2019-11-22
     * @return array
     */
    protected static function getCommonHeader()
    {
        return [
            'Content-Type: application/json',
            'Authorization: key=' . env('FCM_SERVER_KEY'),		//env('FCM_SERVER_KEY')旧版服务器密钥
        ];
    }
}
  1. 生成消息
 namespace App\Util\FCM;

/**
 * Google FCM 工具类
 */
class Message
{
    public static $common = [
        'name' => null,
        'data' => null,
        "notification" => null,
        "android" => null,
        "webpush" => null,
        "apns" => null,
        "fcm_options" => null,
        "token" => null,
        "topic" => null,
        "condition" => null,
    ];

    /**
     * 生成topic推送数据
     * @author jeanku
     * @date 2019-11-22
     * @param string $topic required topic key
     * @param array $messgeData required message data
     * @return array
     */
    public static function formatTopicMessage($topic, $messgeData)
    {
        self::$common['topic'] = $topic;
        self::$common['webpush'] = $messgeData;
        return ['message' => array_filter(self::$common)];
    }

    /**
     * 获取web端推送的数据
     * @author jeanku
     * @date 2019-11-22
     * @param array $notification required [
     *      'title' => '',
     *      'body' => '',
     * ]
     * @param array $data option []
     * @param array $header option []
     * @param array $fcm_options option [
     *      'link' => 'http://***'          //点击跳转的链接
     * ]
     * @return array
     */
    public static function getWebMessage($notification, $data = null, $header = null, $fcm_options = null)
    {
        return array_filter([
            'header' => $header,
            'data' => $data,
            'notification' => $notification,
            'fcm_options' => $fcm_options,
        ]);
    }
}
  1. Curl代码
 namespace App\Util\FCM;

/**
 * Curl类
 *
 * @desc curl Class support post&get
 * @package \Ananzu\Util
 * @date 2016-05-18
 */
class Curl
{
    const REQUEST_TIMEOUT = 30;                                             //超时时间


    protected $_ch = null;                                                  //curl的句柄
    protected $param_type = 0;                                              //参数类型

    protected $_header = [];                                                //curl的句柄
    protected static $_instance = null;

    protected function __construct()
    {
    }

    protected static function instance()
    {
        if (empty(self::$_instance)) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    /**
     * set header
     * @date 2019-11-22
     * @param array $header required 请求header
     * @return Curl|null
     */
    public static function setHeader($header)
    {
        self::instance()->_header = $header;
        return self::instance();
    }

    /**
     * 传参方式
     * @date 2019-11-22
     * @param int $type requrie 传参方式 0:默认 1:raw
     * @return Curl|null
     */
    public static function setParamType($type)
    {
        self::instance()->param_type = $type;
        return self::instance();
    }

    /**
     * GET request
     * @date 2019-11-22
     * @param string $url required 请求的url
     * @param array $params required 请求的参数
     * @param int $timeout 请求的超时时间
     * @return array
     * @throws \Exception
     */
    public static function get($url, $params = array(), $timeout = self::REQUEST_TIMEOUT)
    {
        return self::instance()->request($url, 'GET', $params, $timeout);
    }

    /**
     * POST request
     * @date 2019-11-22
     * @param string $url required 请求的url
     * @param array $params required 请求的参数
     * @param int $timeout 请求的超时时间
     * @return array
     * @throws \Exception
     */
    public static function post($url, $params = array(), $timeout = self::REQUEST_TIMEOUT)
    {
        return self::instance()->request($url, 'POST', $params, $timeout);
    }

    /**
     * request 请求的方法
     * @date 2019-11-22
     * @param string $url required 请求的url
     * @param string $method required 请求的method
     * @param array $params required 请求的参数
     * @param int $timeout 请求的超时时间
     * @demo Curl::request('http://asd.com.cn',['name'=>123,'age'=>88],Curl::CURL_REQUEST_POST)
     * @return array
     * @throws \Exception
     */
    protected static function request($url, $method, $params = array(), $timeout = self::REQUEST_TIMEOUT)
    {
        $model = self::instance();
        $model->_ch = curl_init();
        $model->_setParams($url, $params, $method);
        curl_setopt($model->_ch, CURLOPT_TIMEOUT, $timeout);
        curl_setopt($model->_ch, CURLOPT_HTTPHEADER, $model->_header);
        curl_setopt($model->_ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($model->_ch, CURLOPT_HEADER, 0);
        $aRes = curl_exec($model->_ch);
        if ($error = curl_errno($model->_ch)) {
            throw new \Exception(curl_error($model->_ch), $error);
        }
        curl_close($model->_ch);
        return $aRes;
    }

    /**
     * ssl处理 如果是https,则设置相关的配置信息
     * @date 2016-05-18
     * @param string $url required 请求的url
     * @return void
     */
    protected function _setSsl($url)
    {
        if (true === strstr($url, 'https://', true)) {
            curl_setopt($this->_ch, CURLOPT_SSL_VERIFYPEER, 0);
            curl_setopt($this->_ch, CURLOPT_SSL_VERIFYHOST, 2);
            curl_setopt($this->_ch, CURLOPT_DNS_USE_GLOBAL_CACHE, 0);
            curl_setopt($this->_ch, CURLOPT_FOLLOWLOCATION, 1);
        }
    }

    /**
     * curl请求方式和POST|GET请求数据处理
     * @date 2016-05-18
     * @param string $url required 请求的url
     * @param array $data required 请求的参数
     * @param string $method required 请求的方式 POST|GET
     * @return bool
     */
    protected function _setParams($url, $data, $method)
    {
        $this->_setSsl($url);
        switch ($method) {
            case 'POST':
                if ($this->param_type == 1) {
                    $_postData = is_array($data) ? json_encode($data) : $data;
                } else {
                    $_postData = is_array($data) ? http_build_query($data) : $data;
                }
                curl_setopt($this->_ch, CURLOPT_POST, true);
                curl_setopt($this->_ch, CURLOPT_POSTFIELDS, $_postData);
                curl_setopt($this->_ch, CURLOPT_URL, $url);
                break;
            case 'GET':
                $_getData = is_array($data) ? http_build_query($data) : $data;
                $uri = preg_match('/\?/', $url) ? '&' . $_getData : '?' . $_getData;
                curl_setopt($this->_ch, CURLOPT_URL, $url . $uri);
                break;
            default:
                return false;
        }
    }
}
  1. 推送消息
 namespace App\Util\FCM;

/**
 * Google FCM 工具类
 */
class Notification
{
    /**
     * 将设备添加到主题
     * @author jeanku
     * @date 2019-11-22
     * @param array $data require 请求数据
     * @return array
     * @throws \Exception
     */
    public static function push($data)
    {
        $url = 'https://fcm.googleapis.com/v1/projects/longhash-notification/messages:send';
        return Curl::setParamType(1)->setHeader(self::getAccessTokenHeader())->post($url, $data);
    }

    /**
     * 推送消息的请求header
     * @date 2019-11-22
     * @return array
     * @throws \Google_Exception
     */
    protected static function getAccessTokenHeader()
    {
        return [
            'Content-Type: application/json',
            'Authorization: Bearer ' . AccessToken::getAccessToken(env('SERVICE_ACCOUNT_JSON_FILE')),
        ];
    }
}
  1. 具体调用
# 订阅
$topic_name = "article_push";		//自定义的主题名称 字符串
$register_token = '****';			//前端注册的设备register_token
SubscribeModule::addTopic($topic_name, $register_token);

# 订阅消息推送
$notification = ['title' => 'test', 'body' => 'this is a test'];		//推送名称 描述
$option = ["link" => 'http://www.***.com/uri'];							//点击推送跳转的地址
$webMsg = Message::getWebMessage(notification, null, null, $option);	//生成推送到浏览器的消息格式
$topicWebMsg = Message::formatTopicMessage($topic_name, $msg);			//生成主题推送消息		
Notification::push($topicWebMsg);										//推送消息

#单个推送
//todo
#多个推送
//todo

2.2 旧版 HTTP 协议

如果使用旧版http 协议来处理, 推荐一个第三方拓展github地址
旧版推送就非常简单了, 就是一个http请求, 需要在header添加Content-Type: application/json 和 Authorization: key=我是旧版服务密匙PHP实现FCM消息推送_第4张图片
PHP实现FCM消息推送_第5张图片

你可能感兴趣的:(php,php,FCM,notification,webpush,GCM)