├── .gitignore ├── tests ├── bootstrap.php └── Wrep │ └── Notificato │ └── Test │ ├── Apns │ ├── Mock │ │ ├── MockGatewayFactory.php │ │ └── MockGateway.php │ ├── Feedback │ │ ├── FeedbackTest.php │ │ └── TupleTest.php │ ├── MessageEnvelopeTest.php │ ├── SenderTest.php │ ├── GatewayTest.php │ ├── CertificateTest.php │ └── MessageTest.php │ ├── resources │ └── certificate_corrupt.pem │ └── NotificatoTest.php ├── src └── Wrep │ └── Notificato │ ├── Apns │ ├── Exception │ │ ├── ValidationException.php │ │ └── InvalidCertificateException.php │ ├── GatewayFactory.php │ ├── entrust_2048_ca.pem │ ├── CertificateFactory.php │ ├── Feedback │ │ ├── FeedbackFactory.php │ │ ├── Tuple.php │ │ └── Feedback.php │ ├── SslSocket.php │ ├── Sender.php │ ├── MessageEnvelope.php │ ├── MessageBuilder.php │ ├── Gateway.php │ ├── Certificate.php │ └── Message.php │ └── Notificato.php ├── .travis.yml ├── sami.config.php ├── phpunit.xml.dist ├── composer.json ├── License ├── Upgrade.md ├── doc ├── Readme.md ├── feedback.md ├── certificate.md ├── multiple-certs.md └── push.md ├── Contribute.md └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /vendor/ 3 | /composer.lock 4 | /tests/Wrep/Notificato/Test/resources/paspas.pem -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('Wrep\Notificato\Test', __DIR__); -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/Exception/ValidationException.php: -------------------------------------------------------------------------------- 1 | add('master', 'master branch') 8 | ->addFromTags('1.*') 9 | ; 10 | 11 | return new Sami(dirname(__FILE__) . '/src', array( 'title' => 'Notificato API', 12 | 'build_dir' => dirname(__FILE__) . '/../notificato-apidocs/%version%', 13 | 'cache_dir' => dirname(__FILE__) . '/../notificato-apidocs/cache/%version%', 14 | 'versions' => $versions)); 15 | -------------------------------------------------------------------------------- /tests/Wrep/Notificato/Test/Apns/Mock/MockGateway.php: -------------------------------------------------------------------------------- 1 | sendQueue->isEmpty()) 14 | { 15 | // Get the next message to send 16 | $messageEnvelope = $this->sendQueue->dequeue(); 17 | $messageEnvelope->setStatus(MessageEnvelope::STATUS_NOERRORS); 18 | } 19 | } 20 | 21 | public function getMessageEnvelopeStore() 22 | { 23 | return $this->messageEnvelopeStore; 24 | } 25 | 26 | public function retrieveMessageEnvelope($identifier) 27 | { 28 | return parent::retrieveMessageEnvelope($identifier); 29 | } 30 | } -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ./tests/ 20 | 21 | 22 | 23 | 24 | 25 | slow 26 | realpush 27 | 28 | 29 | 30 | 31 | 32 | ./vendor 33 | ./tests 34 | 35 | 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wrep/notificato", 3 | "type": "library", 4 | "description": "Takes care of push notifications in your PHP projects.", 5 | "homepage": "https://github.com/mac-cain13/notificato", 6 | "keywords": ["push", "notifications", "pushnotifications", "push notifications", "apns", "aps", "ios"], 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Mathijs Kadijk", 11 | "email": "mkadijk@gmail.com" 12 | } 13 | ], 14 | "support": { 15 | "issues": "https://github.com/mac-cain13/notificato/issues" 16 | }, 17 | "require": { 18 | "php": ">=5.4", 19 | "ext-spl": "*", 20 | "ext-sockets": "*", 21 | "ext-openssl": "*", 22 | "lib-openssl": "*", 23 | "psr/log": "1.0.*" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "4.4.*", 27 | "sami/sami": "dev-master" 28 | }, 29 | "suggest": { 30 | "wrep/notificato-symfony": "Integrate Notificato into Symfony" 31 | }, 32 | "replace": { 33 | "wrep/notificare": "*" 34 | }, 35 | "autoload": { 36 | "psr-0": { "Wrep\\Notificato\\": "src/" } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Wrep/Notificato/Test/resources/certificate_corrupt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIS2qgprFqPxECAggA 3 | MBQGCCqGSIb3DQMHBAgD1kGN4ZslJgSCBMi1xk9jhlPxP3FyaMIUq8QmckXCs3Sa 4 | 9g73NQbtqZwI+9X5OhpSg/2ALxlCCjbqvzgSu8gfFZ4yo+Xd8VucZDmDSpzZGDod 5 | A .... MANY LINES LIKE THAT .... .... MANY LINES LIKE THAT .... 6 | X0R+meOaudPTBxoSgCCM51poFgaqt4l6VlTN4FRpj+c/WZeoMM/BVXO+nayuIMyH 7 | blK948UAda/bWVmZjXfY4Tztah0CuqlAldOQBzu8TwE7WDwo5S7lo5u0EXEoqCCq 8 | H0ga/iLNvWYexG7FHLRiq5hTj0g9mUPEbeTXuPtOkTEb/0ckVE2iZH9l7g5edmUZ 9 | GEs= 10 | -----END ENCRYPTED PRIVATE KEY----- 11 | -----BEGIN CERTIFICATE----- 12 | MIIDXTCCAkWgAwIBAgIJAJC1HiIAZAiIMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 13 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 14 | aWRnaXRzIFB0eSBMdGQwHhcNMTExMjMxMDg1OTQ0WhcNMTIxMjMwMDg1OTQ0WjBF 15 | A .... MANY LINES LIKE THAT .... .... MANY LINES LIKE THAT .... 16 | JjyzfN746vaInA1KxYEeI1Rx5KXY8zIdj6a7hhphpj2E04LDdw7r495dv3UgEgpR 17 | C3Fayua4DRHyZOLmlvQ6tIChY0ClXXuefbmVSDeUHwc8YufRAERp2GfQnL2JlPUL 18 | B7xxt8BVc69rLeHV15A0qyx77CLSj3tCx2IUXVqRs5mlSbq094NBxsauYcm0A6Jq 19 | vA== 20 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2015 Mathijs Kadijk - https://github.com/mac-cain13 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 9 | to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or 12 | substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 16 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 17 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/Wrep/Notificato/Test/Apns/Feedback/FeedbackTest.php: -------------------------------------------------------------------------------- 1 | certificate = new Certificate(__DIR__ . '/../../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 16 | $this->feedback = new Feedback($this->certificate); 17 | } 18 | 19 | public function testGetCertificate() 20 | { 21 | $this->assertEquals($this->certificate, $this->feedback->getCertificate(), 'Incorrect certificate after constuction'); 22 | } 23 | 24 | /** 25 | * @group realpush 26 | */ 27 | public function testFeedback() 28 | { 29 | $this->certificate = new Certificate(__DIR__ . '/../../resources/paspas.pem'); 30 | $this->feedback = new Feedback($this->certificate); 31 | 32 | $tuples = $this->feedback->receive(); 33 | $this->assertTrue(is_array($tuples), 'Tuples should be in an array'); 34 | //print_r($tuples); // This is quite usefull to see if there is something comming back from Apple 35 | } 36 | } -------------------------------------------------------------------------------- /Upgrade.md: -------------------------------------------------------------------------------- 1 | # Upgrade guide 2 | This document gives an overview of what's changed between versions. Backwards incompatible changes are described as wel as the most important new features. 3 | 4 | ## From 1.1 to 1.2 5 | 6 | No breaking changes, just support for the new production certificates from Apple. 7 | 8 | ## From 1.0 to 1.1 9 | 10 | ### Message creation 11 | 12 | **Backwards incompatible changes:** 13 | 14 | * The `Message`-object is now read-only, all setters are removed 15 | * The `MessageBuilder` class is introduced to create `Message`-objects 16 | * The `MessageFactory` class is removed in favour of the `MessageBuilder`-class 17 | * The `Message::validateLength` method is gone, as the constructor now validates the length on creation 18 | 19 | *New features:* 20 | 21 | * `Message` is now serializable for easy storage 22 | * `Message::__toString` is implemented and dumps the contents of the message for debugging 23 | 24 | ### Certificates 25 | 26 | **Backwards incompatible changes:** 27 | 28 | * If the certificate is invalid the class now throws an `InvalidCertificateException` instead of `InvalidArgumentException` 29 | 30 | *New features:* 31 | 32 | * `Certificate::isValidated` is introduced and indicates if the certificate was validated on construction 33 | 34 | ### Sending and status 35 | 36 | **Backwards incompatible changes:** 37 | 38 | * `MessageEnvelope::STATUS_PAYLOADTOOLONG` status is removed as constructing messages immediatly throws an exception -------------------------------------------------------------------------------- /doc/Readme.md: -------------------------------------------------------------------------------- 1 | # Notificato documentation 2 | We want Notificato to be straight forward and easy to use, but we believe that a library should also be as flexible and powerful as possible to not block you from using it in unexpected situations. This docs try to give you an overview of how to use Notificato, from the "Getting Started" examples to some of the more complex use cases. 3 | 4 | ## Topics 5 | 6 | 1. [Howto generate an Apple Push Notification Service certificate](certificate.md) 7 | 2. [Pushing messages](push.md) 8 | 3. [Reading the feedback service](feedback.md) 9 | 4. [Using multiple certificates](multiple-certs.md) 10 | 11 | ## More resources 12 | 13 | 1. [Notificato API documentation](http://mac-cain13.github.com/notificato/master/) 14 | 2. [Apples Local and Push Notification Programming Guide](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW9) 15 | 3. [Apple on Provisioning and Development](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ProvisioningDevelopment/ProvisioningDevelopment.html#//apple_ref/doc/uid/TP40008194-CH104-SW1) 16 | 17 | ## Still have questions? 18 | If you have read the docs and still have some questions feel free to [submit an issue](https://github.com/mac-cain13/notificato/issues/new). We'll try to get back at you as soon as possible, but please keep in mind this is a sideproject! 19 | -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/entrust_2048_ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u 3 | ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp 4 | bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV 5 | BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx 6 | NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 7 | d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl 8 | MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u 9 | ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 10 | MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL 11 | Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr 12 | hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW 13 | nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi 14 | VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E 15 | BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ 16 | KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy 17 | T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf 18 | zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT 19 | J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e 20 | nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE= 21 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/CertificateFactory.php: -------------------------------------------------------------------------------- 1 | setDefaultCertificate( $this->createCertificate($pemFile, $passphrase, $validate, $endpointEnv) ); 24 | } 25 | } 26 | 27 | /** 28 | * Set the default certificate 29 | * 30 | * @param Certificate|null The certificate to use as default 31 | */ 32 | public function setDefaultCertificate(Certificate $defaultCertificate = null) 33 | { 34 | $this->defaultCertificate = $defaultCertificate; 35 | } 36 | 37 | /** 38 | * Get the current default certificate 39 | * 40 | * @return Certificate|null 41 | */ 42 | public function getDefaultCertificate() 43 | { 44 | return $this->defaultCertificate; 45 | } 46 | 47 | /** 48 | * Create a Certificate 49 | * 50 | * @param string Path to the PEM certificate file 51 | * @param string|null Passphrase to use with the PEM file 52 | * @param boolean Set to false to skip the validation of the certificate, default true 53 | * @param string|null APNS environment this certificate is valid for, by default autodetects during validation 54 | * @return Certificate 55 | */ 56 | public function createCertificate($pemFile, $passphrase = null, $validate = true, $endpointEnv = null) 57 | { 58 | return new Certificate($pemFile, $passphrase, $validate, $endpointEnv); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/Feedback/FeedbackFactory.php: -------------------------------------------------------------------------------- 1 | setCertificateFactory($certificateFactory); 20 | } 21 | 22 | /** 23 | * Set a certificate factory to fetch the default certificate from 24 | * 25 | * @param CertificateFactory|null The certificate factory to use when no specific certificate is given on feedback creation 26 | */ 27 | public function setCertificateFactory(CertificateFactory $certificateFactory = null) 28 | { 29 | $this->certificateFactory = $certificateFactory; 30 | } 31 | 32 | /** 33 | * Get the current certificate factory 34 | * 35 | * @return CertificateFactory|null 36 | */ 37 | public function getCertificateFactory() 38 | { 39 | return $this->certificateFactory; 40 | } 41 | 42 | /** 43 | * Create a Feedback object 44 | * 45 | * @param Certificate|null The certificate to use or null to use the default certificate from the given certificate factory 46 | * @return Feedback 47 | */ 48 | public function createFeedback(Certificate $certificate = null) 49 | { 50 | // Check if a certificate is given, if not use the default certificate 51 | if (null == $certificate && $this->getCertificateFactory() != null) { 52 | $certificate = $this->getCertificateFactory()->getDefaultCertificate(); 53 | } 54 | 55 | // Check if there is a certificate to use after falling back on the default certificate 56 | if (null == $certificate) { 57 | throw new \RuntimeException('No certificate given for the creation of the feedback service and no default certificate available.'); 58 | } 59 | 60 | return new Feedback($certificate); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/Feedback/Tuple.php: -------------------------------------------------------------------------------- 1 | 0, ' . $invalidatedAtTimestamp . ' given.'); 31 | } 32 | 33 | // Check if a devicetoken is given 34 | if (null == $deviceToken) { 35 | throw new \InvalidArgumentException('No device token given.'); 36 | } 37 | 38 | // Check if the devicetoken is a valid hexadecimal string 39 | if (!ctype_xdigit($deviceToken)) { 40 | throw new \InvalidArgumentException('Invalid device token given, no hexadecimal: ' . $deviceToken); 41 | } 42 | 43 | // Save the data 44 | $this->invalidatedAt = new \DateTime('@' . (int)$invalidatedAtTimestamp); 45 | $this->deviceToken = $deviceToken; 46 | $this->certificate = $certificate; 47 | } 48 | 49 | /** 50 | * Moment the device unregistered. 51 | * Note: Check if the device didn't re-register after this moment before deleting it! 52 | * Note: This DateTime object is in the UTC timezone. 53 | * 54 | * @return \DateTime 55 | */ 56 | public function getInvalidatedAt() 57 | { 58 | return $this->invalidatedAt; 59 | } 60 | 61 | /** 62 | * Get the device token of the device that unregistered 63 | * 64 | * @return string 65 | */ 66 | public function getDeviceToken() 67 | { 68 | return $this->deviceToken; 69 | } 70 | 71 | /** 72 | * Get the certificate used while receiving this tuple 73 | * 74 | * @return Certificate 75 | */ 76 | public function getCertificate() 77 | { 78 | return $this->certificate; 79 | } 80 | } -------------------------------------------------------------------------------- /tests/Wrep/Notificato/Test/Apns/Feedback/TupleTest.php: -------------------------------------------------------------------------------- 1 | certificate = new Certificate(__DIR__ . '/../../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 16 | $this->tuple = new Tuple(1362432924, 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $this->certificate); 17 | } 18 | 19 | public function testGetInvalidatedAt() 20 | { 21 | $this->assertEquals(new \DateTime('@1362432924'), $this->tuple->getInvalidatedAt(), 'Incorrect invalidation moment after constuction'); 22 | } 23 | 24 | public function testGetDeviceToken() 25 | { 26 | $this->assertEquals('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $this->tuple->getDeviceToken(), 'Incorrect device token after constuction'); 27 | } 28 | 29 | public function testGetCertificate() 30 | { 31 | $this->assertEquals($this->certificate, $this->tuple->getCertificate(), 'Incorrect certificate after constuction'); 32 | } 33 | 34 | public function testNoInvalidatedAtTimestamp() 35 | { 36 | $this->setExpectedException('\InvalidArgumentException', 'Invalidated at timestamp must be > 0'); 37 | new Tuple(null, 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $this->certificate); 38 | } 39 | 40 | public function testInvalidInvalidatedAtTimestamp() 41 | { 42 | $this->setExpectedException('\InvalidArgumentException', 'Invalidated at timestamp must be > 0'); 43 | new Tuple(0, 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $this->certificate); 44 | } 45 | 46 | public function testNoDevicetokenTimestamp() 47 | { 48 | $this->setExpectedException('\InvalidArgumentException', 'No device token given.'); 49 | new Tuple(1362432924, null, $this->certificate); 50 | } 51 | 52 | public function testNoHexDevicetokenTimestamp() 53 | { 54 | $this->setExpectedException('\InvalidArgumentException', 'Invalid device token given, no hexadecimal'); 55 | new Tuple(1362432924, 'qfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $this->certificate); 56 | } 57 | 58 | public function testToShortDevicetokenTimestamp() 59 | { 60 | $this->setExpectedException('\InvalidArgumentException', 'Invalid device token given, incorrect length'); 61 | new Tuple(1362432924, 'fff', $this->certificate); 62 | } 63 | } -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/Feedback/Feedback.php: -------------------------------------------------------------------------------- 1 | connect(Certificate::ENDPOINT_TYPE_FEEDBACK); 33 | 34 | // Read all data from the feedback service 35 | while ( !feof($this->getConnection()) ) 36 | { 37 | // Make sure signals to this process are respected and handled 38 | if (function_exists('pcntl_signal_dispatch')) { 39 | pcntl_signal_dispatch(); 40 | } 41 | 42 | // Fetch the available feedback data from the socket 43 | $feedbackData .= fread($this->getConnection(), 16384); 44 | 45 | // Loop over all tuples in the current feedbackdata 46 | while (strlen($feedbackData) >= Tuple::BINARY_LENGTH) 47 | { 48 | // Get the first tuple out of the data and unpack the data 49 | $binaryTupleData = substr($feedbackData, 0, Tuple::BINARY_LENGTH); 50 | $tupleData = unpack('Ntimestamp/ntokenLength/H*deviceToken', $binaryTupleData); 51 | 52 | // Create a tuple object from it 53 | $tuples[] = new Tuple($tupleData['timestamp'], $tupleData['deviceToken'], $this->getCertificate()); 54 | 55 | // Remove the tuple from the feedbackdata 56 | $feedbackData = substr($feedbackData, Tuple::BINARY_LENGTH); 57 | } 58 | 59 | // All messages send, wait some time for an APNS response 60 | $read = array($this->getConnection()); 61 | $write = $except = null; 62 | $changedStreams = stream_select($read, $write, $except, 0, self::READ_TIMEOUT); 63 | 64 | // Did waiting for the response succeed? 65 | if (false === $changedStreams) 66 | { 67 | // We'll just stop reading and do not throw an error, because we don't want to loose any tuples 68 | break; 69 | } 70 | } 71 | 72 | // And we're done, disconnect from the service 73 | $this->disconnect(); 74 | 75 | $this->logger->info('Apns\Feedback recieved ' . count($tuples) . ' tuples from APNS feedback service using certificate "' . $this->getCertificate()->getFingerprint() . '"'); 76 | 77 | return $tuples; 78 | } 79 | } -------------------------------------------------------------------------------- /doc/feedback.md: -------------------------------------------------------------------------------- 1 | # Reading the APNS feedback service 2 | After you've started [pushing messages](push.md) you have to check what devices unregistered for your notifications. Note that Apple [monitors providers](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3) for their diligence in checking the feedback service. So it's just as important to implement this feedback service as it is to get sending the pushmessages! 3 | 4 | ## Receiving feedback 5 | This example will show you how to read the feedback service: 6 | ```php 7 | // First we get the a Notificato instance and tell it what certificate to use as default certificate 8 | $notificato = new Notificato('./certificate.pem', 'passphrase-to-use'); 9 | 10 | // Now read all "tuples" from the feedback service, be aware that this method is blocking 11 | $tuples = $notificato->receiveFeedback(); 12 | 13 | // The tuples contain information about what device unregistered and when it did unregister. 14 | // Don't forget to check if the device reregistered after the "invalidated at" date! 15 | foreach ($tuples as $tuple) 16 | { 17 | echo 'Device ' . $tuple->getDeviceToken() . ' invalidated at ' . $tuple->getInvalidatedAt()->format(\DateTime::ISO8601) . PHP_EOL; 18 | } 19 | ``` 20 | 21 | ## How often should I check for feedback? 22 | As always, it depends. The most important thing is that you do check it. 23 | 24 | If you just send a few messages to a few devices a few times a day a cronjob running every night processing the feedback would be fine. You won't be sending to many messages to unregistered devices and this is an easy solution to implement. 25 | 26 | A somewhat more high profile webservice that sends more often to more users also benefits from reading the service more often. Discarding unused tokens will improve sending performance. An easy solution would be to read the feedback service once an hour with an cronjob. A more advanced setup could read feedback after sending a batch of messages. 27 | 28 | ## How fast does a token show up? 29 | We see that Apple does update the feedback service a few moments after you've send a message to a non-existing device token or after the device unregistered itself. Let's say they take one minute to update the feedback service. (This is just our experience no guarantees!) 30 | 31 | ## Do I need to implement this if I only push to Passbook Passes? 32 | Yes, you do. Passbook Passes will unregister themself with the webservice you provide the passes with, but it is possible the pass can't unregister properly. For example because the device is reset without an internet connection available. The feedback service will help you to cleanup these kind of tokens. -------------------------------------------------------------------------------- /tests/Wrep/Notificato/Test/Apns/MessageEnvelopeTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('\Wrep\Notificato\Apns\Message') 15 | ->disableOriginalConstructor() 16 | ->getMock(); 17 | 18 | $this->messageEnvelope = new MessageEnvelope(1, $message); 19 | } 20 | 21 | public function testIdentifier() 22 | { 23 | $this->assertEquals(1, $this->messageEnvelope->getIdentifier()); 24 | } 25 | 26 | /** 27 | * @dataProvider illigalConstructionArguments 28 | */ 29 | public function testIlligalConstuction($identifier, $message) 30 | { 31 | $this->setExpectedException('InvalidArgumentException', 'is invalid, must be an integer above zero.'); 32 | new MessageEnvelope($identifier, $message); 33 | } 34 | 35 | public function illigalConstructionArguments() 36 | { 37 | $message = $this->getMockBuilder('\Wrep\Notificato\Apns\Message') 38 | ->disableOriginalConstructor() 39 | ->getMock(); 40 | 41 | return array( 42 | array(0, $message), 43 | array(-1, $message) 44 | ); 45 | } 46 | 47 | public function testInitialStatus() 48 | { 49 | $this->assertEquals(-1, $this->messageEnvelope->getStatus()); 50 | } 51 | 52 | public function testChangeStatus() 53 | { 54 | $this->messageEnvelope->setStatus(0); 55 | $this->assertEquals(0, $this->messageEnvelope->getStatus()); 56 | } 57 | 58 | public function testChangeToIllegalStatus() 59 | { 60 | $this->setExpectedException('InvalidArgumentException', 'is not a valid status.'); 61 | $this->messageEnvelope->setStatus(987); 62 | $this->assertEquals(-1, $this->messageEnvelope->getStatus()); 63 | } 64 | 65 | public function testChangeFinalState() 66 | { 67 | $this->messageEnvelope->setStatus(257); 68 | 69 | $this->setExpectedException('RuntimeException', 'Cannot change status from final state'); 70 | $this->messageEnvelope->setStatus(8); 71 | 72 | $this->assertEquals(257, $this->messageEnvelope->getStatus()); 73 | } 74 | 75 | public function testStatusDescription() 76 | { 77 | $this->messageEnvelope->setStatus(MessageEnvelope::STATUS_EARLIERERROR); 78 | $this->assertEquals('Failed due earlier error, will retry with other envelope', $this->messageEnvelope->getStatusDescription()); 79 | } 80 | 81 | public function testFinalStatus() 82 | { 83 | $retryEnvelope = new MessageEnvelope(2, $this->messageEnvelope->getMessage()); 84 | $this->messageEnvelope->setStatus(257, $retryEnvelope); 85 | 86 | $retryEnvelope->setStatus(8); 87 | $this->assertEquals(8, $this->messageEnvelope->getFinalStatus()); 88 | } 89 | 90 | public function testFinalStatusDescription() 91 | { 92 | $retryEnvelope = new MessageEnvelope(2, $this->messageEnvelope->getMessage()); 93 | $this->messageEnvelope->setStatus(257, $retryEnvelope); 94 | 95 | $retryEnvelope->setStatus(8); 96 | $this->assertEquals('[APNS] Invalid token', $this->messageEnvelope->getFinalStatusDescription()); 97 | } 98 | 99 | public function testSetOurselfsAsRetryEnvelope() 100 | { 101 | $this->setExpectedException('InvalidArgumentException', 'Retry envelope cannot be set to this envelope.'); 102 | $this->messageEnvelope->setStatus(0, $this->messageEnvelope); 103 | } 104 | } -------------------------------------------------------------------------------- /tests/Wrep/Notificato/Test/Apns/SenderTest.php: -------------------------------------------------------------------------------- 1 | sender = new Sender(); 21 | $this->sender->setGatewayFactory(new MockGatewayFactory()); 22 | } 23 | 24 | private function getCertificate($fingerprint) 25 | { 26 | // Create cert 27 | $certificate = $this->getMockBuilder('\Wrep\Notificato\Apns\Certificate') 28 | ->disableOriginalConstructor() 29 | ->getMock(); 30 | $certificate->expects($this->any()) 31 | ->method('getFingerprint') 32 | ->will($this->returnValue($fingerprint)); 33 | 34 | return $certificate; 35 | } 36 | 37 | private function getCertificateFactory($defaultCertificate) 38 | { 39 | // Create cert 40 | $certificateFactory = $this->getMockBuilder('\Wrep\Notificato\Apns\CertificateFactory') 41 | ->disableOriginalConstructor() 42 | ->getMock(); 43 | $certificateFactory->expects($this->any()) 44 | ->method('getDefaultCertificate') 45 | ->will($this->returnValue($defaultCertificate)); 46 | 47 | return $certificateFactory; 48 | } 49 | 50 | public function testSend() 51 | { 52 | $message = $this->getMockBuilder('\Wrep\Notificato\Apns\Message') 53 | ->disableOriginalConstructor() 54 | ->getMock(); 55 | $message->expects($this->any()) 56 | ->method('getCertificate') 57 | ->will($this->returnValue( $this->getCertificate('a') )); 58 | 59 | $this->assertEquals(0, $this->sender->getQueueLength()); 60 | 61 | $messageEnvelope = $this->sender->send($message); 62 | 63 | $this->assertEquals(0, $this->sender->getQueueLength()); 64 | $this->assertEquals(MessageEnvelope::STATUS_NOERRORS, $messageEnvelope->getStatus()); 65 | } 66 | 67 | public function testQueueAndFlush() 68 | { 69 | $messageA = $this->getMockBuilder('\Wrep\Notificato\Apns\Message') 70 | ->disableOriginalConstructor() 71 | ->getMock(); 72 | $messageA->expects($this->any()) 73 | ->method('getCertificate') 74 | ->will($this->returnValue( $this->getCertificate('a') )); 75 | $messageB = $this->getMockBuilder('\Wrep\Notificato\Apns\Message') 76 | ->disableOriginalConstructor() 77 | ->getMock(); 78 | $messageB->expects($this->any()) 79 | ->method('getCertificate') 80 | ->will($this->returnValue( $this->getCertificate('b') )); 81 | $messageC = $this->getMockBuilder('\Wrep\Notificato\Apns\Message') 82 | ->disableOriginalConstructor() 83 | ->getMock(); 84 | $messageC->expects($this->any()) 85 | ->method('getCertificate') 86 | ->will($this->returnValue( $this->getCertificate('c') )); 87 | 88 | // Connect and queue the messages 89 | $sender = new Sender( $this->getCertificate('a') ); 90 | $sender->setGatewayFactory(new MockGatewayFactory()); 91 | 92 | for ($i = 1; $i <= 5; $i++) 93 | { 94 | $sender->queue($messageA); 95 | $sender->queue($messageB); 96 | $sender->queue($messageC); 97 | $this->assertEquals($i * 3, $sender->getQueueLength()); 98 | } 99 | 100 | // Send the messages 101 | $sender->flush(); 102 | $this->assertEquals(0, $sender->getQueueLength()); 103 | } 104 | } -------------------------------------------------------------------------------- /Contribute.md: -------------------------------------------------------------------------------- 1 | # Thank you! 2 | 3 | Thank you for taking some of your precious time helping this project move forward. Really great that you're showing interest in contributing. 4 | 5 | This guide will help you get started with contributing to Notificato. You don't have to be an expert to help us, every small tweak, crazy idea and/or bugreport is highly appreciated! 6 | 7 | # Contributing 8 | 9 | ## Team members 10 | 11 | * Mathijs Kadijk / [mac-cain13](https://github.com/mac-cain13) - Lead development 12 | * Rick Pastoor / [rickpastoor](https://github.com/rickpastoor) - Development (also initiator of the [Symfony2 bundle](https://github.com/wrep/notificato-symfony)) 13 | 14 | ## Learn & listen 15 | 16 | This section includes ways to get started with this open source project. Most important is to read the docs and scan the issue tracker before so you're sure your idea/bugreport/patch isn't already in the make/being fixed: 17 | 18 | * [Readme.md](Readme.md) 19 | * [Notificato Documentation](doc/Readme.md) 20 | * [Notificato API Documentation](http://wrep.github.com/notificato/master/) 21 | * [Issue tracker](https://github.com/wrep/notificato/issues) 22 | 23 | ## Adding new features 24 | 25 | This section includes some advice on how to build new features & what kind of you should . 26 | 27 | * Fork the project and **make you changes against the development branch**. 28 | * Follow the currently used coding style 29 | * Make sure the inline code comments are up to date 30 | * Write a test for the new features 31 | * Make sure all tests pass 32 | * Submit a pull request and clearly state what you've added! 33 | 34 | Don't know if your test is good enough or not sure your change is good enough? Don't hesitate to submit an issue or send the incomplete pull request. We'll take a look, point you in the right direction or make some corrections, no problem! 35 | 36 | So don’t get discouraged! We estimate that the response time from the maintainers is around: 2/3 days 37 | 38 | # Bug triage 39 | 40 | * You can help report bugs by filing them here: https://github.com/wrep/notificato/issues/new 41 | * You can look through the existing bugs here: https://github.com/wrep/notificato/issues 42 | 43 | * Look at existing bugs and help us understand if: 44 | ** The bug is reproducible? What are the steps to reproduce? 45 | 46 | * Tips for a great bugreport: 47 | * State what you expected 48 | * Describe what happend instead 49 | * Give the steps to reproduce the bug 50 | * Include your PHP version 51 | * Include the Notificato version your using 52 | * If you know a workaround/fix please include it 53 | 54 | # Documentation 55 | 56 | Code needs explanation, and sometimes those who know the code well have trouble explaining it to someone just getting into it. If you find something unclear, incorrect or missing from the documentation please file a bugreport! 57 | 58 | * You can help us improve the documentation by: 59 | * Filing a bugreport about your problem 60 | * Forking the project and submit a pull request with expanded/corrected docs 61 | * Typo corrections are also welcome 62 | 63 | # Community 64 | If you write a blog about Notificato, use it in your project, recommended it to a friend or feel it completes your life, please feel free to send me a mail at mkadijk@gmail.com. I'll see if I can mention projects/blogs somewhere and it's very motivating to hear something from happy users. 65 | 66 | *Please don't mail me for support, but use the issuetracker and StackOverflow/forums for that so more people can help, thanks!* 67 | 68 | # Your first bugfix 69 | A very quick and dirty overview of the steps to take to submit your first bugfix: 70 | 71 | * Click fork here on Github and check out the repository 72 | * Run `composer install --dev` to install dependencies 73 | * Run the tests with `php ./vendor/bin/phpunit` (To check if anything is broken atm) 74 | * Go code your bugfix/feature and use the tests to test if it works 75 | * Make sure you've done the things stated in "Adding new features" 76 | * Commit and push it back to your fork 77 | * Click the "Pull request"-button and write a nice message what you've done 78 | * Wait for us to reply! :) -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Notificato [![Build Status of Master](https://travis-ci.org/mac-cain13/notificato.png?branch=master)](https://travis-ci.org/mac-cain13/notificato) 2 | **Notificato takes care of push notifications in your PHP projects.** 3 | 4 | > *Italian:* **notificato** è: participio passato *English:* **notified** 5 | 6 | ## Why use Notificato instead of X? 7 | Notificato has some advantages not all other PHP push libraries have: 8 | 9 | 1. Supports multiple APNS certificates, so you can push to multiple Apps/Passbook Passes 10 | 2. Takes excellent care of PHPs buggy SSL-sockets, handles quirks and error responses correctly 11 | 3. Well tested with unit tests and nice Object-Oriented structure 12 | 13 | ## Installation 14 | Installation with [Composer](http://getcomposer.org) is recommended. Run the require command to add Notificato to your project: 15 | 16 | `composer require wrep/notificato` 17 | 18 | *Suggestion:* 19 | There is also a [Notificato for Symfony bundle](https://github.com/rickpastoor/notificato-symfony) available, highly recommended for Symfony2 & Symfony3 users. 20 | 21 | ## Getting started 22 | 1. Take a look at the snippet below for a impression how Notificato works 23 | 2. [Read the documentation](/doc/Readme.md) it will help you with common use cases 24 | 3. Check out the [API docs](http://mac-cain13.github.io/notificato/master/) for a deeper understanding what Notificato is capable of 25 | 26 | ```php 27 | messageBuilder() 48 | ->setDeviceToken('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') 49 | ->setBadge(1) 50 | ->build(); 51 | 52 | // The message is ready, let's send it! 53 | // Be aware that this method is blocking and on failure Notificato will retry if necessary 54 | $messageEnvelope = $notificato->send($message); 55 | 56 | // The returned envelope contains usefull information about how many retries where needed and if sending succeeded 57 | echo $messageEnvelope->getFinalStatusDescription(); 58 | } 59 | 60 | /** 61 | * This example reads all unregistered devices from Apples feedback service 62 | */ 63 | public function readFeedbackService() 64 | { 65 | // First we get the a Notificato instance and tell it what certificate to use as default certificate 66 | $notificato = new Notificato('./certificate.pem', 'passphrase-to-use'); 67 | 68 | // Now read all "tuples" from the feedback service, be aware that this method is blocking 69 | $tuples = $notificato->receiveFeedback(); 70 | 71 | // The tuples contain information about what device unregistered and when it did unregister. 72 | // Don't forget to check if the device reregistered after the "invaidated at" date! 73 | foreach ($tuples as $tuple) 74 | { 75 | echo 'Device ' . $tuple->getDeviceToken() . ' invalidated at ' . $tuple->getInvalidatedAt()->format(\DateTime::ISO8601) . PHP_EOL; 76 | } 77 | } 78 | } 79 | 80 | $gettingStarted = new GettingStarted(); 81 | $gettingStarted->sendOnePushNotification(); 82 | $gettingStarted->readFeedbackService(); 83 | ``` 84 | 85 | ## Contribute 86 | We'll love contributions, read [Contribute.md](Contribute.md) for some more info on what you can do and stuff that you should know if you want to help! 87 | 88 | ## License & Credits 89 | Notificato is released under the [MIT License](License) by [Mathijs Kadijk](https://github.com/mac-cain13), so feel free to use it in commercial and non-commercial projects. 90 | -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/SslSocket.php: -------------------------------------------------------------------------------- 1 | certificate = $certificate; 37 | $this->CACertificatePath = dirname(__FILE__) . '/entrust_2048_ca.pem'; 38 | $this->connectTimeout = ini_get('default_socket_timeout'); 39 | $this->connection = null; 40 | $this->setLogger(new NullLogger()); 41 | } 42 | 43 | /** 44 | * Sets a logger instance on the object 45 | * 46 | * @param LoggerInterface $logger 47 | */ 48 | public function setLogger(LoggerInterface $logger) 49 | { 50 | $this->logger = $logger; 51 | } 52 | 53 | /** 54 | * Get the certificate used with this connection 55 | * 56 | * @return Certificate 57 | */ 58 | public function getCertificate() 59 | { 60 | return $this->certificate; 61 | } 62 | 63 | /** 64 | * Get the SSL connection resource 65 | * 66 | * @return resource|null 67 | */ 68 | protected function getConnection() 69 | { 70 | return $this->connection; 71 | } 72 | 73 | /** 74 | * Open the connection 75 | */ 76 | protected function connect($endpointType = Certificate::ENDPOINT_TYPE_GATEWAY) 77 | { 78 | $this->logger->debug('Connecting Apns\SslSocket to the APNS ' . $endpointType . ' service with certificate "' . $this->getCertificate()->getDescription() . '"'); 79 | 80 | // Create the SSL context 81 | $streamContext = stream_context_create(); 82 | stream_context_set_option($streamContext, 'ssl', 'local_cert', $this->certificate->getPemFile()); 83 | 84 | if ($this->certificate->hasPassphrase()) { 85 | stream_context_set_option($streamContext, 'ssl', 'passphrase', $this->certificate->getPassphrase()); 86 | } 87 | 88 | // Verify peer if an Authority Certificate is available 89 | if (null !== $this->CACertificatePath) 90 | { 91 | stream_context_set_option($streamContext, 'ssl', 'verify_peer', true); 92 | stream_context_set_option($streamContext, 'ssl', 'cafile', $this->CACertificatePath); 93 | } 94 | 95 | // Open the connection 96 | $errorCode = $errorString = null; 97 | $this->connection = @stream_socket_client($this->certificate->getEndpoint($endpointType), $errorCode, $errorString, $this->connectTimeout, STREAM_CLIENT_CONNECT, $streamContext); 98 | 99 | // Check if the connection succeeded 100 | if (false == $this->connection) 101 | { 102 | $this->connection = null; 103 | 104 | // Set a somewhat more clear error message on error 0 105 | if (0 == $errorCode) { 106 | $errorString = 'Error before connecting, please check your certificate and passphrase combo and the given CA certificate if any.'; 107 | } 108 | 109 | throw new \UnexpectedValueException('Failed to connect to ' . $this->certificate->getEndpoint($endpointType) . ' with error #' . $errorCode . ' "' . $errorString . '".'); 110 | } 111 | 112 | // Set stream in non-blocking mode and make writes unbuffered 113 | stream_set_blocking($this->connection, 0); 114 | stream_set_write_buffer($this->connection, 0); 115 | } 116 | 117 | /** 118 | * Disconnect from the endpoint 119 | */ 120 | protected function disconnect() 121 | { 122 | $this->logger->debug('Disconnecting Apns\SslSocket from the APNS service with certificate "' . $this->getCertificate()->getDescription() . '"'); 123 | 124 | // Check if there is a socket to disconnect 125 | if (is_resource($this->connection)) 126 | { 127 | // Disconnect and unset the connection variable 128 | fclose($this->connection); 129 | } 130 | 131 | $this->connection = null; 132 | } 133 | } -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/Sender.php: -------------------------------------------------------------------------------- 1 | setGatewayFactory(new GatewayFactory()); 21 | $this->gatewayPool = array(); 22 | $this->setLogger(new NullLogger()); 23 | } 24 | 25 | /** 26 | * Set the gateway factory to use for creating connections to the APNS gateway 27 | * 28 | * @param GatewayFactory The gateway factory to use 29 | */ 30 | public function setGatewayFactory(GatewayFactory $gatewayFactory) 31 | { 32 | $this->gatewayFactory = $gatewayFactory; 33 | } 34 | 35 | /** 36 | * Get the current gateway factory 37 | * 38 | * @return GatewayFactory 39 | */ 40 | public function getGatewayFactory() 41 | { 42 | return $this->gatewayFactory; 43 | } 44 | 45 | /** 46 | * Sets a logger instance on the object 47 | * 48 | * @param LoggerInterface $logger 49 | */ 50 | public function setLogger(LoggerInterface $logger) 51 | { 52 | $this->logger = $logger; 53 | 54 | // Also update the logger on all current gateways in our pool 55 | foreach ($this->gatewayPool as $gateway) { 56 | $gateway->setLogger($logger); 57 | } 58 | } 59 | 60 | /** 61 | * Queues a message and flushes the gateway connection it must be send over immediately 62 | * Note: If you send multiple messages, queue as many as possible and flush them at once for maximum performance 63 | * 64 | * @param Message The message to send 65 | * @return MessageEnvelope 66 | */ 67 | public function send(Message $message) 68 | { 69 | // Queue the message and flush the associated gateway 70 | $messageEnvelope = $this->queue($message); 71 | $this->flush( $message->getCertificate() ); 72 | 73 | // Return the envelope 74 | return $messageEnvelope; 75 | } 76 | 77 | /** 78 | * Queue a message on the correct APNS gateway connection 79 | * 80 | * @param Message The message to queue 81 | * @param int The times Notificato should retry to deliver the message on failure (deprecated and ignored) 82 | * @return MessageEnvelope 83 | */ 84 | public function queue(Message $message) 85 | { 86 | // Get the gateway for the certificate 87 | $gateway = $this->getGatewayForCertificate( $message->getCertificate() ); 88 | 89 | // Queue the message 90 | return $gateway->queue($message); 91 | } 92 | 93 | /** 94 | * Count of all queued messages 95 | * 96 | * @return int 97 | */ 98 | public function getQueueLength() 99 | { 100 | $queueLength = 0; 101 | 102 | foreach ($this->gatewayPool as $gateway) 103 | { 104 | $queueLength += $gateway->getQueueLength(); 105 | } 106 | 107 | return $queueLength; 108 | } 109 | 110 | /** 111 | * Send all queued messages 112 | * 113 | * @param Certificate|null When given only the gateway connection for the given certificate is flushed 114 | */ 115 | public function flush(Certificate $certificate = null) 116 | { 117 | // Check if we must flush a specific gateway 118 | if (null == $certificate) 119 | { 120 | // No, flush the whole gateway pool 121 | foreach ($this->gatewayPool as $gateway) 122 | { 123 | $gateway->flush(); 124 | } 125 | } 126 | else 127 | { 128 | // Yes, flush only the requested gateway 129 | $this->getGatewayForCertificate($certificate)->flush(); 130 | } 131 | } 132 | 133 | /** 134 | * Get/create the gateway associated with the given certificate 135 | * 136 | * @param Certificate The certificate to get the gateway conenction for 137 | * @return Gateway 138 | */ 139 | private function getGatewayForCertificate(Certificate $certificate) 140 | { 141 | // Get the fingerprint of the certificate 142 | $fingerprint = $certificate->getFingerprint(); 143 | 144 | // If no gateway is available for this certificate create one 145 | if ( !isset($this->gatewayPool[$fingerprint]) ) 146 | { 147 | $this->gatewayPool[$fingerprint] = $this->getGatewayFactory()->createGateway($certificate); 148 | $this->gatewayPool[$fingerprint]->setLogger($this->logger); 149 | } 150 | 151 | // Return the gateway connection for this certificate 152 | return $this->gatewayPool[$fingerprint]; 153 | } 154 | } -------------------------------------------------------------------------------- /doc/certificate.md: -------------------------------------------------------------------------------- 1 | # Generate APNS Certificate 2 | To push to your iOS/Mac App or to Passbook you'll need to generate an APNS certificate in the [Apple developer portal](https://developer.apple.com/account/overview.action). This page will guide you through this process. 3 | 4 | *Note that Apple also has some usefull instructions on [Provisioning and Development](https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ProvisioningDevelopment.html).* 5 | 6 | ## 1. Create a certificate signing request 7 | You have two options to create your certificate signing request (CSR), with Keychain App on your Mac or using the commandline. 8 | 9 | ### a. Using Keychain on your Mac 10 | 1. Open the Keychain App 11 | 2. Choose from the menu: Keychain > Certificate assistent > Request certificate from certificate authority… 12 | 3. Fill out the e-mail and name you want in the certificate and choose "Save to disk" 13 | 4. Go ahead and save the file somewhere you can find it again 14 | 15 | ### b. Using the commandline 16 | 1. Open the terminal and go to a folder where you can put the certificate files 17 | 2. Generate a new private key and CSR: `openssl req -nodes -newkey rsa:2048 -keyout private.plain.key -out certrequest.csr -subj "/emailAddress=email/CN=Name/C=US"` 18 | 3. Secure the private key with an passphrase: `openssl rsa -in private.plain.key -des3 -out private.key` 19 | 20 | ## 2. Generate the certificate 21 | Now that you have your CSR head over to the [iOS](https://developer.apple.com/account/ios/certificate/certificateList.action) or [Mac](https://developer.apple.com/account/mac/certificate/certificateList.action) "Certificates, Identifiers & Profiles" page in the Member Center. 22 | 23 | ### a. For your iOS App 24 | 1. Click "App IDs" 25 | 2. Look up/create the correct App ID, make sure it's an "Explicit App ID" 26 | 3. Under "App Services" check the "Push Notifications" box (when editting you must first click "Settings" to check the box) 27 | 4. When creating a new App ID continue to the next page 28 | 5. Now upload the CSR you just generated by clicking create certificate under the Production and/or Development SSL Certificate 29 | 6. Go through the wizard and generate and download your certificate 30 | 31 | *Note: After this you must update all provisioning profiles with this App ID before push notifications will work! Edit something in the related profile to trigger regeneration and download the profile again. See also the ["Creating and Installing the Provisioning Profile"](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ProvisioningDevelopment.html#//apple_ref/doc/uid/TP40008194-CH104-SW5) section.* 32 | 33 | ### b. For your Passbook Pass 34 | 1. Click "Pass Type IDs" 35 | 2. Look up the correct Pass Type ID and click "Settings" 36 | 4. Click "Create certificate..." 37 | 5. Click "Continue", upload the CSR you just generated 38 | 6. Click "Generate", wait for the certificate to be generated and then download it! 39 | 40 | *Note: The certificate to sign your pass with is exactly the same certificate used for push!* 41 | 42 | *Note: To generate a fresh Pass Type ID just follow the on screen instructions and upload the generated CSR during the process.* 43 | 44 | ## 3. Export certificate to PEM 45 | You now have your certificate, time to convert it to a Notificato compatible format. 46 | 47 | ### a. If you used Keychain for CSR generation 48 | 1. Click the `.cer`-file so Keychain will import it 49 | 2. Open Keychain and lookup the certificate 50 | 3. Select **both** the certificate and the private key associated with it 51 | 4. Right click on the selection and choose "Export 2 items…" 52 | 5. Choose "Personal Information Exchange (.p12)" format and save it to disk as "keychainexport.p12" 53 | 6. Convert the `.p12`-file to `.pem` format by running: `openssl pkcs12 -in keychainexport.p12 -out certificate.pem` 54 | 55 | *Note: This will first ask for the passphrase you encrypted the p12 with while exporting from Keychain, then it will ask for a new passphrase to encrypt the pem-file with.* 56 | 57 | ### b. If you used the commandline for CSR generation 58 | 1. Make sure the downloaded `.cer`-file is in the same folder as the other generated files 59 | 2. Open the terminal and go to the folder the certificate files are in 60 | 3. Convert Apples certificate to PEM format: `openssl x509 -inform der -in aps_development.cer -out aps_development.pem` 61 | 3. Then add the key and certificate together: `cat aps_development.pem private.key > certificate.pem` 62 | 63 | Now `certificate.pem` is the certificate file you can use to push messages with, of course it will only work with choosen APNS environment and App/Passbook Pass you generated the certificate for. [Now go push something!](push.md) -------------------------------------------------------------------------------- /tests/Wrep/Notificato/Test/NotificatoTest.php: -------------------------------------------------------------------------------- 1 | notificato = new Notificato(); 14 | } 15 | 16 | public function testDefaultCertificate() 17 | { 18 | // TODO: We should be able to retrieve the CertificateFactory and test the default certificate on that instance 19 | // this is quite a hack to test the default cert 20 | $this->notificato = new Notificato(__DIR__ . '/resources/certificate_corrupt.pem', 'pem-passphrase', false, 'sandbox'); 21 | $message = $this->notificato->messageBuilder() 22 | ->setDeviceToken('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') 23 | ->build(); 24 | 25 | $this->assertEquals(__DIR__ . '/resources/certificate_corrupt.pem', $message->getCertificate()->getPemFile()); 26 | $this->assertEquals('pem-passphrase', $message->getCertificate()->getPassphrase()); 27 | $this->assertEquals('sandbox', $message->getCertificate()->getEnvironment()); 28 | } 29 | 30 | public function testCreateCertificate() 31 | { 32 | $certificateFactory = $this->getMockBuilder('\Wrep\Notificato\Apns\CertificateFactory') 33 | ->disableOriginalConstructor() 34 | ->getMock(); 35 | 36 | $certificateFactory->expects($this->once()) 37 | ->method('createCertificate') 38 | ->with($this->equalTo('cert.pem'), $this->equalTo('passphrase'), $this->equalTo(true), $this->equalTo(null)); 39 | 40 | $this->notificato->setCertificateFactory($certificateFactory); 41 | $this->notificato->createCertificate('cert.pem', 'passphrase'); 42 | } 43 | 44 | public function testCreateMessage() 45 | { 46 | $certificate = $this->getMockBuilder('\Wrep\Notificato\Apns\Certificate') 47 | ->disableOriginalConstructor() 48 | ->getMock(); 49 | 50 | $this->notificato->messageBuilder() 51 | ->setDeviceToken('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') 52 | ->setCertificate($certificate) 53 | ->build(); 54 | } 55 | 56 | public function testQueue() 57 | { 58 | $message = $this->getMockBuilder('\Wrep\Notificato\Apns\Message') 59 | ->disableOriginalConstructor() 60 | ->getMock(); 61 | 62 | $sender = $this->getMockBuilder('\Wrep\Notificato\Apns\Sender') 63 | ->disableOriginalConstructor() 64 | ->getMock(); 65 | 66 | $sender->expects($this->once()) 67 | ->method('queue') 68 | ->with($this->equalTo($message)); 69 | 70 | $this->notificato->setSender($sender); 71 | $this->notificato->queue($message); 72 | } 73 | 74 | public function testFlush() 75 | { 76 | $certificate = $this->getMockBuilder('\Wrep\Notificato\Apns\Certificate') 77 | ->disableOriginalConstructor() 78 | ->getMock(); 79 | 80 | $sender = $this->getMockBuilder('\Wrep\Notificato\Apns\Sender') 81 | ->disableOriginalConstructor() 82 | ->getMock(); 83 | 84 | $sender->expects($this->once()) 85 | ->method('flush') 86 | ->with($this->equalTo($certificate)); 87 | 88 | $this->notificato->setSender($sender); 89 | $this->notificato->flush($certificate); 90 | } 91 | 92 | public function testSend() 93 | { 94 | $message = $this->getMockBuilder('\Wrep\Notificato\Apns\Message') 95 | ->disableOriginalConstructor() 96 | ->getMock(); 97 | 98 | $sender = $this->getMockBuilder('\Wrep\Notificato\Apns\Sender') 99 | ->disableOriginalConstructor() 100 | ->getMock(); 101 | 102 | $sender->expects($this->once()) 103 | ->method('send') 104 | ->with($this->equalTo($message)); 105 | 106 | $this->notificato->setSender($sender); 107 | $this->notificato->send($message); 108 | } 109 | 110 | public function testReceiveFeedback() 111 | { 112 | $certificate = $this->getMockBuilder('\Wrep\Notificato\Apns\Certificate') 113 | ->disableOriginalConstructor() 114 | ->getMock(); 115 | 116 | $feedback = $this->getMockBuilder('\Wrep\Notificato\Apns\Feedback\Feedback') 117 | ->disableOriginalConstructor() 118 | ->getMock(); 119 | 120 | $feedback->expects($this->once()) 121 | ->method('receive') 122 | ->with() 123 | ->will($this->returnValue('returnValue')); 124 | 125 | $feedbackFactory = $this->getMockBuilder('\Wrep\Notificato\Apns\Feedback\FeedbackFactory') 126 | ->disableOriginalConstructor() 127 | ->getMock(); 128 | 129 | $feedbackFactory->expects($this->once()) 130 | ->method('createFeedback') 131 | ->with($this->equalTo($certificate)) 132 | ->will($this->returnValue($feedback)); 133 | 134 | $this->notificato->setFeedbackFactory($feedbackFactory); 135 | $this->assertEquals('returnValue', $this->notificato->receiveFeedback($certificate)); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /doc/multiple-certs.md: -------------------------------------------------------------------------------- 1 | # Using multiple certificates 2 | Here we describe a somewhat more complex use case where we use multiple certificates to push messages to multiple Apps/passes at once. This is very usefull in cases where you, for example, use one CMS to push to different Apps or want to use one CMS to push to your production and development App at once. 3 | 4 | *Make sure you've read about [basic pushing](push.md) and [reading the feedback service](feedback.md) before you read this.* 5 | 6 | ## Pushing multiple messages to multiple Apps 7 | This example pushes 2 messages to two different Apps. The big difference is that you pass the certificate you want to use to the message, this way Notificato knows what connection to use for that particular message. There is no default certificate used. 8 | ```php 9 | // First we get the a Notificato instance, note that we don't pass it a default certificate! 10 | $notificato = new Notificato(); 11 | 12 | // Now we create the certificate objects for both Apps 13 | $certificateAppFoo = $notificato->createCertificate('./certificate-app-foo.pem', 'passphrase-here'); 14 | $certificateAppBar = $notificato->createCertificate('./certificate-app-bar.pem', 'the-passphrase'); 15 | 16 | // Create an array to save the message envelopes in 17 | $messageEnvelopes = array(); 18 | 19 | /** Now send a message to App Foo **/ 20 | // First we get a fresh message from Notificato and set the device token, certificate, alert and sound 21 | // Note that we pass the certificate to the message, as we're not using a default certificate anymore 22 | $builder = $notificato->messageBuilder() 23 | ->setDeviceToken($deviceToken) 24 | ->setCertificate($certificateAppFoo) 25 | ->setAlert('Pilot: They\'re looking for us in the wrong place.') 26 | ->setSound('lost-sound'); 27 | 28 | // Queue the message for sending 29 | $messageEnvelopes[] = $notificato->queue( $builder->build() ); 30 | 31 | /** Now send a message to App Bar **/ 32 | // We reuse the builder and update it with the new device token, certificate and alert 33 | // Note that we pass the certificate to the message, as we're not using a default certificate anymore 34 | $builder = $notificato->messageBuilder() 35 | ->setDeviceToken($deviceToken) 36 | ->setCertificate($certificateAppBar) 37 | ->setAlert('Charlie: It was imaginary peanut butter, actually.'); 38 | 39 | // Queue the message for sending 40 | $messageEnvelopes[] = $notificato->queue( $builder->build() ); 41 | 42 | // Now all messages are queued, lets send them at once 43 | // Be aware that this method is blocking and on failure Notificato will retry if necessary 44 | $notificato->flush(); 45 | 46 | // The returned envelopes contains usefull information about how many retries where needed and if sending succeeded 47 | foreach ($messageEnvelopes as $messageEnvelope) 48 | { 49 | echo $messageEnvelope->getIdentifier() . ' ' . $messageEnvelope->getFinalStatusDescription() . PHP_EOL; 50 | } 51 | ``` 52 | 53 | *Note: You can still pass the Notificato constructor a default certificate, this certificate will be set on the `MessageBuilder` by default. Use the setCertificate method to use alternative certificates.* 54 | 55 | ## Receiving feedback for all your certificates 56 | Now we've send the messages we must read the feedback service for all the certificates that are in use. Again the biggest difference is that we don't use a default certificate that we pass to Notificato, but pass a specific certificate to the `receiveFeedback`-method. 57 | ```php 58 | // First we get the a Notificato instance, note that we don't pass it a default certificate! 59 | $notificato = new Notificato(); 60 | 61 | // Now we create the certificate objects for both Apps 62 | $certificateAppFoo = $notificato->createCertificate('./certificate-app-foo.pem', 'passphrase-here'); 63 | $certificateAppBar = $notificato->createCertificate('./certificate-app-bar.pem', 'the-passphrase'); 64 | 65 | /** Get feedback for App Foo **/ 66 | // Now read all "tuples" from the feedback service, be aware that this method is blocking 67 | $tuples = $notificato->receiveFeedback($certificateAppFoo); 68 | 69 | // The tuples contain information about what device unregistered and when it did unregister. 70 | // Don't forget to check if the device reregistered after the "invalidated at" date! 71 | foreach ($tuples as $tuple) 72 | { 73 | echo '[App Foo] Device ' . $tuple->getDeviceToken() . ' invalidated at ' . $tuple->getInvalidatedAt()->format(\DateTime::ISO8601) . PHP_EOL; 74 | } 75 | 76 | /** Get feedback for App Bar **/ 77 | // Now read all "tuples" from the feedback service, be aware that this method is blocking 78 | $tuples = $notificato->receiveFeedback($certificateAppBar); 79 | 80 | // The tuples contain information about what device unregistered and when it did unregister. 81 | // Don't forget to check if the device reregistered after the "invalidated at" date! 82 | foreach ($tuples as $tuple) 83 | { 84 | echo '[App Bar] Device ' . $tuple->getDeviceToken() . ' invalidated at ' . $tuple->getInvalidatedAt()->format(\DateTime::ISO8601) . PHP_EOL; 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /tests/Wrep/Notificato/Test/Apns/GatewayTest.php: -------------------------------------------------------------------------------- 1 | certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 18 | $this->gateway = new Gateway($this->certificate); 19 | } 20 | 21 | public function testGetCertificate() 22 | { 23 | $this->assertEquals($this->certificate, $this->gateway->getCertificate()); 24 | } 25 | 26 | public function testInitialQueueLength() 27 | { 28 | $this->assertEquals(0, $this->gateway->getQueueLength()); 29 | } 30 | 31 | public function testQueue() 32 | { 33 | $message = $this->getMockBuilder('\Wrep\Notificato\Apns\Message') 34 | ->disableOriginalConstructor() 35 | ->getMock(); 36 | 37 | $envelope = $this->gateway->queue($message); 38 | 39 | $this->assertEquals(MessageEnvelope::STATUS_NOTSEND, $envelope->getStatus()); 40 | $this->assertEquals(1, $this->gateway->getQueueLength()); 41 | } 42 | 43 | public function testConnectionFail() 44 | { 45 | $message = $this->getMockBuilder('\Wrep\Notificato\Apns\Message') 46 | ->disableOriginalConstructor() 47 | ->getMock(); 48 | 49 | $this->certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', 'passphrase', false, Certificate::ENDPOINT_ENV_PRODUCTION); 50 | $this->gateway = new Gateway($this->certificate); 51 | 52 | $this->gateway->queue($message); 53 | 54 | $this->setExpectedException('UnexpectedValueException', 'Error before connecting, please check your certificate and passphrase combo and the given CA certificate if any.'); 55 | $this->gateway->flush(); 56 | } 57 | 58 | /** 59 | * @group realpush 60 | */ 61 | public function testFlush() 62 | { 63 | $this->certificate = new Certificate(__DIR__ . '/../resources/paspas.pem'); 64 | $this->gateway = new Gateway($this->certificate); 65 | 66 | // Create a correct and incorrect message 67 | $message = new Message('2f9a6ca974ce0b4897fcc171c6a4a9a28f98c36b32962566ab83bbfa0e372c19', $this->certificate); 68 | $incorrectMessage = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $this->certificate); 69 | 70 | // Connect and queue the messages 71 | $gateway = new Gateway($this->certificate); 72 | $successEnvelope = $gateway->queue($message); 73 | $failEnvelope = $gateway->queue($incorrectMessage); 74 | $retryEnvelope = $gateway->queue($message); 75 | 76 | // Send the messages 77 | $gateway->flush(); 78 | 79 | // Get the retry envelope 80 | $retrySuccessEnvelope = $retryEnvelope->getRetryEnvelope(); 81 | $this->assertInstanceOf('\Wrep\Notificato\Apns\MessageEnvelope', $retrySuccessEnvelope, 'Retried message has no retry envelope.'); 82 | 83 | // Check for the expected statusses 84 | $this->assertEquals(MessageEnvelope::STATUS_NOERRORS, $successEnvelope->getStatus()); 85 | $this->assertEquals(8, $failEnvelope->getStatus()); 86 | $this->assertEquals(MessageEnvelope::STATUS_EARLIERERROR, $retryEnvelope->getStatus()); 87 | $this->assertEquals(MessageEnvelope::STATUS_NOERRORS, $retrySuccessEnvelope->getStatus()); 88 | } 89 | 90 | /** 91 | * @group realpush 92 | */ 93 | public function testRetry() 94 | { 95 | $this->certificate = new Certificate(__DIR__ . '/../resources/paspas.pem'); 96 | $this->gateway = new Gateway($this->certificate); 97 | 98 | // Create a correct and incorrect message 99 | $message = new Message('2f9a6ca974ce0b4897fcc171c6a4a9a28f98c36b32962566ab83bbfa0e372c19', $this->certificate); 100 | $incorrectMessage = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $this->certificate); 101 | 102 | // Connect and queue the messages 103 | $gateway = new Gateway($this->certificate); 104 | $successEnvelope = $gateway->queue($message); 105 | $failEnvelope = $gateway->queue($incorrectMessage); 106 | $retryEnvelope = $gateway->queue($message); 107 | 108 | // Send the messages 109 | $gateway->flush(); 110 | 111 | // Get the retry envelope 112 | $retrySuccessEnvelope = $retryEnvelope->getRetryEnvelope(); 113 | $this->assertNull($retrySuccessEnvelope, 'Retried message has an unexpected retry envelope.'); 114 | 115 | // Check for the expected statusses 116 | $this->assertEquals(MessageEnvelope::STATUS_NOERRORS, $successEnvelope->getStatus()); 117 | $this->assertEquals(8, $failEnvelope->getStatus()); 118 | } 119 | 120 | public function testStoreMessageEnvelope() 121 | { 122 | $this->gateway = new \Wrep\Notificato\Test\Apns\Mock\MockGateway($this->certificate); 123 | $message = $this->getMockBuilder('\Wrep\Notificato\Apns\Message') 124 | ->disableOriginalConstructor() 125 | ->getMock(); 126 | $message->expects($this->any()) 127 | ->method('getCertificate') 128 | ->will($this->returnValue($this->certificate)); 129 | 130 | // Check that each message is stored into the message envelope store 131 | $firstEnvelope = $this->gateway->queue($message); 132 | $this->assertEquals(1, count($this->gateway->getMessageEnvelopeStore()), 'Message envelope not stored.'); 133 | 134 | for ($i = 1; $i < 1000; $i++) 135 | { 136 | $this->gateway->queue($message); 137 | $this->assertEquals($i+1, count($this->gateway->getMessageEnvelopeStore()), 'Message envelope not stored.'); 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /doc/push.md: -------------------------------------------------------------------------------- 1 | # Pushing messages 2 | Before you can start pushing make sure you have [generated a PEM certificate](certificate.md) you'll need this before you can start pushing. Ready? We'll lets push something! 3 | 4 | ## My first pushmessage 5 | In this example we'll send one pushmessage to a device. It's the most basic example to give you an idea of how Notificato works: 6 | ```php 7 | // First we get a Notificato instance and tell it what certificate to use as default certificate 8 | $notificato = new Notificato('./certificate.pem', 'passphrase-to-use'); 9 | 10 | // Now we get a fresh messagebuilder from Notificato 11 | // This message will be send to device with pushtoken 'fffff...' 12 | // it will automaticly be associated with the default certificate 13 | // and we will set the red badge on the App icon to 1 14 | $message = $notificato->messageBuilder() 15 | ->setDeviceToken('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') 16 | ->setBadge(1) 17 | ->build(); 18 | 19 | // The message is ready, let's send it! 20 | // Be aware that this method is blocking and on failure Notificato will retry if necessary 21 | $messageEnvelope = $notificato->send($message); 22 | 23 | // The returned envelope contains usefull information about how many retries where needed and if sending succeeded 24 | echo $messageEnvelope->getFinalStatusDescription(); 25 | ``` 26 | 27 | ## Pushing multiple messages 28 | When sending multiple messages you shouldn't use the `send`-method, but queue all messages and then send them at once. This will improve the performance and prevent unnecessary reconnects to Apple. 29 | ```php 30 | // First we get the a Notificato instance and tell it what certificate to use as default certificate 31 | $notificato = new Notificato('./certificate.pem', 'passphrase-to-use'); 32 | 33 | // Create an array to save the message envelopes in 34 | $messageEnvelopes = array(); 35 | 36 | // Get the builder 37 | $builder = $notificato->messageBuilder() 38 | ->setAlert('Sayid: I'm a survivor of a plane crash.'); 39 | 40 | // Let's assume $pushinformation contains the device specific push information we need 41 | foreach ($pushinformation as $deviceToken => $badge) 42 | { 43 | // Update the message for this device 44 | $builder->setBadge($badge) 45 | ->setDeviceToken($deviceToken); 46 | 47 | // Queue the message for sending 48 | $messageEnvelopes[] = $notificato->queue( $builder->build() ); 49 | } 50 | 51 | // Now all messages are queued, lets send them at once 52 | // Be aware that this method is blocking and on failure Notificato will retry if necessary 53 | $notificato->flush(); 54 | 55 | // The returned envelopes contains usefull information about how many retries where needed and if sending succeeded 56 | foreach ($messageEnvelopes as $messageEnvelope) 57 | { 58 | echo $messageEnvelope->getIdentifier() . ' ' . $messageEnvelope->getFinalStatusDescription() . PHP_EOL; 59 | } 60 | ``` 61 | 62 | ## The full blown example 63 | Here we try to show as many options in one example as possible to give you an idea of what is possible. 64 | ```php 65 | // First we get the a Notificato instance and tell it what certificate to use as default certificate 66 | // We've disabled validation of the certificate because our PHP/OS doesn't parse it correctly and we set the environment ourselfs 67 | $notificato = new Notificato('./certificate.pem', 'passphrase-to-use', false, Certificate::ENDPOINT_ENV_SANDBOX); 68 | $notificato->setLogger( new Psr\Log\NullLogger() ); 69 | 70 | // Create an array to save the message envelopes in 71 | $messageEnvelopes = array(); 72 | 73 | // Get the builder 74 | $builder = $notificato->messageBuilder() 75 | ->setExpiresAt(new \DateTime('+1 hour')) 76 | ->setAlert('The numbers are 4, 8, 15, 16, 23 and 42', 'accept-button', 'launch-image') 77 | ->setSound('spookysound') 78 | ->setContentAvailable(false) 79 | ->setPayload( array('persons' => array('Locke', 'Reyes', 'Ford', 'Jarrah', 'Shephard', 'Kwon')) ); 80 | 81 | // Let's assume $pushinformation contains all push information we need 82 | foreach ($pushinformation as $deviceToken => $badge) 83 | { 84 | // Update the message for this device 85 | $builder->setBadge($badge) 86 | ->setDeviceToken($deviceToken); 87 | 88 | // Queue the message for sending 89 | $messageEnvelopes[] = $notificato->queue($builder->build()); 90 | } 91 | 92 | // Now all messages are queued, lets send them at once 93 | // Be aware that this method is blocking and on failure Notificato will retry if necessary 94 | $notificato->flush(); 95 | 96 | // The returned envelopes contains usefull information about how many retries where needed and if sending succeeded 97 | foreach ($messageEnvelopes as $messageEnvelope) 98 | { 99 | echo $messageEnvelope->getIdentifier() . ' ' . $messageEnvelope->getFinalStatusDescription() . PHP_EOL; 100 | } 101 | ``` 102 | 103 | ## What you should know 104 | 1. It is recommended to set an expiry date whenever you can, not setting this could make Apple discard the message instantly when the device is offline. 105 | 2. Sending a message without setting any content in it is completely valid, this is used with Passbook Passes. 106 | 3. All options and methods on the Message object can be found in the [API docs](http://wrep.github.com/notificato/master/Wrep/Notificato/Apns/Message.html). 107 | 4. You really should check the resulting state of the message envelopes and handle errors, if you don't the failed messages are just gone. 108 | 5. Logging is supported and can give you usefull information on what is happening. Set any [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) compatible logger with `$notificato->setLogger($logger)`. 109 | 110 | ## What's next? 111 | Now you've send your messages you must [read the feedback service](feedback.md) once in a while. 112 | -------------------------------------------------------------------------------- /src/Wrep/Notificato/Notificato.php: -------------------------------------------------------------------------------- 1 | setLogger( new NullLogger() ); 31 | $this->setSender( new Apns\Sender() ); 32 | 33 | $this->setCertificateFactory( new Apns\CertificateFactory($pemFile, $passphrase, $validate, $endpointEnv) ); 34 | $this->setFeedbackFactory( new Apns\Feedback\FeedbackFactory($this->certificateFactory) ); 35 | } 36 | 37 | /** 38 | * Create an APNS Certificate 39 | * 40 | * @param string Path to the PEM certificate file 41 | * @param string|null Passphrase to use with the PEM file 42 | * @param boolean Set to false to skip the validation of the certificate, default true 43 | * @param string|null APNS environment this certificate is valid for, by default autodetects during validation 44 | * @return Apns\Certificate 45 | */ 46 | public function createCertificate($pemFile, $passphrase = null, $validate = true, $endpointEnv = null) 47 | { 48 | return $this->certificateFactory->createCertificate($pemFile, $passphrase, $validate, $endpointEnv); 49 | } 50 | 51 | /** 52 | * Create a Message builder 53 | * 54 | * @return Apns\MessageBuilder 55 | */ 56 | public function messageBuilder() 57 | { 58 | $builder = Apns\Message::builder(); 59 | 60 | if ($this->certificateFactory->getDefaultCertificate() != null) { 61 | $builder->setCertificate( $this->certificateFactory->getDefaultCertificate() ); 62 | } 63 | 64 | return $builder; 65 | } 66 | 67 | /** 68 | * Queue a message on the correct APNS gateway connection 69 | * 70 | * @param Apns\Message The message to queue 71 | * @return Apns\MessageEnvelope 72 | */ 73 | public function queue(Apns\Message $message) 74 | { 75 | return $this->sender->queue($message); 76 | } 77 | 78 | /** 79 | * Send all queued messages 80 | * 81 | * @param Apns\Certificate|null When given only the gateway connection for the given certificate is flushed 82 | */ 83 | public function flush(Apns\Certificate $certificate = null) 84 | { 85 | $this->sender->flush($certificate); 86 | } 87 | 88 | /** 89 | * Queues a message and flushes the gateway connection it must be send over immediately 90 | * Note: If you send multiple messages, queue as many as possible and flush them at once for maximum performance 91 | * 92 | * @param Apns\Message The message to send 93 | * @return Apns\MessageEnvelope 94 | */ 95 | public function send(Apns\Message $message) 96 | { 97 | return $this->sender->send($message); 98 | } 99 | 100 | /** 101 | * Receive the feedback tuples from APNS 102 | * 103 | * @param Apns\Certificate|null The certificate to use to connect to APNS, default use the default certificate 104 | * @return array Array containing FeedbackTuples received from Apple 105 | */ 106 | public function receiveFeedback(Apns\Certificate $certificate = null) 107 | { 108 | $feedback = $this->feedbackFactory->createFeedback($certificate); 109 | $feedback->setLogger($this->logger); 110 | 111 | return $feedback->receive(); 112 | } 113 | 114 | /** 115 | * Sets the sender to use. 116 | * Note: The given sender will get the logger used by this Notificato object 117 | * 118 | * @param Apns\Sender $sender 119 | */ 120 | public function setSender(Apns\Sender $sender) 121 | { 122 | $this->sender = $sender; 123 | $this->sender->setLogger($this->logger); 124 | } 125 | 126 | /** 127 | * Sets a logger instance on the object. 128 | * Note: The sender is automaticly updated with this logger 129 | * 130 | * @param Psr\Log\LoggerInterface $logger 131 | */ 132 | public function setLogger(LoggerInterface $logger) 133 | { 134 | $this->logger = $logger; 135 | 136 | // Also update the logger of the sender 137 | if ($this->sender instanceOf LoggerAwareInterface) { 138 | $this->sender->setLogger($logger); 139 | } 140 | } 141 | 142 | /** 143 | * Sets the certificate factory to use. 144 | * Note: If you set a certificate factory you are responsible for setting the correct default certificate. 145 | * Note: The FeedbackFactory and MessageFactory are automaticly updated with the given CertificateFactory. 146 | * 147 | * @param Apns\CertificateFactory $messageFactory 148 | */ 149 | public function setCertificateFactory(Apns\CertificateFactory $certificateFactory) 150 | { 151 | $this->certificateFactory = $certificateFactory; 152 | 153 | // Also update the certificate factory of the feedback factory 154 | if (null !== $this->feedbackFactory) { 155 | $this->feedbackFactory->setCertificateFactory($this->certificateFactory); 156 | } 157 | } 158 | 159 | /** 160 | * Sets the feedback factory to use. 161 | * Note: The certificate factory is automaticly set to the factory used by this Notificato object 162 | * 163 | * @param Apns\FeedbackFactory $feedbackFactory 164 | */ 165 | public function setFeedbackFactory(Apns\Feedback\FeedbackFactory $feedbackFactory) 166 | { 167 | $this->feedbackFactory = $feedbackFactory; 168 | $this->feedbackFactory->setCertificateFactory($this->certificateFactory); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/MessageEnvelope.php: -------------------------------------------------------------------------------- 1 | 'Not send to APNS', 47 | self::STATUS_NOERRORS => '[APNS] No errors encountered', 48 | 49 | // APNS final states 50 | 1 => '[APNS] Processing error', 51 | 2 => '[APNS] Missing device token', 52 | 3 => '[APNS] Missing topic', 53 | 4 => '[APNS] Missing payload', 54 | 5 => '[APNS] Invalid token size', 55 | 6 => '[APNS] Invalid topic size', 56 | 7 => '[APNS] Invalid payload size', 57 | 8 => '[APNS] Invalid token', 58 | 255 => '[APNS] Unknown error', 59 | 60 | // Notificato internal final states 61 | self::STATUS_SENDFAILED => 'Sending failed, will retry with other envelope', 62 | self::STATUS_EARLIERERROR => 'Failed due earlier error, will retry with other envelope' 63 | ); 64 | 65 | /** 66 | * Construct MessageEnvelope 67 | * 68 | * @param int Unique number to the relevant APNS connection to identify this message 69 | * @param Message The message that's is contained by this envelope 70 | */ 71 | public function __construct($identifier, Message $message) 72 | { 73 | // A message id greater then 0 is required 74 | if ( !(is_int($identifier) && $identifier > 0) ) { 75 | throw new \InvalidArgumentException('Message ID #' . $identifier . ' is invalid, must be an integer above zero.'); 76 | } 77 | 78 | // A message is required 79 | if (null == $message) { 80 | throw new \InvalidArgumentException('No message given.'); 81 | } 82 | 83 | // Save the given parameters 84 | $this->identifier = $identifier; 85 | $this->message = $message; 86 | $this->status = -1; 87 | $this->retryEnvelope = null; 88 | } 89 | 90 | /** 91 | * Unique number to the relevant APNS connection to identify this message 92 | * 93 | * @return int 94 | */ 95 | public function getIdentifier() 96 | { 97 | return $this->identifier; 98 | } 99 | 100 | /** 101 | * The message that's is contained by this envelope 102 | * 103 | * @return Message 104 | */ 105 | public function getMessage() 106 | { 107 | return $this->message; 108 | } 109 | 110 | /** 111 | * Get the envelope used for the retry 112 | * 113 | * @return MessageEnvelope 114 | */ 115 | public function getRetryEnvelope() 116 | { 117 | return $this->retryEnvelope; 118 | } 119 | 120 | /** 121 | * Set the status of this message envelope 122 | * only possible if there is no final state set yet. 123 | * 124 | * @param int One of the keys in self::$statusDescriptionMapping 125 | * @param MessageEnvelope|null Envelope for the retry of this MessageEnvelope 126 | */ 127 | public function setStatus($status, $envelope = null) 128 | { 129 | // Check if we're not in a final state yet 130 | if ($this->status > 0) { 131 | throw new \RuntimeException('Cannot change status from final state ' . $this->status . ' to state ' . $status . '.'); 132 | } 133 | 134 | // Check if this is a valid state 135 | if ( !in_array($status, array_keys(self::$statusDescriptionMapping)) ) { 136 | throw new \InvalidArgumentException('Status ' . $status . ' is not a valid status.'); 137 | } 138 | 139 | // Check if the retry envelope is not this envelope 140 | if ($this === $envelope) { 141 | throw new \InvalidArgumentException('Retry envelope cannot be set to this envelope.'); 142 | } 143 | 144 | // Save it! 145 | $this->status = $status; 146 | $this->retryEnvelope = $envelope; 147 | } 148 | 149 | /** 150 | * Get the current status of this message envelope 151 | * 152 | * @return int 153 | */ 154 | public function getStatus() 155 | { 156 | return $this->status; 157 | } 158 | 159 | /** 160 | * Get a description of the current status of this message envelope 161 | * 162 | * @return string 163 | */ 164 | public function getStatusDescription() 165 | { 166 | return self::$statusDescriptionMapping[$this->getStatus()]; 167 | } 168 | 169 | /** 170 | * Get the final status after all retries. 171 | * Use this method to know how the message ended up after all retries. 172 | * 173 | * @return int 174 | */ 175 | public function getFinalStatus() 176 | { 177 | $currentEnvelope = $this; 178 | while ( null != $currentEnvelope->getRetryEnvelope() ) { 179 | $currentEnvelope = $currentEnvelope->getRetryEnvelope(); 180 | } 181 | 182 | return $currentEnvelope->getStatus(); 183 | } 184 | 185 | /** 186 | * Get a description of the final status after all retries. 187 | * Use this method to know how the message ended up after all retries. 188 | * 189 | * @return string 190 | */ 191 | public function getFinalStatusDescription() 192 | { 193 | return self::$statusDescriptionMapping[$this->getFinalStatus()]; 194 | } 195 | 196 | /** 197 | * Get the message that this envelope contains in binary APNS compatible format 198 | * 199 | * @return string 200 | */ 201 | public function getBinaryMessage() 202 | { 203 | $jsonMessage = $this->getMessage()->getJson(); 204 | $jsonMessageLength = strlen($jsonMessage); 205 | 206 | $binaryMessage = pack('CNNnH*', self::BINARY_COMMAND, $this->getIdentifier(), $this->getMessage()->getExpiresAt(), self::BINARY_DEVICETOKEN_SIZE, $this->getMessage()->getDeviceToken()) . pack('n', $jsonMessageLength); 207 | return $binaryMessage . $jsonMessage; 208 | } 209 | } -------------------------------------------------------------------------------- /tests/Wrep/Notificato/Test/Apns/CertificateTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('\Wrep\Notificato\Apns\Certificate', $certificate, 'Certificate of incorrect classtype.'); 16 | } 17 | 18 | /** 19 | * @dataProvider correctConstructorArguments 20 | */ 21 | public function testGetPemFile($pemFile, $passphrase, $validate, $endpoint) 22 | { 23 | $certificate = new Certificate($pemFile, $passphrase, $validate, $endpoint); 24 | $this->assertEquals(realpath($pemFile), $certificate->getPemFile(), 'Got incorrect PEM file path from getter.'); 25 | } 26 | 27 | /** 28 | * @dataProvider correctConstructorArguments 29 | */ 30 | public function testHasPassphrase($pemFile, $passphrase, $validate, $endpoint, $hasPassphrase) 31 | { 32 | $certificate = new Certificate($pemFile, $passphrase, $validate, $endpoint); 33 | $this->assertEquals($hasPassphrase, $certificate->hasPassphrase(), 'Has passphrase returned incorrect result.'); 34 | } 35 | 36 | /** 37 | * @dataProvider correctConstructorArguments 38 | */ 39 | public function testGetPassphrase($pemFile, $passphrase, $validate, $endpoint) 40 | { 41 | $certificate = new Certificate($pemFile, $passphrase, $validate, $endpoint); 42 | $this->assertEquals($passphrase, $certificate->getPassphrase(), 'Get passphrase returned incorrect passphrase.'); 43 | } 44 | 45 | /** 46 | * @dataProvider correctConstructorArguments 47 | */ 48 | public function testGetFingerprint($pemFile, $passphrase, $validate, $endpoint, $hasPassphrase, $fingerprint) 49 | { 50 | $certificate = new Certificate($pemFile, $passphrase, $validate, $endpoint); 51 | $this->assertEquals($fingerprint, $certificate->getFingerprint(), 'Got incorrect fingerprint of PEM file.'); 52 | } 53 | 54 | public function correctConstructorArguments() 55 | { 56 | return array( 57 | array(__DIR__ . '/.././resources/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION, false, '9f1db6cc07170c41001b0e92e943747a3bee3aa2'), 58 | array(__DIR__ . '/.././resources/../resources/certificate_corrupt.pem', '', false, Certificate::ENDPOINT_ENV_PRODUCTION, false, '9f1db6cc07170c41001b0e92e943747a3bee3aa2'), 59 | array(__DIR__ . '/../resources/certificate_corrupt.pem', 'thisIsThePassphrase', false, Certificate::ENDPOINT_ENV_PRODUCTION, true, '9f1db6cc07170c41001b0e92e943747a3bee3aa2'), 60 | array(__DIR__ . '/../resources/certificate_corrupt.pem', 'thisIsThePassphrase', false, Certificate::ENDPOINT_ENV_SANDBOX, true, '8f34cc9e3de410bd045f777b1f36e004a2449aa7') 61 | ); 62 | } 63 | 64 | public function testGetEndpoint() 65 | { 66 | $certificate = new Certificate(__DIR__ . '/.././resources/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 67 | $this->assertEquals('ssl://gateway.push.apple.com:2195', $certificate->getEndpoint(Certificate::ENDPOINT_TYPE_GATEWAY), 'Got incorrect production gateway endpoint.'); 68 | $this->assertEquals('ssl://feedback.push.apple.com:2196', $certificate->getEndpoint(Certificate::ENDPOINT_TYPE_FEEDBACK), 'Got incorrect production feedback endpoint.'); 69 | 70 | $certificate = new Certificate(__DIR__ . '/.././resources/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_SANDBOX); 71 | $this->assertEquals('ssl://gateway.sandbox.push.apple.com:2195', $certificate->getEndpoint(Certificate::ENDPOINT_TYPE_GATEWAY), 'Got incorrect sandbox gateway endpoint.'); 72 | $this->assertEquals('ssl://feedback.sandbox.push.apple.com:2196', $certificate->getEndpoint(Certificate::ENDPOINT_TYPE_FEEDBACK), 'Got incorrect sandbox feedback endpoint.'); 73 | } 74 | 75 | public function testInvalidGetEndpointProduction() 76 | { 77 | $certificate = new Certificate(__DIR__ . '/.././resources/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 78 | $this->setExpectedException('InvalidArgumentException', 'is not a valid endpoint type.'); 79 | $certificate->getEndpoint('invalid'); 80 | } 81 | 82 | public function testInvalidGetEndpointSandbox() 83 | { 84 | $certificate = new Certificate(__DIR__ . '/.././resources/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_SANDBOX); 85 | $this->setExpectedException('InvalidArgumentException', 'is not a valid endpoint type.'); 86 | $certificate->getEndpoint('invalid'); 87 | } 88 | 89 | /** 90 | * @dataProvider incorrectConstructorArguments 91 | */ 92 | public function testIncorrectConstruction($pemFile, $passphrase, $validate, $endpoint) 93 | { 94 | $this->setExpectedException('InvalidArgumentException'); 95 | new Certificate($pemFile, $passphrase, $validate, $endpoint); 96 | } 97 | 98 | public function incorrectConstructorArguments() 99 | { 100 | return array( 101 | array(null, null, false, Certificate::ENDPOINT_ENV_PRODUCTION), 102 | array(null, '', false, Certificate::ENDPOINT_ENV_PRODUCTION), 103 | array(null, 'thisIsThePassphrase', false, Certificate::ENDPOINT_ENV_PRODUCTION), 104 | array('', null, false, Certificate::ENDPOINT_ENV_PRODUCTION), 105 | array('', '', false, Certificate::ENDPOINT_ENV_PRODUCTION), 106 | array('', 'thisIsThePassphrase', false, Certificate::ENDPOINT_ENV_PRODUCTION), 107 | array(__DIR__ . '/../resources/certificate_doesnotexists.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION), 108 | array(__DIR__ . '/../resources/certificate_doesnotexists.pem', '', false, Certificate::ENDPOINT_ENV_PRODUCTION), 109 | array(__DIR__ . '/../resources/certificate_doesnotexists.pem', 'thisIsThePassphrase', false, Certificate::ENDPOINT_ENV_PRODUCTION), 110 | array(__DIR__ . '/../resources/certificate_corrupt.pem', 'thisIsThePassphrase', false, null) 111 | ); 112 | } 113 | 114 | /** 115 | * @group realpush 116 | */ 117 | public function testValidationWithValidCert() 118 | { 119 | $cert = new Certificate(__DIR__ . '/../resources/paspas.pem', null); 120 | $this->assertNotNull($cert->getValidTo()); 121 | } 122 | 123 | public function testValidationWithCorruptCert() 124 | { 125 | $this->setExpectedException('InvalidArgumentException', 'Unable to parse certificate'); 126 | new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null); 127 | } 128 | } -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/MessageBuilder.php: -------------------------------------------------------------------------------- 1 | deviceToken = $deviceToken; 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * Set the certificate to use when 36 | * 37 | * @param Certificate The certificate that must be used for the APNS connection this message is send over 38 | * @return MessageBuilder 39 | */ 40 | public function setCertificate(Certificate $certificate) 41 | { 42 | $this->certificate = $certificate; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Set the moment this message should expire or null if APNS should not store the message at all. 49 | * The last message for a device is stored at APNS for delivery until this moment if the device is offline. 50 | * 51 | * @param \DateTime|null Date until the message should be stored for delivery 52 | * @return MessageBuilder 53 | */ 54 | public function setExpiresAt(\DateTime $expiresAt = null) 55 | { 56 | $this->expiresAt = $expiresAt; 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Set the alert to display. 63 | * See also: http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1 64 | * 65 | * @param string|null The text of the alert to display or null to set no alert 66 | * @param string|null The localization key to use for the action button 67 | * @param string|null The name of the launch image to use 68 | * @return MessageBuilder 69 | */ 70 | public function setAlert($body, $actionLocKey = null, $launchImage = null) 71 | { 72 | // Check if we must use an JSON object 73 | if (null == $actionLocKey && null == $launchImage) 74 | { 75 | // No, just use a string 76 | $this->alert = $body; 77 | } 78 | else 79 | { 80 | // Yes, use an object 81 | $this->alert = array('body' => $body); 82 | 83 | if ($actionLocKey) { 84 | $this->alert['action-loc-key'] = $actionLocKey; 85 | } 86 | 87 | if ($launchImage) { 88 | $this->alert['launch-image'] = $launchImage; 89 | } 90 | } 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Set the localized alert to display. 97 | * See also: http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1 98 | * 99 | * @param string The localization key to use for the text of the alert 100 | * @param array The arguments that fill the gaps in the locKey text 101 | * @param string|null The localization key to use for the action button 102 | * @param string|null The name of the launch image to use 103 | * @return MessageBuilder 104 | */ 105 | public function setAlertLocalized($locKey, $locArgs = array(), $actionLocKey = null, $launchImage = null) 106 | { 107 | // Set the alert 108 | $this->alert = array('loc-key' => $locKey, 'loc-args' => $locArgs); 109 | 110 | if ($actionLocKey) { 111 | $this->alert['action-loc-key'] = $actionLocKey; 112 | } 113 | 114 | if ($launchImage) { 115 | $this->alert['launch-image'] = $launchImage; 116 | } 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * Add custom (iOS 8+) actions to the alert you display 123 | * Note: You must also call setAlert() or setAlertLocalized() to make a complete alert 124 | * 125 | * @param string The identifier of the custom action 126 | * @param string The text of the alert to display 127 | * @return MessageBuilder 128 | */ 129 | public function addAlertAction($id, $title) 130 | { 131 | $this->alertActions[] = array('id' => $id, 'title' => $title); 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * Add localized custom (iOS 8+) actions to the alert you display 138 | * Note: You must also call setAlert() or setAlertLocalized() to make a complete alert 139 | * 140 | * @param string The identifier of the custom action 141 | * @param string The text of the alert to display 142 | * @return MessageBuilder 143 | */ 144 | public function addAlertActionLocalized($id, $locKey, $locArgs = array()) 145 | { 146 | $this->alertActions[] = array('id' => $id, 'locKey' => $locKey, 'locArgs' => $locArgs); 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * Set the badge to display on the App icon 153 | * 154 | * @param int|null The badge number to display 155 | * @return MessageBuilder 156 | */ 157 | public function setBadge($badge) 158 | { 159 | $this->badge = $badge; 160 | 161 | return $this; 162 | } 163 | 164 | /** 165 | * Clear the badge from the App icon 166 | * 167 | * @return MessageBuilder 168 | */ 169 | public function clearBadge() 170 | { 171 | $this->setBadge(0); 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Set the sound that will be played when this message is received 178 | * 179 | * @param string Optional string of the sound to play, no string will play the default sound 180 | * @return MessageBuilder 181 | */ 182 | public function setSound($sound = 'default') 183 | { 184 | $this->sound = $sound; 185 | 186 | return $this; 187 | } 188 | 189 | /** 190 | * Set newsstand content availability flag that will trigger the newsstand item to download new content 191 | * 192 | * @param boolean True when new newsstand content is available, false when not 193 | * @return MessageBuilder 194 | */ 195 | public function setContentAvailable($contentAvailable) 196 | { 197 | $this->contentAvailable = (bool)$contentAvailable; 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * Set the category identifier for this message used by the app to display custom actions 204 | * 205 | * @param string String of the category identifier 206 | * @return MessageBuilder 207 | */ 208 | public function setCategory($category) 209 | { 210 | $this->category = $category; 211 | 212 | return $this; 213 | } 214 | 215 | /** 216 | * Set custom payload to go with the message 217 | * 218 | * @param array|json|null The payload to send as array or JSON string 219 | * @return MessageBuilder 220 | */ 221 | public function setPayload($payload) 222 | { 223 | $this->payload = $payload; 224 | 225 | return $this; 226 | } 227 | 228 | /** 229 | * Build the message 230 | * 231 | * @return Message 232 | * @throws \InvalidArgumentException On invalid or missing arguments 233 | * @throws \LengthException On too long message 234 | */ 235 | public function build() 236 | { 237 | if (null == $this->certificate) { 238 | throw new \InvalidArgumentException('The certificate cannot be null.'); 239 | } 240 | 241 | // Fold alert actions into the alert 242 | if (count($this->alertActions) > 0) 243 | { 244 | if (is_string($this->alert)) { 245 | $this->alert = array('body' => $this->alert, 'actions' => $this->alertActions); 246 | } else if (is_array($this->alert)) { 247 | $this->alert['actions'] = $this->alertActions; 248 | } 249 | } 250 | 251 | return new Message( $this->deviceToken, 252 | $this->certificate, 253 | $this->alert, 254 | $this->badge, 255 | $this->sound, 256 | $this->payload, 257 | $this->category, 258 | $this->contentAvailable, 259 | $this->expiresAt); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/Gateway.php: -------------------------------------------------------------------------------- 1 | lastMessageId = 0; 38 | $this->messageEnvelopeStore = array(); 39 | $this->sendQueue = new \SplQueue(); 40 | } 41 | 42 | /** 43 | * Queue a message for sending 44 | * 45 | * @param Message The message object to queue for sending 46 | * @return MessageEnvelope 47 | */ 48 | public function queue(Message $message) 49 | { 50 | // Bump the message ID 51 | $this->lastMessageId++; 52 | 53 | // Put the message in an envelope 54 | $envelope = new MessageEnvelope($this->lastMessageId, $message); 55 | 56 | // Save the message so we can update it later on 57 | $this->storeMessageEnvelope($envelope); 58 | 59 | // Queue and return the envelope 60 | $this->logger->debug('Queuing Apns\Message #' . $this->lastMessageId . ' to device "' . $message->getDeviceToken() . '" on Apns\Gateway with certificate "' . $this->getCertificate()->getDescription() . '"'); 61 | $this->sendQueue->enqueue($envelope); 62 | 63 | return $envelope; 64 | } 65 | 66 | /** 67 | * Count of all queued messages 68 | * 69 | * @return int 70 | */ 71 | public function getQueueLength() 72 | { 73 | return $this->sendQueue->count(); 74 | } 75 | 76 | /** 77 | * Send all queued messages 78 | */ 79 | public function flush() 80 | { 81 | // Don't do anything if the queue is empty 82 | if ($this->sendQueue->isEmpty()) { 83 | $this->logger->info('Flushing the already empty queue of Apns\Gateway with certificate "' . $this->getCertificate()->getDescription() . '"'); 84 | return; 85 | } 86 | 87 | // Connect to APNS if needed 88 | if (!is_resource($this->getConnection())) { 89 | $this->connect(); 90 | } 91 | 92 | $this->logger->info('Flushing ' . $this->getQueueLength() . ' messages from the queue of Apns\Gateway with certificate "' . $this->getCertificate()->getDescription() . '"'); 93 | 94 | // Handle all messages in the queue 95 | while (!$this->sendQueue->isEmpty()) 96 | { 97 | // Make sure signals to this process are respected and handled 98 | if (function_exists('pcntl_signal_dispatch')) { 99 | pcntl_signal_dispatch(); 100 | } 101 | 102 | // Get the next message to send 103 | $messageEnvelope = $this->sendQueue->dequeue(); 104 | $binaryMessage = $messageEnvelope->getBinaryMessage(); 105 | 106 | // Send the message and check if all the bytes are written 107 | $bytesSend = (int)fwrite($this->getConnection(), $binaryMessage); 108 | if (strlen($binaryMessage) !== $bytesSend) 109 | { 110 | // Something did go wrong while sending this message, retry 111 | $retryMessageEnvelope = $this->queue( $messageEnvelope->getMessage() ); 112 | $messageEnvelope->setStatus(MessageEnvelope::STATUS_SENDFAILED, $retryMessageEnvelope); 113 | $this->logger->debug('Failed to send Apns\Message #' . $this->lastMessageId . ' "' . $messageEnvelope->getStatusDescription() . '" to device "' . $messageEnvelope->getMessage()->getDeviceToken() . '" on Apns\Gateway with certificate "' . $this->getCertificate()->getDescription() . '"'); 114 | 115 | // Sending failed, need to reconnect the socket 116 | $this->waitAndCheckForErrorResponse(); 117 | $this->disconnect(); 118 | $this->connect(); 119 | } 120 | else 121 | { 122 | // Mark the message as send without errors 123 | $messageEnvelope->setStatus(MessageEnvelope::STATUS_NOERRORS); 124 | 125 | // Take a nap to give PHP some time to relax after sending data over the socket 126 | usleep(self::SEND_INTERVAL); 127 | 128 | // Check for errors 129 | $this->checkForErrorResponse(); 130 | } 131 | } 132 | 133 | $this->waitAndCheckForErrorResponse(); 134 | 135 | // If there are requeued messages, initiate a new flush 136 | if ($this->getQueueLength() > 0) 137 | { 138 | $this->flush(); 139 | } 140 | else 141 | { 142 | // Clear the message envelope store 143 | $this->clearMessageEnvelopeStore(); 144 | } 145 | } 146 | 147 | private function waitAndCheckForErrorResponse() 148 | { 149 | // All messages send, wait some time for an APNS response 150 | $read = array($this->getConnection()); 151 | $write = $except = null; 152 | $changedStreams = stream_select($read, $write, $except, 0, self::READ_TIMEOUT); 153 | 154 | // Did waiting for the response succeed? 155 | if (false === $changedStreams) 156 | { 157 | throw new \RuntimeException('Could not stream_select the APNS connection.'); 158 | } 159 | // Did we receive a response? 160 | else if ($changedStreams > 0) 161 | { 162 | // Handle the response 163 | $this->checkForErrorResponse(); 164 | } 165 | } 166 | 167 | /** 168 | * Check the connection for an error response from APNS 169 | */ 170 | private function checkForErrorResponse() 171 | { 172 | // Check if there is something to read from the socket 173 | $errorResponse = fread($this->getConnection(), self::ERROR_RESPONSE_SIZE); 174 | if (false !== $errorResponse && self::ERROR_RESPONSE_SIZE === strlen($errorResponse)) 175 | { 176 | // Got an error, disconnect 177 | $this->disconnect(); 178 | 179 | // Decode the error response 180 | $errorMessage = unpack('Ccommand/Cstatus/Nidentifier', $errorResponse); 181 | 182 | // Validate the message 183 | if (self::ERROR_RESPONSE_COMMAND != $errorMessage['command']) { 184 | throw new \RuntimeException('APNS responded with corrupt errormessage.'); 185 | } 186 | 187 | // Mark the message that triggered the error as failed 188 | $failedMessageEnvelope = $this->retrieveMessageEnvelope($errorMessage['identifier']); 189 | if (null != $failedMessageEnvelope) 190 | { 191 | $failedMessageEnvelope->setStatus($errorMessage['status']); 192 | $this->logger->warning('Failed to send message #' . $failedMessageEnvelope->getIdentifier() . ' "' . $failedMessageEnvelope->getStatusDescription() . '" to device "' . $failedMessageEnvelope->getMessage()->getDeviceToken() . '" from the queue of Apns\Gateway with certificate "' . $this->getCertificate()->getDescription() . '"'); 193 | } 194 | else 195 | { 196 | $this->logger->error('Failed retrieve message envelope for message #' . $errorMessage['identifier'] . ' that failed sending with statuscode #' . $errorMessage['status'] . ' from the queue of Apns\Gateway with certificate "' . $this->getCertificate()->getDescription() . '"'); 197 | } 198 | 199 | // All messages that are send after the failed message should be send again 200 | $this->logger->info('Requeueing ' . ($this->lastMessageId - $errorMessage['identifier']) . ' messages that where send after the failed message to the queue of Apns\Gateway with certificate "' . $this->getCertificate()->getDescription() . '"'); 201 | $lastMessageToResend = $this->lastMessageId; 202 | for ($messageId = $errorMessage['identifier'] + 1; $lastMessageToResend >= $messageId; $messageId++) 203 | { 204 | // Get the message envelope 205 | $messageEnvelope = $this->retrieveMessageEnvelope($messageId); 206 | 207 | // Check if it's send without errors 208 | if (null !== $messageEnvelope && $messageEnvelope->getStatus() == MessageEnvelope::STATUS_NOERRORS) 209 | { 210 | // Mark the message as failed due earlier error and requeue the message 211 | $retryMessageEnvelope = $this->queue( $messageEnvelope->getMessage() ); 212 | $messageEnvelope->setStatus(MessageEnvelope::STATUS_EARLIERERROR, $retryMessageEnvelope); 213 | $this->logger->debug('Failed to send Apns\Message #' . $this->lastMessageId . ' "' . $messageEnvelope->getStatusDescription() . '" to device "' . $messageEnvelope->getMessage()->getDeviceToken() . '" on Apns\Gateway with certificate "' . $this->getCertificate()->getDescription() . '"'); 214 | } 215 | else 216 | { 217 | $this->logger->warning('Could not requeue message #' . $this->lastMessageId . ' "Envelope already purged from envelope store" to the queue of Apns\Gateway with certificate "' . $this->getCertificate()->getDescription() . '"'); 218 | } 219 | } 220 | 221 | // Reconnect and go on 222 | $this->connect(); 223 | } 224 | } 225 | 226 | /** 227 | * Store a message envelope for later reference (error handling etc) 228 | * 229 | * @param MessageEnvelope The envelope to story 230 | */ 231 | protected function storeMessageEnvelope(MessageEnvelope $envelope) 232 | { 233 | // Add the given anvelope to the messages array 234 | $this->messageEnvelopeStore[self::MESSAGE_ENVELOPE_STORE_PREFIX . $envelope->getIdentifier()] = $envelope; 235 | } 236 | 237 | /** 238 | * Retrieve a stored envelope 239 | * 240 | * @return MessageEnvelope|null 241 | */ 242 | protected function retrieveMessageEnvelope($identifier) 243 | { 244 | $envelope = null; 245 | 246 | // Fetch the requested anvelope if we have any 247 | if ( isset($this->messageEnvelopeStore[self::MESSAGE_ENVELOPE_STORE_PREFIX . $identifier]) ) { 248 | $envelope = $this->messageEnvelopeStore[self::MESSAGE_ENVELOPE_STORE_PREFIX . $identifier]; 249 | } 250 | 251 | return $envelope; 252 | } 253 | 254 | /** 255 | * Wipes all envelopes from the store 256 | */ 257 | protected function clearMessageEnvelopeStore() 258 | { 259 | $this->messageEnvelopeStore = array(); 260 | } 261 | } -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/Certificate.php: -------------------------------------------------------------------------------- 1 | array( 31 | self::ENDPOINT_TYPE_GATEWAY => 'ssl://gateway.push.apple.com:2195', 32 | self::ENDPOINT_TYPE_FEEDBACK => 'ssl://feedback.push.apple.com:2196' 33 | ), 34 | self::ENDPOINT_ENV_SANDBOX => array( 35 | self::ENDPOINT_TYPE_GATEWAY => 'ssl://gateway.sandbox.push.apple.com:2195', 36 | self::ENDPOINT_TYPE_FEEDBACK => 'ssl://feedback.sandbox.push.apple.com:2196' 37 | ) 38 | ); 39 | 40 | private $pemFile; 41 | private $passphrase; 42 | private $endpointEnv; 43 | private $fingerprint; 44 | 45 | private $isValidated; 46 | private $description; 47 | private $validFrom; 48 | private $validTo; 49 | 50 | /** 51 | * APNS Certificate constructor 52 | * 53 | * @param string Path to the PEM certificate file 54 | * @param string|null Passphrase to use with the PEM file 55 | * @param boolean Set to false to skip the validation of the certificate, default true 56 | * @param string|null APNS environment this certificate is valid for, by default autodetects during validation 57 | * 58 | * @throws InvalidCertificateException 59 | * @throws \InvalidArgumentException 60 | */ 61 | public function __construct($pemFile, $passphrase = null, $validate = true, $endpointEnv = null) 62 | { 63 | // Check if the given PEM file does exists and expand the path 64 | $absolutePemFilePath = realpath($pemFile); 65 | if (!is_file($absolutePemFilePath)) { 66 | throw new InvalidCertificateException('Could not find the given PEM file "' . $pemFile . '".'); 67 | } 68 | 69 | // Save the given parameters 70 | $this->pemFile = $absolutePemFilePath; 71 | $this->passphrase = $passphrase; 72 | $this->endpointEnv = $endpointEnv; 73 | $this->fingerprint = null; 74 | $this->isValidated = false; 75 | 76 | // Parse (and validate) the certificate 77 | if ($validate) 78 | { 79 | $this->parseCertificate(); 80 | $this->isValidated = true; 81 | } 82 | 83 | // A valid endpoint is required by now 84 | if (null == $this->endpointEnv) { 85 | throw new InvalidCertificateException('No endpoint given and/or detected from certificate.'); 86 | } else if (self::ENDPOINT_ENV_PRODUCTION !== $this->endpointEnv && self::ENDPOINT_ENV_SANDBOX !== $this->endpointEnv) { 87 | throw new \InvalidArgumentException('Invalid endpoint given: ' . $endpointEnv); 88 | } 89 | } 90 | 91 | /** 92 | * Parse and validate the certificate and private key, also extracts usefull data and sets it on this object 93 | * Also throws exceptions if the certificate/private key doesn't seem to be a valid APNS cert 94 | */ 95 | private function parseCertificate() 96 | { 97 | $now = new \DateTime(); 98 | 99 | // Parse the certificate 100 | $certificateData = openssl_x509_parse( file_get_contents($this->getPemFile()) ); 101 | if (false == $certificateData) { 102 | throw new InvalidCertificateException('Unable to parse certificate "' . $this->getPemFile() . '", are you sure this is a valid PEM certificate?'); 103 | } 104 | 105 | // Validate the "valid from" timestamp 106 | if (isset($certificateData['validFrom_time_t'])) 107 | { 108 | $validFrom = new \DateTime('@' . $certificateData['validFrom_time_t']); 109 | if ($validFrom > $now) { 110 | throw new InvalidCertificateException('Certificate "' . $this->getPemFile() . '" not yet valid, valid from ' . $validFrom->format(\DateTime::ISO8601) . '.'); 111 | } 112 | 113 | $this->validFrom = $validFrom; 114 | } 115 | else { 116 | throw new InvalidCertificateException('Certificate "' . $this->getPemFile() . '" has no valid from timestamp.'); 117 | } 118 | 119 | // Validate the "valid to" timestamp 120 | if (isset($certificateData['validTo_time_t'])) 121 | { 122 | $validTo = new \DateTime('@' . $certificateData['validTo_time_t']); 123 | if ($validTo < $now) 124 | { 125 | throw new InvalidCertificateException('Certificate "' . $this->getPemFile() . '" expired, was valid until ' . $validTo->format(\DateTime::ISO8601) . '.'); 126 | } 127 | 128 | $this->validTo = $validTo; 129 | } 130 | else { 131 | throw new InvalidCertificateException('Certificate "' . $this->getPemFile() . '" has no valid to timestamp.'); 132 | } 133 | 134 | // Check if the certificate was issued by Apple 135 | if (!isset($certificateData['issuer']) || !isset($certificateData['issuer']['O']) || 'Apple Inc.' != $certificateData['issuer']['O']) { 136 | throw new InvalidCertificateException('Certificate "' . $this->getPemFile() . '" does not list Apple Inc. as the issuer.'); 137 | } 138 | 139 | // Check if the there is an environment hidden in the certificate 140 | if (isset($certificateData['subject']) && isset($certificateData['subject']['CN'])) 141 | { 142 | $this->description = $certificateData['subject']['CN']; 143 | 144 | if (null === $this->endpointEnv) 145 | { 146 | if (strpos($certificateData['subject']['CN'], 'Pass Type ID') === 0 || 147 | strpos($certificateData['subject']['CN'], 'Apple Push Services') === 0 || 148 | strpos($certificateData['subject']['CN'], 'Apple Production IOS Push Services') === 0 || 149 | strpos($certificateData['subject']['CN'], 'Apple Production Mac Push Services') === 0) { 150 | // Passbook Pass certificate & APNS Production/hybrid certs, should be on production 151 | $this->endpointEnv = self::ENDPOINT_ENV_PRODUCTION; 152 | } else if ( strpos($certificateData['subject']['CN'], 'Apple Development IOS Push Services') === 0 || 153 | strpos($certificateData['subject']['CN'], 'Apple Development Mac Push Services') === 0) { 154 | // APNS Development, should always be on sandbox 155 | $this->endpointEnv = self::ENDPOINT_ENV_SANDBOX; 156 | } else { 157 | throw new InvalidCertificateException('Could not detect APNS environment based on the CN string "' . $certificateData['subject']['CN'] . '" in certificate "' . $this->getPemFile() . '".'); 158 | } 159 | } 160 | } 161 | else 162 | { 163 | throw new InvalidCertificateException('No APNS environment information found in certificate "' . $this->getPemFile() . '".'); 164 | } 165 | 166 | // Validate the private key by loading it 167 | $privateKey = openssl_pkey_get_private('file://' . $this->getPemFile(), $this->getPassphrase() ); 168 | if (false === $privateKey) { 169 | throw new InvalidCertificateException('Could not extract the private key from certificate "' . $this->getPemFile() . '", please check if the given passphrase is correct and if it contains a private key.'); 170 | } 171 | 172 | // If a passphrase is given, the private key may not be loaded without it 173 | if ($this->getPassphrase() != null) 174 | { 175 | // Try to load the private key without the passphrase (should fail) 176 | $privateKey = openssl_pkey_get_private('file://' . $this->getPemFile() ); 177 | if (false !== $privateKey) { 178 | throw new InvalidCertificateException('Passphrase given, but the private key in "' . $this->getPemFile() . '" is not encrypted, please make sure you are using the correct certificate/passphrase combination.'); 179 | } 180 | } 181 | } 182 | 183 | /** 184 | * Get the path to the PEM file 185 | * 186 | * @return string 187 | */ 188 | public function getPemFile() 189 | { 190 | return $this->pemFile; 191 | } 192 | 193 | /** 194 | * Checks if there is a passphrase to use with the certificate 195 | * 196 | * @return boolean 197 | */ 198 | public function hasPassphrase() 199 | { 200 | return (strlen($this->passphrase) > 0); 201 | } 202 | 203 | /** 204 | * Passphrase to use with the PEM file 205 | * 206 | * @return string 207 | */ 208 | public function getPassphrase() 209 | { 210 | return $this->passphrase; 211 | } 212 | 213 | /** 214 | * Get the APNS environment this certificate is associated with 215 | * 216 | * @return Certificate::ENDPOINT_ENV_PRODUCTION|Certificate::ENDPOINT_ENV_SANDBOX 217 | */ 218 | public function getEnvironment() 219 | { 220 | return $this->endpointEnv; 221 | } 222 | 223 | /** 224 | * Check if this certificate is validated 225 | * 226 | * @return boolean 227 | */ 228 | public function isValidated() 229 | { 230 | return $this->isValidated; 231 | } 232 | 233 | /** 234 | * An as humanreadable as possible description of the certificate to identify the certificate 235 | * 236 | * @return string 237 | */ 238 | public function getDescription() 239 | { 240 | $description = $this->description; 241 | 242 | if (null == $description) { 243 | $description = $this->getFingerprint(); 244 | } 245 | 246 | return $description; 247 | } 248 | 249 | /** 250 | * Get moment this certificate will become valid 251 | * Note: Will return null if certificate validation was disabled 252 | * 253 | * @return \DateTime|null 254 | */ 255 | public function getValidFrom() 256 | { 257 | return $this->validFrom; 258 | } 259 | 260 | /** 261 | * Get moment this certificate will expire 262 | * Note: Will return null if certificate validation was disabled 263 | * 264 | * @return \DateTime|null 265 | */ 266 | public function getValidTo() 267 | { 268 | return $this->validTo; 269 | } 270 | 271 | /** 272 | * Get the endpoint this certificate is valid for 273 | * 274 | * @param string The type of endpoint you want 275 | * @return string 276 | * 277 | * @throws \InvalidArgumentException 278 | */ 279 | public function getEndpoint($endpointType) 280 | { 281 | // Check if the endpoint type is valid 282 | if (self::ENDPOINT_TYPE_GATEWAY !== $endpointType && self::ENDPOINT_TYPE_FEEDBACK !== $endpointType ) { 283 | throw new \InvalidArgumentException($endpointType . ' is not a valid endpoint type.'); 284 | } 285 | 286 | return self::$endpoints[$this->endpointEnv][$endpointType]; 287 | } 288 | 289 | /** 290 | * Get a unique hash of the certificate 291 | * this can be used to check if two Apns\Certificate objects are the same 292 | * 293 | * @return string 294 | */ 295 | public function getFingerprint() 296 | { 297 | // Calculate fingerprint if unknown 298 | if (null == $this->fingerprint) { 299 | $this->fingerprint = sha1( $this->endpointEnv . sha1_file($this->getPemFile()) ); 300 | } 301 | 302 | return $this->fingerprint; 303 | } 304 | 305 | /** 306 | * String representation of object 307 | * 308 | * @return string 309 | */ 310 | public function serialize() 311 | { 312 | return serialize(array( $this->pemFile, 313 | $this->passphrase, 314 | $this->endpointEnv, 315 | 316 | $this->isValidated, 317 | $this->description, 318 | $this->validFrom, 319 | $this->validTo)); 320 | } 321 | 322 | /** 323 | * Constructs the object from serialized data 324 | * 325 | * @param string Serialized data 326 | */ 327 | public function unserialize($serialized) 328 | { 329 | list( $this->pemFile, 330 | $this->passphrase, 331 | $this->endpointEnv, 332 | 333 | $this->isValidated, 334 | $this->description, 335 | $this->validFrom, 336 | $this->validTo) = unserialize($serialized); 337 | 338 | // Fingerprint should be recalculated 339 | $this->fingerprint = null; 340 | } 341 | } -------------------------------------------------------------------------------- /src/Wrep/Notificato/Apns/Message.php: -------------------------------------------------------------------------------- 1 | setDeviceToken($deviceToken); 53 | $this->certificate = $certificate; 54 | $this->expiresAt = (null == $expiresAt) ? 0 : $expiresAt->format('U'); 55 | 56 | // Set the defaults 57 | $this->setAlert($alert); 58 | $this->setBadge($badge); 59 | $this->sound = (null == $sound) ? null : (string)$sound; 60 | $this->setPayload($payload); 61 | $this->category = $category; 62 | $this->contentAvailable = (bool)$contentAvailable; 63 | 64 | // Validate the length of the message 65 | if (strlen($this->getJson()) > 2048) { 66 | throw new \LengthException('Length of the message exceeds the maximum of 2048 characters.'); 67 | } 68 | } 69 | 70 | /** 71 | * Check if this message is short enough to be send to iOS 7 or OS X 72 | * Note: iOS 8 support messages up to 2048 bytes, OS X and iOS 7 and below support messages up to 256 bytes 73 | * 74 | * @return boolean Wheter you can send this message savely to older OSses 75 | */ 76 | public function isCompatibleWithSmallPayloadSize() 77 | { 78 | return (strlen($this->getJson()) <= 256); 79 | } 80 | 81 | /** 82 | * Get the device token of the receiving device 83 | * 84 | * @return string 85 | */ 86 | public function getDeviceToken() 87 | { 88 | return $this->deviceToken; 89 | } 90 | 91 | /** 92 | * Get the certificate that should be used for this message 93 | * 94 | * @return Certificate 95 | */ 96 | public function getCertificate() 97 | { 98 | return $this->certificate; 99 | } 100 | 101 | /** 102 | * Get the moment this message expires 103 | * 104 | * @return int Unix timestamp of expiry moment or zero if no specific expiry moment is set 105 | */ 106 | public function getExpiresAt() 107 | { 108 | return $this->expiresAt; 109 | } 110 | 111 | /** 112 | * Get the current alert 113 | * 114 | * @return string|array 115 | */ 116 | public function getAlert() 117 | { 118 | return $this->alert; 119 | } 120 | 121 | /** 122 | * Get the value of the badge as set in this message 123 | * 124 | * @return int|null 125 | */ 126 | public function getBadge() 127 | { 128 | return $this->badge; 129 | } 130 | 131 | /** 132 | * Get the sound that will be played when this message is received 133 | * 134 | * @return string|null 135 | */ 136 | public function getSound() 137 | { 138 | return $this->sound; 139 | } 140 | 141 | /** 142 | * Get the category identifier that will be used to determine custom actions 143 | * 144 | * @return string|null 145 | */ 146 | public function getCategory() 147 | { 148 | return $this->category; 149 | } 150 | 151 | /** 152 | * Get newsstand content availability flag that will trigger the newsstand item to download new content 153 | * 154 | * @return boolean True when new content is available, false when not 155 | */ 156 | public function getContentAvailable() 157 | { 158 | return $this->contentAvailable; 159 | } 160 | 161 | /** 162 | * Get the current payload 163 | * 164 | * @return array|null 165 | */ 166 | public function getPayload() 167 | { 168 | return $this->payload; 169 | } 170 | 171 | /** 172 | * Get the JSON payload that should be send to the APNS 173 | * 174 | * @return string 175 | * @throws \RuntimeException When unable to create JSON, for example because of non-UTF-8 characters 176 | */ 177 | public function getJson() 178 | { 179 | // Get message and aps array to create JSON from 180 | $message = array(); 181 | $aps = array(); 182 | 183 | // If we have a payload replace the message object by the payload 184 | if (null !== $this->payload) { 185 | $message = $this->payload; 186 | } 187 | 188 | // Add the alert if any 189 | if (null !== $this->alert) { 190 | $aps['alert'] = $this->alert; 191 | } 192 | 193 | // Add the badge if any 194 | if (null !== $this->badge) { 195 | $aps['badge'] = $this->badge; 196 | } 197 | 198 | // Add the sound if any 199 | if (null !== $this->sound) { 200 | $aps['sound'] = $this->sound; 201 | } 202 | 203 | // Add category identifier if any 204 | if (null !== $this->category) { 205 | $aps['category'] = $this->category; 206 | } 207 | 208 | // Add the content-available flag if set 209 | if (true == $this->contentAvailable) { 210 | $aps['content-available'] = 1; 211 | } 212 | 213 | // Check if APS data is set 214 | if (count($aps) > 0) { 215 | $message['aps'] = $aps; 216 | } 217 | 218 | // Encode as JSON object 219 | $json = json_encode($message); 220 | if (false == $json) { 221 | throw new \RuntimeException('Failed to convert APNS\Message to JSON, are all strings UTF-8?', json_last_error()); 222 | } 223 | 224 | return $json; 225 | } 226 | 227 | /** 228 | * String representation of object 229 | * 230 | * @return string 231 | */ 232 | public function serialize() 233 | { 234 | return serialize(array( $this->deviceToken, 235 | $this->certificate, 236 | $this->expiresAt, 237 | $this->alert, 238 | $this->badge, 239 | $this->sound, 240 | $this->payload, 241 | $this->contentAvailable)); 242 | } 243 | 244 | /** 245 | * Constructs the object from serialized data 246 | * 247 | * @param string Serialized data 248 | */ 249 | public function unserialize($serialized) 250 | { 251 | list( $this->deviceToken, 252 | $this->certificate, 253 | $this->expiresAt, 254 | $this->alert, 255 | $this->badge, 256 | $this->sound, 257 | $this->payload, 258 | $this->contentAvailable) = unserialize($serialized); 259 | } 260 | 261 | /** 262 | * Get a string representation of this object 263 | * 264 | * @return string 265 | */ 266 | public function __toString() 267 | { 268 | return get_class($this) . ':' . PHP_EOL . 269 | ' Device token: ' . $this->getDeviceToken() . PHP_EOL . 270 | ' Certificate: ' . $this->getCertificate()->getPemFile() . PHP_EOL . 271 | ' Expires timestamp: ' . $this->getExpiresAt() . PHP_EOL . 272 | ' Badge: ' . $this->getBadge() . PHP_EOL . 273 | ' Sound: ' . $this->getSound() . PHP_EOL . 274 | ' Content avail.: ' . $this->getContentAvailable() . PHP_EOL . 275 | ' Alert: ' . json_encode($this->getAlert(), JSON_PRETTY_PRINT) . PHP_EOL . 276 | ' Payload: ' . json_encode($this->getPayload(), JSON_PRETTY_PRINT) . PHP_EOL; 277 | } 278 | 279 | /** 280 | * Set the receiver of the message 281 | * 282 | * @param string Receiver of this message 283 | * @throws \InvalidArgumentException On invalid or missing arguments 284 | */ 285 | private function setDeviceToken($deviceToken) 286 | { 287 | // Check if a devicetoken is given 288 | if (null == $deviceToken) { 289 | throw new \InvalidArgumentException('No device token given.'); 290 | } 291 | 292 | // Check if the devicetoken is a valid hexadecimal string 293 | if (!ctype_xdigit($deviceToken)) { 294 | throw new \InvalidArgumentException('Invalid device token given, no hexadecimal: ' . $deviceToken); 295 | } 296 | 297 | // Set the devicetoken 298 | $this->deviceToken = $deviceToken; 299 | } 300 | 301 | /** 302 | * Set the alert to display. 303 | * See also: http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1 304 | * 305 | * @param array|null The alert to display or null to set no alert 306 | * @throws \InvalidArgumentException On invalid or missing arguments 307 | */ 308 | private function setAlert($alert) 309 | { 310 | if (null == $alert) 311 | { 312 | // No alert is okay 313 | $this->alert = null; 314 | } 315 | else if ( is_string($alert) ) 316 | { 317 | // String only alert is okay 318 | $this->alert = $alert; 319 | } 320 | else if ( is_array($alert) ) 321 | { 322 | if ( isset($alert['body']) || (isset($alert['loc-key']) && isset($alert['loc-args'])) ) 323 | { 324 | // Valid alert, just fine! 325 | $this->alert = $alert; 326 | } 327 | else 328 | { 329 | // Alert must contain correct keys 330 | throw new \InvalidArgumentException('Invalid alert for message. Alert does not contain a body key or the loc-key and loc-args keys.'); 331 | } 332 | } 333 | else 334 | { 335 | // Alert must be null or an array 336 | throw new \InvalidArgumentException('Invalid alert for message. Alert was not an array or null value.'); 337 | } 338 | } 339 | 340 | /** 341 | * Set the badge to display on the App icon 342 | * 343 | * @param int|null The badge number to display, zero to remove badge 344 | * @throws \InvalidArgumentException On invalid or missing arguments 345 | */ 346 | private function setBadge($badge) 347 | { 348 | // Validate the badge int 349 | if ((int)$badge < 0) { 350 | throw new \InvalidArgumentException('Badge must be 0 or higher.'); 351 | } 352 | 353 | // Cast to int or set to null 354 | $this->badge = (null === $badge) ? null : (int)$badge; 355 | } 356 | 357 | /** 358 | * Set custom payload to go with the message 359 | * 360 | * @param array|json|null The payload to send as array or JSON string 361 | * @throws \InvalidArgumentException On invalid or missing arguments 362 | */ 363 | private function setPayload($payload) 364 | { 365 | if ( (is_string($payload) && empty($payload)) || (is_array($payload) && count($payload) == 0) ) 366 | { 367 | // Empty strings or arrays are not allowed 368 | throw new \InvalidArgumentException('Invalid payload for message. Payload was empty, but not null)'); 369 | } 370 | else if (is_array($payload) || null === $payload) 371 | { 372 | if ( isset($payload['aps']) ) 373 | { 374 | // Reserved key is used 375 | throw new \InvalidArgumentException('Invalid payload for message. Custom payload may not contain the reserved "aps" key.'); 376 | } 377 | else 378 | { 379 | // This is okay, set as payload 380 | $this->payload = $payload; 381 | } 382 | } 383 | else 384 | { 385 | // Try to decode JSON string payload 386 | $payload = json_decode($payload, true); 387 | 388 | // Check if decoding the payload worked 389 | if (null === $payload) { 390 | throw new \InvalidArgumentException('Invalid payload for message. Payload was invalid JSON.'); 391 | } 392 | 393 | // Set as payload 394 | $this->payload = $payload; 395 | } 396 | } 397 | } -------------------------------------------------------------------------------- /tests/Wrep/Notificato/Test/Apns/MessageTest.php: -------------------------------------------------------------------------------- 1 | setDeviceToken('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')->setCertificate($certificate)->build(); 14 | 15 | $this->assertInstanceOf('\Wrep\Notificato\Apns\Message', $message, 'Message of incorrect classtype.'); 16 | $this->assertEquals('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $message->getDeviceToken(), 'Incorrect token retrieved.'); 17 | } 18 | 19 | /** 20 | * @dataProvider incorrectConstructorArguments 21 | */ 22 | public function testInvalidConstruction($deviceToken, $certificate, $alert, $badge, $sound, $payload, $category, $contentAvailable, $expiresAt) 23 | { 24 | $this->setExpectedException('InvalidArgumentException'); 25 | $message = new Message($deviceToken, $certificate, $alert, $badge, $sound, $payload, $category, $contentAvailable, $expiresAt); 26 | } 27 | 28 | public function incorrectConstructorArguments() 29 | { 30 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 31 | 32 | return array( 33 | array( 'thisisnotanhexstring!', $certificate, null, 5, 'default', null, null, null, null ), 34 | array( '', $certificate, null, 5, 'default', null, null, null, null ), 35 | array( 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, -1, 'default', null, null, null, null ), 36 | array( 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, 'default', 'invalidjsonstring {}', null, null, null ) 37 | ); 38 | } 39 | 40 | /** 41 | * @dataProvider correctExpiryArguments 42 | */ 43 | public function testExpiry($expiryDate) 44 | { 45 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 46 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, 0, 'default', null, null, null, $expiryDate); 47 | 48 | if (null == $expiryDate) 49 | { 50 | $this->assertEquals(0, $message->getExpiresAt()); 51 | } 52 | else 53 | { 54 | $this->assertEquals($expiryDate->format('U'), $message->getExpiresAt()); 55 | } 56 | } 57 | 58 | public function correctExpiryArguments() 59 | { 60 | return array( 61 | array( new \DateTime('2020-12-12 12:12:12') ), 62 | array( new \DateTime('tomorrow') ), 63 | array(null) 64 | ); 65 | } 66 | 67 | /** 68 | * @dataProvider correctAlertArguments 69 | */ 70 | public function testAlert($alert) 71 | { 72 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 73 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, $alert, 0, 'default', null, null, null, null); 74 | 75 | $this->assertEquals($alert, $message->getAlert()); 76 | } 77 | 78 | public function correctAlertArguments() 79 | { 80 | return array( 81 | array( null ), 82 | array( 'alert-body' ), 83 | array( array('body' => 'alert-body', 'action-loc-key' => 'action') ), 84 | array( array('body' => 'alert-body', 'action-loc-key' => 'action', 'launch-image' => 'image') ), 85 | array( array('body' => 'alert-body', 'launch-image' => 'image') ), 86 | 87 | array('loc-key' => 'alert-loc-key', 'loc-args' => array()), 88 | array('loc-key' => 'alert-loc-key', 'loc-args' => array()), 89 | array('loc-key' => 'alert-loc-key', 'loc-args' => array(), 'launch-image' => 'image'), 90 | array('loc-key' => 'alert-loc-key', 'loc-args' => array(), 'action-loc-key' => 'action'), 91 | array('loc-key' => 'alert-loc-key', 'loc-args' => array(), 'action-loc-key' => 'action', 'launch-image' => 'image'), 92 | array('loc-key' => 'alert-loc-key', 'loc-args' => array('1', '2')), 93 | array('loc-key' => 'alert-loc-key', 'loc-args' => array('1', '2'), 'launch-image' => 'image'), 94 | array('loc-key' => 'alert-loc-key', 'loc-args' => array('1', '2'), 'action-loc-key' => 'action'), 95 | array('loc-key' => 'alert-loc-key', 'loc-args' => array('1', '2'), 'action-loc-key' => 'action', 'launch-image' => 'image') 96 | ); 97 | } 98 | 99 | /** 100 | * @dataProvider incorrectAlertArguments 101 | */ 102 | public function testInvalidAlert($alert) 103 | { 104 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 105 | 106 | $this->setExpectedException('InvalidArgumentException'); 107 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, $alert, 0, 'default', null, null, null, null); 108 | 109 | } 110 | 111 | public function incorrectAlertArguments() 112 | { 113 | return array( 114 | array( array('action-loc-key' => 'action') ), 115 | array( array('loc-key' => 'alert-loc-key') ), 116 | array( array('loc-args' => array()) ) 117 | ); 118 | } 119 | 120 | public function testBadge() 121 | { 122 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 123 | 124 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, 999, 'default', null, null, null, null); 125 | $this->assertEquals(999, $message->getBadge(), 'Setting badge to 999 did not persist.'); 126 | 127 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, 0, 'default', null, null, null, null); 128 | $this->assertEquals(0, $message->getBadge(), 'Clearing the badge did not persist.'); 129 | 130 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, 'default', null, null, null, null); 131 | $this->assertNull($message->getBadge(), 'Unsetting the badge did not persist.'); 132 | } 133 | 134 | public function testSound() 135 | { 136 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 137 | 138 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, 'funkybeat', null, null, null, null, null); 139 | $this->assertEquals('funkybeat', $message->getSound(), 'Setting sound to funkybeat did not persist.'); 140 | 141 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, 'default', null, null, null, null, null); 142 | $this->assertEquals('default', $message->getSound(), 'Setting sound to default did not persist.'); 143 | 144 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, null, null, null, null, null); 145 | $this->assertNull($message->getSound(), 'Unsetting the sound did not persist.'); 146 | } 147 | 148 | public function testCategory() 149 | { 150 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 151 | 152 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, null, 'testcat', null, null); 153 | $this->assertEquals('testcat', $message->getCategory(), 'Setting category to testcat did not persist.'); 154 | 155 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, null, null, null, null); 156 | $this->assertNull(null, $message->getCategory(), 'Nulling the category did not persist.'); 157 | } 158 | 159 | public function testContentAvailable() 160 | { 161 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 162 | 163 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, null, null, true, null); 164 | $this->assertEquals(true, $message->getContentAvailable(), 'Setting ContentAvailable to true did not persist.'); 165 | 166 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, null, null, false, null); 167 | $this->assertEquals(false, $message->getContentAvailable(), 'Disabling the ContentAvailable did not persist.'); 168 | 169 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, null, null, null, null); 170 | $this->assertEquals(false, $message->getContentAvailable(), 'Nulling the ContentAvailable did not persist.'); 171 | } 172 | 173 | /** 174 | * @dataProvider correctPayloadArguments 175 | */ 176 | public function testPayload($payload) 177 | { 178 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 179 | 180 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, $payload, null, null, null); 181 | $this->assertEquals($payload, $message->getPayload(), 'Setting payload did not persist.'); 182 | } 183 | 184 | public function correctPayloadArguments() 185 | { 186 | return array( 187 | array( array('payload' => 'hasAString') ), 188 | array( array('payload' => array('key' => 'value')) ), 189 | array( array('this', 'is', 'some', 'payload') ), 190 | array( array('p' => str_pad('a', 248)) ) 191 | ); 192 | } 193 | 194 | /** 195 | * @dataProvider longPayloadArguments 196 | */ 197 | public function testTooLongPayload($payload) 198 | { 199 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 200 | 201 | $this->setExpectedException('\LengthException'); 202 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, $payload, null, null, null); 203 | $this->assertEquals($payload, $message->getPayload(), 'Setting payload did not persist.'); 204 | } 205 | 206 | public function longPayloadArguments() 207 | { 208 | return array( 209 | array( array('p' => str_pad('a', 2041))), 210 | array( array('p' => str_pad('a', 2048))), 211 | array( array('p' => str_pad('a', 4000))) 212 | ); 213 | } 214 | 215 | /** 216 | * @dataProvider shortPayloadArguments 217 | */ 218 | public function testCompatibleWithSmallPayload($payload) 219 | { 220 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 221 | 222 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, $payload, null, null, null); 223 | $this->assertEquals($payload, $message->getPayload(), 'Setting payload did not persist.'); 224 | $this->assertEquals(true, $message->isCompatibleWithSmallPayloadSize()); 225 | } 226 | 227 | public function shortPayloadArguments() 228 | { 229 | return array( 230 | array( array('p' => str_pad('a', 248))), 231 | array( array('p' => str_pad('a', 100))), 232 | array( array('p' => str_pad('a', 5))) 233 | ); 234 | } 235 | 236 | /** 237 | * @dataProvider tooBigForShortPayloadArguments 238 | */ 239 | public function testIncompatibleWithSmallPayload($payload) 240 | { 241 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 242 | 243 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, $payload, null, null, null); 244 | $this->assertEquals($payload, $message->getPayload(), 'Setting payload did not persist.'); 245 | $this->assertEquals(false, $message->isCompatibleWithSmallPayloadSize()); 246 | } 247 | 248 | public function tooBigForShortPayloadArguments() 249 | { 250 | return array( 251 | array( array('p' => str_pad('a', 249))), 252 | array( array('p' => str_pad('a', 300))), 253 | array( array('p' => str_pad('a', 400))) 254 | ); 255 | } 256 | 257 | /** 258 | * @dataProvider incorrectPayloadArguments 259 | */ 260 | public function testInvalidPayload($payload) 261 | { 262 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 263 | 264 | $this->setExpectedException('InvalidArgumentException', 'Invalid payload for message.'); 265 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, null, null, null, $payload, null, null, null); 266 | $this->assertEquals($payload, $message->getPayload(), 'Setting payload did not persist.'); 267 | } 268 | 269 | public function incorrectPayloadArguments() 270 | { 271 | return array( 272 | array( '' ), 273 | array( array() ), 274 | array( 'CompletelyNotJSON' ), 275 | array( 'Z{ "some": "invalid JSON" }' ) 276 | ); 277 | } 278 | 279 | public function testGetJson() 280 | { 281 | $certificate = new Certificate(__DIR__ . '/../resources/certificate_corrupt.pem', null, false, Certificate::ENDPOINT_ENV_PRODUCTION); 282 | $message = new Message('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $certificate, 'alert', 3, 'sound', array('payload' => array( 'some' => 'payloadhere' )), 'mycategory', true, new \DateTime('1970-01-01T00:01:00Z')); 283 | $this->assertJsonStringEqualsJsonString(json_encode(array('payload' => array( 'some' => 'payloadhere' ), 'aps' => array('badge' => 3, 'alert' => 'alert', 'sound' => 'sound', 'category' => 'mycategory', 'content-available' => 1)), JSON_FORCE_OBJECT), $message->getJson()); 284 | } 285 | } --------------------------------------------------------------------------------