Easy APNS Apple Push Notification Service using PHP & MySQL

PHP Source

Before you get started: You can download the source code files here:

Once you have installed the MySQL Tables and added the appropriate code to your Apple Delegate file, you need to get PHP ready to receive device registrations.

STEP 1: Make a new file named class_APNS.php

This is the main APNs file that does all the dirty work.

You will need to change the following variables in this file:

  1. (line 55) $logPath: Must be the absolute path to your log file.
  2. (line 72) $certificate: Must be the absolute path to your Apple Production Certificate
  3. (line 96) $sandboxCertificate: Must be the absolute path to your Apple Development Certificate
<?PHP

/**
 * @category Apple Push Notification Service using PHP & MySQL
 * @package APNS
 * @author Peter Schmalfeldt <manifestinteractive@gmail.com>
 * @license http://www.apache.org/licenses/LICENSE-2.0
 * @link http://code.google.com/p/easyapns/
 */

/**
 * Begin Document
 */

class APNS {

	/**
	* Connection to MySQL
	*
	* @var string
	* @access private
	*/
	private $db;

	/**
	* Array of APNS Connection Settings
	*
	* @var array
	* @access private
	*/
	private $apnsData;

	/**
	* Whether to trigger errors
	*
	* @var bool
	* @access private
	*/
	private $showErrors = true;

	/**
	* Whether APNS should log errors
	*
	* @var bool
	* @access private
	*/
	private $logErrors = true;

	/**
	* Log path for APNS errors
	*
	* @var string
	* @access private
	*/
	private $logPath = '/usr/local/apns/apns.log';

	/**
	* Max files size of log before it is truncated. 1048576 = 1MB.  Added incase you do not add to a log
	* rotator so this script will not accidently make gigs of error logs if there are issues with install
	*
	* @var int
	* @access private
	*/
	private $logMaxSize = 1048576; // max log size before it is truncated

	/**
	* Absolute path to your Production Certificate
	*
	* @var string
	* @access private
	*/
	private $certificate = '/usr/local/apns/apns.pem';

	/**
	* Apples Production APNS Gateway
	*
	* @var string
	* @access private
	*/
	private $ssl = 'ssl://gateway.push.apple.com:2195';

	/**
	* Apples Production APNS Feedback Service
	*
	* @var string
	* @access private
	*/
	private $feedback = 'ssl://feedback.push.apple.com:2196';

	/**
	* Absolute path to your Development Certificate
	*
	* @var string
	* @access private
	*/
	private $sandboxCertificate = '/usr/local/apns/apns-dev.pem'; // change this to your development certificate absolute path

	/**
	* Apples Sandbox APNS Gateway
	*
	* @var string
	* @access private
	*/
	private $sandboxSsl = 'ssl://gateway.sandbox.push.apple.com:2195';

	/**
	* Apples Sandbox APNS Feedback Service
	*
	* @var string
	* @access private
	*/
	private $sandboxFeedback = 'ssl://feedback.sandbox.push.apple.com:2196';

	/**
	* Message to push to user
	*
	* @var string
	* @access private
	*/
	private $message;

	/**
	 * Constructor.
	 *
	 * Initializes a database connection and perfoms any tasks that have been assigned.
	 *
	 * Create a new PHP file named apns.php on your website...
	 *
	 * <code>
	 * <?php
	 * $db = new DbConnect();
	 * $db->show_errors();
	 * $apns = new APNS($db);
	 * ?>
 	 * </code>
	 *
	 * Your iPhone App Delegate.m file will point to a PHP file with this APNS Object.  The url will end up looking something like:
	 * https://secure.yourwebsite.com/apns.php?task=register&appname=My%20App&appversion=1.0.1&deviceuid=e018c2e46efe185d6b1107aa942085a59bb865d9&devicetoken=43df9e97b09ef464a6cf7561f9f339cb1b6ba38d8dc946edd79f1596ac1b0f66&devicename=My%20Awesome%20iPhone&devicemodel=iPhone&deviceversion=3.1.2&pushbadge=enabled&pushalert=disabled&pushsound=enabled
     *
     * @param object $db Database Object
	 * @param array $args Optional arguments passed through $argv or $_GET
     * @access 	public
     */
	function __construct($db, $args=NULL) {
		$this->db = $db;
		$this->checkSetup();
		$this->apnsData = array(
			'production'=>array(
				'certificate'=>$this->certificate,
				'ssl'=>$this->ssl,
				'feedback'=>$this->feedback
			),
			'sandbox'=>array(
				'certificate'=>$this->sandboxCertificate,
				'ssl'=>$this->sandboxSsl,
				'feedback'=>$this->sandboxFeedback
			)
		);
		if(!empty($args)){
			switch($args['task']){
				case "register":
					$this->_registerDevice(
						$args['appname'],
						$args['appversion'],
						$args['deviceuid'],
						$args['devicetoken'],
						$args['devicename'],
						$args['devicemodel'],
						$args['deviceversion'],
						$args['pushbadge'],
						$args['pushalert'],
						$args['pushsound']
					);
					break;

				case "fetch";
					$this->_fetchMessages();
					break;

				default:
					echo "No APNS Task Provided...\n";
					break;
			}
		}
	}

