├── 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 | [](https://packagist.org/packages/aksafan/yii2-fcm-both-api)
6 | [](https://packagist.org/packages/aksafan/yii2-fcm-both-api)
7 | [](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 |
--------------------------------------------------------------------------------