├── tests └── bootstrap.php ├── TODO.md ├── CHANGELOG.md ├── source ├── responses │ ├── legacyApi │ │ ├── TopicResponseInterface.php │ │ ├── GroupManagementResponseInterface.php │ │ ├── GroupResponseInterface.php │ │ ├── TopicResponse.php │ │ ├── GroupManagementResponse.php │ │ ├── TokenResponseInterface.php │ │ ├── GroupResponse.php │ │ ├── LegacyAbstractResponse.php │ │ └── TokenResponse.php │ ├── TopicSubscribeResponseInterface.php │ ├── apiV1 │ │ ├── TokenResponseInterface.php │ │ ├── ApiV1AbstractResponse.php │ │ └── TokenResponse.php │ ├── StaticResponseFactory.php │ ├── TopicSubscribeResponse.php │ └── AbstractResponse.php ├── builders │ ├── OptionsBuilder.php │ ├── StaticBuilderFactory.php │ ├── TopicSubscriptionOptionsBuilder.php │ ├── GroupManagementOptionsBuilder.php │ ├── apiV1 │ │ └── MessageOptionsBuilder.php │ └── legacyApi │ │ └── MessageOptionsBuilder.php ├── requests │ ├── GroupManagementRequest.php │ ├── StaticRequestFactory.php │ ├── Request.php │ ├── AbstractRequest.php │ ├── ApiV1Request.php │ └── LegacyApiRequest.php ├── components │ └── Fcm.php ├── auth │ └── ServiceAccount.php └── helpers │ ├── ErrorsHelper.php │ └── OptionsHelper.php ├── .gitignore ├── LICENSE ├── composer.json └── README.md /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | setMessageId($responseBody[self::MESSAGE_ID]); 22 | $this->setResult(true); 23 | } 24 | 25 | if (array_key_exists(self::ERROR, $responseBody)) { 26 | $this->setErrorMessage($responseBody[self::ERROR]); 27 | $this->setErrorStatusDescription($responseBody[self::ERROR]); 28 | } 29 | } 30 | 31 | /** 32 | * Returns the error message from sending push to topic(s). 33 | * 34 | * @return string 35 | */ 36 | public function getErrorMessage() 37 | { 38 | return $this->errorMessage; 39 | } 40 | 41 | /** 42 | * Sets the error message from sending push to topic(s). 43 | * 44 | * @param string $errorMessage 45 | */ 46 | private function setErrorMessage(string $errorMessage) 47 | { 48 | $this->errorMessage = $errorMessage; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /source/responses/legacyApi/GroupManagementResponse.php: -------------------------------------------------------------------------------- 1 | setNotificationKey($responseBody[self::NOTIFICATION_KEY]); 26 | $this->setResult(true); 27 | } 28 | } 29 | 30 | /** 31 | * Returns notification_key - a unique identifier of the device group. 32 | * 33 | * @return string 34 | */ 35 | public function getNotificationKey() 36 | { 37 | return $this->notificationKey; 38 | } 39 | 40 | /** 41 | * Sets notification_key - a unique identifier of the device group. 42 | * 43 | * @param string $notificationKey 44 | */ 45 | private function setNotificationKey(string $notificationKey) 46 | { 47 | $this->notificationKey = $notificationKey; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /source/requests/GroupManagementRequest.php: -------------------------------------------------------------------------------- 1 | =7.0", 24 | "yiisoft/yii2": "~2.0.6", 25 | "guzzlehttp/guzzle": "^6.0", 26 | "google/auth": "^1.2.1" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit" : "4.7.*" 30 | }, 31 | "repositories": [ 32 | { 33 | "type": "composer", 34 | "url": "https://asset-packagist.org" 35 | } 36 | ], 37 | "autoload": { 38 | "psr-4": { 39 | "aksafan\\fcm\\": "" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/responses/apiV1/TokenResponseInterface.php: -------------------------------------------------------------------------------- 1 | validateConfigs(); 40 | $request = StaticRequestFactory::build($this->apiVersion, $this->apiParams, $reason); 41 | $request->setResponse(StaticResponseFactory::build($this->apiVersion, $request)); 42 | 43 | return $request; 44 | } 45 | 46 | /** 47 | * Validates required params. 48 | * 49 | * @throws \ReflectionException 50 | * @throws InvalidArgumentException 51 | */ 52 | private function validateConfigs() 53 | { 54 | foreach ((new ReflectionClass($this))->getProperties(ReflectionProperty::IS_PUBLIC) as $param) { 55 | if (! $this->{$param->getName()}) { 56 | throw new InvalidArgumentException($param->getName().' param must be set.'); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/builders/StaticBuilderFactory.php: -------------------------------------------------------------------------------- 1 | topic = $topic; 41 | } 42 | 43 | /** 44 | * @param array $tokens 45 | * 46 | * @throws InvalidArgumentException 47 | */ 48 | public function setTokensForTopic(array $tokens) 49 | { 50 | OptionsHelper::validateTokensValue($tokens); 51 | self::$tokens = $tokens; 52 | } 53 | 54 | /** 55 | * @param boolean $subscriptionStatus Flag for subscribe (true) or unsubscribe (false) to (from) FCM topic. 56 | */ 57 | public function setSubscribeToTopic(bool $subscriptionStatus) 58 | { 59 | $this->subscriptionStatus = $subscriptionStatus; 60 | } 61 | 62 | /** 63 | * @return null|string 64 | */ 65 | public function getTopic() 66 | { 67 | return $this->topic ?? null; 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | public static function getTokens(): array 74 | { 75 | return self::$tokens ?? []; 76 | } 77 | 78 | /** 79 | * @return bool 80 | */ 81 | public function getSubscriptionStatus(): bool 82 | { 83 | return $this->subscriptionStatus; 84 | } 85 | 86 | /** 87 | * Builds request body data. 88 | * 89 | * @return array 90 | */ 91 | public function build(): array 92 | { 93 | return self::$tokens; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /source/responses/StaticResponseFactory.php: -------------------------------------------------------------------------------- 1 | getReason()) { 38 | return new TopicSubscribeResponse(); 39 | } 40 | 41 | if (static::LEGACY_API === $apiVersion) { 42 | if (StaticBuilderFactory::FOR_TOKEN_SENDING === $request->getReason()) { 43 | return new LegacyTokenResponse(); 44 | } 45 | if (StaticBuilderFactory::FOR_TOPIC_SENDING === $request->getReason()) { 46 | return new LegacyTopicResponse(); 47 | } 48 | if (StaticBuilderFactory::FOR_GROUP_SENDING === $request->getReason()) { 49 | return new LegacyGroupResponse(); 50 | } 51 | if (StaticBuilderFactory::FOR_GROUP_MANAGEMENT === $request->getReason()) { 52 | return new GroupManagementResponse(); 53 | } 54 | } 55 | 56 | if (static::API_V1 === $apiVersion && StaticBuilderFactory::FOR_TOKEN_SENDING === $request->getReason()) { 57 | return new TokenResponse(); 58 | } 59 | 60 | throw new \InvalidArgumentException('api param must be in ['.implode(', ', static::AVAILABLE_API_VERSIONS).'].'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /source/responses/apiV1/ApiV1AbstractResponse.php: -------------------------------------------------------------------------------- 1 | getBody()->getContents(); 30 | if (200 === ($statusCode = $response->getStatusCode())) { 31 | return true; 32 | } 33 | 34 | if (404 === $statusCode) { 35 | $decodedResponseContents = $this->getResponseBody($response); 36 | if (isset($decodedResponseContents['error']['message']) && 'Requested entity was not found.' === $decodedResponseContents['error']['message']) { 37 | return true; 38 | } 39 | } 40 | 41 | if (400 === $statusCode) { 42 | \Yii::error(ErrorsHelper::getStatusCodeErrorMessage($statusCode, $responseContents, $this), ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 43 | \Yii::error('Something in the request data was wrong: check if all data{...}values are converted to strings.', ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 44 | $this->setErrorStatusDescription(ErrorsHelper::STATUS_CODE_400, $responseContents); 45 | 46 | return false; 47 | } 48 | 49 | if (403 === $statusCode) { 50 | \Yii::error(ErrorsHelper::getStatusCodeErrorMessage($statusCode, self::UNAUTHORIZED_REQUEST_EXCEPTION_MESSAGE, $this), ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 51 | \Yii::error('To use the new FCM HTTP v1 API, you need to enable FCM API on your Google API dashboard first - https://console.developers.google.com/apis/library/fcm.googleapis.com/.', ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 52 | $this->setErrorStatusDescription(ErrorsHelper::STATUS_CODE_403, $responseContents); 53 | 54 | return false; 55 | } 56 | 57 | \Yii::error(ErrorsHelper::getStatusCodeErrorMessage($statusCode, $responseContents, $this), ErrorsHelper::GUZZLE_HTTP_CLIENT_OTHER_ERRORS); 58 | $this->setErrorStatusDescription(ErrorsHelper::OTHER_STATUS_CODES, $responseContents); 59 | 60 | return false; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /source/responses/legacyApi/GroupResponse.php: -------------------------------------------------------------------------------- 1 | setNumberSuccess((int) $responseBody[self::SUCCESS]); 34 | } 35 | if (array_key_exists(self::FAILURE, $responseBody)) { 36 | $this->setNumberFailure((int) $responseBody[self::FAILURE]); 37 | } 38 | if ($this->getNumberFailure() > 0) { 39 | if (array_key_exists(self::FAILED_REGISTRATION_IDS, $responseBody) && \is_array($failedRegistrationIds = $responseBody[self::FAILED_REGISTRATION_IDS])) { 40 | foreach ($failedRegistrationIds as $registrationId) { 41 | $this->setTokensFailed((string) $registrationId); 42 | } 43 | } 44 | } else { 45 | $this->setResult(true); 46 | } 47 | } 48 | 49 | /** 50 | * Returns the number of token successfully sent. 51 | * 52 | * @return int 53 | */ 54 | public function getNumberSuccess() 55 | { 56 | return $this->numberSuccess; 57 | } 58 | 59 | /** 60 | * Returns the number of token unsuccessfully sent. 61 | * 62 | * @return int 63 | */ 64 | public function getNumberFailure() 65 | { 66 | return $this->numberFailure; 67 | } 68 | 69 | /** 70 | * Returns an array of tokens unsuccessfully sent. 71 | * 72 | * @return array 73 | */ 74 | public function getTokensFailed(): array 75 | { 76 | return $this->tokensFailed; 77 | } 78 | 79 | /** 80 | * Sets the number of token successfully sent. 81 | * 82 | * @param int $numberSuccess 83 | */ 84 | private function setNumberSuccess(int $numberSuccess) 85 | { 86 | $this->numberSuccess = $numberSuccess; 87 | } 88 | 89 | /** 90 | * Sets the number of token unsuccessfully sent. 91 | * 92 | * @param int $numberFailure 93 | */ 94 | private function setNumberFailure(int $numberFailure) 95 | { 96 | $this->numberFailure = $numberFailure; 97 | } 98 | 99 | /** 100 | * Sets the number of token unsuccessfully sent. 101 | * 102 | * @param string $token 103 | */ 104 | private function setTokensFailed(string $token) 105 | { 106 | $this->tokensFailed[] = $token; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /source/requests/AbstractRequest.php: -------------------------------------------------------------------------------- 1 | httpClient; 41 | } 42 | 43 | /** 44 | * Sets Http client. 45 | * 46 | * @param Client $httpClient 47 | */ 48 | public function setHttpClient(Client $httpClient) 49 | { 50 | $this->httpClient = $httpClient; 51 | } 52 | 53 | /** 54 | * Sets ResponseInterface. 55 | * 56 | * @param $response 57 | */ 58 | public function setResponse(AbstractResponse $response) 59 | { 60 | $this->response = $response; 61 | } 62 | 63 | /** 64 | * Gets ResponseInterface. 65 | * 66 | * @return AbstractResponse 67 | */ 68 | public function getResponse(): AbstractResponse 69 | { 70 | return $this->response; 71 | } 72 | 73 | /** 74 | * Gets Request reason: sending message or (un)subscribing to(from) topic. 75 | * 76 | * @return string 77 | */ 78 | public function getReason(): string 79 | { 80 | return $this->reason; 81 | } 82 | 83 | /** 84 | * Sets Request reason. 85 | * 86 | * @param string $reason 87 | */ 88 | public function setReason(string $reason) 89 | { 90 | $this->reason = $reason; 91 | } 92 | 93 | /** 94 | * @param string $topic 95 | * @param array $tokens 96 | * 97 | * @return self|Request 98 | */ 99 | public function subscribeToTopic(string $topic, array $tokens): AbstractRequest 100 | { 101 | $this->getOptionBuilder()->setTopic($topic); 102 | $this->getOptionBuilder()->setTokensForTopic($tokens); 103 | $this->getOptionBuilder()->setSubscribeToTopic(true); 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * @param string $topic 110 | * @param array $tokens 111 | * 112 | * @return self|Request 113 | */ 114 | public function unsubscribeFromTopic(string $topic, array $tokens): AbstractRequest 115 | { 116 | $this->getOptionBuilder()->setTopic($topic); 117 | $this->getOptionBuilder()->setTokensForTopic($tokens); 118 | $this->getOptionBuilder()->setSubscribeToTopic(false); 119 | 120 | return $this; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /source/responses/legacyApi/LegacyAbstractResponse.php: -------------------------------------------------------------------------------- 1 | getStatusCode())) { 38 | return true; 39 | } 40 | 41 | $responseContents = $response->getBody()->getContents(); 42 | if (400 === $statusCode) { 43 | \Yii::error(ErrorsHelper::getStatusCodeErrorMessage($statusCode, $responseContents, $this), ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 44 | \Yii::error('Something in the request data was wrong: check if all data{...}values are converted to strings.', ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 45 | $this->setErrorStatusDescription(ErrorsHelper::STATUS_CODE_400, $responseContents); 46 | 47 | return false; 48 | } 49 | 50 | if (401 === $statusCode) { 51 | \Yii::error(ErrorsHelper::getStatusCodeErrorMessage($statusCode, self::UNAUTHORIZED_REQUEST_EXCEPTION_MESSAGE, $this), ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 52 | \Yii::error('To use the new FCM HTTP Legacy API, you need to enable FCM API on your Google API dashboard first - https://console.developers.google.com/apis/library/fcm.googleapis.com/.', ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 53 | $this->setErrorStatusDescription(ErrorsHelper::STATUS_CODE_403, $responseContents); 54 | return false; 55 | } 56 | 57 | \Yii::error(ErrorsHelper::getStatusCodeErrorMessage($statusCode, $responseContents, $this), ErrorsHelper::GUZZLE_HTTP_CLIENT_OTHER_ERRORS); 58 | $this->setErrorStatusDescription(ErrorsHelper::OTHER_STATUS_CODES, $responseContents); 59 | $this->setRetryAfter($response); 60 | 61 | return false; 62 | } 63 | 64 | /** 65 | * Returns retryAfter 66 | * 67 | * @return int|null 68 | */ 69 | public function getRetryAfter() 70 | { 71 | return $this->retryAfter; 72 | } 73 | 74 | /** 75 | * Returns retryAfter 76 | * 77 | * @param ResponseInterface $responseObject 78 | */ 79 | private function setRetryAfter(ResponseInterface $responseObject) 80 | { 81 | $responseHeader = $responseObject->getHeaders(); 82 | if (array_keys($responseHeader, 'Retry-After')) { 83 | $this->retryAfter = $responseHeader['Retry-After']; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /source/builders/GroupManagementOptionsBuilder.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 50 | } 51 | 52 | /** 53 | * @param array $tokens 54 | * 55 | * @throws InvalidArgumentException 56 | */ 57 | public function setTokensForGroup(array $tokens) 58 | { 59 | OptionsHelper::validateTokensValue($tokens); 60 | $this->tokens = $tokens; 61 | } 62 | 63 | /** 64 | * @param string $notificationKeyName Is a name or identifier (e.g., it can be a username) that is unique to a given group. 65 | */ 66 | public function setNotificationKeyName(string $notificationKeyName) 67 | { 68 | $this->notificationKeyName = $notificationKeyName; 69 | } 70 | 71 | /** 72 | * @param string $notificationKey Unique identifier of the device group. This value is returned in the response for a successful create operation, and is required for all subsequent operations on the device group. 73 | */ 74 | public function setNotificationKey(string $notificationKey) 75 | { 76 | $this->notificationKey = $notificationKey; 77 | } 78 | 79 | /** 80 | * @return string 81 | */ 82 | public function getOperation() 83 | { 84 | return $this->operation; 85 | } 86 | 87 | /** 88 | * @return array 89 | */ 90 | public function getTokensForGroup() 91 | { 92 | return $this->tokens; 93 | } 94 | 95 | /** 96 | * @return string 97 | */ 98 | public function getNotificationKeyName() 99 | { 100 | return $this->notificationKeyName; 101 | } 102 | 103 | /** 104 | * @return string 105 | */ 106 | public function getNotificationKey() 107 | { 108 | return $this->notificationKey; 109 | } 110 | 111 | /** 112 | * Builds request body data. 113 | * 114 | * @return array 115 | */ 116 | public function build(): array 117 | { 118 | return array_filter([ 119 | 'operation' => $this->getOperation(), 120 | 'notification_key_name' => $this->getNotificationKeyName(), 121 | 'notification_key' => $this->getNotificationKey(), 122 | 'registration_ids' => $this->getTokensForGroup(), 123 | ]); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /source/responses/TopicSubscribeResponse.php: -------------------------------------------------------------------------------- 1 | tokensWithError; 29 | } 30 | 31 | /** 32 | * Parses the response from (un)subscribing to(from) topic. 33 | * 34 | * @param array $responseBody 35 | */ 36 | public function parseResponse(array $responseBody) //TODO check response or rename, need to clarify logic 37 | { 38 | if (array_key_exists(self::RESULTS, $responseBody) && \is_array($results = $responseBody[self::RESULTS])) { 39 | $tokens = TopicSubscriptionOptionsBuilder::getTokens(); 40 | $tokensWithError = []; 41 | /** @var array $results */ 42 | foreach ($results as $id => $result) { 43 | if (\is_array($result) && ! empty($result) && array_key_exists($id, $tokens) && array_key_exists('error', $result)) { 44 | $this->setTokensWithError([ 45 | 'token' => $tokens[$id], 46 | 'error' => $result['error'], 47 | ]); 48 | } 49 | } 50 | $this->tokensWithError = $tokensWithError; 51 | } 52 | if (empty($this->tokensWithError)) { 53 | $this->setResult(true); 54 | } 55 | } 56 | 57 | /** 58 | * Check if the response given by fcm is parsable. 59 | * 60 | * @param ResponseInterface $response 61 | * 62 | * @return bool 63 | */ 64 | public function validateResponse($response): bool 65 | { 66 | if (null === $response) { 67 | return false; 68 | } 69 | 70 | if (200 === ($statusCode = $response->getStatusCode())) { 71 | return true; 72 | } 73 | 74 | $responseContents = $response->getBody()->getContents(); 75 | if (400 === $statusCode) { 76 | \Yii::error(ErrorsHelper::getStatusCodeErrorMessage($statusCode, $responseContents, $this), ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 77 | \Yii::error('Something in the request data was wrong: check if all data{...}values are converted to strings.', ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 78 | $this->setErrorStatusDescription(ErrorsHelper::STATUS_CODE_400, $responseContents); 79 | 80 | return false; 81 | } 82 | 83 | if (401 === $statusCode) { 84 | \Yii::error(ErrorsHelper::getStatusCodeErrorMessage($statusCode, self::UNAUTHORIZED_REQUEST_EXCEPTION_MESSAGE, $this), ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 85 | \Yii::error('To use the new FCM HTTP Legacy API, you need to enable FCM API on your Google API dashboard first - https://console.developers.google.com/apis/library/fcm.googleapis.com/.', ErrorsHelper::GUZZLE_HTTP_CLIENT_ERROR); 86 | $this->setErrorStatusDescription(ErrorsHelper::STATUS_CODE_403, $responseContents); 87 | return false; 88 | } 89 | 90 | \Yii::error(ErrorsHelper::getStatusCodeErrorMessage($statusCode, $responseContents, $this), ErrorsHelper::GUZZLE_HTTP_CLIENT_OTHER_ERRORS); 91 | $this->setErrorStatusDescription(ErrorsHelper::OTHER_STATUS_CODES, $responseContents); 92 | 93 | return false; 94 | } 95 | 96 | /** 97 | * Sets tokens that was unsuccessfully (un)subscribe to topic with their corresponded errors. 98 | * 99 | * @param array $tokensWithError 100 | */ 101 | public function setTokensWithError(array $tokensWithError) 102 | { 103 | $this->tokensWithError[] = $tokensWithError; 104 | } 105 | 106 | 107 | } 108 | -------------------------------------------------------------------------------- /source/responses/AbstractResponse.php: -------------------------------------------------------------------------------- 1 | validateResponse($responseObject)) { 41 | $this->parseResponse($this->getResponseBody($responseObject)); 42 | } 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Returns FCM Legacy API error code with id description. 49 | * 50 | * Official google documentation: 51 | * 52 | * @link https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode 53 | * @link https://firebase.google.com/docs/cloud-messaging/http-server-ref#table9 54 | * 55 | * @return string 56 | */ 57 | public function getErrorStatusDescription() 58 | { 59 | return $this->errorStatusDescription; 60 | } 61 | 62 | /** 63 | * Result of result of request. 64 | * 65 | * @return bool 66 | */ 67 | public function isResultOk(): bool 68 | { 69 | return $this->result; 70 | } 71 | 72 | /** 73 | * Returns messageId - the identifier of the message sent. 74 | * 75 | * @return string 76 | */ 77 | public function getMessageId() 78 | { 79 | return $this->messageId; 80 | } 81 | 82 | /** 83 | * Returns response body as a array. 84 | * 85 | * @param ResponseInterface $responseObject 86 | * 87 | * @return array 88 | * 89 | * @throws \ErrorException In order that response from FCM is not valid and parseable. 90 | */ 91 | protected function getResponseBody(ResponseInterface $responseObject): array 92 | { 93 | $result = json_decode((string) $responseObject->getBody(), true); 94 | if (\is_array($result)) { 95 | return $result; 96 | } 97 | \Yii::error('Response from FCM is not valid. Response code = '.$responseObject->getStatusCode().'. Response info = '.(string) $responseObject->getBody()->getContents(), ErrorsHelper::INVALID_FCM_RESPONSE); 98 | 99 | throw new \ErrorException('Response from FCM is not valid. Look through logs for more info.'); 100 | } 101 | 102 | /** 103 | * Sets messageId - the identifier of the message sent. 104 | * 105 | * @param string $messageId 106 | */ 107 | protected function setMessageId(string $messageId) 108 | { 109 | $this->messageId = $messageId; 110 | } 111 | 112 | /** 113 | * Sets errorStatusDescription property according to the given error status. 114 | * For appropriate error statuses check constants of 'ErrorsHelper' class. 115 | * 116 | * @param string $errorStatus 117 | * @param string $additionalInfo 118 | */ 119 | protected function setErrorStatusDescription(string $errorStatus, string $additionalInfo = '') 120 | { 121 | $this->errorStatusDescription = ErrorsHelper::getFcmErrorMessage($errorStatus, $additionalInfo); 122 | } 123 | 124 | /** 125 | * Sets result of request. 126 | * 127 | * @param bool $result 128 | */ 129 | protected function setResult(bool $result) 130 | { 131 | $this->result = $result; 132 | } 133 | 134 | /** 135 | * Check if the response given by fcm is parsable. 136 | * 137 | * @param ResponseInterface $response 138 | * 139 | * @return bool 140 | */ 141 | abstract public function validateResponse($response): bool; 142 | 143 | /** 144 | * Parses the response from sending message. 145 | * 146 | * @param array $responseBody 147 | */ 148 | abstract public function parseResponse(array $responseBody); 149 | } 150 | -------------------------------------------------------------------------------- /source/builders/apiV1/MessageOptionsBuilder.php: -------------------------------------------------------------------------------- 1 | data = $data; 92 | } 93 | 94 | /** 95 | * @param string $title 96 | * @param string $body 97 | */ 98 | public function setNotification(string $title, string $body) 99 | { 100 | $this->notification = [ 101 | 'title' => $title, 102 | 'body' => $body, 103 | ]; 104 | } 105 | 106 | /** 107 | * @param array $config 108 | */ 109 | public function setAndroidConfig(array $config) 110 | { 111 | $this->androidConfig = $config; 112 | } 113 | 114 | /** 115 | * @param array $config 116 | */ 117 | public function setApnsConfig(array $config) 118 | { 119 | $this->apnsConfig = $config; 120 | } 121 | 122 | /** 123 | * @param array $config 124 | */ 125 | public function setWebPushConfig(array $config) 126 | { 127 | $this->webPushConfig = $config; 128 | } 129 | 130 | /** 131 | * @param boolean Flag for testing the request without actually delivering the message. 132 | */ 133 | public function setValidateOnly(bool $validateOnly) 134 | { 135 | $this->validateOnly = $validateOnly; 136 | } 137 | 138 | /** 139 | * @return bool 140 | */ 141 | public function getValidateOnly(): bool 142 | { 143 | return $this->validateOnly; 144 | } 145 | 146 | /** 147 | * @return null|string 148 | */ 149 | public static function getToken() 150 | { 151 | return self::TOKEN === self::getTarget() ? self::getTargetValue() : null; 152 | } 153 | 154 | /** 155 | * @return string 156 | */ 157 | public static function getTarget(): string 158 | { 159 | return (string) self::$target; 160 | } 161 | 162 | /** 163 | * @return array|string 164 | */ 165 | public static function getTargetValue() 166 | { 167 | return self::$targetValue; 168 | } 169 | 170 | /** 171 | * Builds request body data. 172 | * 173 | * @return array 174 | */ 175 | public function build(): array 176 | { 177 | return array_filter([ 178 | self::getTarget() => self::getTargetValue(), 179 | 'data' => $this->data, 180 | 'notification' => $this->notification, 181 | 'android' => $this->androidConfig, 182 | 'apns' => $this->apnsConfig, 183 | 'webpush' => $this->webPushConfig, 184 | ]); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /source/auth/ServiceAccount.php: -------------------------------------------------------------------------------- 1 | authConfig = $authConfig; 46 | } 47 | 48 | /** 49 | * Authorizes an http request. 50 | * 51 | * @param array|string $scope Scope of the requested credentials @see https://developers.google.com/identity/protocols/googlescopes 52 | * 53 | * @return ClientInterface|\GuzzleHttp\Client 54 | * 55 | * @throws \Exception 56 | */ 57 | public function authorize($scope) 58 | { 59 | return CredentialsLoader::makeHttpClient($this->getCredentials($scope)); 60 | } 61 | 62 | /** 63 | * Returns the Firebase project id 64 | * 65 | * @return string 66 | */ 67 | public function getProjectId() 68 | { 69 | if (!isset($this->authConfig['project_id']) || empty($this->authConfig['project_id'])) { 70 | throw new UnexpectedValueException('project_id not found in auth config file!'); 71 | } 72 | 73 | return $this->authConfig['project_id']; 74 | } 75 | 76 | /** 77 | * Gets the client_email service account filed 78 | * 79 | * @return string 80 | */ 81 | public function getClientEmail() 82 | { 83 | if (!isset($this->authConfig['client_email']) || empty($this->authConfig['client_email'])) { 84 | throw new UnexpectedValueException('client_email not found in auth config file!'); 85 | } 86 | 87 | return $this->authConfig['client_email']; 88 | } 89 | 90 | /** 91 | * Encodes a JWT token 92 | * 93 | * @param string $uid Unique id 94 | * @param array $claims array of optional claims 95 | * 96 | * @return string 97 | */ 98 | public function encodeJWT($uid, array $claims = []): string 99 | { 100 | $clientEmail = $this->getClientEmail(); 101 | $now = time(); 102 | $payload = [ 103 | 'iss' => $clientEmail, 104 | 'sub' => $clientEmail, 105 | 'aud' => 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit', 106 | 'iat' => $now, 107 | 'exp' => $now + 3600, 108 | 'uid' => $uid, 109 | 'claims' => $claims, 110 | ]; 111 | 112 | return JWT::encode($payload, $this->getPrivateKey(), 'RS256'); 113 | } 114 | 115 | /** 116 | * Decodes a Firebase JWT 117 | * 118 | * @param string $jwt JWT string 119 | * @param string $public_key Public key 120 | * 121 | * @return object 122 | */ 123 | public function decodeJWT($jwt, $public_key) 124 | { 125 | return JWT::decode($jwt, $public_key, ['RS256']); 126 | } 127 | 128 | /** 129 | * @param CacheItemPoolInterface $cache 130 | */ 131 | public function setCacheHandler(CacheItemPoolInterface $cache) 132 | { 133 | $this->cache = $cache; 134 | } 135 | 136 | /** 137 | * Gets the credentials. 138 | * 139 | * @param $scope array|string Scope of the requested credentials @see https://developers.google.com/identity/protocols/googlescopes 140 | * 141 | * @return ServiceAccountCredentials|UserRefreshCredentials|FetchAuthTokenCache 142 | */ 143 | protected function getCredentials($scope) 144 | { 145 | $credentials = CredentialsLoader::makeCredentials($scope, $this->authConfig); 146 | //OAuth token caching 147 | if (null !== $this->cache) { 148 | $credentials = new FetchAuthTokenCache($credentials, [], $this->cache); 149 | } 150 | 151 | return $credentials; 152 | } 153 | 154 | /** 155 | * Gets the private_key service account filed 156 | * 157 | * @return string 158 | */ 159 | protected function getPrivateKey() 160 | { 161 | if (!isset($this->authConfig['private_key']) || empty($this->authConfig['private_key'])) { 162 | throw new UnexpectedValueException('private_key not found in auth config file!'); 163 | } 164 | 165 | return $this->authConfig['private_key']; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /source/helpers/ErrorsHelper.php: -------------------------------------------------------------------------------- 1 | 'No more information is available about this error.', 36 | self::INVALID_ARGUMENT => 'Request parameters were invalid. An extension of type google.rpc.BadRequest is returned to specify which field was invalid.', 37 | self::UNREGISTERED => 'App instance was unregistered from FCM. This usually means that the token used is no longer valid and a new one must be used.', 38 | self::NOT_FOUND => 'App instance was unregistered from FCM. This usually means that the token used is no longer valid and a new one must be used.', 39 | self::SENDER_ID_MISMATCH => 'The authenticated sender ID is different from the sender ID for the registration token.', 40 | self::QUOTA_EXCEEDED => 'Sending limit exceeded for the message target. An extension of type google.rpc.QuotaFailure is returned to specify which quota got exceeded.', 41 | self::APNS_AUTH_ERROR => 'APNs certificate or auth key was invalid or missing.', 42 | self::UNAVAILABLE => 'The server is overloaded.', 43 | self::INTERNAL => 'An unknown internal error occurred.', 44 | self::STATUS_CODE_400 => 'Something in the request data was wrong: check if all data{...}values are converted to strings and look through logs', 45 | self::STATUS_CODE_403 => 'To use the new FCM HTTP v1 API, you need to enable FCM API on your Google API dashboard first - https://console.developers.google.com/apis/library/fcm.googleapis.com/.', 46 | self::OTHER_STATUS_CODES => 'Something happened with request to FCM. Check logs for more information.', 47 | ]; 48 | 49 | const INVALID_FCM_RESPONSE = 'invalid_fcm_response'; 50 | const PARSE_ERROR = 'parse_error'; 51 | const GUZZLE_HTTP_CLIENT = 'guzzle_http_client'; 52 | const GUZZLE_HTTP_CLIENT_ERROR = 'guzzle_http_client_error'; 53 | const GUZZLE_HTTP_CLIENT_OTHER_ERRORS = 'guzzle_http_client_other_errors'; 54 | 55 | /** @var array Possible logs' errors */ 56 | const LOGS_ERRORS = [ 57 | self::INVALID_FCM_RESPONSE, 58 | self::PARSE_ERROR, 59 | self::GUZZLE_HTTP_CLIENT, 60 | self::GUZZLE_HTTP_CLIENT_ERROR, 61 | self::GUZZLE_HTTP_CLIENT_OTHER_ERRORS, 62 | ]; 63 | 64 | /** 65 | * @param string $errorCodeName 66 | * 67 | * @return string 68 | */ 69 | public static function getFcmErrorDescription(string $errorCodeName): string 70 | { 71 | return array_key_exists($errorCodeName, self::ERROR_CODE_ENUMS) ? self::ERROR_CODE_ENUMS[$errorCodeName] : ''; 72 | } 73 | 74 | /** 75 | * @param string $errorCodeName 76 | * @param string $additionalInfo 77 | * 78 | * @return string 79 | */ 80 | public static function getFcmErrorMessage(string $errorCodeName, string $additionalInfo = ''): string 81 | { 82 | return 'FcmError['.$errorCodeName.']: '.self::getFcmErrorDescription($errorCodeName).(!empty($additionalInfo) ? '. Additional info: '.$additionalInfo : ''); 83 | } 84 | 85 | /** 86 | * @param ClientException|null $e 87 | * 88 | * @return string 89 | */ 90 | public static function getGuzzleClientExceptionMessage($e): string 91 | { 92 | $errorStatusCode = ''; 93 | $reasonPhrase = ''; 94 | $message = ''; 95 | if (null !== $e->getResponse()) { 96 | $errorStatusCode = $e->getResponse()->getStatusCode(); 97 | $reasonPhrase = $e->getResponse()->getReasonPhrase(); 98 | $message = $e->getMessage(); 99 | } 100 | 101 | return 'Guzzle ClientException has occurred. Status code = '.$errorStatusCode.'. Reason = '.$reasonPhrase.'Exception message = '.$message; 102 | } 103 | 104 | /** 105 | * @param GuzzleException $e 106 | * 107 | * @return string 108 | */ 109 | public static function getGuzzleExceptionMessage(GuzzleException $e): string 110 | { 111 | return 'Guzzle Exception has occurred. Exception message = '.$e->getMessage().'. Trace = '.$e->getTraceAsString(); 112 | } 113 | 114 | /** 115 | * @param string $statusCode 116 | * @param string $responseBody 117 | * @param AbstractResponse $response 118 | * 119 | * @return string 120 | */ 121 | public static function getStatusCodeErrorMessage(string $statusCode, string $responseBody, AbstractResponse $response): string 122 | { 123 | return 'Http client '.$statusCode.', request reason is = '.self::getResponseType($response).'. Response = '.$responseBody; 124 | } 125 | 126 | /** 127 | * @param AbstractResponse $response 128 | * 129 | * @return string 130 | */ 131 | private static function getResponseType(AbstractResponse $response): string 132 | { 133 | if ($response instanceof GroupManagementResponse) { 134 | return StaticBuilderFactory::FOR_GROUP_MANAGEMENT; 135 | } 136 | if ($response instanceof LegacyGroupResponse) { 137 | return StaticBuilderFactory::FOR_GROUP_SENDING; 138 | } 139 | if ($response instanceof LegacyTokenResponse) { 140 | return StaticBuilderFactory::FOR_TOKEN_SENDING; 141 | } 142 | if ($response instanceof LegacyTopicResponse) { 143 | return StaticBuilderFactory::FOR_TOPIC_SENDING; 144 | } 145 | if ($response instanceof TokenResponse) { 146 | return StaticBuilderFactory::FOR_TOKEN_SENDING; 147 | } 148 | if ($response instanceof TopicSubscribeResponse) { 149 | return StaticBuilderFactory::FOR_TOPIC_MANAGEMENT; 150 | } 151 | 152 | return StaticBuilderFactory::UNKNOWN_REASON; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /source/requests/ApiV1Request.php: -------------------------------------------------------------------------------- 1 | serviceAccount = new ServiceAccount($apiParams['privateKey']); 46 | $this->setHttpClient($this->serviceAccount->authorize(self::FCM_AUTH_URL)); 47 | $this->setReason($reason); 48 | $this->optionBuilder = StaticBuilderFactory::build($reason, $this); 49 | } 50 | 51 | /** 52 | * Sets target (token|topic|condition) and its value. 53 | * 54 | * @param string $target 55 | * @param string $value 56 | * 57 | * @return Request 58 | */ 59 | public function setTarget(string $target, $value): Request 60 | { 61 | $this->getOptionBuilder()->setTarget($target, (string) $value); 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Sets data message info. 68 | * 69 | * @param array $data 70 | * 71 | * @throws InvalidArgumentException 72 | * 73 | * @return self 74 | */ 75 | public function setData(array $data): Request 76 | { 77 | $this->getOptionBuilder()->setData($data); 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * @param string $title 84 | * @param string $body 85 | * 86 | * @return self 87 | */ 88 | public function setNotification(string $title, string $body): Request 89 | { 90 | $this->getOptionBuilder()->setNotification($title, $body); 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * @param array $config 97 | * 98 | * @return self 99 | */ 100 | public function setAndroidConfig(array $config): Request 101 | { 102 | $this->getOptionBuilder()->setAndroidConfig($config); 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * @param array $config 109 | * 110 | * @return self 111 | */ 112 | public function setApnsConfig(array $config): Request 113 | { 114 | $this->getOptionBuilder()->setApnsConfig($config); 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * @param array $config 121 | * 122 | * @return self 123 | */ 124 | public function setWebPushConfig(array $config): Request 125 | { 126 | $this->getOptionBuilder()->setWebPushConfig($config); 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * @param bool $validateOnly Flag for testing the request without actually delivering the message. 133 | * 134 | * @return self 135 | */ 136 | public function validateOnly(bool $validateOnly = true): Request 137 | { 138 | $this->getOptionBuilder()->setValidateOnly($validateOnly); 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * Sends POST request 145 | * 146 | * @return AbstractResponse 147 | * 148 | * @throws \Exception 149 | */ 150 | public function send(): AbstractResponse 151 | { 152 | try { 153 | $responseObject = $this->getHttpClient()->request(self::POST, $this->getUrl(), $this->getRequestOptions()); 154 | } catch (ClientException $e) { 155 | \Yii::error(ErrorsHelper::getGuzzleClientExceptionMessage($e), ErrorsHelper::GUZZLE_HTTP_CLIENT); 156 | $responseObject = $e->getResponse(); 157 | } catch (GuzzleException $e) { 158 | \Yii::error(ErrorsHelper::getGuzzleExceptionMessage($e), ErrorsHelper::GUZZLE_HTTP_CLIENT); 159 | $responseObject = null; 160 | } 161 | 162 | return $this->getResponse()->handleResponse($responseObject); 163 | } 164 | 165 | /** 166 | * Builds the headers for the request. 167 | * 168 | * @return array 169 | */ 170 | public function getHeaders(): array 171 | { 172 | return [ 173 | 'Content-Type' => 'application/json', 174 | ]; 175 | } 176 | 177 | /** 178 | * Builds request url. 179 | * 180 | * @return string 181 | * 182 | * @throws \Exception 183 | */ 184 | public function getUrl(): string 185 | { 186 | if (StaticBuilderFactory::FOR_TOPIC_MANAGEMENT === $this->getReason()) { 187 | return $this->getOptionBuilder()->getSubscriptionStatus() ? self::TOPIC_ADD_SUBSCRIPTION_URL : self::TOPIC_REMOVE_SUBSCRIPTION_URL; 188 | } 189 | 190 | return self::SEND_MESSAGE_URL.$this->serviceAccount->getProjectId().self::SEND_MESSAGE_URL_PARAMS; 191 | } 192 | 193 | /** 194 | * Builds request options. 195 | * 196 | * @return array 197 | */ 198 | public function getRequestOptions(): array 199 | { 200 | if (StaticBuilderFactory::FOR_TOPIC_MANAGEMENT === $this->getReason()) { 201 | return $this->getSubscribeTopicOptions(); 202 | } 203 | 204 | return $this->getSendMessageOptions(); 205 | } 206 | 207 | /** 208 | * @return MessageOptionsBuilder|TopicSubscriptionOptionsBuilder 209 | */ 210 | public function getOptionBuilder() 211 | { 212 | return $this->optionBuilder; 213 | } 214 | 215 | /** 216 | * Returns the request options. 217 | * 218 | * @return array 219 | */ 220 | private function getSendMessageOptions(): array 221 | { 222 | return [ 223 | 'headers' => $this->getHeaders(), 224 | 'json' => [ 225 | 'validate_only' => $this->getOptionBuilder()->getValidateOnly(), 226 | 'message' => $this->getOptionBuilder()->build(), 227 | ], 228 | ]; 229 | } 230 | 231 | /** 232 | * Returns the request options. 233 | * 234 | * @return array 235 | */ 236 | private function getSubscribeTopicOptions(): array 237 | { 238 | return [ 239 | 'headers' => array_merge($this->getHeaders(), ['access_token_auth' => 'true']), 240 | 'json' => [ 241 | 'to' => OptionsBuilder::TOPICS_PATH . $this->getOptionBuilder()->getTopic(), 242 | 'registration_tokens' => $this->getOptionBuilder()->build(), 243 | ], 244 | ]; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /source/responses/apiV1/TokenResponse.php: -------------------------------------------------------------------------------- 1 | error = true; 63 | $this->setErrorStatus($errors); 64 | $this->setErrorCode($errors); 65 | $this->setErrorMessage($errors); 66 | $this->setErrorDetails($errors); 67 | $this->checkToBeDeleted($this->getErrorStatus(), $this->getErrorCodeFromErrorDetails($this->errorDetails)); 68 | $this->setErrorStatusDescription($this->getErrorStatus()); 69 | } 70 | 71 | if (array_key_exists(self::NAME, $responseBody)) { 72 | $this->setResult(true); 73 | $this->setRawMessageId((string) $responseBody[self::NAME]); 74 | $this->setMessageId($this->getMessageIdFromRawResponse((string) $responseBody[self::NAME])); 75 | } 76 | } 77 | 78 | /** 79 | * Returns rawMessageId - the raw message response from FMC. 80 | * 81 | * @return string 82 | */ 83 | public function getRawMessageId() 84 | { 85 | return $this->rawMessageId; 86 | } 87 | 88 | /** 89 | * Returns if there was an error during sending push notification. 90 | * 91 | * @return bool 92 | */ 93 | public function getError(): bool 94 | { 95 | return $this->error; 96 | } 97 | 98 | /** 99 | * Returns FCM APIv1 error status. 100 | * 101 | * @return string 102 | */ 103 | public function getErrorStatus() 104 | { 105 | return $this->errorStatus; 106 | } 107 | 108 | /** 109 | * Returns FCM APIv1 error code. 110 | * 111 | * Official google documentation: 112 | * 113 | * @link https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode 114 | * 115 | * @return int 116 | */ 117 | public function getErrorCode() 118 | { 119 | return $this->errorCode; 120 | } 121 | 122 | /** 123 | * Returns FCM APIv1 error message. 124 | * 125 | * @return string 126 | */ 127 | public function getErrorMessage() 128 | { 129 | return $this->errorMessage; 130 | } 131 | 132 | /** 133 | * Returns FCM APIv1 error details. 134 | * 135 | * @return array 136 | */ 137 | public function getErrorDetails(): array 138 | { 139 | return $this->errorDetails; 140 | } 141 | 142 | /** 143 | * Returns token(s) to delete. 144 | * 145 | * You should remove all tokens returned by this method from your database 146 | * 147 | * @return array 148 | */ 149 | public function getTokensToDelete(): array 150 | { 151 | return $this->tokensToDelete; 152 | } 153 | 154 | /** 155 | * Sets FCM APIv1 error status. 156 | * 157 | * @param array $errors 158 | */ 159 | private function setErrorStatus(array $errors) 160 | { 161 | if (array_key_exists(self::ERROR_STATUS, $errors)) { 162 | $this->errorStatus = (string)$errors[self::ERROR_STATUS]; 163 | } 164 | } 165 | 166 | /** 167 | * Sets FCM APIv1 error code. 168 | * 169 | * Official google documentation: 170 | * 171 | * @link https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode 172 | * 173 | * @param array $errors 174 | */ 175 | private function setErrorCode(array $errors) 176 | { 177 | if (array_key_exists(self::ERROR_CODE, $errors)) { 178 | $this->errorCode = $errors[self::ERROR_CODE]; 179 | } 180 | } 181 | 182 | /** 183 | * Sets FCM APIv1 error message. 184 | * 185 | * @param array $errors 186 | */ 187 | private function setErrorMessage(array $errors) 188 | { 189 | if (array_key_exists(self::ERROR_MESSAGE, $errors)) { 190 | $this->errorMessage = $errors[self::ERROR_MESSAGE]; 191 | } 192 | } 193 | 194 | /** 195 | * Returns FCM APIv1 error details. 196 | * 197 | * @param array $errors 198 | */ 199 | private function setErrorDetails(array $errors) 200 | { 201 | if (array_key_exists(self::ERROR_DETAILS, $errors)) { 202 | $this->errorDetails = \is_array($errors[self::ERROR_DETAILS]) ? $errors[self::ERROR_DETAILS] : []; 203 | } 204 | } 205 | 206 | /** 207 | * Sets rawMessageId - the raw message response from FMC. 208 | * 209 | * @param string $rawMessageId 210 | */ 211 | public function setRawMessageId(string $rawMessageId) 212 | { 213 | $this->rawMessageId = $rawMessageId; 214 | } 215 | 216 | /** 217 | * Checks if current token needs to be deleted. 218 | * 219 | * @param string $errorCodeName 220 | * @param string $errorCodeFromErrorDetails 221 | */ 222 | private function checkToBeDeleted(string $errorCodeName, string $errorCodeFromErrorDetails) 223 | { 224 | if (ErrorsHelper::UNREGISTERED === $errorCodeName || ErrorsHelper::UNREGISTERED === $errorCodeFromErrorDetails) { 225 | $this->tokensToDelete[] = MessageOptionsBuilder::getToken(); 226 | } 227 | } 228 | 229 | /** 230 | * Gets the message id from raw FCM message response. 231 | * 232 | * @param string $rawMessage 233 | * 234 | * @return string 235 | */ 236 | private function getMessageIdFromRawResponse(string $rawMessage): string 237 | { 238 | $result = substr($rawMessage, strrpos($rawMessage, '/' ) + 1); 239 | 240 | return false !== $result ? $result : 'Message id parse error.'; 241 | } 242 | 243 | /** 244 | * Returns error code from error details. 245 | * 246 | * @param array $errorDetails 247 | * 248 | * @return string 249 | */ 250 | private function getErrorCodeFromErrorDetails(array $errorDetails): string 251 | { 252 | $errorCode = ''; 253 | foreach ($errorDetails as $errorDetail) { 254 | if (array_key_exists('errorCode', $errorDetail)) { 255 | $errorCode = $errorDetail['errorCode']; 256 | break; 257 | } 258 | } 259 | 260 | return $errorCode; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /source/responses/legacyApi/TokenResponse.php: -------------------------------------------------------------------------------- 1 | numberTokensSuccess; 71 | } 72 | 73 | /** 74 | * Get the number of device which thrown an error. 75 | * 76 | * @return int 77 | */ 78 | public function getNumberFailure() 79 | { 80 | return $this->numberTokensFailure; 81 | } 82 | 83 | /** 84 | * Get the number of device that you need to modify their token. 85 | * 86 | * @return int 87 | */ 88 | public function getNumberModification() 89 | { 90 | return $this->numberTokenModify; 91 | } 92 | 93 | /** 94 | * get token to delete. 95 | * 96 | * remove all tokens returned by this method in your database 97 | * 98 | * @return array 99 | */ 100 | public function getTokensToDelete() 101 | { 102 | return $this->tokensToDelete; 103 | } 104 | 105 | /** 106 | * get token to modify. 107 | * 108 | * key: oldToken 109 | * value: new token 110 | * 111 | * find the old token in your database and replace it with the new one 112 | * 113 | * @return array 114 | */ 115 | public function getTokensToModify() 116 | { 117 | return $this->tokensToModify; 118 | } 119 | 120 | /** 121 | * Get tokens that you should resend using exponential backoof. 122 | * 123 | * @return array 124 | */ 125 | public function getTokensToRetry() 126 | { 127 | return $this->tokensToRetry; 128 | } 129 | 130 | /** 131 | * Get tokens that thrown an error. 132 | * 133 | * key : token 134 | * value : error 135 | * 136 | * In production, remove these tokens from you database 137 | * 138 | * @return array 139 | */ 140 | public function getTokensWithError() 141 | { 142 | return $this->tokensWithError; 143 | } 144 | 145 | /** 146 | * check if missing tokens was given to the request 147 | * If true, remove all the empty token in your database. 148 | * 149 | * @return bool 150 | */ 151 | public function hasMissingToken() 152 | { 153 | return $this->hasMissingToken; 154 | } 155 | 156 | /** 157 | * Parses the response from sending message. 158 | * 159 | * @param array $responseBody 160 | */ 161 | public function parseResponse(array $responseBody) 162 | { 163 | if (array_key_exists(self::MULTICAST_ID, $responseBody)) { 164 | $this->setMessageId((string) $responseBody[self::MULTICAST_ID]); 165 | } 166 | 167 | if (array_key_exists(self::SUCCESS, $responseBody)) { 168 | $this->setNumberSuccess((int) $responseBody[self::SUCCESS]); 169 | } 170 | 171 | if (array_key_exists(self::FAILURE, $responseBody)) { 172 | $this->setNumberFailure((int) $responseBody[self::FAILURE]); 173 | } 174 | 175 | if (array_key_exists(self::CANONICAL_IDS, $responseBody)) { 176 | $this->setNumberModification((int) $responseBody[self::CANONICAL_IDS]); 177 | } 178 | 179 | if ($this->needResultParsing($responseBody)) { 180 | $this->parseResult($responseBody); 181 | } else { 182 | $this->setResult(true); 183 | } 184 | } 185 | 186 | /** 187 | * @param array $responseBody 188 | */ 189 | private function parseResult(array $responseBody) 190 | { 191 | if (! \is_array($results = $responseBody[self::RESULTS])) { 192 | \Yii::error('Parse error. ResponseBody = '.json_encode($responseBody), ErrorsHelper::PARSE_ERROR); 193 | } 194 | /** @var array $results */ 195 | foreach ($results as $index => $result) { 196 | if (\is_array($result)) { 197 | if (array_key_exists(self::ERROR, $result)) { 198 | $this->setErrorStatusDescription($result[self::ERROR]); 199 | } 200 | if ( 201 | !$this->isSent($result) && 202 | !$this->needToBeModify($index, $result) && 203 | !$this->needToBeDeleted($index, $result) && 204 | !$this->needToResend($index, $result) && 205 | !$this->checkMissingToken($result) 206 | ) { 207 | $this->addErrors($index, $result); 208 | } 209 | } 210 | } 211 | } 212 | 213 | /** 214 | * Sets the number of device reached with success. 215 | * 216 | * @param int $numberTokensSuccess 217 | */ 218 | private function setNumberSuccess(int $numberTokensSuccess) 219 | { 220 | $this->numberTokensSuccess = $numberTokensSuccess; 221 | } 222 | 223 | /** 224 | * Sets the number of device which thrown an error. 225 | * 226 | * @param int $numberTokensFailure 227 | */ 228 | private function setNumberFailure(int $numberTokensFailure) 229 | { 230 | $this->numberTokensFailure = $numberTokensFailure; 231 | } 232 | 233 | /** 234 | * Sets the number of device that you need to modify their token. 235 | * 236 | * @param int $numberTokenModify 237 | */ 238 | private function setNumberModification(int $numberTokenModify) 239 | { 240 | $this->numberTokenModify = $numberTokenModify; 241 | } 242 | 243 | /** 244 | * @param array $responseBody 245 | * 246 | * @return bool 247 | */ 248 | private function needResultParsing(array $responseBody): bool 249 | { 250 | return array_key_exists(self::RESULTS, $responseBody) && ($this->numberTokensFailure > 0 || $this->numberTokenModify > 0); 251 | } 252 | 253 | /** 254 | * @param array $results 255 | * 256 | * @return bool 257 | */ 258 | private function isSent(array $results): bool 259 | { 260 | return array_key_exists(self::MESSAGE_ID, $results) && !array_key_exists(self::REGISTRATION_ID, $results); 261 | } 262 | 263 | /** 264 | * @param $index 265 | * @param array $result 266 | * 267 | * @return bool 268 | */ 269 | private function needToBeModify($index, array $result): bool 270 | { 271 | if (array_key_exists(self::MESSAGE_ID, $result) && array_key_exists(self::REGISTRATION_ID, $result)) { 272 | $tokens = MessageOptionsBuilder::getTokens(); 273 | if (array_key_exists($index, $tokens)) { 274 | $this->tokensToModify[$tokens[$index]] = $result[self::REGISTRATION_ID]; 275 | } 276 | 277 | return true; 278 | } 279 | 280 | return false; 281 | } 282 | 283 | /** 284 | * @param $index 285 | * @param array $result 286 | * 287 | * @return bool 288 | */ 289 | private function needToBeDeleted($index, array $result): bool 290 | { 291 | if (array_key_exists(self::ERROR, $result) && (\in_array(self::NOT_REGISTERED, $result, true) || \in_array(self::INVALID_REGISTRATION, $result, true))) { 292 | $tokens = MessageOptionsBuilder::getTokens(); 293 | if (array_key_exists($index, $tokens)) { 294 | $this->tokensToDelete[] = $tokens[$index]; 295 | } 296 | 297 | return true; 298 | } 299 | 300 | return false; 301 | } 302 | 303 | /** 304 | * @param $index 305 | * @param array $result 306 | * 307 | * @return bool 308 | */ 309 | private function needToResend($index, array $result): bool 310 | { 311 | if (array_key_exists(self::ERROR, $result) && (\in_array(self::UNAVAILABLE, $result, true) || \in_array(self::DEVICE_MESSAGE_RATE_EXCEEDED, $result, true) || \in_array(self::INTERNAL_SERVER_ERROR, $result, true))) { 312 | $tokens = MessageOptionsBuilder::getTokens(); 313 | if (array_key_exists($index, $tokens)) { 314 | $this->tokensToRetry[] = $tokens[$index]; 315 | } 316 | 317 | return true; 318 | } 319 | 320 | return false; 321 | } 322 | 323 | /** 324 | * @param array $result 325 | * 326 | * @return bool 327 | */ 328 | private function checkMissingToken(array $result): bool 329 | { 330 | $hasMissingToken = (array_key_exists(self::ERROR, $result) && \in_array(self::MISSING_REGISTRATION, $result, true)); 331 | 332 | $this->hasMissingToken = (bool) ($this->hasMissingToken | $hasMissingToken); 333 | 334 | return $hasMissingToken; 335 | } 336 | 337 | /** 338 | * @param $index 339 | * @param array $result 340 | */ 341 | private function addErrors($index, array $result) 342 | { 343 | $tokens = MessageOptionsBuilder::getTokens(); 344 | if (array_key_exists(self::ERROR, $result) && array_key_exists($index, $tokens) && $tokens[$index]) { 345 | $this->tokensWithError[$tokens[$index]] = $result[self::ERROR]; 346 | } 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /source/helpers/OptionsHelper.php: -------------------------------------------------------------------------------- 1 | self::LEGACY_API_ANDROID_OPTIONS, 114 | self::IOS => self::LEGACY_API_IOS_OPTIONS, 115 | self::WEB_PUSH => self::LEGACY_API_WEB_PUSH_OPTIONS, 116 | ]; 117 | 118 | const MAX_TOKENS_PER_REQUEST = 1000; 119 | 120 | /** 121 | * Validates priority for supporting by FCM. 122 | * 123 | * @param string $priority 124 | * 125 | * @throws InvalidArgumentException 126 | */ 127 | public static function validatePriority(string $priority) 128 | { 129 | if (! \in_array($priority, self::PRIORITY_OPTIONS, true)) { 130 | throw new InvalidArgumentException('priority is not valid, please refer to the documentation or use the constants OptionsHelper::PRIORITY_OPTIONS'); 131 | } 132 | } 133 | 134 | /** 135 | * Validates operations to do for device group management. 136 | * 137 | * @param string $operation 138 | * 139 | * @throws InvalidArgumentException 140 | */ 141 | public static function validateGroupOperation(string $operation) 142 | { 143 | if (! \in_array($operation, self::GROUP_OPERATIONS, true)) { 144 | throw new InvalidArgumentException('operation is not valid, please refer to the documentation or use the constants OptionsHelper::GROUP_OPERATIONS'); 145 | } 146 | } 147 | 148 | /** 149 | * Validates FCM APIv1 target and its value 150 | * 151 | * Official FCM documentation 152 | * 153 | * @link https://firebase.google.com/docs/cloud-messaging/admin/send-messages#send_to_a_condition 154 | * @link https://firebase.google.com/docs/cloud-messaging/admin/send-messages#send_to_a_topic 155 | * 156 | * @param string $target 157 | * @param string $value 158 | * 159 | * @throws InvalidArgumentException 160 | */ 161 | public static function validateApiV1Target(string $target, string $value) 162 | { 163 | switch ($target) { 164 | case MessageOptionsBuilder::TOPIC_CONDITION: 165 | self::validateConditionValue($value); 166 | break; 167 | case MessageOptionsBuilder::TOPIC: 168 | self::validateTopicValue($value); 169 | break; 170 | case MessageOptionsBuilder::TOKEN: 171 | break; 172 | default: 173 | throw new InvalidArgumentException('Invalid target type "'.$target.'", valid type: "'.implode(', ', MessageOptionsBuilder::TYPES)); 174 | } 175 | } 176 | 177 | /** 178 | * Validates FCM Legacy API target and its value 179 | * 180 | * @param string $target 181 | * @param string|array $value 182 | * 183 | * @throws InvalidArgumentException 184 | */ 185 | public static function validateLegacyApiTarget(string $target, $value) 186 | { 187 | switch ($target) { 188 | case LegacyMessageOptionsBuilder::TOPIC_CONDITION: 189 | self::validateConditionValue($value); 190 | break; 191 | case LegacyMessageOptionsBuilder::TOPIC: 192 | self::validateTopicValue($value); 193 | break; 194 | case LegacyMessageOptionsBuilder::TOKEN: 195 | break; 196 | case LegacyMessageOptionsBuilder::TOKENS: 197 | self::validateTokensValue($value); 198 | break; 199 | case LegacyMessageOptionsBuilder::GROUP: 200 | break; 201 | default: 202 | throw new InvalidArgumentException('Invalid target type "'.$target.'", valid type: "'.implode(', ', LegacyMessageOptionsBuilder::TYPES)); 203 | } 204 | } 205 | 206 | /** 207 | * Validates FCM APIv1 data 208 | * 209 | * @param array $data 210 | * 211 | * @throws InvalidArgumentException 212 | */ 213 | public static function validateData(array $data) 214 | { 215 | foreach ($data as $key => $value) { 216 | if (! \is_string($key) || ! \is_string($value)) { 217 | throw new InvalidArgumentException('The keys and values in message data must be all strings.'); 218 | } 219 | } 220 | } 221 | 222 | /** 223 | * Validates notification messages options for Android|IOS|Web push 224 | * 225 | * @param array $data 226 | * @param string $platform 227 | * 228 | * @throws InvalidArgumentException 229 | */ 230 | public static function validateLegacyApiPlatformConfig(array $data, string $platform) 231 | { 232 | foreach ($data as $key => $value) { 233 | if (! \is_string($key) || ! \is_string($value)) { 234 | throw new InvalidArgumentException('The keys and values in '.$platform.' notification messages options must be all strings.'); 235 | } 236 | if (! \in_array($key, self::getPlatformOptions($platform), true)) { 237 | throw new InvalidArgumentException('The keys in '.$platform.' notification messages options must be appropriate according to official documentation. Look for of the class "OptionsHelper" constants'); 238 | } 239 | } 240 | } 241 | 242 | /** 243 | * Validates FCM topic value 244 | * One can choose any topic name that matches the regular expression: "[a-zA-Z0-9-_.~%]+". 245 | * 246 | * @param string $value 247 | */ 248 | public static function validateTopicValue(string $value) 249 | { 250 | $value = trim(preg_replace('@^/topic/@', '', $value), '/'); 251 | if (preg_match('/[^a-zA-Z0-9-_.~]$/', $value)) { 252 | throw new InvalidArgumentException(sprintf('Malformed topic name "%s".', $value)); 253 | } 254 | } 255 | 256 | /** 257 | * Validates tokens value 258 | * 259 | * Official FCM documentation 260 | * 261 | * @link https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_topics_2 262 | * 263 | * @param array $value 264 | */ 265 | public static function validateTokensValue($value) 266 | { 267 | if (! \is_array($value)) { 268 | throw new InvalidArgumentException('Tokens target value must be an array'); 269 | } 270 | if (empty($value)) { 271 | throw new InvalidArgumentException('An empty array of tokens given'); 272 | } 273 | if (\count($value) > self::MAX_TOKENS_PER_REQUEST) { 274 | throw new InvalidArgumentException('You can use only 1000 devices in a single request'); 275 | } 276 | } 277 | 278 | /** 279 | * Validates FCM APIv1 condition value 280 | * 281 | * Official FCM documentation 282 | * 283 | * APIv1 284 | * @link https://firebase.google.com/docs/cloud-messaging/admin/send-messages#send_to_a_condition 285 | * 286 | * Legacy API 287 | * @link https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_topics 288 | * 289 | * @param string $value 290 | */ 291 | private static function validateConditionValue(string $value) 292 | { 293 | $value = str_replace('"', "'", $value); 294 | if ((substr_count($value, "'") % 2) !== 0) { 295 | throw new InvalidArgumentException(sprintf('The condition "%s" contains an uneven amount of quotes.', $value)); 296 | } 297 | } 298 | 299 | /** 300 | * Returns platform options. 301 | * 302 | * @param string $platform 303 | * 304 | * @return array 305 | */ 306 | private static function getPlatformOptions(string $platform): array 307 | { 308 | if (array_key_exists($platform, self::PLATFORM_OPTIONS)) { 309 | return self::PLATFORM_OPTIONS[$platform]; 310 | } 311 | 312 | throw new InvalidArgumentException('platform must be appropriate. Look for of the class "OptionsHelper" constants'); 313 | } 314 | } -------------------------------------------------------------------------------- /source/builders/legacyApi/MessageOptionsBuilder.php: -------------------------------------------------------------------------------- 1 | data = $data; 136 | } 137 | 138 | /** 139 | * @param string $title 140 | * @param string $body 141 | */ 142 | public function setNotification(string $title, string $body) 143 | { 144 | $this->notification = [ 145 | 'title' => $title, 146 | 'body' => $body, 147 | ]; 148 | } 149 | 150 | /** 151 | * This parameter identifies a group of messages (e.g., with collapse_key: "Updates Available") that can be collapsed, so that only the last message gets sent when delivery can be resumed. 152 | * A maximum of 4 different collapse keys is allowed at any given time. 153 | * 154 | * @param string $collapseKey 155 | */ 156 | public function setCollapseKey(string $collapseKey) 157 | { 158 | $this->collapseKey = $collapseKey; 159 | } 160 | 161 | /** 162 | * Sets the priority of the message. Valid values are "normal" and "high." 163 | * By default, messages are sent with normal priority. 164 | * 165 | * @param string $priority 166 | * 167 | * @throws InvalidArgumentException 168 | */ 169 | public function setPriority(string $priority) 170 | { 171 | OptionsHelper::validatePriority($priority); 172 | $this->priority = $priority; 173 | } 174 | 175 | /** 176 | * Supports only Android and Ios. 177 | * 178 | * An inactive client app is awoken. 179 | * On iOS, use this field to represent content-available in the APNS payload. 180 | * On Android, data messages wake the app by default. 181 | * On Chrome, currently not supported. 182 | * 183 | * @param bool $contentAvailable 184 | */ 185 | public function setContentAvailable(bool $contentAvailable) 186 | { 187 | $this->contentAvailable = $contentAvailable; 188 | } 189 | 190 | /** 191 | * Supports iOS 10+ 192 | * 193 | * When a notification is sent and this is set to true, 194 | * the content of the notification can be modified before it is displayed. 195 | * 196 | * @param bool $isMutableContent 197 | */ 198 | public function setMutableContent(bool $isMutableContent) 199 | { 200 | $this->mutableContent = $isMutableContent; 201 | } 202 | 203 | /** 204 | * This parameter specifies how long the message should be kept in FCM storage if the device is offline. 205 | * 206 | * @param int $timeToLive (in second) min:0 max:2419200 207 | * 208 | * @throws InvalidArgumentException 209 | */ 210 | public function setTimeToLive(int $timeToLive) 211 | { 212 | if ($timeToLive < 0 || $timeToLive > 2419200) { 213 | throw new InvalidArgumentException("time to live must be between 0 and 2419200, current value is: {$timeToLive}"); 214 | } 215 | $this->timeToLive = $timeToLive; 216 | } 217 | 218 | /** 219 | * This parameter specifies the package name of the application where the registration tokens must match in order to receive the message. 220 | * 221 | * @param string $restrictedPackageName 222 | */ 223 | public function setRestrictedPackageName(string $restrictedPackageName) 224 | { 225 | $this->restrictedPackageName = $restrictedPackageName; 226 | } 227 | 228 | /** 229 | * This parameter, when set to true, allows developers to test a request without actually sending a message. 230 | * It should only be used for the development. 231 | * 232 | * @param bool $isDryRun 233 | */ 234 | public function setDryRun(bool $isDryRun) 235 | { 236 | $this->dryRun = $isDryRun; 237 | } 238 | 239 | /** 240 | * @param array $config 241 | */ 242 | public function setAndroidConfig(array $config) 243 | { 244 | OptionsHelper::validateLegacyApiPlatformConfig($config, OptionsHelper::ANDROID); 245 | $this->androidConfig = $config; 246 | } 247 | 248 | /** 249 | * @param array $config 250 | */ 251 | public function setApnsConfig(array $config) 252 | { 253 | OptionsHelper::validateLegacyApiPlatformConfig($config, OptionsHelper::IOS); 254 | $this->apnsConfig = $config; 255 | } 256 | 257 | /** 258 | * @param array $config 259 | */ 260 | public function setWebPushConfig(array $config) 261 | { 262 | OptionsHelper::validateLegacyApiPlatformConfig($config, OptionsHelper::WEB_PUSH); 263 | $this->webPushConfig = $config; 264 | } 265 | 266 | /** 267 | * @return array 268 | */ 269 | public static function getTokens(): array 270 | { 271 | if (self::TOKENS === self::getTarget()) { 272 | return \is_array(self::getTargetValue()) ? self::getTargetValue() : []; 273 | } 274 | if (self::TOKEN === self::getTarget()) { 275 | return [self::getTargetValue()]; 276 | } 277 | 278 | return []; 279 | } 280 | 281 | /** 282 | * @return string 283 | */ 284 | public static function getTarget(): string 285 | { 286 | return (string) self::$target; 287 | } 288 | 289 | /** 290 | * @return array|string 291 | */ 292 | public static function getTargetValue() 293 | { 294 | return self::$targetValue; 295 | } 296 | 297 | /** 298 | * Builds request body data. 299 | * 300 | * @return array 301 | */ 302 | public function build(): array 303 | { 304 | return array_filter([ 305 | $this->getTargetBodyParam() => self::getTargetValue(), 306 | 'notification' => $this->getNotificationOptions(), 307 | 'data' => $this->getData(), 308 | 'priority' => $this->getPriority(), 309 | 'collapse_key' => $this->getCollapseKey(), 310 | 'content_available' => $this->isContentAvailable(), 311 | 'mutable_content' => $this->isMutableContent(), 312 | 'time_to_live' => $this->getTimeToLive(), 313 | 'restricted_package_name' => $this->getRestrictedPackageName(), 314 | 'dry_run' => $this->isDryRun(), 315 | ]); 316 | } 317 | 318 | /** 319 | * @return array 320 | */ 321 | private function getData() 322 | { 323 | return $this->data; 324 | } 325 | 326 | /** 327 | * Gets the collapseKey. 328 | * 329 | * @return null|string 330 | */ 331 | private function getCollapseKey() 332 | { 333 | return $this->collapseKey; 334 | } 335 | 336 | /** 337 | * Gets the priority. 338 | * 339 | * @return null|string 340 | */ 341 | private function getPriority() 342 | { 343 | return $this->priority; 344 | } 345 | 346 | /** 347 | * Is content available. 348 | * 349 | * @return bool 350 | */ 351 | private function isContentAvailable(): bool 352 | { 353 | return $this->contentAvailable; 354 | } 355 | 356 | /** 357 | * Is mutable content 358 | * 359 | * @return bool 360 | */ 361 | private function isMutableContent(): bool 362 | { 363 | return $this->mutableContent; 364 | } 365 | 366 | /** 367 | * Gets time to live. 368 | * 369 | * @return null|int 370 | */ 371 | private function getTimeToLive() 372 | { 373 | return $this->timeToLive; 374 | } 375 | 376 | /** 377 | * Gets restricted package name. 378 | * 379 | * @return null|string 380 | */ 381 | private function getRestrictedPackageName() 382 | { 383 | return $this->restrictedPackageName; 384 | } 385 | 386 | /** 387 | * Is dry run. 388 | * 389 | * @return bool 390 | */ 391 | private function isDryRun(): bool 392 | { 393 | return $this->dryRun; 394 | } 395 | 396 | /** 397 | * Returns Notification options or Android|Apns|Web push notification options. The first one is not empty. 398 | * Empty array will be returned if none is not empty. 399 | * 400 | * @return array 401 | */ 402 | private function getNotificationOptions(): array 403 | { 404 | $options = []; 405 | if (! empty($this->notification)) { 406 | $options = array_merge($options, $this->notification); 407 | } 408 | if (! empty($this->androidConfig)) { 409 | $options = array_merge($options, $this->androidConfig); 410 | } 411 | if (! empty($this->apnsConfig)) { 412 | $options = array_merge($options, $this->apnsConfig); 413 | } 414 | if (! empty($this->webPushConfig)) { 415 | $options = array_merge($options, $this->webPushConfig); 416 | } 417 | 418 | return $options; 419 | } 420 | 421 | /** 422 | * Returns appropriate target body param. 423 | * 424 | * @link https://firebase.google.com/docs/cloud-messaging/http-server-ref#downstream-http-messages-json 425 | * 426 | * @return string 427 | */ 428 | private function getTargetBodyParam(): string 429 | { 430 | $target = self::getTarget(); 431 | 432 | if (\in_array($target, self::TO_TYPES, true)) { 433 | return self::TO_BODY_PARAM; 434 | } 435 | if (self::TOPIC_CONDITION === $target) { 436 | return self::CONDITION_BODY_PARAM; 437 | } 438 | if (self::TOKENS === $target) { 439 | return self::REGISTRATION_IDS_BODY_PARAM; 440 | } 441 | 442 | return self::TO_BODY_PARAM; 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /source/requests/LegacyApiRequest.php: -------------------------------------------------------------------------------- 1 | serverKey = $apiParams['serverKey']; 55 | $this->senderId = $apiParams['senderId']; 56 | $this->setHttpClient(new Client()); 57 | $this->setReason($reason); 58 | $this->optionBuilder = StaticBuilderFactory::build($reason, $this); 59 | } 60 | 61 | /** 62 | * Sets target (token|tokens|topic|topics|group) and its value. 63 | * 64 | * @param string $target 65 | * @param string|array $value 66 | * 67 | * @return LegacyApiRequest 68 | */ 69 | public function setTarget(string $target, $value): Request 70 | { 71 | $this->getOptionBuilder()->setTarget($target, $value); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Sets data message info. 78 | * 79 | * @param array $data 80 | * 81 | * @return LegacyApiRequest 82 | */ 83 | public function setData(array $data): Request 84 | { 85 | $this->getOptionBuilder()->setData($data); 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * @param string $title 92 | * @param string $body 93 | * 94 | * @return self 95 | */ 96 | public function setNotification(string $title, string $body): Request 97 | { 98 | $this->getOptionBuilder()->setNotification($title, $body); 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * @param array $config 105 | * 106 | * @return self 107 | */ 108 | public function setAndroidConfig(array $config): Request 109 | { 110 | $this->getOptionBuilder()->setAndroidConfig($config); 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * @param array $config 117 | * 118 | * @return self 119 | */ 120 | public function setApnsConfig(array $config): Request 121 | { 122 | $this->getOptionBuilder()->setApnsConfig($config); 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * @param array $config 129 | * 130 | * @return self 131 | */ 132 | public function setWebPushConfig(array $config): Request 133 | { 134 | $this->getOptionBuilder()->setWebPushConfig($config); 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * This parameter identifies a group of messages (e.g., with collapse_key: "Updates Available") that can be collapsed, so that only the last message gets sent when delivery can be resumed. 141 | * A maximum of 4 different collapse keys is allowed at any given time. 142 | * 143 | * @param string $collapseKey 144 | * 145 | * @return LegacyApiRequest 146 | */ 147 | public function setCollapseKey(string $collapseKey): Request 148 | { 149 | $this->getOptionBuilder()->setCollapseKey($collapseKey); 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * Sets the priority of the message. Valid values are "normal" and "high." 156 | * By default, messages are sent with normal priority. 157 | * 158 | * @param string $priority 159 | * 160 | * @return LegacyApiRequest 161 | * 162 | * @throws InvalidArgumentException 163 | * @throws \ReflectionException 164 | */ 165 | public function setPriority(string $priority): Request 166 | { 167 | $this->getOptionBuilder()->setPriority($priority); 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * Supports only Android and Ios. 174 | * 175 | * An inactive client app is awoken. 176 | * On iOS, use this field to represent content-available in the APNS payload. 177 | * On Android, data messages wake the app by default. 178 | * On Chrome, currently not supported. 179 | * 180 | * @param bool $contentAvailable 181 | * 182 | * @return LegacyApiRequest 183 | */ 184 | public function setContentAvailable(bool $contentAvailable): Request 185 | { 186 | $this->getOptionBuilder()->setContentAvailable($contentAvailable); 187 | 188 | return $this; 189 | } 190 | 191 | /** 192 | * Supports iOS 10+ 193 | * 194 | * When a notification is sent and this is set to true, 195 | * the content of the notification can be modified before it is displayed. 196 | * 197 | * @param bool $isMutableContent 198 | * 199 | * @return LegacyApiRequest 200 | */ 201 | public function setMutableContent(bool $isMutableContent): Request 202 | { 203 | $this->getOptionBuilder()->setMutableContent($isMutableContent); 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * This parameter specifies how long the message should be kept in FCM storage if the device is offline. 210 | * 211 | * @param int $timeToLive (in second) min:0 max:2419200 212 | * 213 | * @return LegacyApiRequest 214 | * 215 | * @throws InvalidArgumentException 216 | */ 217 | public function setTimeToLive(int $timeToLive): Request 218 | { 219 | $this->getOptionBuilder()->setTimeToLive($timeToLive); 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * This parameter specifies the package name of the application where the registration tokens must match in order to receive the message. 226 | * 227 | * @param string $restrictedPackageName 228 | * 229 | * @return LegacyApiRequest 230 | */ 231 | public function setRestrictedPackageName(string $restrictedPackageName): Request 232 | { 233 | $this->getOptionBuilder()->setRestrictedPackageName($restrictedPackageName); 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * This parameter, when set to true, allows developers to test a request without actually sending a message. 240 | * It should only be used for the development. 241 | * 242 | * @param bool $validateOnly 243 | * 244 | * @return LegacyApiRequest 245 | */ 246 | public function validateOnly(bool $validateOnly = true): Request 247 | { 248 | $this->getOptionBuilder()->setDryRun($validateOnly); 249 | 250 | return $this; 251 | } 252 | 253 | /** 254 | * Creates device group. 255 | * 256 | * @param string $groupName 257 | * @param array $tokens 258 | * 259 | * @return GroupManagementRequest|Request 260 | */ 261 | public function createGroup(string $groupName, array $tokens): GroupManagementRequest 262 | { 263 | $this->getOptionBuilder()->setOperation(OptionsHelper::GROUP_CREATE); 264 | $this->getOptionBuilder()->setNotificationKeyName($groupName); 265 | $this->getOptionBuilder()->setTokensForGroup($tokens); 266 | 267 | return $this; 268 | } 269 | 270 | /** 271 | * Returns NotificationKey from device group. 272 | * 273 | * @param string $groupName 274 | * 275 | * @return GroupManagementRequest|Request|LegacyApiRequest 276 | */ 277 | public function getNotificationKey(string $groupName): GroupManagementRequest 278 | { 279 | $this->getOptionBuilder()->setNotificationKeyName($groupName); 280 | 281 | return $this; 282 | } 283 | 284 | /** 285 | * Adds token(s) to device group. 286 | * 287 | * @param string $groupName 288 | * @param string $notificationKey 289 | * @param array $tokens 290 | * 291 | * @return GroupManagementRequest|Request 292 | */ 293 | public function addToGroup(string $groupName, string $notificationKey, array $tokens): GroupManagementRequest 294 | { 295 | $this->getOptionBuilder()->setOperation(OptionsHelper::GROUP_ADD); 296 | $this->getOptionBuilder()->setNotificationKeyName($groupName); 297 | $this->getOptionBuilder()->setNotificationKey($notificationKey); 298 | $this->getOptionBuilder()->setTokensForGroup($tokens); 299 | 300 | return $this; 301 | } 302 | 303 | /** 304 | * Removes token(s) from device group. 305 | * 306 | * @param string $groupName 307 | * @param string $notificationKey 308 | * @param array $tokens 309 | * 310 | * @return GroupManagementRequest|Request 311 | */ 312 | public function removeFromGroup(string $groupName, string $notificationKey, array $tokens): GroupManagementRequest 313 | { 314 | $this->getOptionBuilder()->setOperation(OptionsHelper::GROUP_REMOVE); 315 | $this->getOptionBuilder()->setNotificationKeyName($groupName); 316 | $this->getOptionBuilder()->setNotificationKey($notificationKey); 317 | $this->getOptionBuilder()->setTokensForGroup($tokens); 318 | 319 | return $this; 320 | } 321 | 322 | /** 323 | * Sends POST request 324 | * 325 | * @return AbstractResponse 326 | * 327 | * @throws \Exception 328 | */ 329 | public function send(): AbstractResponse 330 | { 331 | try { 332 | $responseObject = $this->getHttpClient()->request(self::POST, $this->getUrl(), $this->getRequestOptions()); 333 | } catch (ClientException $e) { 334 | \Yii::error(ErrorsHelper::getGuzzleClientExceptionMessage($e), ErrorsHelper::GUZZLE_HTTP_CLIENT); 335 | $responseObject = $e->getResponse(); 336 | } catch (GuzzleException $e) { 337 | \Yii::error(ErrorsHelper::getGuzzleExceptionMessage($e), ErrorsHelper::GUZZLE_HTTP_CLIENT); 338 | $responseObject = null; 339 | } 340 | 341 | return $this->getResponse()->handleResponse($responseObject); 342 | } 343 | 344 | /** 345 | * Sends GET request 346 | * 347 | * @return AbstractResponse 348 | * 349 | * @throws \Exception 350 | */ 351 | public function sendGET(): AbstractResponse 352 | { 353 | try { 354 | $responseObject = $this 355 | ->getHttpClient() 356 | ->request( 357 | self::GET, 358 | $this->getNotificationKeyUrl($this->getOptionBuilder()->getNotificationKeyName()), 359 | ['headers' => $this->getHeaders()] 360 | ); 361 | } catch (ClientException $e) { 362 | \Yii::error(ErrorsHelper::getGuzzleClientExceptionMessage($e), ErrorsHelper::GUZZLE_HTTP_CLIENT); 363 | $responseObject = $e->getResponse(); 364 | } catch (GuzzleException $e) { 365 | \Yii::error(ErrorsHelper::getGuzzleExceptionMessage($e), ErrorsHelper::GUZZLE_HTTP_CLIENT); 366 | $responseObject = null; 367 | } 368 | 369 | return $this->getResponse()->handleResponse($responseObject); 370 | } 371 | 372 | /** 373 | * Builds the headers for the request. 374 | * 375 | * @return array 376 | */ 377 | public function getHeaders(): array 378 | { 379 | return [ 380 | 'Authorization' => 'key='.$this->getServerKey(), 381 | 'Content-Type' => 'application/json', 382 | 'project_id' => $this->getSenderId(), 383 | ]; 384 | } 385 | 386 | /** 387 | * Builds request url. 388 | * 389 | * @return string 390 | */ 391 | public function getUrl(): string 392 | { 393 | if (StaticBuilderFactory::FOR_TOPIC_MANAGEMENT === $this->getReason()) { //TODO: add topic url 394 | return $this->optionBuilder->getSubscriptionStatus() ? self::TOPIC_ADD_SUBSCRIPTION_URL : self::TOPIC_REMOVE_SUBSCRIPTION_URL; 395 | } 396 | if (StaticBuilderFactory::FOR_GROUP_MANAGEMENT === $this->getReason()) { 397 | return self::MANAGE_GROUP_URL; 398 | } 399 | 400 | return self::SEND_MESSAGE_URL; 401 | } 402 | 403 | /** 404 | * Builds request options. 405 | * 406 | * @return array 407 | */ 408 | public function getRequestOptions(): array 409 | { 410 | if (StaticBuilderFactory::FOR_TOPIC_MANAGEMENT === $this->getReason()) { 411 | return $this->getSubscribeTopicOptions(); 412 | } 413 | 414 | return $this->getSendMessageOptions(); 415 | } 416 | 417 | /** 418 | * @return OptionsBuilder|MessageOptionsBuilder|GroupManagementOptionsBuilder|TopicSubscriptionOptionsBuilder 419 | */ 420 | public function getOptionBuilder() 421 | { 422 | return $this->optionBuilder; 423 | } 424 | 425 | /** 426 | * Gets serverKey. 427 | * 428 | * @return string 429 | */ 430 | private function getServerKey(): string 431 | { 432 | return $this->serverKey; 433 | } 434 | 435 | /** 436 | * Gets senderId. 437 | * 438 | * @return string 439 | */ 440 | private function getSenderId(): string 441 | { 442 | return $this->senderId; 443 | } 444 | 445 | /** 446 | * Returns the request options. 447 | * 448 | * @return array 449 | */ 450 | private function getSendMessageOptions(): array 451 | { 452 | return [ 453 | 'headers' => $this->getHeaders(), 454 | 'json' => $this->getOptionBuilder()->build(), 455 | ]; 456 | } 457 | 458 | /** 459 | * Returns the request options. 460 | * 461 | * @return array 462 | */ 463 | private function getSubscribeTopicOptions(): array 464 | { 465 | return 466 | [ 467 | 'headers' => $this->getHeaders(), 468 | 'json' => [ 469 | 'to' => OptionsBuilder::TOPICS_PATH.$this->optionBuilder->getTopic(), 470 | 'registration_tokens' => $this->optionBuilder->build(), 471 | ], 472 | ]; 473 | } 474 | 475 | /** 476 | * Builds request url for grabbing NotificationKey. 477 | * 478 | * @param string $groupName 479 | * 480 | * @return string 481 | */ 482 | private function getNotificationKeyUrl(string $groupName): string 483 | { 484 | return self::MANAGE_GROUP_URL.self::NOTIFICATION_KEY_NAME_PARAM.$groupName; 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yii2-fcm-both-api 2 | Yii2 Extension for sending push notification with both [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/) (FCM) HTTP Server Protocols (APIs). 3 | 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/aksafan/yii2-fcm-both-api/v/stable)](https://packagist.org/packages/aksafan/yii2-fcm-both-api) 6 | [![Total Downloads](https://poser.pugx.org/aksafan/yii2-fcm-both-api/downloads)](https://packagist.org/packages/aksafan/yii2-fcm-both-api) 7 | [![Build Status](https://travis-ci.org/aksafan/yii2-fcm-both-api.svg?branch=master)](https://travis-ci.org/aksafan/yii2-fcm-both-api) 8 | 9 | This extension supports sending push notification through both currently supported FCM API versions: 10 | - [HTTP v1 API](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages) 11 | - [Legacy HTTP Server Protocol](https://firebase.google.com/docs/cloud-messaging/http-server-ref) 12 | 13 | > Note: The XMPP protocol is not currently supported. 14 | 15 | # Installation 16 | 17 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). Check the [composer.json](https://github.com/aksafan/yii2-fcm-both-api/blob/master/composer.json) for this extension's requirements and dependencies. 18 | 19 | To install, either run 20 | 21 | ``` 22 | $ php composer.phar require aksafan/yii2-fcm-both-api 23 | ``` 24 | 25 | or add 26 | 27 | ``` 28 | "aksafan/yii2-fcm-both-api": "*" 29 | ``` 30 | 31 | to the `require` section of your `composer.json` file. 32 | 33 | 34 | Configuration 35 | ------------- 36 | 37 | ##### In order to use this library, you have to configure the Fcm class in your application configuration. 38 | 39 | For ApiV1: 40 | 41 | ```php 42 | return [ 43 | //.... 44 | 'components' => [ 45 | 'fcm' => [ 46 | 'class' => 'aksafan\fcm\source\components\Fcm', 47 | 'apiVersion' => \aksafan\fcm\source\requests\StaticRequestFactory::API_V1, 48 | 'apiParams' => [ 49 | 'privateKey' => '/path/to/your/file/privateKeyFile.json', 50 | ], 51 | ], 52 | ] 53 | ]; 54 | ``` 55 | 56 | > `privateKey` - used to authenticate the service account and authorize it to access Firebase services. You must [generate](https://firebase.google.com/docs/cloud-messaging/auth-server#authorize_http_v1_send_requests) a private key file in JSON format and use this key to retrieve a short-lived OAuth 2.0 token. 57 | `privateKey` can be set with json-file `'/path/to/your/file/privateKeyFile.json'` or simply with json-string `'{"type":"service_account"}'` 58 | 59 | For Legacy API: 60 | 61 | ```php 62 | return [ 63 | //.... 64 | 'components' => [ 65 | 'fcm' => [ 66 | 'class' => 'aksafan\fcm\source\components\Fcm', 67 | 'apiVersion' => \aksafan\fcm\source\requests\StaticRequestFactory::LEGACY_API, 68 | 'apiParams' => [ 69 | 'serverKey' => 'aef', 70 | 'senderId' => 'fwef', 71 | ], 72 | ], 73 | ] 74 | ]; 75 | ``` 76 | > `serverKey` - a server key that authorizes your app server for access to Google services, including sending messages via the Firebase Cloud Messaging legacy protocols. You obtain the server key when you create your Firebase project. You can view it in the [Cloud Messaging](https://console.firebase.google.com/project/_/settings/cloudmessaging/) tab of the Firebase console Settings pane. 77 | 78 | > `senderId` - a unique numerical value created when you create your Firebase project, available in the [Cloud Messaging](https://console.firebase.google.com/project/_/settings/cloudmessaging/) tab of the Firebase console Settings pane. The sender ID is used to identify each sender that can send messages to the client app. 79 | 80 | ##### Also add this to your Yii.php file in the root directory of the project for IDE code autocompletion. 81 | 82 | ```php 83 | /** 84 | * Class WebApplication 85 | * Include only Web application related components here. 86 | * 87 | * @property \aksafan\fcm\source\components\Fcm $fcm 88 | */ 89 | class WebApplication extends yii\web\Application 90 | { 91 | } 92 | ``` 93 | 94 | ##### Now you can get access to extension's methods through: 95 | 96 | ```php 97 | Yii::$app->fcm 98 | ``` 99 | 100 | ##### In order to use both HTTP v1 and legacy API at the same time, you need to register them separately: 101 | 102 | ```php 103 | return [ 104 | //.... 105 | 'components' => [ 106 | 'fcmApiV1' => [ 107 | 'class' => 'aksafan\fcm\source\components\Fcm', 108 | 'apiVersion' => \aksafan\fcm\source\requests\StaticRequestFactory::API_V1, 109 | 'apiParams' => [ 110 | 'privateKey' => '/path/to/your/file/privateKeyFile.json', 111 | ], 112 | ], 113 | 'fcmLegacyApi' => [ 114 | 'class' => 'aksafan\fcm\source\components\Fcm', 115 | 'apiVersion' => \aksafan\fcm\source\requests\StaticRequestFactory::LEGACY_API, 116 | 'apiParams' => [ 117 | 'serverKey' => 'aef', 118 | 'senderId' => 'fwef', 119 | ], 120 | ], 121 | ] 122 | ]; 123 | ``` 124 | Now you can use `Yii::$app->fcmApiV1` when need APIV1 and `Yii::$app->fcmLegacyApi` for legacy one. 125 | 126 | Basic Usage 127 | ----------- 128 | 129 | > **_N.B._** _The main thing about this library is to be used in queues, so we tried to balance between OOP and low amount of objects used._ 130 | 131 | #### APIv1 132 | 133 | _The APIv1 part of extension can send:_ 134 | 1. A message to a [specific token (device)](https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_specific_devices). 135 | 2. A message to a given [topic](https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_topics). 136 | 3. A message to several [topics by condition](https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_topics). 137 | 138 | _A message can contain:_ 139 | 1. Two types of [messages](https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages): [Notification messages](https://firebase.google.com/docs/cloud-messaging/concept-options#notifications) and [Data messages](https://firebase.google.com/docs/cloud-messaging/concept-options#data_messages) (both are optional). 140 | 2. Their [combination](https://firebase.google.com/docs/cloud-messaging/concept-options#notification-messages-with-optional-data-payload). 141 | 3. Target platform specific [configuration](https://firebase.google.com/docs/cloud-messaging/concept-options#customizing_a_message_across_platforms). 142 | 143 | ##### Send push-notification to a single token (device) 144 | You need to have a registration token for the target device. Registration tokens are strings generated by the client FCM SDKs. 145 | Each of the Firebase client SDKs are able to generate these registration tokens: [iOS](https://firebase.google.com/docs/cloud-messaging/ios/client#access_the_registration_token), [Android](https://firebase.google.com/docs/cloud-messaging/android/client#sample-register), [Web](https://firebase.google.com/docs/cloud-messaging/js/client#access_the_registration_token). 146 | In order to sent push to the single token you need to use `setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOKEN, 'your_token')` method with `\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOKEN` constant: 147 | 148 | ```php 149 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 150 | $result = Yii::$app 151 | ->fcm 152 | ->createRequest() 153 | ->setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOKEN, 'your_token') 154 | ->send(); 155 | ``` 156 | 157 | ##### Send push-notification to a topic 158 | Based on the publish/subscribe model, FCM topic messaging allows you to send a message to multiple devices that have opted in to a particular topic. You compose topic messages as needed, and FCM handles routing and delivering the message reliably to the right devices. 159 | 160 | > To (un)subscribe devices to a topic look for `Topic management` section below 161 | 162 | For example, users of a local weather forecasting app could opt in to a “severe weather alerts” topic and receive notifications of storms threatening specified areas. Users of a sports app could subscribe to automatic updates in live game scores for their favorite teams. 163 | 164 | _The main things to keep in mind about topics:_ 165 | - Developers can choose any topic name that matches the regular expression: `[a-zA-Z0-9-_.~%]+`. 166 | - Topic messaging supports unlimited topics and subscriptions for each app. 167 | - Topic messaging is best suited for content such as news, weather, or other publicly available information. 168 | - Topic messages are optimized for throughput rather than latency. For fast, secure delivery to single devices or small groups of devices, target messages to registration tokens, **not topics**. 169 | 170 | In order to sent push to the topic you need to use `setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOPIC, 'your_token')` method with `\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOPIC` constant: 171 | 172 | ```php 173 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 174 | $result = Yii::$app 175 | ->fcm 176 | ->createRequest() 177 | ->setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOPIC, 'some-topic') 178 | ->send(); 179 | ``` 180 | 181 | ##### Send push-notification to a combination of topics by condition 182 | The condition is a boolean expression that specifies the target topics. 183 | For example, the following condition will send messages to devices that are subscribed to `'TopicA'` and either `'TopicB'` or `'TopicC'`: 184 | ```php 185 | "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)" 186 | ``` 187 | FCM first evaluates any conditions in parentheses, and then evaluates the expression from left to right. 188 | In the above expression, a user subscribed to any single topic does not receive the message. 189 | Likewise, a user who does not subscribe to TopicA does not receive the message. 190 | These combinations do receive it: 191 | - `TopicA` and `TopicB` 192 | - `TopicA` and `TopicC` 193 | 194 | In order to sent push to the several topics by condition you need to use `setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOPIC_CONDITION, 'your_token')` method with `\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOPIC_CONDITION` constant: 195 | 196 | ```php 197 | $condition = "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"; 198 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 199 | $result = Yii::$app 200 | ->fcm 201 | ->createRequest() 202 | ->setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOPIC_CONDITION, $condition) 203 | ->send(); 204 | ``` 205 | 206 | ##### Send push-notification with only 'Data' message type. 207 | 208 | ```php 209 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 210 | $result = Yii::$app 211 | ->fcm 212 | ->createRequest() 213 | ->setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOKEN, 'your_token') 214 | ->setData(['a' => '1', 'b' => 'test']) 215 | ->send(); 216 | ``` 217 | 218 | ##### Send push-notification with only 'Notification' message type. 219 | 220 | ```php 221 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 222 | $result = Yii::$app 223 | ->fcm 224 | ->createRequest() 225 | ->setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOKEN, 'your_token') 226 | ->setNotification('Test Title', 'Test Description') 227 | ->send(); 228 | ``` 229 | 230 | 231 | ##### Send push-notification with both 'Notification' and 'Data' message type. 232 | 233 | ```php 234 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 235 | $result = Yii::$app 236 | ->fcm 237 | ->createRequest() 238 | ->setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOKEN, 'your_token') 239 | ->setData(['a' => '1', 'b' => 'test']) 240 | ->setNotification('Test Title', 'Test Description') 241 | ->send(); 242 | ``` 243 | 244 | ##### Send push-notification without 'Notification' and 'Data' message type. 245 | 246 | ```php 247 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 248 | $result = Yii::$app 249 | ->fcm 250 | ->createRequest() 251 | ->setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOKEN, 'your_token') 252 | ->send(); 253 | ``` 254 | 255 | ##### Send push-notification with platform specific configuration. 256 | 257 | ###### Android [config](https://firebase.google.com/docs/cloud-messaging/admin/send-messages#android-specific_fields): 258 | ```php 259 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 260 | $result = Yii::$app 261 | ->fcm 262 | ->createRequest() 263 | ->setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOKEN, 'your_token') 264 | ->setAndroidConfig([ 265 | 'ttl' => '3600s', 266 | 'priority' => 'normal', 267 | 'notification' => [ 268 | 'title' => 'Android Title', 269 | 'body' => 'Android Description.', 270 | 'icon' => 'stock_ticker_update', 271 | 'color' => '#ff0000', 272 | ], 273 | ]) 274 | ->send(); 275 | ``` 276 | 277 | ###### APNs [config](https://firebase.google.com/docs/cloud-messaging/admin/send-messages#apns-specific_fields): 278 | ```php 279 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 280 | $result = Yii::$app 281 | ->fcm 282 | ->createRequest() 283 | ->setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOKEN, 'your_token') 284 | ->setApnsConfig([ 285 | 'headers' => [ 286 | 'apns-priority' => '10', 287 | ], 288 | 'payload' => [ 289 | 'aps' => [ 290 | 'alert' => [ 291 | 'title' => 'iOS Title', 292 | 'body' => 'iOS Description.', 293 | ], 294 | 'badge' => 42, 295 | ], 296 | ], 297 | ]) 298 | ->send(); 299 | ``` 300 | 301 | ###### Web-push [config](https://firebase.google.com/docs/cloud-messaging/admin/send-messages#webpush-specific_fields): 302 | ```php 303 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 304 | $result = Yii::$app 305 | ->fcm 306 | ->createRequest() 307 | ->setTarget(\aksafan\fcm\source\builders\apiV1\MessageOptionsBuilder::TOKEN, 'your_token') 308 | ->setWebPushConfig([ 309 | 'notification' => [ 310 | 'title' => 'Web push Title', 311 | 'body' => 'Web push Description.', 312 | 'icon' => 'https://my-server/icon.png', 313 | ], 314 | ]) 315 | ->send(); 316 | ``` 317 | 318 | ###### All together [config](https://firebase.google.com/docs/cloud-messaging/admin/send-messages#putting_it_all_together): 319 | ```php 320 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 321 | $result = $fcm 322 | ->createRequest() 323 | ->setTarget(MessageOptionsBuilder::TOKEN, $token) 324 | ->setData(['a' => '1', 'b' => 'test']) 325 | ->setNotification('Test Title', 'Test Description') 326 | ->setAndroidConfig([ 327 | 'ttl' => '3600s', 328 | 'priority' => 'normal', 329 | 'notification' => [ 330 | 'title' => 'Android Title', 331 | 'body' => 'Andorid Description.', 332 | 'icon' => 'push_icon', 333 | 'color' => '#ff0000', 334 | ], 335 | ]) 336 | ->setApnsConfig([ 337 | 'headers' => [ 338 | 'apns-priority' => '10', 339 | ], 340 | 'payload' => [ 341 | 'aps' => [ 342 | 'alert' => [ 343 | 'title' => 'iOS Title', 344 | 'body' => 'iOS Description.', 345 | ], 346 | 'badge' => 42, 347 | ], 348 | ], 349 | ]) 350 | ->setWebPushConfig([ 351 | 'notification' => [ 352 | 'title' => 'Web push Title', 353 | 'body' => 'Web push Description.', 354 | 'icon' => 'https://my-server/icon.png', 355 | ], 356 | ]) 357 | ->send(); 358 | ``` 359 | 360 | > **_N.B._** _Pay attention that platform specific config will replace the general one._ 361 | 362 | In example above Android client will receive this notification info: 363 | ```php 364 | 'title' => 'Android Title', 365 | 'body' => 'Android Description.' 366 | ``` 367 | and **NOT** this: 368 | ```php 369 | 'title' => 'Test Title', 370 | 'body' => 'Test Description.' 371 | ``` 372 | 373 | ##### Handling response. 374 | 375 | After sending the request to FCM you will get an instance of `\aksafan\fcm\source\responses\apiV1\TokenResponse`. 376 | ```php 377 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 378 | $result = $fcm 379 | ->createRequest() 380 | ->setTarget(MessageOptionsBuilder::TOKEN, $token) 381 | ->setData(['a' => '1', 'b' => 'test']) 382 | ->setNotification('Test Title', 'Test Description') 383 | ->send(); 384 | 385 | if ($result->isResultOk()) { 386 | echo $result->getRawMessageId(); 387 | echo $result->getMessageId(); 388 | } else { 389 | $tokensToDelete = $result->getTokensToDelete(); 390 | $errorDetails = $result->getErrorDetails(); 391 | echo $result->getErrorStatusDescription(); 392 | } 393 | ``` 394 | If the result is OK you can get raw message from FCM in format - `projects/your_project_id/messages/message_id`: 395 | ```php 396 | $result->getRawMessageId(); 397 | ``` 398 | or only the message ID in format - `message_id`: 399 | ```php 400 | $result->getMessageId(); 401 | ``` 402 | If something has happened, you can get error description with the information about the problem: 403 | ```php 404 | $result->getErrorStatusDescription(); 405 | ``` 406 | Also tokens, that should be deleted from your DB, if the problem was with invalid tokens: 407 | ```php 408 | $result->getTokensToDelete(); 409 | ``` 410 | As well as the technical information from FCM: 411 | ```php 412 | $result->getError(); 413 | $result->getErrorStatus(); 414 | $result->getErrorCode(); 415 | $result->getErrorMessage(); 416 | $result->getErrorDetails(); 417 | ``` 418 | 419 | ##### Validating messages ([dry run mode](https://firebase.google.com/docs/cloud-messaging/admin/send-messages#sending_in_the_dry_run_mode)). 420 | 421 | You can validate a message by sending a validation-only request to the Firebase REST API. 422 | ```php 423 | /** @var \aksafan\fcm\source\responses\apiV1\TokenResponse $result */ 424 | $result = $fcm 425 | ->createRequest() 426 | ->setTarget(MessageOptionsBuilder::TOKEN, $token) 427 | ->setData(['a' => '1', 'b' => 'test']) 428 | ->setNotification('Test Title', 'Test Description') 429 | ->validateOnly() 430 | ->send(); 431 | 432 | if ($result->isResultOk()) { 433 | echo $result->getRawMessageId(); 434 | } else { 435 | echo $result->getErrorStatusDescription(); 436 | } 437 | ``` 438 | If the message is invalid, you will receive info in error description: 439 | ```php 440 | $result->getErrorStatusDescription() 441 | ``` 442 | If it is valid, you will get fake_message - `projects/your_project_id/messages/fake_message_id`: 443 | ```php 444 | $result->getMessageId(); 445 | ``` 446 | 447 | #### Legacy API 448 | 449 | _The Legacy API part of extension can send:_ 450 | 1. A message to a [specific token (device)](https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_specific_devices_2). 451 | 2. A message to a several tokens (devices). 452 | 3. A message to [device group](https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_device_groups). 453 | 4. A message to a given [topic](https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_topics_2). 454 | 5. A message to several [topics by condition](https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_topics_2). 455 | 456 | _A message can contain:_ 457 | 1. Two types of [messages](https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages): [Notification messages](https://firebase.google.com/docs/cloud-messaging/concept-options#notifications) and [Data messages](https://firebase.google.com/docs/cloud-messaging/concept-options#data_messages) (both are optional). 458 | 2. Their [combination](https://firebase.google.com/docs/cloud-messaging/concept-options#notification-messages-with-optional-data-payload). 459 | 3. Target platform specific [configuration](https://firebase.google.com/docs/cloud-messaging/concept-options#customizing_a_message_across_platforms). 460 | 461 | > Note: Legacy HTTP Server Protocol is still under support, is used by many people and is not in trouble of being deprecated, but Google aimed us to use HTTP v1 API. 462 | 463 | ##### Send push-notification to a single token (device) 464 | You need to have a registration token for the target device. Registration tokens are strings generated by the client FCM SDKs. 465 | Each of the Firebase client SDKs are able to generate these registration tokens: [iOS](https://firebase.google.com/docs/cloud-messaging/ios/client#access_the_registration_token), [Android](https://firebase.google.com/docs/cloud-messaging/android/client#sample-register), [Web](https://firebase.google.com/docs/cloud-messaging/js/client#access_the_registration_token). 466 | 467 | In order to sent push to the single token you need to use `setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, 'your_token')` method with `\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN` constant: 468 | 469 | ```php 470 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 471 | $result = $fcm 472 | ->createRequest() 473 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, $token) 474 | ->setData(['a' => '1', 'b' => '2']) 475 | ->setNotification('Send push-notification to a single token (device)', 'Test description') 476 | ->send(); 477 | ``` 478 | 479 | ##### Send push-notification to multiple tokens (devices) 480 | Max amount of tokens in one request is 1000. 481 | In order to sent push to the single token you need to use `setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKENS, ['your_token_1', 'your_token_2', 'your_token_3'])` method with `\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKENS` constant: 482 | 483 | ```php 484 | $tokens = [ 485 | 'your_token_1', 486 | 'your_token_2', 487 | 'your_token_3', 488 | ]; 489 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 490 | $result = $fcm 491 | ->createRequest() 492 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKENS, $tokens) 493 | ->setData(['a' => '1', 'b' => '2']) 494 | ->setNotification('Send push-notification to multiple tokens (devices)', 'Test description') 495 | ->send(); 496 | ``` 497 | 498 | ##### Send push-notification to a topic 499 | Based on the publish/subscribe model, FCM topic messaging allows you to send a message to multiple devices that have opted in to a particular topic. You compose topic messages as needed, and FCM handles routing and delivering the message reliably to the right devices. 500 | 501 | For example, users of a local weather forecasting app could opt in to a “severe weather alerts” topic and receive notifications of storms threatening specified areas. Users of a sports app could subscribe to automatic updates in live game scores for their favorite teams. 502 | 503 | _The main things to keep in mind about topics:_ 504 | - Developers can choose any topic name that matches the regular expression: `[a-zA-Z0-9-_.~%]+`. 505 | - Topic messaging supports unlimited topics and subscriptions for each app. 506 | - Topic messaging is best suited for content such as news, weather, or other publicly available information. 507 | - Topic messages are optimized for throughput rather than latency. For fast, secure delivery to single devices or small groups of devices, target messages to registration tokens, **not topics**. 508 | 509 | In order to sent push to the topic you need to use `setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOPIC, 'your_token')` method with `\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOPIC` constant 510 | and `createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_SENDING)` with `\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_SENDING` constant: 511 | 512 | ```php 513 | /** @var \aksafan\fcm\source\responses\legacyApi\TopicResponse $result */ 514 | $result = $fcm 515 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_SENDING) 516 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOPIC, 'a-topic') 517 | ->setNotification('Test title', 'Test description') 518 | ->send(); 519 | ``` 520 | 521 | ##### Send push-notification to a combination of topics by condition 522 | The condition is a boolean expression that specifies the target topics. 523 | For example, the following condition will send messages to devices that are subscribed to `'TopicA'` and either `'TopicB'` or `'TopicC'`: 524 | ```php 525 | "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)" 526 | ``` 527 | FCM first evaluates any conditions in parentheses, and then evaluates the expression from left to right. 528 | In the above expression, a user subscribed to any single topic does not receive the message. 529 | Likewise, a user who does not subscribe to TopicA does not receive the message. 530 | These combinations do receive it: 531 | - `TopicA` and `TopicB` 532 | - `TopicA` and `TopicC` 533 | 534 | In order to sent push to the several topics by condition you need to use `setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOPIC_CONDITION, 'your_token')` method with `\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOPIC_CONDITION` constant: 535 | and `createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_SENDING)` with `\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_SENDING` constant: 536 | 537 | ```php 538 | $condition = "'a-topic' in topics && ('b-topic' in topics || 'b-topic' in topics)"; 539 | /** @var \aksafan\fcm\source\responses\legacyApi\TopicResponse $result */ 540 | $result = $fcm 541 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_SENDING) 542 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOPIC_CONDITION, $condition) 543 | ->setNotification('Test title', 'Test description') 544 | ->send(); 545 | ``` 546 | 547 | ##### Send push-notification to a group of tokens (devices) 548 | FCM device groups allows you to send a message to multiple devices that have opted in to a particular group. You compose group of devices as needed, and FCM handles routing and delivering the message reliably to the right devices. 549 | 550 | In order to sent push to the topic you need to use `setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::GROUP, 'your_notification_key')` method with `\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::GROUP` constant: 551 | and `createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_SENDING)` with `\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_SENDING` constant: 552 | ```php 553 | $notificationKey = 'your_notification_key'; 554 | /** @var \aksafan\fcm\source\responses\legacyApi\GroupResponse $result */ 555 | $result = $fcm 556 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_SENDING) 557 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::GROUP, $notificationKey) 558 | ->setData(['a' => '1', 'b' => '2']) 559 | ->setNotification('Test title', 'Test description') 560 | ->send(); 561 | ``` 562 | 563 | ##### Send push-notification with only 'Data' message type. 564 | 565 | ```php 566 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 567 | $result = $fcm 568 | ->createRequest() 569 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, $token) 570 | ->setData(['a' => '1', 'b' => 'test']) 571 | ->send(); 572 | ``` 573 | 574 | ##### Send push-notification with only 'Notification' message type. 575 | 576 | ```php 577 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 578 | $result = $fcm 579 | ->createRequest() 580 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, $token) 581 | ->setNotification('Test title', 'Test Description') 582 | ->send(); 583 | ``` 584 | 585 | ##### Send push-notification with both 'Notification' and 'Data' message type. 586 | 587 | ```php 588 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 589 | $result = $fcm 590 | ->createRequest() 591 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, $token) 592 | ->setData(['a' => '1', 'b' => 'test']) 593 | ->setNotification('Send push-notification with both \'Notification\' and \'Data\' message type.', 'Test Description') 594 | ->send(); 595 | ``` 596 | 597 | ##### Send push-notification without 'Notification' and 'Data' message type. 598 | 599 | ```php 600 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 601 | $result = Yii::$app 602 | ->fcm 603 | ->createRequest() 604 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, 'your_token') 605 | ->send(); 606 | ``` 607 | 608 | ##### Send push-notification with platform specific configuration. 609 | 610 | ###### Android [config](https://firebase.google.com/docs/cloud-messaging/http-server-ref#table2b): 611 | ```php 612 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 613 | $result = $fcm 614 | ->createRequest() 615 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, $token) 616 | ->setAndroidConfig([ 617 | 'title' => 'Android Title', 618 | 'body' => 'Android Description.', 619 | 'icon' => 'stock_ticker_update', 620 | 'color' => '#ff0000', 621 | ]) 622 | ->setPriority(\aksafan\fcm\source\helpers\OptionsHelper::HIGH) 623 | ->send(); 624 | ``` 625 | 626 | ###### APNs [config](https://firebase.google.com/docs/cloud-messaging/http-server-ref#table2a): 627 | ```php 628 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 629 | $result = $fcm 630 | ->createRequest() 631 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, $token) 632 | ->setApnsConfig([ 633 | 'title' => 'iOS Title', 634 | 'body' => 'iOS Description.', 635 | 'title_loc_key' => 'iOS Title loc key.', 636 | 'badge' => '42', 637 | 'sound' => 'bingbong.aiff', 638 | ]) 639 | ->send(); 640 | ``` 641 | 642 | ###### Web-push [config](https://firebase.google.com/docs/cloud-messaging/http-server-ref#table2c): 643 | ```php 644 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 645 | $result = $fcm 646 | ->createRequest() 647 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, $token) 648 | ->setWebPushConfig([ 649 | 'title' => 'Web push Title', 650 | 'body' => 'Web push Description.', 651 | 'icon' => 'https://my-server/icon.png', 652 | 'click_action' => 'click-action', 653 | ]) 654 | ->send(); 655 | ``` 656 | 657 | ###### All together [config](https://firebase.google.com/docs/cloud-messaging/http-server-ref#notification-payload-support). 658 | You can set additional configurations with these methods: 659 | ```php 660 | ->setCollapseKey(') 661 | ->setPriority() 662 | ->setContentAvailable() 663 | ->setMutableContent() 664 | ->setTimeToLive() 665 | ->setRestrictedPackageName() 666 | ->validateOnly() 667 | ``` 668 | All together can be: 669 | ```php 670 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 671 | $result = $fcm 672 | ->createRequest() 673 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, $token) 674 | ->setData(['a' => '1', 'b' => '2']) 675 | ->setNotification('Test title', 'Test description') 676 | ->setAndroidConfig([ 677 | 'title' => 'Android Title', 678 | 'body' => 'Android Description.', 679 | 'icon' => 'stock_ticker_update', 680 | 'color' => '#ff0000', 681 | ]) 682 | ->setApnsConfig([ 683 | 'title' => 'iOS Title', 684 | 'body' => 'iOS Description.', 685 | 'title_loc_key' => 'iOS Title loc key.', 686 | 'badge' => '42', 687 | 'sound' => 'bingbong.aiff', 688 | ]) 689 | ->setWebPushConfig([ 690 | 'title' => 'Web push Title', 691 | 'body' => 'Web push Description.', 692 | 'icon' => 'https://my-server/icon.png', 693 | 'click_action' => 'click-action', 694 | ]) 695 | ->setCollapseKey('collapse_key') 696 | ->setPriority(\aksafan\fcm\source\helpers\OptionsHelper::NORMAL) 697 | ->setContentAvailable(true) 698 | ->setMutableContent(false) 699 | ->setTimeToLive(300) 700 | ->setRestrictedPackageName('restricted_package_mame') 701 | ->validateOnly(false) 702 | ->send(); 703 | ``` 704 | 705 | > **_N.B._** _Pay attention that platform specific configuration will replace the general one and repeating from other platform configurations (shortage in legacy API version), one by one in order:_ 706 | ```$xslt 707 | GeneralNotificationConfig 708 | -> 709 | AndroidConfig 710 | -> 711 | ApnsConfig 712 | -> 713 | WebPushConfig 714 | ``` 715 | In example above any client will receive this notification info: 716 | ```php 717 | 'title' => 'Web push Title', 718 | 'body' => 'Web push Description.', 719 | 'icon' => 'https://my-server/icon.png', 720 | 'color' => '#ff0000', 721 | 'title_loc_key' => 'iOS Title loc key.', 722 | 'badge' => '42', 723 | 'sound' => 'bingbong.aiff', 724 | 'click_action' => 'click-action', 725 | ``` 726 | and **NOT** all mentioned. 727 | 728 | ##### Handling response. 729 | 730 | After sending the request to FCM you will get an instance of these: 731 | - `\aksafan\fcm\source\responses\legacyApi\TokenResponse` 732 | - `\aksafan\fcm\source\responses\legacyApi\TopicResponse` 733 | - `\aksafan\fcm\source\responses\legacyApi\GroupResponse` 734 | 735 | ###### For sending push-messages to single (multiply) token(s): 736 | ```php 737 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 738 | $result = $fcm 739 | ->createRequest() 740 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, $token) 741 | ->setData(['a' => '1', 'b' => '2']) 742 | ->setNotification('Test title', 'Test description') 743 | ->send(); 744 | 745 | if ($result->isResultOk()) { 746 | echo 'MessageId '.$result->getMessageId(); 747 | echo 'NumberSuccess '.$result->getNumberSuccess(); 748 | echo 'NumberFailure '.$result->getNumberFailure(); 749 | echo 'NumberModification '.$result->getNumberModification(); 750 | } else { 751 | echo 'numberSuccess '.$result->getNumberSuccess(); 752 | echo 'numberFailure '.$result->getNumberFailure(); 753 | echo 'numberModification '.$result->getNumberModification(); 754 | echo 'TokensToDelete '.$result->getTokensToDelete(); 755 | echo 'TokensToModify '.$result->getTokensToModify(); 756 | echo 'TokensToRetry '.$result->getTokensToRetry(); 757 | echo 'RetryAfter '.$result->getRetryAfter(); 758 | echo 'TokensWithError '.$result->getTokensWithError(); 759 | echo 'ErrorStatusDescription '.$result->getErrorStatusDescription(); 760 | } 761 | ``` 762 | If the result is OK you can get the message ID in format - `message_id`: 763 | ```php 764 | $result->getMessageId(); 765 | ``` 766 | You can see the number of successfully sent messages: 767 | ```php 768 | $result->getNumberSuccess(); 769 | ``` 770 | The number of failed attempts: 771 | ```php 772 | $result->getNumberFailure(); 773 | ``` 774 | The number of device that you need to modify their token: 775 | ```php 776 | $result->getNumberModification(); 777 | ``` 778 | If something has happened, you can get error description with the information about the problem: 779 | ```php 780 | $result->getErrorStatusDescription(); 781 | ``` 782 | > List of common errors and info about handling them for legacy API is [here](https://firebase.google.com/docs/cloud-messaging/http-server-ref#table9) 783 | 784 | Also tokens, that should be deleted from your DB as invalid ones: 785 | ```php 786 | $result->getTokensToDelete(); 787 | ``` 788 | Tokens, that should be changed in your storage - `['oldToken' => 'newToken']`: 789 | ```php 790 | $result->getTokensToModify(); 791 | ``` 792 | Tokens, that should be resend. You should to use [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) to retry sending: 793 | ```php 794 | $result->getTokensToRetry(); 795 | ``` 796 | Time to retry after if it was in response headers: 797 | ```php 798 | $result->getRetryAfter(); 799 | ``` 800 | Tokens, that have errors. You should check these tokens in order to delete broken ones: 801 | ```php 802 | $result->getTokensWithError(); 803 | ``` 804 | 805 | ###### For sending push-messages to topic or topics by condition: 806 | ```php 807 | /** @var \aksafan\fcm\source\responses\legacyApi\TopicResponse $result */ 808 | $result = $fcm 809 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_SENDING) 810 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOPIC, 'a-topic') 811 | ->setNotification('Test title', 'Test description') 812 | ->send(); 813 | 814 | if ($result->isResultOk()) { 815 | echo 'MessageId '.$result->getMessageId(); 816 | } else { 817 | echo 'ErrorMessage '.$result->getErrorMessage(); 818 | echo 'ErrorStatusDescription '.$result->getErrorStatusDescription(); 819 | } 820 | ``` 821 | If the result is OK you can get the message ID in format - `message_id`: 822 | ```php 823 | $result->getMessageId(); 824 | ``` 825 | If something has happened, you can get error description with the information about the problem: 826 | ```php 827 | $result->getErrorStatusDescription(); 828 | ``` 829 | And error message: 830 | ```php 831 | $result->getErrorMessage(); 832 | ``` 833 | 834 | 835 | ###### For sending push-messages to device group: 836 | ```php 837 | $notificationKey = 'your_notification_key'; 838 | /** @var \aksafan\fcm\source\responses\legacyApi\GroupResponse $result */ 839 | $result = $fcm 840 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_SENDING) 841 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::GROUP, $notificationKey) 842 | ->setData(['a' => '1', 'b' => '2']) 843 | ->setNotification('Test title', 'Test description') 844 | ->send(); 845 | 846 | if ($result->isResultOk()) { 847 | echo 'numberSuccess ' . $result->getNumberSuccess(); 848 | echo 'numberFailure ' . $result->getNumberFailure(); 849 | } else { 850 | echo 'numberSuccess ' . $result->getNumberSuccess(); 851 | echo 'numberFailure ' . $result->getNumberFailure(); 852 | echo 'tokensFailed ' . print_r($tokensFailed = $result->getTokensFailed()); 853 | echo 'ErrorStatusDescription '.$result->getErrorStatusDescription(); 854 | } 855 | ``` 856 | If the result is OK you can get the number of tokens successfully sent messages to: 857 | ```php 858 | $result->getNumberSuccess(); 859 | ``` 860 | The number of tokens unsuccessfully sent messages to: 861 | ```php 862 | $result->getNumberFailure(); 863 | ``` 864 | > If the server attempts to send a message to a device group that has no members, the response will be with 0 success and 0 failure. 865 | 866 | If something has happened, you can get error description with the information about the problem: 867 | ```php 868 | $result->getErrorStatusDescription(); 869 | ``` 870 | And an array of tokens failed: 871 | ```php 872 | $result->getTokensFailed(); 873 | ``` 874 | > When a message fails to be delivered to one or more of the registration tokens associated with a notification_key, the app server should retry with backoff between retries. 875 | 876 | 877 | ##### Validating messages ([dry run mode](https://firebase.google.com/docs/cloud-messaging/admin/send-messages#sending_in_the_dry_run_mode)). 878 | 879 | You can validate a message by sending a validation-only request to the Firebase REST API. 880 | ```php 881 | /** @var \aksafan\fcm\source\responses\legacyApi\TokenResponse $result */ 882 | $result = $fcm 883 | ->createRequest() 884 | ->setTarget(\aksafan\fcm\source\builders\legacyApi\MessageOptionsBuilder::TOKEN, $token) 885 | ->setData(['a' => '1', 'b' => 'test']) 886 | ->setNotification('Validating messages', 'Test Description') 887 | ->validateOnly() 888 | ->send(); 889 | 890 | if ($result->isResultOk()) { 891 | echo $result->getMessageId(); 892 | } else { 893 | echo $result->getErrorStatusDescription(); 894 | } 895 | ``` 896 | If the message is invalid, you will receive info in error description: 897 | ```php 898 | $result->getErrorStatusDescription() 899 | ``` 900 | If it is valid, you will get fake_message - `-1`: 901 | ```php 902 | $result->getMessageId(); 903 | ``` 904 | 905 | ##### Device group [management](https://firebase.google.com/docs/cloud-messaging/android/device-group#managing_device_groups). 906 | 907 | There are three main definitions here: 908 | - `groupName` - is a name or identifier (e.g., it can be a username) that is unique to a given group; 909 | - `notificationKey` - identifies the device group by mapping a particular group (typically a user) to all of the group's associated registration tokens; 910 | - `tokens` - an array of registration tokens for each device you want to add to the group. 911 | 912 | > The `groupName` and `notificationKey` are unique to a group of registration tokens. It is important that `groupName` is unique per client app if you have multiple client apps for the same sender ID. This ensures that messages only go to the intended target app. 913 | 914 | > Optionally, Android client apps can manage device groups from the client side. 915 | 916 | ###### Create group: 917 | To create a device group, you need to use `createGroup(string $groupName, array $tokens)` method with a name for the group and a list of registration tokens for the devices. 918 | Also creating request with `\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_MANAGEMENT` argument. 919 | ```php 920 | $groupName = 'test-group'; 921 | $tokens = [ 922 | 'your_token_1', 923 | 'your_token_2', 924 | 'your_token_3', 925 | 'your_token_4', 926 | 'your_token_5', 927 | ]; 928 | /** @var \aksafan\fcm\source\responses\legacyApi\GroupManagementResponse $result */ 929 | $result = $fcm 930 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_MANAGEMENT) 931 | ->createGroup($groupName, $tokens) 932 | ->send(); 933 | if ($result->isResultOk()) { 934 | echo 'NotificationKey '.$notificationKey = $result->getNotificationKey(); 935 | } else { 936 | echo 'getErrorStatusDescription '. $result->getErrorStatusDescription(); 937 | } 938 | ``` 939 | After sending the request to FCM you will get an instance of `\aksafan\fcm\source\responses\legacyApi\GroupManagementResponse`. 940 | 941 | FCM returns a new `notificationKey` that represents the device group. Save it and the corresponding `groupName` to use in subsequent operations. 942 | 943 | If there was an error, you will receive info in error description: 944 | ```php 945 | $result->getErrorStatusDescription() 946 | ``` 947 | > **_N.B._** _You can add to group up to 1,000 devices in a single request. If you provide an array with over 1,000 registration tokens, the request will fail with an `InvalidArgumentException`._ 948 | 949 | ###### Retrieve `notificationKey` from a group: 950 | If you need to retrieve an existing `notificationKey`, you need to use `getNotificationKey(string $groupName)` method with a name for the group. 951 | Also creating request with `\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_MANAGEMENT` argument. 952 | ```php 953 | $groupName = 'test-group'; 954 | /** @var \aksafan\fcm\source\responses\legacyApi\GroupManagementResponse $result */ 955 | $result = $fcm 956 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_MANAGEMENT) 957 | ->getNotificationKey($groupName) 958 | ->sendGET(); 959 | if ($result->isResultOk()) { 960 | echo 'NotificationKey '.$notificationKey = $result->getNotificationKey(); 961 | echo '
'; 962 | } else { 963 | echo 'getErrorStatusDescription '. $result->getErrorStatusDescription(); 964 | echo '
'; 965 | } 966 | ``` 967 | After sending the request to FCM you will get an instance of `\aksafan\fcm\source\responses\legacyApi\GroupManagementResponse`. 968 | 969 | FCM returns the `notificationKey` that represents the device group. 970 | 971 | If there was an error, you will receive info in error description: 972 | ```php 973 | $result->getErrorStatusDescription() 974 | ``` 975 | 976 | > __Note__: Notification_key_name is not required for adding/removing registration tokens, but including it protects you against accidentally using the incorrect notification_key. In order to be safe, current extension make you use `groupName` always. 977 | 978 | ###### Add token(s) to group: 979 | 980 | You can add tokens to device group passing registration tokens to the `addToGroup(string $groupName, string $notificationKey, array $tokens)` method and creating request with `\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_MANAGEMENT` argument: 981 | ```php 982 | $groupName = 'test-group'; 983 | $tokens = [ 984 | 'your_token_6', 985 | 'your_token_7', 986 | 'your_token_8', 987 | 'your_token_9', 988 | 'your_token_10', 989 | ]; 990 | /** @var \aksafan\fcm\source\responses\legacyApi\GroupManagementResponse $result */ 991 | $result = $fcm 992 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_MANAGEMENT) 993 | ->addToGroup($groupName, $notificationKey, $tokens) 994 | ->send(); 995 | if ($result->isResultOk()) { 996 | echo 'NotificationKey '.$result->getNotificationKey(); 997 | } else { 998 | echo 'getErrorStatusDescription '. $result->getErrorStatusDescription(); 999 | } 1000 | ``` 1001 | After sending the request to FCM you will get an instance of `\aksafan\fcm\source\responses\legacyApi\GroupManagementResponse`. 1002 | 1003 | FCM returns a new `notificationKey` that represents the device group. 1004 | 1005 | If there was an error, you will receive info in error description: 1006 | ```php 1007 | $result->getErrorStatusDescription() 1008 | ``` 1009 | 1010 | ###### Remove token(s) from group: 1011 | 1012 | You can add tokens to device group passing registration tokens to the `removeFromGroup(string $groupName, string $notificationKey, array $tokens)` method and creating request with `\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_MANAGEMENT` argument: 1013 | ```php 1014 | $groupName = 'test-group'; 1015 | $tokens = [ 1016 | 'your_token_6', 1017 | 'your_token_7', 1018 | ]; 1019 | /** @var \aksafan\fcm\source\responses\legacyApi\GroupManagementResponse $result */ 1020 | $result = $fcm 1021 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_GROUP_MANAGEMENT) 1022 | ->removeFromGroup($groupName, $notificationKey, $tokens) 1023 | ->send(); 1024 | if ($result->isResultOk()) { 1025 | echo 'NotificationKey '.$result->getNotificationKey(); 1026 | echo '
'; 1027 | } else { 1028 | echo 'getErrorStatusDescription '. $result->getErrorStatusDescription(); 1029 | echo '
'; 1030 | } 1031 | ``` 1032 | After sending the request to FCM you will get an instance of `\aksafan\fcm\source\responses\legacyApi\GroupManagementResponse`. 1033 | 1034 | FCM returns a new `notificationKey` that represents the device group. 1035 | 1036 | If there was an error, you will receive info in error description: 1037 | ```php 1038 | $result->getErrorStatusDescription() 1039 | ``` 1040 | 1041 | > If you remove all existing registration tokens from a device group, FCM deletes the device group. 1042 | 1043 | 1044 | #### Topic [management](https://firebase.google.com/docs/cloud-messaging/admin/manage-topic-subscriptions). 1045 | 1046 | > __Note__: You can (un)subscribe to (from) topic through both API versions, just chose your favorite and [configure](#Configuration) it properly. 1047 | 1048 | ##### [Subscribe](https://firebase.google.com/docs/cloud-messaging/admin/manage-topic-subscriptions#subscribe_to_a_topic) to a topic: 1049 | 1050 | You can subscribe one or multiple devices to a topic by passing registration tokens to the `subscribeToTopic(string $topic, array $tokens)` method and creating request with `\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_MANAGEMENT` argument: 1051 | ```php 1052 | $topic = 'test-topic'; 1053 | $tokens = [ 1054 | 'your_token_1', 1055 | 'your_token_2', 1056 | 'your_token_3', 1057 | 'your_token_4', 1058 | 'your_token_5', 1059 | ]; 1060 | /** @var \aksafan\fcm\source\responses\TopicSubscribeResponse $result */ 1061 | $result = $fcm 1062 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_MANAGEMENT) 1063 | ->subscribeToTopic($topic, $tokens) 1064 | ->send(); 1065 | ``` 1066 | 1067 | > **_N.B._** _You can subscribe up to 1,000 devices in a single request. If you provide an array with over 1,000 registration tokens, the request will fail with an `InvalidArgumentException`._ 1068 | 1069 | After sending the request to FCM you will get an instance of `\aksafan\fcm\source\responses\TopicSubscribeResponse`. 1070 | Now you can grab tokens with [errors](https://firebase.google.com/docs/cloud-messaging/admin/errors): 1071 | ```php 1072 | $tokensWithError = $result->getTopicTokensWithError(); 1073 | ``` 1074 | This is an array of tokens with their correspondents errors in format: 1075 | ```php 1076 | $tokensWithError = [ 1077 | [ 1078 | 'token' => 'your_token_2', 1079 | 'error' => 'INVALID_ARGUMENT', 1080 | ], 1081 | [ 1082 | 'token' => 'your_token_4', 1083 | 'error' => 'INVALID_ARGUMENT', 1084 | ], 1085 | [ 1086 | 'token' => 'your_token_5', 1087 | 'error' => 'INVALID_ARGUMENT', 1088 | ], 1089 | ]; 1090 | ``` 1091 | All other tokens were subscribed correctly. 1092 | 1093 | ##### [Unsubscribe](https://firebase.google.com/docs/cloud-messaging/admin/manage-topic-subscriptions#unsubscribe_from_a_topic) from a topic: 1094 | 1095 | You can unubscribe one or multiple devices to a topic by passing registration tokens to the `unsubscribeFromTopic(string $topic, array $tokens)` method and creating request with `\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_MANAGEMENT` argument: 1096 | ```php 1097 | $topic = 'test-topic'; 1098 | $tokens = [ 1099 | 'your_token_1', 1100 | 'your_token_2', 1101 | 'your_token_3', 1102 | 'your_token_4', 1103 | 'your_token_5', 1104 | ]; 1105 | /** @var \aksafan\fcm\source\responses\TopicSubscribeResponse $result */ 1106 | $result = $fcm 1107 | ->createRequest(\aksafan\fcm\source\builders\StaticBuilderFactory::FOR_TOPIC_MANAGEMENT) 1108 | ->unsubscribeFromTopic($topic, $tokens) 1109 | ->send(); 1110 | ``` 1111 | 1112 | > **_N.B._** _You can unsubscribe up to 1,000 devices in a single request. If you provide an array with over 1,000 registration tokens, the request will fail with a messaging/invalid-argument error._ 1113 | 1114 | After sending the request to FCM you will get an instance of `\aksafan\fcm\source\responses\TopicSubscribeResponse`. 1115 | Now you can grab tokens with [errors](https://firebase.google.com/docs/cloud-messaging/admin/errors): 1116 | ```php 1117 | $tokensWithError = $result->getTopicTokensWithError(); 1118 | ``` 1119 | This is an array of tokens with their correspondents errors in format: 1120 | ```php 1121 | $tokensWithError = [ 1122 | [ 1123 | 'token' => 'your_token_2', 1124 | 'error' => 'INVALID_ARGUMENT', 1125 | ], 1126 | [ 1127 | 'token' => 'your_token_4', 1128 | 'error' => 'INVALID_ARGUMENT', 1129 | ], 1130 | [ 1131 | 'token' => 'your_token_5', 1132 | 'error' => 'INVALID_ARGUMENT', 1133 | ], 1134 | ]; 1135 | ``` 1136 | All other tokens were unsubscribed correctly. 1137 | 1138 | #### Logging 1139 | 1140 | This extension uses native Yii2 error logger through `\Yii::error()`. 1141 | List of used error categories can be found here `aksafan\fcm\source\helpers\ErrorsHelper::LOGS_ERRORS`. 1142 | 1143 | 1144 | License 1145 | ------- 1146 | 1147 | Copyright 2018 by Anton Khainak. 1148 | 1149 | Available under the MIT license. 1150 | 1151 | Complete FCM documentation can be found here [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/). 1152 | --------------------------------------------------------------------------------