	/**
	 * Check Setup
	 *
	 * Check to make sure that the certificates are available and also provide a notice if they are not as secure as they could be.
	 *
     * @access private
     */
	private function checkSetup(){
		if(!file_exists($this->certificate)) $this->_triggerError('ERROR: Missing Production Certificate.', E_USER_ERROR);
		if(!file_exists($this->sandboxCertificate)) $this->_triggerError('ERROR: Missing Sandbox Certificate.', E_USER_ERROR);

		clearstatcache();
    	$certificateMod = substr(sprintf('%o', fileperms($this->certificate)), -3);
		$sandboxCertificateMod = substr(sprintf('%o', fileperms($this->sandboxCertificate)), -3); 

		if($certificateMod>644)  $this->_triggerError('NOTICE: Production Certificate is insecure! Suggest chmod 644.');
		if($sandboxCertificateMod>644)  $this->_triggerError('NOTICE: Sandbox Certificate is insecure! Suggest chmod 644.');
	}

	/**
	 * Register Apple device
	 *
	 * Using your Delegate file to auto register the device on application launch.  This will happen automatically from the Delegate.m file in your iPhone Application using our code.
	 *
	 * @param sting $appname Application Name
	 * @param sting $appversion Application Version
	 * @param sting $deviceuid 40 charater unique user id of Apple device
	 * @param sting $devicetoken 64 character unique device token tied to device id
	 * @param sting $devicename User selected device name
	 * @param sting $devicemodel Modle of device 'iPhone' or 'iPod'
	 * @param sting $deviceversion Current version of device
	 * @param sting $pushbadge Whether Badge Pushing is Enabled or Disabled
 	 * @param sting $pushalert Whether Alert Pushing is Enabled or Disabled
 	 * @param sting $pushsound Whether Sound Pushing is Enabled or Disabled
     * @access private
     */
	private function _registerDevice($appname, $appversion, $deviceuid, $devicetoken, $devicename, $devicemodel, $deviceversion, $pushbadge, $pushalert, $pushsound){

		if(strlen($appname)==0) $this->_triggerError('ERROR: Application Name must not be blank.', E_USER_ERROR);
		else if(strlen($appversion)==0) $this->_triggerError('ERROR: Application Version must not be blank.', E_USER_ERROR);
		else if(strlen($deviceuid)!=40) $this->_triggerError('ERROR: Device ID must be 40 characters in length.', E_USER_ERROR);
		else if(strlen($devicetoken)!=64) $this->_triggerError('ERROR: Device Token must be 64 characters in length.', E_USER_ERROR);
		else if(strlen($devicename)==0) $this->_triggerError('ERROR: Device Name must not be blank.', E_USER_ERROR);
		else if(strlen($devicemodel)==0) $this->_triggerError('ERROR: Device Model must not be blank.', E_USER_ERROR);
		else if(strlen($deviceversion)==0) $this->_triggerError('ERROR: Device Version must not be blank.', E_USER_ERROR);
		else if($pushbadge!='disabled' && $pushbadge!='enabled') $this->_triggerError('ERROR: Push Badge must be either Enabled or Disabled.', E_USER_ERROR);
		else if($pushalert!='disabled' && $pushalert!='enabled') $this->_triggerError('ERROR: Push Alert must be either Enabled or Disabled.', E_USER_ERROR);
		else if($pushsound!='disabled' && $pushsound!='enabled') $this->_triggerError('ERROR: Push Sount must be either Enabled or Disabled.', E_USER_ERROR);

		$appname = $this->db->prepare($appname);
		$appversion = $this->db->prepare($appversion);
		$deviceuid = $this->db->prepare($deviceuid);
		$devicetoken = $this->db->prepare($devicetoken);
		$devicename = $this->db->prepare($devicename);
		$devicemodel = $this->db->prepare($devicemodel);
		$deviceversion = $this->db->prepare($deviceversion);
		$pushbadge = $this->db->prepare($pushbadge);
		$pushalert = $this->db->prepare($pushalert);
		$pushsound = $this->db->prepare($pushsound);

		// store device for push notifications
		$this->db->query("SET NAMES 'utf8';"); // force utf8 encoding if not your default
		$sql = "INSERT INTO `apns_devices`
				VALUES (
					NULL,
					'{$appname}',
					'{$appversion}',
					'{$deviceuid}',
					'{$devicetoken}',
					'{$devicename}',
					'{$devicemodel}',
					'{$deviceversion}',
					'{$pushbadge}',
					'{$pushalert}',
					'{$pushsound}',
					'production',
					'active',
					NOW(),
					NOW()
				)
				ON DUPLICATE KEY UPDATE
				`devicetoken`='{$devicetoken}',
				`devicename`='{$devicename}',
				`devicemodel`='{$devicemodel}',
				`deviceversion`='{$deviceversion}',
				`pushbadge`='{$pushbadge}',
				`pushalert`='{$pushalert}',
				`pushsound`='{$pushsound}',
				`status`='active',
				`modified`=NOW();";
		$this->db->query($sql);
	}

	/**
	 * Unregister Apple device
	 *
	 * This gets called automatically when Apple's Feedback Service responds with an invalid token.
	 *
	 * @param sting $token 64 character unique device token tied to device id
     * @access private
     */
	private function _unregisterDevice($token){
		$sql = "UPDATE `apns_devices`
				SET `status`='uninstalled'
				WHERE `devicetoken`='{$token}'
				LIMIT 1;";
		$this->db->query($sql);
	}

