Completed
Branch 2.x (d034bb)
by Julián
11:57
created

FcmAdapter::getPushClient()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 0
1
<?php
2
3
/*
4
 * Unified push notification services abstraction (http://github.com/juliangut/tify).
5
 *
6
 * @license BSD-3-Clause
7
 * @link https://github.com/juliangut/tify
8
 * @author Julián Gutiérrez <[email protected]>
9
 */
10
11
namespace Jgut\Tify\Adapter;
12
13
use Jgut\Tify\Adapter\Fcm\DefaultFactory;
14
use Jgut\Tify\Adapter\Fcm\Factory;
15
use Jgut\Tify\Adapter\Traits\ParameterTrait;
16
use Jgut\Tify\Adapter\Traits\SandboxTrait;
17
use Jgut\Tify\Notification;
18
use Jgut\Tify\Receiver\FcmReceiver;
19
use Jgut\Tify\Result;
20
use ZendService\Google\Exception\RuntimeException as GcmRuntimeException;
21
22
/**
23
 * FCM service adapter.
24
 *
25
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
26
 */
27
class FcmAdapter implements PushAdapter
28
{
29
    use ParameterTrait;
30
    use SandboxTrait;
31
32
    const PARAMETER_API_KEY = 'api_key';
33
34
    const RESPONSE_OK = 'OK';
35
    const RESPONSE_MISSING_REGISTRATION = 'MissingRegistration';
36
    const RESPONSE_INVALID_REGISTRATION = 'InvalidRegistration';
37
    const RESPONSE_NOT_REGISTERED = 'NotRegistered';
38
    const RESPONSE_INVALID_PACKAGE_NAME = 'InvalidPackageName';
39
    const RESPONSE_MISMATCH_SENDER_ID = 'MismatchSenderId';
40
    const RESPONSE_MESSAGE_TOO_BIG = 'MessageTooBig';
41
    const RESPONSE_INVALID_DATA_KEY = 'InvalidDataKey';
42
    const RESPONSE_INVALID_TTL = 'InvalidTtl';
43
    const RESPONSE_TIMEOUT = 'Timeout';
44
    const RESPONSE_INTERNAL_SERVER_ERROR = 'InternalServerError';
45
    const RESPONSE_DEVICE_MESSAGE_RATE_EXCEEDED = 'DeviceMessageRateExceeded';
46
    const RESPONSE_TOPIC_MESSAGE_RATE_EXCEEDED = 'TopicsMessageRateExceeded';
47
    const RESPONSE_SERVER_UNAVAILABLE = 'ServerUnavailable';
48
    const RESPONSE_AUTHENTICATION_ERROR = 'AuthenticationError';
49
    const RESPONSE_INVALID_MESSAGE = 'InvalidMessage';
50
    const RESPONSE_BADLY_FORMATTED_RESPONSE = 'BadlyFormattedResponse';
51
    const RESPONSE_UNKNOWN_ERROR = 'Unknown';
52
53
    /**
54
     * Status codes mapping.
55
     *
56
     * @var array
57
     */
58
    protected static $statusCodeMap = [
59
        self::RESPONSE_OK => Result::STATUS_SUCCESS,
60
61
        self::RESPONSE_MISSING_REGISTRATION => Result::STATUS_INVALID_MESSAGE,
62
        self::RESPONSE_INVALID_PACKAGE_NAME => Result::STATUS_INVALID_MESSAGE,
63
        self::RESPONSE_MISMATCH_SENDER_ID => Result::STATUS_INVALID_MESSAGE,
64
        self::RESPONSE_MESSAGE_TOO_BIG => Result::STATUS_INVALID_MESSAGE,
65
        self::RESPONSE_INVALID_DATA_KEY => Result::STATUS_INVALID_MESSAGE,
66
        self::RESPONSE_INVALID_TTL => Result::STATUS_INVALID_MESSAGE,
67
        self::RESPONSE_INVALID_MESSAGE => Result::STATUS_INVALID_MESSAGE,
68
69
        self::RESPONSE_INVALID_REGISTRATION => Result::STATUS_INVALID_DEVICE,
70
        self::RESPONSE_NOT_REGISTERED => Result::STATUS_INVALID_DEVICE,
71
72
        self::RESPONSE_DEVICE_MESSAGE_RATE_EXCEEDED => Result::STATUS_RATE_ERROR,
73
        self::RESPONSE_TOPIC_MESSAGE_RATE_EXCEEDED => Result::STATUS_RATE_ERROR,
74
75
        self::RESPONSE_AUTHENTICATION_ERROR => Result::STATUS_AUTH_ERROR,
76
77
        self::RESPONSE_TIMEOUT => Result::STATUS_SERVER_ERROR,
78
        self::RESPONSE_INTERNAL_SERVER_ERROR => Result::STATUS_SERVER_ERROR,
79
        self::RESPONSE_SERVER_UNAVAILABLE => Result::STATUS_SERVER_ERROR,
80
        self::RESPONSE_BADLY_FORMATTED_RESPONSE => Result::STATUS_SERVER_ERROR,
81
82
        self::RESPONSE_UNKNOWN_ERROR => Result::STATUS_UNKNOWN_ERROR,
83
    ];
84
85
    /**
86
     * Status messages mapping.
87
     *
88
     * @see https://developers.google.com/cloud-messaging/http-server-ref
89
     *
90
     * @var array
91
     */
92
    protected static $statusMessageMap = [
93
        self::RESPONSE_OK => 'OK',
94
95
        self::RESPONSE_MISSING_REGISTRATION => 'Missing Registration Token',
96
        self::RESPONSE_INVALID_PACKAGE_NAME => 'Invalid Package Name',
97
        self::RESPONSE_MISMATCH_SENDER_ID => 'Mismatched Sender',
98
        self::RESPONSE_MESSAGE_TOO_BIG => 'Message Too Big',
99
        self::RESPONSE_INVALID_DATA_KEY => 'Invalid Data Key',
100
        self::RESPONSE_INVALID_TTL => 'Invalid Time to Live',
101
        self::RESPONSE_INVALID_MESSAGE => 'Invalid message',
102
103
        self::RESPONSE_INVALID_REGISTRATION => 'Invalid Registration Token',
104
        self::RESPONSE_NOT_REGISTERED => 'Unregistered Device',
105
106
        self::RESPONSE_DEVICE_MESSAGE_RATE_EXCEEDED => 'Device Message Rate Exceeded',
107
        self::RESPONSE_TOPIC_MESSAGE_RATE_EXCEEDED => 'Topics Message Rate Exceeded',
108
109
        self::RESPONSE_AUTHENTICATION_ERROR => 'Authentication Error',
110
111
        self::RESPONSE_TIMEOUT => 'Timeout',
112
        self::RESPONSE_INTERNAL_SERVER_ERROR => 'Internal Server Error',
113
        self::RESPONSE_SERVER_UNAVAILABLE => 'Server Unavailable',
114
        self::RESPONSE_BADLY_FORMATTED_RESPONSE => 'Bad Formatted Response',
115
116
        self::RESPONSE_UNKNOWN_ERROR => 'Unknown Error',
117
    ];
118
119
    /**
120
     * FCM service factory.
121
     *
122
     * @var Factory
123
     */
124
    protected $factory;
125
126
    /**
127
     * Push service client.
128
     *
129
     * @var \ZendService\Google\Gcm\Client
130
     */
131
    protected $pushClient;
132
133
    /**
134
     * FCM service adapter constructor.
135
     *
136
     * @param array   $parameters
137
     * @param bool    $sandbox
138
     * @param Factory $factory
0 ignored issues
show
Documentation introduced by
Should the type for parameter $factory not be null|Factory?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
139
     *
140
     * @throws \Jgut\Tify\Exception\AdapterException
141
     */
142
    public function __construct(array $parameters = [], $sandbox = false, Factory $factory = null)
143
    {
144
        $this->assignParameters($parameters);
145
        $this->setSandbox($sandbox);
146
147
        // @codeCoverageIgnoreStart
148
        if ($factory === null) {
149
            $factory = new DefaultFactory;
150
        }
151
        // @codeCoverageIgnoreEnd
152
153
        $this->factory = $factory;
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     *
159
     * @throws \InvalidArgumentException
160
     * @throws \ZendService\Google\Exception\InvalidArgumentException
161
     * @throws \ZendService\Google\Exception\RuntimeException
162
     */
163
    public function push(Notification $notification)
164
    {
165
        $client = $this->getPushClient();
166
167
        $pushResults = [];
168
169
        foreach ($this->getPushMessage($notification) as $pushMessage) {
170
            $date = new \DateTime('now', new \DateTimeZone('UTC'));
171
172
            try {
173
                $pushResponses = $client->send($pushMessage)->getResults();
174
175
                foreach ($pushMessage->getRegistrationIds() as $token) {
176
                    $statusCode = self::RESPONSE_OK;
177
178
                    if (!array_key_exists($token, $pushResponses)) {
179
                        $statusCode = self::RESPONSE_UNKNOWN_ERROR;
180
                    } elseif (array_key_exists('error', $pushResponses[$token])) {
181
                        $statusCode = $pushResponses[$token]['error'];
182
                    }
183
184
                    $pushResults[] = new Result(
185
                        $token,
186
                        $date,
187
                        self::$statusCodeMap[$statusCode],
188
                        self::$statusMessageMap[$statusCode]
189
                    );
190
                }
191
            // @codeCoverageIgnoreStart
192
            } catch (GcmRuntimeException $exception) {
193
                $statusCode = $this->getErrorCodeFromException($exception);
194
195
                foreach ($pushMessage->getRegistrationIds() as $token) {
196
                    $pushResults[] = new Result(
197
                        $token,
198
                        $date,
199
                        self::$statusCodeMap[$statusCode],
200
                        self::$statusMessageMap[$statusCode]
201
                    );
202
                }
203
            }
204
            // @codeCoverageIgnoreEnd
205
        }
206
207
        return $pushResults;
208
    }
209
210
    /**
211
     * Get opened push client.
212
     *
213
     * @return \ZendService\Google\Gcm\Client
214
     */
215
    protected function getPushClient()
216
    {
217
        if ($this->pushClient === null) {
218
            $this->pushClient = $this->factory->buildPushClient($this->getParameter(static::PARAMETER_API_KEY));
219
        }
220
221
        return $this->pushClient;
222
    }
223
224
    /**
225
     * Get service formatted push message.
226
     *
227
     * @param Notification $notification
228
     *
229
     * @throws \ZendService\Google\Exception\InvalidArgumentException
230
     * @throws \ZendService\Google\Exception\RuntimeException
231
     *
232
     * @return \Generator|\ZendService\Google\Gcm\Message[]
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use \Generator.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
233
     */
234
    protected function getPushMessage(Notification $notification)
235
    {
236
        foreach (array_chunk($notification->getReceivers(), 100) as $receivers) {
237
            $tokens = array_map(
238
                function ($receiver) {
239
                    return $receiver instanceof FcmReceiver ? $receiver->getToken() : null;
240
                },
241
                $receivers
242
            );
243
244
            $pushMessage = $this->factory->buildPushMessage(array_filter($tokens), $notification);
245
246
            if ($this->isSandbox()) {
247
                $pushMessage->setDryRun(true);
248
            }
249
250
            yield $pushMessage;
251
        }
252
    }
253
254
    /**
255
     * Extract error code from exception.
256
     *
257
     * @param GcmRuntimeException $exception
258
     *
259
     * @return string
260
     */
261
    protected function getErrorCodeFromException(GcmRuntimeException $exception)
262
    {
263
        $message = $exception->getMessage();
264
265
        if ($message === '500 Internal Server Error') {
266
            return static::RESPONSE_INTERNAL_SERVER_ERROR;
267
        }
268
269
        if (preg_match('/^503 Server Unavailable/', $message)) {
270
            return static::RESPONSE_SERVER_UNAVAILABLE;
271
        }
272
273
        if ($message === '400 Bad Request; invalid message') {
274
            return static::RESPONSE_INVALID_MESSAGE;
275
        }
276
277
        if ($message === '401 Forbidden; Authentication Error') {
278
            return static::RESPONSE_AUTHENTICATION_ERROR;
279
        }
280
281
        if ($message === 'Response body did not contain a valid JSON response') {
282
            return static::RESPONSE_BADLY_FORMATTED_RESPONSE;
283
        }
284
285
        return static::RESPONSE_UNKNOWN_ERROR;
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     */
291
    protected function getDefinedParameters()
292
    {
293
        return [static::PARAMETER_API_KEY];
294
    }
295
296
    /**
297
     * {@inheritdoc}
298
     */
299
    protected function getRequiredParameters()
300
    {
301
        return [static::PARAMETER_API_KEY];
302
    }
303
}
304