apns-php
 All Data Structures Files Functions Variables Groups Pages
Push.php
Go to the documentation of this file.
1 <?php
2 /**
3  * @file
4  * ApnsPHP_Push class definition.
5  *
6  * LICENSE
7  *
8  * This source file is subject to the new BSD license that is bundled
9  * with this package in the file LICENSE.txt.
10  * It is also available through the world-wide-web at this URL:
11  * http://code.google.com/p/apns-php/wiki/License
12  * If you did not receive a copy of the license and are unable to
13  * obtain it through the world-wide-web, please send an email
14  * to aldo.armiento@gmail.com so we can send you a copy immediately.
15  *
16  * @author (C) 2010 Aldo Armiento (aldo.armiento@gmail.com)
17  * @version $Id$
18  */
19 
20 /**
21  * @defgroup ApnsPHP_Push Push
22  * @ingroup ApplePushNotificationService
23  */
24 
25 /**
26  * The Push Notification Provider.
27  *
28  * The class manages a message queue and sends notifications payload to Apple Push
29  * Notification Service.
30  *
31  * @ingroup ApnsPHP_Push
32  */
34 {
35  const COMMAND_PUSH = 1; /**< @type integer Payload command. */
36 
37  const ERROR_RESPONSE_SIZE = 6; /**< @type integer Error-response packet size. */
38  const ERROR_RESPONSE_COMMAND = 8; /**< @type integer Error-response command code. */
39 
40  const STATUS_CODE_INTERNAL_ERROR = 999; /**< @type integer Status code for internal error (not Apple). */
41 
42  protected $_aErrorResponseMessages = array(
43  0 => 'No errors encountered',
44  1 => 'Processing error',
45  2 => 'Missing device token',
46  3 => 'Missing topic',
47  4 => 'Missing payload',
48  5 => 'Invalid token size',
49  6 => 'Invalid topic size',
50  7 => 'Invalid payload size',
51  8 => 'Invalid token',
52  self::STATUS_CODE_INTERNAL_ERROR => 'Internal error'
53  ); /**< @type array Error-response messages. */
54 
55  protected $_nSendRetryTimes = 3; /**< @type integer Send retry times. */
56 
57  protected $_aServiceURLs = array(
58  'tls://gateway.push.apple.com:2195', // Production environment
59  'tls://gateway.sandbox.push.apple.com:2195' // Sandbox environment
60  ); /**< @type array Service URLs environments. */
61 
62  protected $_aMessageQueue = array(); /**< @type array Message queue. */
63  protected $_aErrors = array(); /**< @type array Error container. */
64 
65  /**
66  * Set the send retry times value.
67  *
68  * If the client is unable to send a payload to to the server retries at least
69  * for this value. The default send retry times is 3.
70  *
71  * @param $nRetryTimes @type integer Send retry times.
72  */
73  public function setSendRetryTimes($nRetryTimes)
74  {
75  $this->_nSendRetryTimes = (int)$nRetryTimes;
76  }
77 
78  /**
79  * Get the send retry time value.
80  *
81  * @return @type integer Send retry times.
82  */
83  public function getSendRetryTimes()
84  {
86  }
87 
88  /**
89  * Adds a message to the message queue.
90  *
91  * @param $message @type ApnsPHP_Message The message.
92  */
93  public function add(ApnsPHP_Message $message)
94  {
95  $sMessagePayload = $message->getPayload();
96  $nRecipients = $message->getRecipientsNumber();
97 
98  $nMessageQueueLen = count($this->_aMessageQueue);
99  for ($i = 0; $i < $nRecipients; $i++) {
100  $nMessageID = $nMessageQueueLen + $i + 1;
101  $this->_aMessageQueue[$nMessageID] = array(
102  'MESSAGE' => $message,
103  'BINARY_NOTIFICATION' => $this->_getBinaryNotification(
104  $message->getRecipient($i),
105  $sMessagePayload,
106  $nMessageID,
107  $message->getExpiry()
108  ),
109  'ERRORS' => array()
110  );
111  }
112  }
113 
114  /**
115  * Sends all messages in the message queue to Apple Push Notification Service.
116  *
117  * @throws ApnsPHP_Push_Exception if not connected to the
118  * service or no notification queued.
119  */
120  public function send()
121  {
122  if (!$this->_hSocket) {
123  throw new ApnsPHP_Push_Exception(
124  'Not connected to Push Notification Service'
125  );
126  }
127 
128  if (empty($this->_aMessageQueue)) {
129  throw new ApnsPHP_Push_Exception(
130  'No notifications queued to be sent'
131  );
132  }
133 
134  $this->_aErrors = array();
135  $nRun = 1;
136  while (($nMessages = count($this->_aMessageQueue)) > 0) {
137  $this->_log("INFO: Sending messages queue, run #{$nRun}: $nMessages message(s) left in queue.");
138 
139  $bError = false;
140  foreach($this->_aMessageQueue as $k => &$aMessage) {
141  if (function_exists('pcntl_signal_dispatch')) {
142  pcntl_signal_dispatch();
143  }
144 
145  $message = $aMessage['MESSAGE'];
146  $sCustomIdentifier = (string)$message->getCustomIdentifier();
147  $sCustomIdentifier = sprintf('[custom identifier: %s]', empty($sCustomIdentifier) ? 'unset' : $sCustomIdentifier);
148 
149  $nErrors = 0;
150  if (!empty($aMessage['ERRORS'])) {
151  foreach($aMessage['ERRORS'] as $aError) {
152  if ($aError['statusCode'] == 0) {
153  $this->_log("INFO: Message ID {$k} {$sCustomIdentifier} has no error ({$aError['statusCode']}), removing from queue...");
154  $this->_removeMessageFromQueue($k);
155  continue 2;
156  } else if ($aError['statusCode'] > 1 && $aError['statusCode'] <= 8) {
157  $this->_log("WARNING: Message ID {$k} {$sCustomIdentifier} has an unrecoverable error ({$aError['statusCode']}), removing from queue without retrying...");
158  $this->_removeMessageFromQueue($k, true);
159  continue 2;
160  }
161  }
162  if (($nErrors = count($aMessage['ERRORS'])) >= $this->_nSendRetryTimes) {
163  $this->_log(
164  "WARNING: Message ID {$k} {$sCustomIdentifier} has {$nErrors} errors, removing from queue..."
165  );
166  $this->_removeMessageFromQueue($k, true);
167  continue;
168  }
169  }
170 
171  $nLen = strlen($aMessage['BINARY_NOTIFICATION']);
172  $this->_log("STATUS: Sending message ID {$k} {$sCustomIdentifier} (" . ($nErrors + 1) . "/{$this->_nSendRetryTimes}): {$nLen} bytes.");
173 
174  $aErrorMessage = null;
175  if ($nLen !== ($nWritten = (int)@fwrite($this->_hSocket, $aMessage['BINARY_NOTIFICATION']))) {
176  $aErrorMessage = array(
177  'identifier' => $k,
178  'statusCode' => self::STATUS_CODE_INTERNAL_ERROR,
179  'statusMessage' => sprintf('%s (%d bytes written instead of %d bytes)',
180  $this->_aErrorResponseMessages[self::STATUS_CODE_INTERNAL_ERROR], $nWritten, $nLen
181  )
182  );
183  }
184  usleep($this->_nWriteInterval);
185 
186  $bError = $this->_updateQueue($aErrorMessage);
187  if ($bError) {
188  break;
189  }
190  }
191 
192  if (!$bError) {
193  $read = array($this->_hSocket);
194  $null = NULL;
195  $nChangedStreams = @stream_select($read, $null, $null, 0, $this->_nSocketSelectTimeout);
196  if ($nChangedStreams === false) {
197  $this->_log('ERROR: Unable to wait for a stream availability.');
198  break;
199  } else if ($nChangedStreams > 0) {
200  $bError = $this->_updateQueue();
201  if (!$bError) {
202  $this->_aMessageQueue = array();
203  }
204  } else {
205  $this->_aMessageQueue = array();
206  }
207  }
208 
209  $nRun++;
210  }
211  }
212 
213  /**
214  * Returns messages in the message queue.
215  *
216  * When a message is successful sent or reached the maximum retry time is removed
217  * from the message queue and inserted in the Errors container. Use the getErrors()
218  * method to retrive messages with delivery error(s).
219  *
220  * @param $bEmpty @type boolean @optional Empty message queue.
221  * @return @type array Array of messages left on the queue.
222  */
223  public function getQueue($bEmpty = true)
224  {
225  $aRet = $this->_aMessageQueue;
226  if ($bEmpty) {
227  $this->_aMessageQueue = array();
228  }
229  return $aRet;
230  }
231 
232  /**
233  * Returns messages not delivered to the end user because one (or more) error
234  * occurred.
235  *
236  * @param $bEmpty @type boolean @optional Empty message container.
237  * @return @type array Array of messages not delivered because one or more errors
238  * occurred.
239  */
240  public function getErrors($bEmpty = true)
241  {
242  $aRet = $this->_aErrors;
243  if ($bEmpty) {
244  $this->_aErrors = array();
245  }
246  return $aRet;
247  }
248 
249  /**
250  * Generate a binary notification from a device token and a JSON-encoded payload.
251  *
252  * @see http://tinyurl.com/ApplePushNotificationBinary
253  *
254  * @param $sDeviceToken @type string The device token.
255  * @param $sPayload @type string The JSON-encoded payload.
256  * @param $nMessageID @type integer @optional Message unique ID.
257  * @param $nExpire @type integer @optional Seconds, starting from now, that
258  * identifies when the notification is no longer valid and can be discarded.
259  * Pass a negative value (-1 for example) to request that APNs not store
260  * the notification at all. Default is 86400 * 7, 7 days.
261  * @return @type string A binary notification.
262  */
263  protected function _getBinaryNotification($sDeviceToken, $sPayload, $nMessageID = 0, $nExpire = 604800)
264  {
265  $nTokenLength = strlen($sDeviceToken);
266  $nPayloadLength = strlen($sPayload);
267 
268  $sRet = pack('CNNnH*', self::COMMAND_PUSH, $nMessageID, $nExpire > 0 ? time() + $nExpire : 0, self::DEVICE_BINARY_SIZE, $sDeviceToken);
269  $sRet .= pack('n', $nPayloadLength);
270  $sRet .= $sPayload;
271 
272  return $sRet;
273  }
274 
275  /**
276  * Parses the error message.
277  *
278  * @param $sErrorMessage @type string The Error Message.
279  * @return @type array Array with command, statusCode and identifier keys.
280  */
281  protected function _parseErrorMessage($sErrorMessage)
282  {
283  return unpack('Ccommand/CstatusCode/Nidentifier', $sErrorMessage);
284  }
285 
286  /**
287  * Reads an error message (if present) from the main stream.
288  * If the error message is present and valid the error message is returned,
289  * otherwhise null is returned.
290  *
291  * @return @type array|null Return the error message array.
292  */
293  protected function _readErrorMessage()
294  {
295  $sErrorResponse = @fread($this->_hSocket, self::ERROR_RESPONSE_SIZE);
296  if ($sErrorResponse === false || strlen($sErrorResponse) != self::ERROR_RESPONSE_SIZE) {
297  return;
298  }
299  $aErrorResponse = $this->_parseErrorMessage($sErrorResponse);
300  if (!is_array($aErrorResponse) || empty($aErrorResponse)) {
301  return;
302  }
303  if (!isset($aErrorResponse['command'], $aErrorResponse['statusCode'], $aErrorResponse['identifier'])) {
304  return;
305  }
306  if ($aErrorResponse['command'] != self::ERROR_RESPONSE_COMMAND) {
307  return;
308  }
309  $aErrorResponse['time'] = time();
310  $aErrorResponse['statusMessage'] = 'None (unknown)';
311  if (isset($this->_aErrorResponseMessages[$aErrorResponse['statusCode']])) {
312  $aErrorResponse['statusMessage'] = $this->_aErrorResponseMessages[$aErrorResponse['statusCode']];
313  }
314  return $aErrorResponse;
315  }
316 
317  /**
318  * Checks for error message and deletes messages successfully sent from message queue.
319  *
320  * @param $aErrorMessage @type array @optional The error message. It will anyway
321  * always be read from the main stream. The latest successful message
322  * sent is the lowest between this error message and the message that
323  * was read from the main stream.
324  * @see _readErrorMessage()
325  * @return @type boolean True if an error was received.
326  */
327  protected function _updateQueue($aErrorMessage = null)
328  {
329  $aStreamErrorMessage = $this->_readErrorMessage();
330  if (!isset($aErrorMessage) && !isset($aStreamErrorMessage)) {
331  return false;
332  } else if (isset($aErrorMessage, $aStreamErrorMessage)) {
333  if ($aStreamErrorMessage['identifier'] <= $aErrorMessage['identifier']) {
334  $aErrorMessage = $aStreamErrorMessage;
335  unset($aStreamErrorMessage);
336  }
337  } else if (!isset($aErrorMessage) && isset($aStreamErrorMessage)) {
338  $aErrorMessage = $aStreamErrorMessage;
339  unset($aStreamErrorMessage);
340  }
341 
342  $this->_log('ERROR: Unable to send message ID ' .
343  $aErrorMessage['identifier'] . ': ' .
344  $aErrorMessage['statusMessage'] . ' (' . $aErrorMessage['statusCode'] . ').');
345 
346  $this->disconnect();
347 
348  foreach($this->_aMessageQueue as $k => &$aMessage) {
349  if ($k < $aErrorMessage['identifier']) {
350  unset($this->_aMessageQueue[$k]);
351  } else if ($k == $aErrorMessage['identifier']) {
352  $aMessage['ERRORS'][] = $aErrorMessage;
353  } else {
354  break;
355  }
356  }
357 
358  $this->connect();
359 
360  return true;
361  }
362 
363  /**
364  * Remove a message from the message queue.
365  *
366  * @param $nMessageID @type integer The Message ID.
367  * @param $bError @type boolean @optional Insert the message in the Error container.
368  * @throws ApnsPHP_Push_Exception if the Message ID is not valid or message
369  * does not exists.
370  */
371  protected function _removeMessageFromQueue($nMessageID, $bError = false)
372  {
373  if (!is_numeric($nMessageID) || $nMessageID <= 0) {
374  throw new ApnsPHP_Push_Exception(
375  'Message ID format is not valid.'
376  );
377  }
378  if (!isset($this->_aMessageQueue[$nMessageID])) {
379  throw new ApnsPHP_Push_Exception(
380  "The Message ID {$nMessageID} does not exists."
381  );
382  }
383  if ($bError) {
384  $this->_aErrors[$nMessageID] = $this->_aMessageQueue[$nMessageID];
385  }
386  unset($this->_aMessageQueue[$nMessageID]);
387  }
388 }
const ERROR_RESPONSE_SIZE
integer Error-response packet size.
Definition: Push.php:37
void _removeMessageFromQueue(integer $nMessageID, boolean $bError=false)
Remove a message from the message queue.
Definition: Push.php:371
integer getSendRetryTimes()
Get the send retry time value.
Definition: Push.php:83
integer getRecipientsNumber()
Get the number of recipients.
Definition: Message.php:107
array null _readErrorMessage()
Reads an error message (if present) from the main stream.
Definition: Push.php:293
string getPayload()
Convert the message in a JSON-encoded payload.
Definition: Message.php:397
$_aErrorResponseMessages
array Error-response messages.
Definition: Push.php:42
void connect()
Connects to Apple Push Notification service server.
Definition: Abstract.php:328
string getRecipient(integer $nRecipient=0)
Get a recipient.
Definition: Message.php:92
string _getBinaryNotification(string $sDeviceToken, string $sPayload, integer $nMessageID=0, integer $nExpire=604800)
Generate a binary notification from a device token and a JSON-encoded payload.
Definition: Push.php:263
array $_aErrors
Error container.
Definition: Push.php:63
const STATUS_CODE_INTERNAL_ERROR
integer Status code for internal error (not Apple).
Definition: Push.php:40
Abstract class: this is the superclass for all Apple Push Notification Service classes.
Definition: Abstract.php:40
boolean disconnect()
Disconnects from Apple Push Notifications service server.
Definition: Abstract.php:356
void add(ApnsPHP_Message $message)
Adds a message to the message queue.
Definition: Push.php:93
void _log(string $sMessage)
Logs a message through the Logger.
Definition: Abstract.php:414
integer getExpiry()
Get the expiry value.
Definition: Message.php:458
array $_aMessageQueue
Message queue.
Definition: Push.php:62
const COMMAND_PUSH
integer Payload command.
Definition: Push.php:35
array _parseErrorMessage(string $sErrorMessage)
Parses the error message.
Definition: Push.php:281
The Push Notification Provider.
Definition: Push.php:33
void setSendRetryTimes(integer $nRetryTimes)
Set the send retry times value.
Definition: Push.php:73
$_aServiceURLs
array Service URLs environments.
Definition: Push.php:57
array getErrors(boolean $bEmpty=true)
Returns messages not delivered to the end user because one (or more) error occurred.
Definition: Push.php:240
integer $_nSendRetryTimes
Send retry times.
Definition: Push.php:55
void send()
Sends all messages in the message queue to Apple Push Notification Service.
Definition: Push.php:120
The Push Notification Message.
Definition: Message.php:34
Exception class.
const ERROR_RESPONSE_COMMAND
integer Error-response command code.
Definition: Push.php:38
array getQueue(boolean $bEmpty=true)
Returns messages in the message queue.
Definition: Push.php:223
boolean _updateQueue(array $aErrorMessage=null)
Checks for error message and deletes messages successfully sent from message queue.
Definition: Push.php:327