	/**
	 * Fetch Messages
	 *
	 * This gets called by a cron job that runs as often as you want.  You might want to set it for every minute.
	 *
	 * @param sting $token 64 character unique device token tied to device id
     * @access private
     */
	private function _fetchMessages(){
		// only send one message per user... oldest message first
		$sql = "SELECT
				`apns_messages`.`pid`,
				`apns_messages`.`message`,
				`apns_devices`.`devicetoken`,
				`apns_devices`.`development`

				FROM `apns_messages` 

				LEFT JOIN `apns_devices` ON
				`apns_devices`.`pid` = `apns_messages`.`fk_device`

				WHERE `apns_messages`.`status`='queued'

				AND `apns_messages`.`delivery` <= NOW() 

				AND `apns_devices`.`status`='active'

				GROUP BY `apns_messages`.`fk_device`

				ORDER BY `apns_messages`.`created` ASC

				LIMIT 100;";

		if($result = $this->db->query($sql)){
			if($result->num_rows){
				while($row = $result->fetch_array(MYSQLI_ASSOC)){
					$pid = $this->db->prepare($row['pid']);
					$message = stripslashes($this->db->prepare($row['message']));
					$token = $this->db->prepare($row['devicetoken']);
					$development = $this->db->prepare($row['development']);
					$this->_pushMessage($pid, $message, $token, $development);
				}
			}
		}
	}

	/**
	 * Push APNS Messages
	 *
	 * This gets called automatically by _fetchMessages.  This is what actually deliveres the message.
	 *
	 * @param int $pid
	 * @param sting $message JSON encoded string
	 * @param sting $token 64 character unique device token tied to device id
	 * @param sting $development Which SSL to connect to, Sandbox or Production
     * @access private
     */
	private function _pushMessage($pid, $message, $token, $development){
		if(strlen($pid)==0) $this->_triggerError('ERROR: Missing message pid.', E_USER_ERROR);
		if(strlen($message)==0) $this->_triggerError('ERROR: Missing message.', E_USER_ERROR);
		if(strlen($token)==0) $this->_triggerError('ERROR: Missing message token.', E_USER_ERROR);
		if(strlen($development)==0) $this->_triggerError('ERROR: Missing development status.', E_USER_ERROR);

		$ctx = stream_context_create();
		stream_context_set_option($ctx, 'ssl', 'local_cert', $this->apnsData[$development]['certificate']);
		$fp = stream_socket_client($this->apnsData[$development]['ssl'], $error, $errorString, 60, STREAM_CLIENT_CONNECT, $ctx);

		if(!$fp){
			$this->_messageFailed($pid);
			$this->_triggerError("NOTICE: Failed to connect to APNS: {$error} {$errorString}.");
		}
		else {
			$msg = chr(0).pack("n",32).pack('H*',$token).pack("n",strlen($message)).$message;
			$fwrite = fwrite($fp, $msg);
			if(!$fwrite) {
				$this->_pushFailed($pid);
				$this->_triggerError("ERROR: Failed writing to stream.", E_USER_ERROR);
			}
			else {
				$this->_pushSuccess($pid);
			}
		}
		fclose($fp);

		$this->_checkFeedback($development);
	}

	/**
	 * Fetch APNS Messages
	 *
	 * This gets called automatically by _pushMessage.  This will check with APNS for any invalid tokens and disable them from receiving further notifications.
	 *
	 * @param sting $development Which SSL to connect to, Sandbox or Production
     * @access private
     */
	private function _checkFeedback($development){
		$ctx = stream_context_create();
		stream_context_set_option($ctx, 'ssl', 'local_cert', $this->apnsData[$development]['certificate']);
		stream_context_set_option($ctx, 'ssl', 'verify_peer', false);
		$fp = stream_socket_client($this->apnsData[$development]['feedback'], $error, $errorString, 60, STREAM_CLIENT_CONNECT, $ctx);

		if(!$fp) $this->_triggerError("NOTICE: Failed to connect to device: {$error} {$errorString}.");
		while ($devcon = fread($fp, 38)){
			$arr = unpack("H*", $devcon);
			$rawhex = trim(implode("", $arr));
			$token = substr($rawhex, 12, 64);
			if(!empty($token)){
				$this->_unregisterDevice($token);
				$this->_triggerError("NOTICE: Unregistering Device Token: {$token}.");
			}
		}
		fclose($fp);
	}

	/**
	 * APNS Push Success
	 *
	 * This gets called automatically by _pushMessage.  When no errors are present, then the message was delivered.
	 *
	 * @param int $pid Primary ID of message that was delivered
     * @access private
     */
	private function _pushSuccess($pid){
		$sql = "UPDATE `apns_messages`
				SET `status`='delivered'
				WHERE `pid`={$pid}
				LIMIT 1;";
		$this->db->query($sql);
	}

	/**
	 * APNS Push Failed
	 *
	 * This gets called automatically by _pushMessage.  If an error is present, then the message was NOT delivered.
	 *
	 * @param int $pid Primary ID of message that was delivered
     * @access private
     */
	private function _pushFailed($pid){
		$sql = "UPDATE `apns_messages`
				SET `status`='failed'
				WHERE `pid`={$pid}
				LIMIT 1;";
		$this->db->query($sql);
	}

	/**
	 * Trigger Error
	 *
	 * Use PHP error handling to trigger User Errors or Notices.  If logging is enabled, errors will be written to the log as well.
	 * Disable on screen errors by setting showErrors to false;
	 *
	 * @param string $error Error String
	 * @param int $type Type of Error to Trigger
     * @access private
     */
	private function _triggerError($error, $type=E_USER_NOTICE){
		$backtrace = debug_backtrace();
		$backtrace = array_reverse($backtrace);
		$error .= "\n";
		$i=1;
		foreach($backtrace as $errorcode){
			$file = ($errorcode['file']!='') ? "-> File: ".basename($errorcode['file'])." (line ".$errorcode['line'].")":"";
			$error .= "\n\t".$i.") ".$errorcode['class']."::".$errorcode['function']." {$file}";
			$i++;
		}
		$error .= "\n\n";
		if($this->logErrors && file_exists($this->logPath)){
			if(filesize($this->logPath) > $this->logMaxSize) $fh = fopen($this->logPath, 'w');
			else $fh = fopen($this->logPath, 'a');
			fwrite($fh, $error);
			fclose($fh);
		}
		if($this->showErrors) trigger_error($error, $type);
	}

	/**
	 * JSON Encode
	 *
	 * Some servers do not have json_encode, so use this instead.
	 *
	 * @param array $array Data to convert to JSON string.
     * @access private
	 * @return string
     */
	private function _jsonEncode($array=false){
		if(is_null($array)) return 'null';
		if($array === false) return 'false';
		if($array === true) return 'true';
		if(is_scalar($array)){
			if(is_float($array)){
				return floatval(str_replace(",", ".", strval($array)));
			}
			if(is_string($array)){
				static $jsonReplaces = array(array("\\", "/", "\n", "\t", "\r", "\b", "\f", '"'), array('\\\\', '\\/', '\\n', '\\t', '\\r', '\\b', '\\f', '\"'));
				return '"' . str_replace($jsonReplaces[0], $jsonReplaces[1], $array) . '"';
			}
			else return $array;
		}
		$isList = true;
		for($i=0, reset($array); $i<count($array); $i++, next($array)){
			if(key($array) !== $i){
				$isList = false;
				break;
			}
		}
		$result = array();
		if($isList){
			foreach($array as $v) $result[] = json_encode($v);
			return '[' . join(',', $result) . ']';
		}
		else {
			foreach ($array as $k => $v) $result[] = json_encode($k).':'.json_encode($v);
			return '{' . join(',', $result) . '}';
		}
	}

	/**
	 * Start a New Message
	 *
	 * <code>
	 * <?php
	 * $db = new DbConnect();
	 * $db->show_errors();
	 * $apns = new APNS($db); // CREATE THE OBJECT
	 * $apns->newMessage(1, '2010-01-01 00:00:00'); // START A MESSAGE... SECOND ARGUMENT ACCEPTS ANY DATETIME STRING
	 * $apns->addMessageAlert('You got your emails.'); // ALERTS ARE TRICKY... SEE EXAMPLES
	 * $apns->addMessageBadge(9); // PASS A NUMBER
	 * $apns->addMessageSound('bingbong.aiff'); // ADD A SOUND
	 * $apns->queueMessage(); // AND SEND IT ON IT'S WAY
	 * ?>
 	 * </code>
	 *
	 * @param int $fk_device Foreign Key to the device you want to send a message to.
	 * @param string $delivery Possible future date to send the message.
     * @access public
     */
	public function newMessage($fk_device, $delivery=NULL){
		if(strlen($fk_device)==0) $this->_triggerError('ERROR: Missing message fk_device.', E_USER_ERROR);
		if(isset($this->message)){
			unset($this->message);
			$this->_triggerError('NOTICE: An existing message already created but not delivered. The previous message has been removed. Use queueMessage() to complete a message.');
		}
		$this->message = array();
		$this->message['aps'] = array();
		$this->message['send']['to'] = $fk_device;
		$this->message['send']['when'] = $delivery;
	}

	/**
	 * Queue Message for Delivery
	 *
	 * <code>
	 * <?php
	 * $db = new DbConnect();
	 * $db->show_errors();
	 * $apns = new APNS($db);
	 * $apns->newMessage(1, '2010-01-01 00:00:00');
	 * $apns->addMessageAlert('You got your emails.');
	 * $apns->addMessageBadge(9);
	 * $apns->addMessageSound('bingbong.aiff');
	 * $apns->queueMessage(); // ADD THE MESSAGE TO QUEUE
	 * ?>
 	 * </code>
	 *
     * @access public
     */
	public function queueMessage(){
		// check to make sure a message was created
		if(!isset($this->message)) $this->_triggerError('NOTICE: You cannot Queue a message that has not been created. Use newMessage() to create a new message.');

		// fetch the users id and check to make sure they have certain notifications enabled before trying to send anything to them.
		$deliver = false;
		$sql = "SELECT `pushbadge`, `pushalert`, `pushsound` FROM `apns_devices` WHERE `pid`={$this->message['send']['to']} AND `status`='active' LIMIT 1;";
		if($result = $this->db->query($sql)){
			if($result->num_rows){
				while($row = $result->fetch_array(MYSQLI_ASSOC)){
					$pushbadge = $this->db->prepare($row['pushbadge']);
					$pushalert = $this->db->prepare($row['pushalert']);
					$pushsound = $this->db->prepare($row['pushsound']);
				}
				$deliver = true;
			}
		}
		// has user disabled messages?
		if($pushbadge=='disabled' && $pushalert=='disabled' && $pushsound=='disabled') $deliver = false;
		if(!$deliver) {
			$this->_triggerError('NOTICE: This user has either disabled push notifications, or does not exist in the database.');
			unset($this->message);
		}
		else {
			// get sending information
			$to = $this->message['send']['to'];
			$when = $this->message['send']['when'];
			unset($this->message['send']);

			// remove notifications that user will not recieve.
			if($pushbadge=='disabled'){
				$this->_triggerError('NOTICE: This user has disabled Push Badge Notifications, Badge will not be delivered.');
				unset($this->message['aps']['badge']);
			}
			if($pushalert=='disabled'){
				$this->_triggerError('NOTICE: This user has disabled Push Alert Notifications, Alert will not be delivered.');
				unset($this->message['aps']['alert']);
			}
			if($pushsound=='disabled'){
				$this->_triggerError('NOTICE: This user has disabled Push Sound Notifications, Sound will not be delivered.');
				unset($this->message['aps']['sound']);
			}

			$fk_device = $this->db->prepare($to);
			$message = $this->_jsonEncode($this->message);
			$message = $this->db->prepare($message);
			$delivery = (!empty($when)) ? "'{$when}'":'NOW()';

			$this->db->query("SET NAMES 'utf8';"); // force utf8 encoding if not your default
			$sql = "INSERT INTO `apns_messages`
					VALUES (
						NULL,
						'{$fk_device}',
						'{$message}',
						{$delivery},
						'queued',
						NOW(),
						NOW()
					);";
			$this->db->query($sql);
			unset($this->message);
		}
	}

	/**
	 * Add Message Alert
	 *
	 * <code>
	 * <?php
	 * $db = new DbConnect();
	 * $db->show_errors();
	 * $apns = new APNS($db);
	 *
	 * // SIMPLE ALERT
	 * $apns->newMessage(1, '2010-01-01 00:00:00');
	 * $apns->addMessageAlert('Message received from Bob'); // MAKES DEFAULT BUTTON WITH BOTH 'Close' AND 'View' BUTTONS
	 * $apns->queueMessage();
	 *
	 * // CUSTOM 'View' BUTTON
	 * $apns->newMessage(1, '2010-01-01 00:00:00');
	 * $apns->addMessageAlert('Bob wants to play poker', 'PLAY'); // MAKES THE 'View' BUTTON READ 'PLAY'
	 * $apns->queueMessage();
	 *
	 * // NO 'View' BUTTON
	 * $apns->newMessage(1, '2010-01-01 00:00:00');
	 * $apns->addMessageAlert('Bob wants to play poker', ''); // MAKES AN ALERT WITH JUST AN 'OK' BUTTON
	 * $apns->queueMessage();
	 *
	 * // CUSTOM LOCALIZATION STRING FOR YOUR APP
	 * $apns->newMessage(1, '2010-01-01 00:00:00');
	 * $apns->addMessageAlert(NULL, NULL, 'GAME_PLAY_REQUEST_FORMAT', array('Jenna', 'Frank'));
	 * $apns->queueMessage();
	 * ?>
 	 * </code>
	 *
	 * @param int $number
     * @access public
     */
	public function addMessageAlert($alert=NULL, $actionlockey=NULL, $lockey=NULL, $locargs=NULL){
		if(!$this->message) $this->_triggerError('ERROR: Must use newMessage() before calling this method.', E_USER_ERROR);
		if(isset($this->message['aps']['alert'])){
			unset($this->message['aps']['alert']);
			$this->_triggerError('NOTICE: An existing alert was already created but not delivered. The previous alert has been removed.');
		}
		switch(true){
			case (!empty($alert) && empty($actionlockey) && empty($lockey) && empty($locargs)):
				if(!is_string($alert)) $this->_triggerError('ERROR: Invalid Alert Format. See documentation for correct procedure.', E_USER_ERROR);
				$this->message['aps']['alert'] = (string)$alert;
				break;

			case (!empty($alert) && !empty($actionlockey) && empty($lockey) && empty($locargs)):
				if(!is_string($alert)) $this->_triggerError('ERROR: Invalid Alert Format. See documentation for correct procedure.', E_USER_ERROR);
				else if(!is_string($actionlockey)) $this->_triggerError('ERROR: Invalid Action Loc Key Format. See documentation for correct procedure.', E_USER_ERROR);
				$this->message['aps']['alert']['body'] = (string)$alert;
				$this->message['aps']['alert']['action-loc-key'] = (string)$actionlockey;
				break;

			case (empty($alert) && empty($actionlockey) && !empty($lockey) && !empty($locargs)):
				if(!is_string($lockey)) $this->_triggerError('ERROR: Invalid Loc Key Format. See documentation for correct procedure.', E_USER_ERROR);
				$this->message['aps']['alert']['loc-key'] = (string)$lockey;
				$this->message['aps']['alert']['loc-args'] = $locargs;
				break;

			default:
				$this->_triggerError('ERROR: Invalid Alert Format. See documentation for correct procedure.', E_USER_ERROR);
				break;
		}
	}

	/**
	 * Add Message Badge
	 *
	 * <code>
	 * <?php
	 * $db = new DbConnect();
	 * $db->show_errors();
	 * $apns = new APNS($db);
	 * $apns->newMessage(1, '2010-01-01 00:00:00');
	 * $apns->addMessageBadge(9); // HAS TO BE A NUMBER
	 * $apns->queueMessage();
	 * ?>
 	 * </code>
	 *
	 * @param int $number
     * @access public
     */
	public function addMessageBadge($number=NULL){
		if(!$this->message) $this->_triggerError('ERROR: Must use newMessage() before calling this method.', E_USER_ERROR);
		if($number) {
			if(isset($this->message['aps']['badge'])) $this->_triggerError('NOTICE: Message Badge has already been created. Overwriting with '.$number.'.');
			$this->message['aps']['badge'] = (int)$number;
		}
	}

	/**
	 * Add Message Custom
	 *
	 * <code>
	 * <?php
	 * $db = new DbConnect();
	 * $db->show_errors();
	 * $apns = new APNS($db);
	 * $apns->newMessage(1, '2010-01-01 00:00:00');
	 * $apns->addMessageCustom('acme1', 42); // CAN BE NUMBER...
	 * $apns->addMessageCustom('acme2', 'foo'); // ... STRING
	 * $apns->addMessageCustom('acme3', array('bang', 'whiz')); // OR ARRAY
	 * $apns->queueMessage();
	 * ?>
 	 * </code>
	 *
	 * @param string $key Name of Custom Object you want to pass back to your iPhone App
	 * @param mixed $value Mixed Value you want to pass back.  Can be int, bool, string, or array.
     * @access public
     */
	public function addMessageCustom($key=NULL, $value=NULL){
		if(!$this->message) $this->_triggerError('ERROR: Must use newMessage() before calling this method.', E_USER_ERROR);
		if(!empty($key) && !empty($value)) {
			if(isset($this->message[$key])){
				unset($this->message[$key]);
				$this->_triggerError('NOTICE: This same Custom Key already exists and has not been delivered. The previous values have been removed.');
			}
			if(!is_string($key)) $this->_triggerError('ERROR: Invalid Key Format. Key must be a string. See documentation for correct procedure.', E_USER_ERROR);
			$this->message[$key] = $value;
		}
	}

	/**
	 * Add Message Sound
	 *
	 * <code>
	 * <?php
	 * $db = new DbConnect();
	 * $db->show_errors();
	 * $apns = new APNS($db);
	 * $apns->newMessage(1, '2010-01-01 00:00:00');
	 * $apns->addMessageSound('bingbong.aiff'); // STRING OF FILE NAME
	 * $apns->queueMessage();
	 * ?>
 	 * </code>
	 *
	 * @param string $sound Name of sound file in your Resources Directory
     * @access public
     */
	public function addMessageSound($sound=NULL){
		if(!$this->message) $this->_triggerError('ERROR: Must use newMessage() before calling this method.', E_USER_ERROR);
		if($sound) {
			if(isset($this->message['aps']['sound'])) $this->_triggerError('NOTICE: Message Sound has already been created. Overwriting with '.$sound.'.');
			$this->message['aps']['sound'] = (string)$sound;
		}
	}
}
?>

STEP 2: Make a new file named class_DbConnect.php

This is a database wrapper that works well with the APNS Class. Read the comments for settings that changed.

You will need to change the following variables in this file:

  1. (line 109) $this->DB_HOST: Usually "localhost", but change it if not
  2. (line 110) $this->DB_USERNAME: Your MySQL username
  3. (line 111) $this->DB_PASSWORD: Your MySQL Password
  4. (line 112) $this->DB_DATABASE: Your MySQL Database
<?PHP

/**
 * @category Apple Push Notification Service using PHP & MySQL
 * @package APNS
 * @author Peter Schmalfeldt <manifestinteractive@gmail.com>
 * @author John Kramlich <me@johnkramlich.com>
 * @license http://www.apache.org/licenses/LICENSE-2.0
 * @link http://code.google.com/p/easyapns/
 */

/**
 * Begin Document
 */

class DbConnect
{
	/**
	* Connection to MySQL.
	*
	* @var string
	*/
	var $link;

	/**
	* Holds the most recent connection.
	*
	* @var string
	*/
	var $recent_link = null;

	/**
	* Holds the contents of the most recent SQL query.
	*
	* @var string
	*/
	var $sql = '';

	/**
	* Holds the number of queries executed.
	*
	* @var integer
	*/
	var $query_count = 0;

	/**
	* The text of the most recent database error message.
	*
	* @var string
	*/
	var $error = '';

	/**
	* The error number of the most recent database error message.
	*
	* @var integer
	*/
	var $errno = '';

	/**
	* Do we currently have a lock in place?
	*
	* @var boolean
	*/
	var $is_locked = false;

	/**
	* Show errors? If set to true, the error message/sql is displayed.
	*
	* @var boolean
	*/
	var $show_errors = false;

	/**
	* Log errors? If set to true, the error message/sql is logged.
	*
	* @var boolean
	*/
	public $log_errors = false;

	/**
	* The Database.
	*
	* @var string
	*/
	public $DB_DATABASE;

	/**
	* The variable used to contain a singleton instance of the database connection.
	*
	* @var string
	*/
	static $instance;

	/**
	* The number of rows affected by the most recent query.
	*
	* @var string
	*/
	public $affected_rows;

	public $insert_id;

	/**
	* Constructor. Initializes a database connection and selects our database.
	*/
	function __construct()
	{
		$this->DB_HOST     = 'localhost';
		$this->DB_USERNAME = 'MYUSERNAME'; // !!! CHANGE ME
		$this->DB_PASSWORD = 'MYPASSWORD'; // !!! CHANGE ME
		$this->DB_DATABASE = 'MYDATABASE'; // !!! CHANGE ME
	}

	/**
	* Singleton pattern to retrieve database connection.
	*
	* @return mixed	MySQL database connection
	*/
	function _get($property)
	{
		if(self::$instance == NULL)
		{
			self::$instance = $this->connect();
		}

		return self::$instance->$property;

	}

	/**
	* Singleton pattern to retrieve database connection.
	*
	* @return mixed	MySQL database connection
	*/
	function Connection()
	{
		if(self::$instance == NULL)
		{
			self::$instance = $this->connect();
		}
		return self::$instance;
	}

	/**
	* Connect to the Database.
	*
	*/
	function connect()
	{
		self::$instance = new mysqli($this->DB_HOST, $this->DB_USERNAME, $this->DB_PASSWORD, $this->DB_DATABASE);

		if (mysqli_connect_errno()) {
			$this->raise_error(printf("Connect failed: %s\n", mysqli_connect_error()));
		}

		return self::$instance;
	}

	/**
	* Executes a sql query. If optional $only_first is set to true, it will
	* return the first row of the result as an array.
	*
	* @param  string  Query to run
	* @param  bool    Return only the first row, as an array?
	* @return mixed
	*/
	function query($sql, $only_first = false)
	{
		if(self::$instance == NULL)
		{
			self::$instance = $this->connect();
		}

		$this->recent_link =& self::$instance;
		$this->sql =& $sql;

		if(!$result = self::$instance->query($sql))
		{
			$this->raise_error(printf("Connect failed: %s\n", self::$instance->error));
		}

		$this->affected_rows = self::$instance->affected_rows;
		$this->insert_id = self::$instance->insert_id;
		$this->query_count++;

		if ($only_first)
		{
			$return = $result->fetch_array(MYSQLI_ASSOC);
			$this->free_result($result);
			return $return;
		}
		return $result;
	}

	/**
	* Fetches a row from a query result and returns the values from that row as an array.
	*
	* @param  string  The query result we are dealing with.
	* @return array
	*/
	function fetch_array($result)
	{
		return @mysql_fetch_assoc($result);
	}

	/**
	* Returns the number of rows in a result set.
	*
	* @param  string  The query result we are dealing with.
	* @return integer
	*/
	function num_rows($result)
	{
		return self::$instance->num_rows;
	}

	/**
	* Retuns the number of rows affected by the most recent query
	*
	* @return integer
	*/
	function affected_rows()
	{
		return self::$instance->affected_rows;
	}

	/**
	* Returns the number of queries executed.
	*
	* @param  none
	* @return integer
	*/
	function num_queries()
	{
		return $this->query_count;
	}

	/**
	* Lock database tables
	*
	* @param   array  Array of table => lock type
	* @return  void
	*/
	function lock($tables)
	{
		if (is_array($tables) AND count($tables))
		{
			$sql = '';

			foreach ($tables AS $name => $type)
			{
				$sql .= (!empty($sql) ? ', ' : '') . "$name $type";
			}

			$this->query("LOCK TABLES $sql");
			$this->is_locked = true;
		}
	}

	/**
	* Unlock tables
	*/
	function unlock()
	{
		if ($this->is_locked)
		{
			$this->query("UNLOCK TABLES");
		}
	}

	/**
	* Returns the ID of the most recently inserted item in an auto_increment field
	*
	* @return  integer
	*/
	function insert_id()
	{
		return self::$instance->insert_id;
	}

	/**
	* Escapes a value to make it safe for using in queries.
	*
	* @param  string  Value to be escaped
	* @param  bool    Do we need to escape this string for a LIKE statement?
	* @return string
	*/
	function prepare($value, $do_like = false)
	{
		if(self::$instance == NULL)
		{
			self::$instance = $this->connect();
		}

		if ($do_like)
		{
			$value = str_replace(array('%', '_'), array('\%', '\_'), $value);
		}

		return self::$instance->real_escape_string($value);
	}

	/**
	* Frees memory associated with a query result.
	*
	* @param  string   The query result we are dealing with.
	* @return boolean
	*/
	function free_result($result)
	{
		return @mysql_free_result($result);
	}

	/**
	* Turns database error reporting on
	*/
	function show_errors()
	{
		$this->show_errors = true;
	}

	/**
	* Turns database error reporting off
	*/
	function hide_errors()
	{
		$this->show_errors = false;
	}

	/**
	* Closes our connection to MySQL.
	*
	* @param  none
	* @return boolean
	*/
	function close()
	{
		$this->sql = '';
		return self::$instance->close();
	}

	/**
	* Returns the MySQL error message.
	*
	* @param  none
	* @return string
	*/
	function error()
	{
		$this->error = (is_null($this->recent_link)) ? '' : self::$instance->error;
		return $this->error;
	}

	/**
	* Returns the MySQL error number.
	*
	* @param  none
	* @return string
	*/
	function errno()
	{
		$this->errno = (is_null($this->recent_link)) ? 0 : self::$instance->errno ;
		return $this->errno;
	}

	/**
	* Gets the url/path of where we are when a MySQL error occurs.
	*
	* @access private
	* @param  none
	* @return string
	*/
	function _get_error_path()
	{
		if ($_SERVER['REQUEST_URI'])
		{
			$errorpath = $_SERVER['REQUEST_URI'];
		}
		else
		{
			if ($_SERVER['PATH_INFO'])
			{
				$errorpath = $_SERVER['PATH_INFO'];
			}
			else
			{
				$errorpath = $_SERVER['PHP_SELF'];
			}

			if ($_SERVER['QUERY_STRING'])
			{
				$errorpath .= '?' . $_SERVER['QUERY_STRING'];
			}
		}

		if (($pos = strpos($errorpath, '?')) !== false)
		{
			$errorpath = urldecode(substr($errorpath, 0, $pos)) . substr($errorpath, $pos);
		}
		else
		{
			$errorpath = urldecode($errorpath);
		}
		return $_SERVER['HTTP_HOST'] . $errorpath;
	}

	/**
	* If there is a database error, the script will be stopped and an error message displayed.
	*
	* @param  string  The error message. If empty, one will be built with $this->sql.
	* @return string
	*/
	function raise_error($error_message = '')
	{
		if ($this->recent_link)
		{
			$this->error = $this->error($this->recent_link);
			$this->errno = $this->errno($this->recent_link);
		}

		if ($error_message == '')
		{
			$this->sql = "Error in SQL query:\n\n" . rtrim($this->sql) . ';';
			$error_message =& $this->sql;
		}
		else
		{
			$error_message = $error_message . ($this->sql != '' ? "\n\nSQL:" . rtrim($this->sql) . ';' : '');
		}

		$message = "<textarea rows=\"10\" cols=\"80\">MySQL Error:\n\n\n$error_message\n\nError: {$this->error}\nError #: {$this->errno}\nFilename: " . $this->_get_error_path() . "\n</textarea>";

		if (!$this->show_errors)
		{
			$message = "<!--\n\n$message\n\n-->";
		}
		else die("There seems to have been a slight problem with our database, please try again later.<br /><br />\n$message");
	}
}

?>

STEP 3: Make a new file named apns.php

This file will be used to register devices and push notifications.

#!/usr/bin/php
<?PHP

/**
 * @category Apple Push Notification Service using PHP & MySQL
 * @package APNS
 * @author Peter Schmalfeldt <manifestinteractive@gmail.com>
 * @license http://www.apache.org/licenses/LICENSE-2.0
 * @link http://code.google.com/p/easyapns/
 */

/**
 * Begin Document
 */

// AUTOLOAD CLASS OBJECTS... YOU CAN USE INCLUDES IF YOU PREFER
if(!function_exists("__autoload")){
	function __autoload($class_name){
		require_once('classes/class_'.$class_name.'.php');
	}
}

// CREATE DATABASE OBJECT ( MAKE SURE TO CHANGE LOGIN INFO IN CLASS FILE )
$db = new DbConnect();
$db->show_errors();

// FETCH $_GET OR CRON ARGUMENTS TO AUTOMATE TASKS
$args = (!empty($_GET)) ? $_GET:array('task'=>$argv[1]);

// CREATE APNS OBJECT, WITH DATABASE OBJECT AND ARGUMENTS
$apns = new APNS($db, $args);
?>

Adding a Message:

The first three steps take care of all the backend work for your server. But all of this would be for nothing, if you cannot send a message to your user... so here is how to do that:

<?PHP

/**
 * @category Apple Push Notification Service using PHP & MySQL
 * @package APNS
 * @author Peter Schmalfeldt <manifestinteractive@gmail.com>
 * @license http://www.apache.org/licenses/LICENSE-2.0
 * @link http://code.google.com/p/easyapns/
 */

/**
 * Begin Document
 */

// AUTOLOAD CLASS OBJECTS... YOU CAN USE INCLUDES IF YOU PREFER
if(!function_exists("__autoload")){
	function __autoload($class_name){
		require_once('classes/class_'.$class_name.'.php');
	}
}

// CREATE DATABASE OBJECT ( MAKE SURE TO CHANGE LOGIN INFO IN CLASS FILE )
$db = new DbConnect();
$db->show_errors();

// FETCH $_GET OR CRON ARGUMENTS TO AUTOMATE TASKS
$apns = new APNS($db);

/**
/*	ACTUAL SAMPLES USING THE 'Examples of JSON Payloads' EXAMPLES (1-5) FROM APPLE'S WEBSITE.
 *	LINK:  http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW15
 */

// APPLE APNS EXAMPLE 1
$apns->newMessage(1);
$apns->addMessageAlert('Message received from Bob');
$apns->addMessageCustom('acme2', array('bang', 'whiz'));
$apns->queueMessage();

// APPLE APNS EXAMPLE 2
$apns->newMessage(1, '2010-01-01 00:00:00'); // FUTURE DATE NOT APART OF APPLE EXAMPLE
$apns->addMessageAlert('Bob wants to play poker', 'PLAY');
$apns->addMessageBadge(5);
$apns->addMessageCustom('acme1', 'bar');
$apns->addMessageCustom('acme2', array('bang', 'whiz'));
$apns->queueMessage();

// APPLE APNS EXAMPLE 3
$apns->newMessage(1);
$apns->addMessageAlert('You got your emails.');
$apns->addMessageBadge(9);
$apns->addMessageSound('bingbong.aiff');
$apns->addMessageCustom('acme1', 'bar');
$apns->addMessageCustom('acme2', 42);
$apns->queueMessage();

// APPLE APNS EXAMPLE 4
$apns->newMessage(1, '2010-01-01 00:00:00');  // FUTURE DATE NOT APART OF APPLE EXAMPLE
$apns->addMessageAlert(NULL, NULL, 'GAME_PLAY_REQUEST_FORMAT', array('Jenna', 'Frank'));
$apns->addMessageSound('chime');
$apns->addMessageCustom('acme', 'foo');
$apns->queueMessage();

// APPLE APNS EXAMPLE 5
$apns->newMessage(1);
$apns->addMessageCustom('acme2', array(5, 8));
$apns->queueMessage();

?>

Sending Messages:

Now that you have some messages added to your queue, you can use a cron job to automatically send your messages for you.