├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── phpcs.xml ├── phpunit.xml.dist ├── src ├── Apns │ ├── Client │ │ ├── AbstractClient.php │ │ ├── Feedback.php │ │ └── Message.php │ ├── Message.php │ ├── Message │ │ └── Alert.php │ └── Response │ │ ├── Feedback.php │ │ └── Message.php └── Exception │ ├── InvalidArgumentException.php │ ├── RuntimeException.php │ └── StreamSocketClientException.php └── test └── Apns ├── FeedbackClientTest.php ├── MessageClientTest.php ├── MessageTest.php └── TestAsset ├── FeedbackClient.php ├── MessageClient.php └── certificate.pem /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .*.un~ 3 | composer.lock 4 | composer.phar 5 | vendor/ 6 | php-cs-fixer.phar 7 | /.project 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | include: 5 | - php: 5.6 6 | - php: 7.0 7 | - php: 7.1 8 | env: 9 | - CS_CHECK=true 10 | - php: 7.2 11 | - php: 7.3 12 | 13 | before_install: 14 | - composer install --no-interaction 15 | 16 | script: 17 | - ./vendor/bin/phpunit 18 | - if [[ $CS_CHECK == 'true' ]]; then ./vendor/bin/phpcs ; fi 19 | 20 | notifications: 21 | email: false 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 1.4.2 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 1.4.1 - 2019-03-14 28 | 29 | ### Added 30 | 31 | - Nothing. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - [#66](https://github.com/zendframework/ZendService_Apple_Apns/pull/66) fixes the schemes used for feedback notification URLs, to ensure they 48 | reference `tlsv1.2` specifically. 49 | 50 | ## 1.4.0 - 2019-03-13 51 | 52 | ### Added 53 | 54 | - Nothing. 55 | 56 | ### Changed 57 | 58 | - [#65](https://github.com/zendframework/ZendService_Apple_Apns/pull/65) changes the URI schemes used to push messages from `tls` to `tlsv1.2` due 59 | to a change in TLS versions supported by the endpoints. 60 | 61 | ### Deprecated 62 | 63 | - Nothing. 64 | 65 | ### Removed 66 | 67 | - Nothing. 68 | 69 | ### Fixed 70 | 71 | - Nothing. 72 | 73 | ## 1.3.1 - 2019-02-07 74 | 75 | ### Added 76 | 77 | - [#64](https://github.com/zendframework/ZendService_Apple_Apns/pull/64) adds support for PHP 7.3. 78 | 79 | ### Changed 80 | 81 | - Nothing. 82 | 83 | ### Deprecated 84 | 85 | - Nothing. 86 | 87 | ### Removed 88 | 89 | - Nothing. 90 | 91 | ### Fixed 92 | 93 | - Nothing. 94 | 95 | ## 1.3.0 - 2018-05-08 96 | 97 | ### Added 98 | 99 | - [#63](https://github.com/zendframework/ZendService_Apple_Apns/pull/63) adds support for PHP 7.1 and 7.2. 100 | 101 | - [#53](https://github.com/zendframework/ZendService_Apple_Apns/pull/53) adds support for the mutable-content Notification field within the `Message` implementation. 102 | 103 | - [#48](https://github.com/zendframework/ZendService_Apple_Apns/pull/48) adds two new methods to `ZendService\Apple\Apns\Message\Alert`: `setAction($key)` and `getAction()`. 104 | These allow specifying an action property for notifications. 105 | 106 | ### Changed 107 | 108 | - [#42](https://github.com/zendframework/ZendService_Apple_Apns/pull/42) modifies the allowed character set for tokens to include uppercase A-F. 109 | 110 | ### Deprecated 111 | 112 | - Nothing. 113 | 114 | ### Removed 115 | 116 | - [#63](https://github.com/zendframework/ZendService_Apple_Apns/pull/63) removes support for PHP 5.3, 5.4, and 5.5. 117 | 118 | - [#63](https://github.com/zendframework/ZendService_Apple_Apns/pull/63) removes support for HHVM. 119 | 120 | ### Fixed 121 | 122 | - [#49](https://github.com/zendframework/ZendService_Apple_Apns/pull/49) fixes how `Message::getPayload()` and `Message::getPayloadJson()` create a 123 | representation of the `aps` key when it is an empty value. With #18, the value was removed, 124 | which was incorrect; it is not rendered as an empty object. 125 | 126 | - [#62](https://github.com/zendframework/ZendService_Apple_Apns/pull/62) modifies the `AbstractClient::connect()` method such that it now 127 | restores the previous error handler after catching a socket-related connection exception. 128 | 129 | ## 1.2.0 - 2015-12-09 130 | 131 | ### Added 132 | 133 | - [#36](https://github.com/zendframework/ZendService_Apple_Apns/pull/36) 134 | Conection failures now raise a ```RuntimeException``` to allow you to catch 135 | stream_socket_client(): SSL: Connection reset by peer warnings. 136 | - [#39](https://github.com/zendframework/ZendService_Apple_Apns/pull/39) Add 137 | safari push support 138 | 139 | ### Deprecated 140 | 141 | - Nothing. 142 | 143 | ### Removed 144 | 145 | - Nothing. 146 | 147 | ### Fixed 148 | 149 | - Nothing. 150 | 151 | ## 1.1.2 - 2015-12-09 152 | 153 | ### Added 154 | 155 | - Nothing. 156 | 157 | ### Deprecated 158 | 159 | - Nothing. 160 | 161 | ### Removed 162 | 163 | - Nothing. 164 | 165 | ### Fixed 166 | 167 | - [#40](https://github.com/zendframework/ZendService_Apple_Apns/pull/40) Add 168 | missing return $this 169 | 170 | ## 1.1.1 - 2015-10-13 171 | 172 | ### Added 173 | 174 | - Nothing. 175 | 176 | ### Deprecated 177 | 178 | - Nothing. 179 | 180 | ### Removed 181 | 182 | - Nothing. 183 | 184 | ### Fixed 185 | 186 | - [#38](https://github.com/zendframework/ZendService_Apple_Apns/pull/38) Fix 187 | apns error response when sending a message. 188 | - [#34](https://github.com/zendframework/ZendService_Apple_Apns/pull/34) Fixed 189 | unit tests execution on travis 190 | 191 | ## 1.1.0 - 2015-07-29 192 | 193 | ### Added 194 | 195 | - [#27](https://github.com/zendframework/ZendService_Apple_Apns/pull/27) Adds in 196 | ANS category support. 197 | - [#29](https://github.com/zendframework/ZendService_Apple_Apns/pull/29) Add in 198 | ANS title, title-loc-key and title-loc-args. 199 | 200 | ### Deprecated 201 | 202 | - Nothing. 203 | 204 | ### Removed 205 | 206 | - Nothing. 207 | 208 | ### Fixed 209 | 210 | - [#26](https://github.com/zendframework/ZendService_Apple_Apns/pull/26) Fixes a 211 | possible infinity fread in certain PHP versions. 212 | - [#28](https://github.com/zendframework/ZendService_Apple_Apns/pull/28) Fixed docblocks 213 | that prevented proper code completion in some editors. 214 | - [#29](https://github.com/zendframework/ZendService_Apple_Apns/pull/29) Force 215 | TLS vs. SSL due to [Apple moving to TLS](https://developer.apple.com/news/?id=10222014a). 216 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2018, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ZendService\Apple\Apns [![Build Status](https://travis-ci.org/zendframework/ZendService_Apple_Apns.png?branch=master)](https://travis-ci.org/zendframework/ZendService_Apple_Apns) 2 | ================================ 3 | 4 | > ## Repository abandoned 2019-12-05 5 | > 6 | > This repository is no longer maintained. 7 | 8 | Provides support for Apple push notifications. 9 | 10 | 11 | ## Requirements 12 | 13 | * PHP >= 5.6 14 | 15 | ## Getting Started 16 | 17 | Install this library using [Composer](http://getcomposer.org/): 18 | 19 | ```bash 20 | $ composer require zendframework/zendservice-apple-apns 21 | ``` 22 | 23 | ## Documentation 24 | 25 | The documentation can be found at: http://framework.zend.com/manual/current/en/modules/zendservice.apple.apns.html 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zendservice-apple-apns", 3 | "description": "OOP Zend Framework wrapper for Apple Push Notification Service", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zf", 7 | "zendframework", 8 | "apns", 9 | "push", 10 | "notification", 11 | "apple" 12 | ], 13 | "support": { 14 | "issues": "https://github.com/zendframework/ZendService_Apple_Apns/issues", 15 | "source": "https://github.com/zendframework/ZendService_Apple_Apns", 16 | "rss": "https://github.com/zendframework/ZendService_Apple_Apns/releases.atom", 17 | "chat": "https://zendframework-slack.herokuapp.com", 18 | "forum": "https://discourse.zendframework.com/c/questions/components" 19 | }, 20 | "require": { 21 | "php": "^5.6 || ^7.0", 22 | "zendframework/zend-json": "^2.0 || ^3.0" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.5", 26 | "zendframework/zend-coding-standard": "~1.0.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "ZendService\\Apple\\": "src/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "ZendServiceTest\\Apple\\": "test/" 36 | } 37 | }, 38 | "config": { 39 | "sort-packages": true 40 | }, 41 | "extra": { 42 | "branch-alias": { 43 | "dev-master": "1.4.x-dev", 44 | "dev-develop": "1.5.x-dev" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src 6 | test 7 | 8 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ./test 9 | 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Apns/Client/AbstractClient.php: -------------------------------------------------------------------------------- 1 | isConnected) { 59 | throw new Exception\RuntimeException('Connection has already been opened and must be closed'); 60 | } 61 | 62 | if (! array_key_exists($environment, $this->uris)) { 63 | throw new Exception\InvalidArgumentException('Environment must be one of PRODUCTION_URI or SANDBOX_URI'); 64 | } 65 | 66 | if (! is_string($certificate) || ! file_exists($certificate)) { 67 | throw new Exception\InvalidArgumentException('Certificate must be a valid path to a APNS certificate'); 68 | } 69 | 70 | $sslOptions = [ 71 | 'local_cert' => $certificate, 72 | ]; 73 | if ($passPhrase !== null) { 74 | if (! is_scalar($passPhrase)) { 75 | throw new Exception\InvalidArgumentException('SSL passphrase must be a scalar'); 76 | } 77 | $sslOptions['passphrase'] = $passPhrase; 78 | } 79 | $this->connect($this->uris[$environment], $sslOptions); 80 | $this->isConnected = true; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Connect to Host 87 | * 88 | * @param string $host 89 | * @param array $ssl 90 | * @return AbstractClient 91 | */ 92 | protected function connect($host, array $ssl) 93 | { 94 | set_error_handler(function ($errno, $errstr, $errfile, $errline) { 95 | throw new StreamSocketClientException($errstr, $errno, 1, $errfile, $errline); 96 | }); 97 | 98 | try { 99 | $this->socket = stream_socket_client( 100 | $host, 101 | $errno, 102 | $errstr, 103 | ini_get('default_socket_timeout'), 104 | STREAM_CLIENT_CONNECT, 105 | stream_context_create( 106 | [ 107 | 'ssl' => $ssl, 108 | ] 109 | ) 110 | ); 111 | } catch (StreamSocketClientException $e) { 112 | restore_error_handler(); 113 | throw new Exception\RuntimeException(sprintf( 114 | 'Unable to connect: %s: %d (%s)', 115 | $host, 116 | $e->getCode(), 117 | $e->getMessage() 118 | )); 119 | } 120 | 121 | restore_error_handler(); 122 | 123 | if (! $this->socket) { 124 | throw new Exception\RuntimeException(sprintf( 125 | 'Unable to connect: %s: %d (%s)', 126 | $host, 127 | $errno, 128 | $errstr 129 | )); 130 | } 131 | stream_set_blocking($this->socket, 0); 132 | stream_set_write_buffer($this->socket, 0); 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Close Connection 139 | * 140 | * @return AbstractClient 141 | */ 142 | public function close() 143 | { 144 | if ($this->isConnected && is_resource($this->socket)) { 145 | fclose($this->socket); 146 | } 147 | $this->isConnected = false; 148 | 149 | return $this; 150 | } 151 | 152 | /** 153 | * Is Connected 154 | * 155 | * @return boolean 156 | */ 157 | public function isConnected() 158 | { 159 | return $this->isConnected; 160 | } 161 | 162 | /** 163 | * Read from the Server 164 | * 165 | * @param int $length 166 | * @return string 167 | */ 168 | protected function read($length = 6) 169 | { 170 | if (! $this->isConnected()) { 171 | throw new Exception\RuntimeException('You must open the connection prior to reading data'); 172 | } 173 | $data = false; 174 | $read = [$this->socket]; 175 | $null = null; 176 | 177 | if (0 < @stream_select($read, $null, $null, 1, 0)) { 178 | $data = @fread($this->socket, (int) $length); 179 | } 180 | 181 | return $data; 182 | } 183 | 184 | /** 185 | * Write Payload to the Server 186 | * 187 | * @param string $payload 188 | * @return int 189 | */ 190 | protected function write($payload) 191 | { 192 | if (! $this->isConnected()) { 193 | throw new Exception\RuntimeException('You must open the connection prior to writing data'); 194 | } 195 | 196 | return @fwrite($this->socket, $payload); 197 | } 198 | 199 | /** 200 | * Destructor 201 | * 202 | * @return void 203 | */ 204 | public function __destruct() 205 | { 206 | $this->close(); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Apns/Client/Feedback.php: -------------------------------------------------------------------------------- 1 | isConnected()) { 38 | throw new Exception\RuntimeException('You must first open the connection by calling open()'); 39 | } 40 | 41 | $tokens = []; 42 | while ($token = $this->read(38)) { 43 | $tokens[] = new FeedbackResponse($token); 44 | } 45 | 46 | return $tokens; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Apns/Client/Message.php: -------------------------------------------------------------------------------- 1 | isConnected()) { 46 | throw new Exception\RuntimeException('You must first open the connection by calling open()'); 47 | } 48 | 49 | $ret = $this->write($message->getPayloadJson()); 50 | if ($ret === false) { 51 | throw new Exception\RuntimeException('Server is unavailable; please retry later'); 52 | } 53 | 54 | return new MessageResponse($this->read()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Apns/Message.php: -------------------------------------------------------------------------------- 1 | id; 95 | } 96 | 97 | /** 98 | * Set Identifier 99 | * 100 | * @param string $id 101 | * @return Message 102 | */ 103 | public function setId($id) 104 | { 105 | if (! is_scalar($id)) { 106 | throw new Exception\InvalidArgumentException('Identifier must be a scalar value'); 107 | } 108 | $this->id = $id; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Get Token 115 | * 116 | * @return string 117 | */ 118 | public function getToken() 119 | { 120 | return $this->token; 121 | } 122 | 123 | /** 124 | * Set Token 125 | * 126 | * @param string $token 127 | * @return Message 128 | */ 129 | public function setToken($token) 130 | { 131 | if (! is_string($token)) { 132 | throw new Exception\InvalidArgumentException(sprintf( 133 | 'Device token must be a string, "%s" given.', 134 | gettype($token) 135 | )); 136 | } 137 | 138 | if (preg_match('/[^0-9a-f]/i', $token)) { 139 | throw new Exception\InvalidArgumentException(sprintf( 140 | 'Device token must be mask "%s". Token given: "%s"', 141 | '/[^0-9a-f]/', 142 | $token 143 | )); 144 | } 145 | 146 | if (strlen($token) != 64) { 147 | throw new Exception\InvalidArgumentException(sprintf( 148 | 'Device token must be a 64 charsets, Token length given: %d.', 149 | mb_strlen($token) 150 | )); 151 | } 152 | 153 | $this->token = $token; 154 | 155 | return $this; 156 | } 157 | 158 | /** 159 | * Get Expiration 160 | * 161 | * @return int 162 | */ 163 | public function getExpire() 164 | { 165 | return $this->expire; 166 | } 167 | 168 | /** 169 | * Set Expiration 170 | * 171 | * @param int|DateTime $expire 172 | * @return Message 173 | */ 174 | public function setExpire($expire) 175 | { 176 | if ($expire instanceof \DateTime) { 177 | $expire = $expire->getTimestamp(); 178 | } elseif (! is_numeric($expire) || $expire != (int) $expire) { 179 | throw new Exception\InvalidArgumentException('Expiration must be a DateTime object or a unix timestamp'); 180 | } 181 | $this->expire = $expire; 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * Get Alert 188 | * 189 | * @return Message\Alert|null 190 | */ 191 | public function getAlert() 192 | { 193 | return $this->alert; 194 | } 195 | 196 | /** 197 | * Set Alert 198 | * 199 | * @param string|Message\Alert|null $alert 200 | * @return Message 201 | */ 202 | public function setAlert($alert) 203 | { 204 | if (! $alert instanceof Message\Alert && ! is_null($alert)) { 205 | $alert = new Message\Alert($alert); 206 | } 207 | $this->alert = $alert; 208 | 209 | return $this; 210 | } 211 | 212 | /** 213 | * Get Badge 214 | * 215 | * @return int|null 216 | */ 217 | public function getBadge() 218 | { 219 | return $this->badge; 220 | } 221 | 222 | /** 223 | * Set Badge 224 | * 225 | * @param int|null $badge 226 | * @return Message 227 | */ 228 | public function setBadge($badge) 229 | { 230 | if ($badge !== null && ! $badge == (int) $badge) { 231 | throw new Exception\InvalidArgumentException('Badge must be null or an integer'); 232 | } 233 | $this->badge = $badge; 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * Get Sound 240 | * 241 | * @return string|null 242 | */ 243 | public function getSound() 244 | { 245 | return $this->sound; 246 | } 247 | 248 | /** 249 | * Set Sound 250 | * 251 | * @param string|null $sound 252 | * @return Message 253 | */ 254 | public function setSound($sound) 255 | { 256 | if ($sound !== null && ! is_string($sound)) { 257 | throw new Exception\InvalidArgumentException('Sound must be null or a string'); 258 | } 259 | $this->sound = $sound; 260 | 261 | return $this; 262 | } 263 | 264 | /** 265 | * Set Mutable Content 266 | * 267 | * @param int|null $value 268 | * @returns Message 269 | */ 270 | public function setMutableContent($value) 271 | { 272 | if ($value !== null && ! is_int($value)) { 273 | throw new Exception\InvalidArgumentException( 274 | 'Mutable Content must be null or an integer, received: ' . gettype($value) 275 | ); 276 | } 277 | 278 | if (is_int($value) && $value !== 1) { 279 | throw new Exception\InvalidArgumentException('Mutable Content supports only 1 as integer value'); 280 | } 281 | 282 | $this->mutableContent = $value; 283 | 284 | return $this; 285 | } 286 | 287 | /** 288 | * Get Content Available 289 | * 290 | * @return int|null 291 | */ 292 | public function getContentAvailable() 293 | { 294 | return $this->contentAvailable; 295 | } 296 | 297 | /** 298 | * Set Content Available 299 | * 300 | * @param int|null $value 301 | * @return Message 302 | */ 303 | public function setContentAvailable($value) 304 | { 305 | if ($value !== null && ! is_int($value)) { 306 | throw new Exception\InvalidArgumentException('Content Available must be null or an integer'); 307 | } 308 | $this->contentAvailable = $value; 309 | 310 | return $this; 311 | } 312 | 313 | /** 314 | * Get Category 315 | * 316 | * @return string|null 317 | */ 318 | public function getCategory() 319 | { 320 | return $this->category; 321 | } 322 | 323 | /** 324 | * Set Category 325 | * 326 | * @param string|null $category 327 | * @return Message 328 | */ 329 | public function setCategory($category) 330 | { 331 | if ($category !== null && ! is_string($category)) { 332 | throw new Exception\InvalidArgumentException('Category must be null or a string'); 333 | } 334 | $this->category = $category; 335 | 336 | return $this; 337 | } 338 | 339 | /** 340 | * Get URL arguments 341 | * 342 | * @return array|null 343 | */ 344 | public function getUrlArgs() 345 | { 346 | return $this->urlArgs; 347 | } 348 | 349 | /** 350 | * Set URL arguments 351 | * 352 | * @param array|null $urlArgs 353 | * @return Message 354 | */ 355 | public function setUrlArgs(array $urlArgs) 356 | { 357 | $this->urlArgs = $urlArgs; 358 | 359 | return $this; 360 | } 361 | 362 | /** 363 | * Get Custom Data 364 | * 365 | * @return array|null 366 | */ 367 | public function getCustom() 368 | { 369 | return $this->custom; 370 | } 371 | 372 | /** 373 | * Set Custom Data 374 | * 375 | * @param array $custom 376 | * @throws Exception\RuntimeException 377 | * @return Message 378 | */ 379 | public function setCustom(array $custom) 380 | { 381 | if (array_key_exists('aps', $custom)) { 382 | throw new Exception\RuntimeException('custom data must not contain aps key as it is reserved by apple'); 383 | } 384 | 385 | $this->custom = $custom; 386 | 387 | return $this; 388 | } 389 | 390 | /** 391 | * Get Payload 392 | * Generate APN array. 393 | * 394 | * @return array 395 | */ 396 | public function getPayload() 397 | { 398 | $message = []; 399 | $aps = []; 400 | if ($this->alert && ($alert = $this->alert->getPayload())) { 401 | $aps['alert'] = $alert; 402 | } 403 | if (! is_null($this->badge)) { 404 | $aps['badge'] = $this->badge; 405 | } 406 | if (! is_null($this->sound)) { 407 | $aps['sound'] = $this->sound; 408 | } 409 | if (! is_null($this->mutableContent)) { 410 | $aps['mutable-content'] = $this->mutableContent; 411 | } 412 | if (! is_null($this->contentAvailable)) { 413 | $aps['content-available'] = $this->contentAvailable; 414 | } 415 | if (! is_null($this->category)) { 416 | $aps['category'] = $this->category; 417 | } 418 | if (! is_null($this->urlArgs)) { 419 | $aps['url-args'] = $this->urlArgs; 420 | } 421 | if (! empty($this->custom)) { 422 | $message = array_merge($this->custom, $message); 423 | } 424 | 425 | $message['aps'] = empty($aps) ? (object) [] : $aps; 426 | 427 | return $message; 428 | } 429 | 430 | /** 431 | * Get Payload JSON 432 | * 433 | * @return string 434 | */ 435 | public function getPayloadJson() 436 | { 437 | $payload = $this->getPayload(); 438 | // don't escape utf8 payloads unless json_encode does not exist. 439 | if (defined('JSON_UNESCAPED_UNICODE')) { 440 | $payload = json_encode($payload, JSON_UNESCAPED_UNICODE); 441 | } else { 442 | $payload = JsonEncoder::encode($payload); 443 | } 444 | $length = strlen($payload); 445 | 446 | return pack('CNNnH*', 1, $this->id, $this->expire, 32, $this->token) 447 | . pack('n', $length) 448 | . $payload; 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /src/Apns/Message/Alert.php: -------------------------------------------------------------------------------- 1 | setBody($body); 96 | } 97 | if ($actionLocKey !== null) { 98 | $this->setActionLocKey($actionLocKey); 99 | } 100 | if ($locKey !== null) { 101 | $this->setLocKey($locKey); 102 | } 103 | if ($locArgs !== null) { 104 | $this->setLocArgs($locArgs); 105 | } 106 | if ($launchImage !== null) { 107 | $this->setLaunchImage($launchImage); 108 | } 109 | if ($title !== null) { 110 | $this->setTitle($title); 111 | } 112 | if ($titleLocKey !== null) { 113 | $this->setTitleLocKey($titleLocKey); 114 | } 115 | if ($titleLocArgs !== null) { 116 | $this->setTitleLocArgs($titleLocArgs); 117 | } 118 | } 119 | 120 | /** 121 | * Get Body 122 | * 123 | * @return string|null 124 | */ 125 | public function getBody() 126 | { 127 | return $this->body; 128 | } 129 | 130 | /** 131 | * Set Body 132 | * 133 | * @param string|null $body 134 | * @return Alert 135 | */ 136 | public function setBody($body) 137 | { 138 | if (! is_null($body) && ! is_scalar($body)) { 139 | throw new Exception\InvalidArgumentException('Body must be null OR a scalar value'); 140 | } 141 | $this->body = $body; 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * Get Action 148 | * 149 | * @return string|null 150 | */ 151 | public function getAction() 152 | { 153 | return $this->action; 154 | } 155 | 156 | /** 157 | * Set Action 158 | * 159 | * @param string|null $key 160 | * @return Alert 161 | */ 162 | public function setAction($key) 163 | { 164 | if (! is_null($key) && ! is_scalar($key)) { 165 | throw new Exception\InvalidArgumentException('Action must be null OR a scalar value'); 166 | } 167 | $this->action = $key; 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * Get Action Locale Key 174 | * 175 | * @return string|null 176 | */ 177 | public function getActionLocKey() 178 | { 179 | return $this->actionLocKey; 180 | } 181 | 182 | /** 183 | * Set Action Locale Key 184 | * 185 | * @param string|null $key 186 | * @return Alert 187 | */ 188 | public function setActionLocKey($key) 189 | { 190 | if (! is_null($key) && ! is_scalar($key)) { 191 | throw new Exception\InvalidArgumentException('ActionLocKey must be null OR a scalar value'); 192 | } 193 | $this->actionLocKey = $key; 194 | 195 | return $this; 196 | } 197 | 198 | /** 199 | * Get Locale Key 200 | * 201 | * @return string|null 202 | */ 203 | public function getLocKey() 204 | { 205 | return $this->locKey; 206 | } 207 | 208 | /** 209 | * Set Locale Key 210 | * 211 | * @param string|null $key 212 | * @return Alert 213 | */ 214 | public function setLocKey($key) 215 | { 216 | if (! is_null($key) && ! is_scalar($key)) { 217 | throw new Exception\InvalidArgumentException('LocKey must be null OR a scalar value'); 218 | } 219 | $this->locKey = $key; 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * Get Locale Arguments 226 | * 227 | * @return array|null 228 | */ 229 | public function getLocArgs() 230 | { 231 | return $this->locArgs; 232 | } 233 | 234 | /** 235 | * Set Locale Arguments 236 | * 237 | * @param array $args 238 | * @return Alert 239 | */ 240 | public function setLocArgs(array $args) 241 | { 242 | foreach ($args as $a) { 243 | if (! is_scalar($a)) { 244 | throw new Exception\InvalidArgumentException('Arguments must only contain scalar values'); 245 | } 246 | } 247 | $this->locArgs = $args; 248 | 249 | return $this; 250 | } 251 | 252 | /** 253 | * Get Launch Image 254 | * 255 | * @return string|null 256 | */ 257 | public function getLaunchImage() 258 | { 259 | return $this->launchImage; 260 | } 261 | 262 | /** 263 | * Set Launch Image 264 | * 265 | * @param string|null $image 266 | * @return Alert 267 | */ 268 | public function setLaunchImage($image) 269 | { 270 | if (! is_null($image) && ! is_scalar($image)) { 271 | throw new Exception\InvalidArgumentException('Launch image must be null OR a scalar value'); 272 | } 273 | $this->launchImage = $image; 274 | 275 | return $this; 276 | } 277 | 278 | /** 279 | * Get Title 280 | * 281 | * @return string|null 282 | */ 283 | public function getTitle() 284 | { 285 | return $this->title; 286 | } 287 | 288 | /** 289 | * Set Title 290 | * 291 | * @param string|null $title 292 | * @return Alert 293 | */ 294 | public function setTitle($title) 295 | { 296 | if (! is_null($title) && ! is_scalar($title)) { 297 | throw new Exception\InvalidArgumentException('Title must be null OR a scalar value'); 298 | } 299 | $this->title = $title; 300 | 301 | return $this; 302 | } 303 | 304 | /** 305 | * Get Title Locale Key 306 | * 307 | * @return string|null 308 | */ 309 | public function getTitleLocKey() 310 | { 311 | return $this->titleLocKey; 312 | } 313 | 314 | /** 315 | * Set Title Locale Key 316 | * 317 | * @param string|null $key 318 | * @return Alert 319 | */ 320 | public function setTitleLocKey($key) 321 | { 322 | if (! is_null($key) && ! is_scalar($key)) { 323 | throw new Exception\InvalidArgumentException('TitleLocKey must be null OR a scalar value'); 324 | } 325 | $this->titleLocKey = $key; 326 | 327 | return $this; 328 | } 329 | 330 | /** 331 | * Get Title Locale Arguments 332 | * 333 | * @return array|null 334 | */ 335 | public function getTitleLocArgs() 336 | { 337 | return $this->titleLocArgs; 338 | } 339 | 340 | /** 341 | * Set Title Locale Arguments 342 | * 343 | * @param array $args 344 | * @return Alert 345 | */ 346 | public function setTitleLocArgs(array $args) 347 | { 348 | foreach ($args as $a) { 349 | if (! is_scalar($a)) { 350 | throw new Exception\InvalidArgumentException('Title Arguments must only contain scalar values'); 351 | } 352 | } 353 | $this->titleLocArgs = $args; 354 | 355 | return $this; 356 | } 357 | 358 | /** 359 | * To Payload 360 | * Formats an APS alert. 361 | * 362 | * @return array|string 363 | */ 364 | public function getPayload() 365 | { 366 | $vars = get_object_vars($this); 367 | if (empty($vars)) { 368 | return null; 369 | } 370 | 371 | $alert = []; 372 | foreach ($vars as $key => $value) { 373 | if (! is_null($value)) { 374 | $key = strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $key)); 375 | $alert[$key] = $value; 376 | } 377 | } 378 | 379 | if (count($alert) === 1) { 380 | return $this->getBody(); 381 | } 382 | 383 | return $alert; 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/Apns/Response/Feedback.php: -------------------------------------------------------------------------------- 1 | parseRawResponse($rawResponse); 42 | } 43 | } 44 | 45 | /** 46 | * Get Token 47 | * 48 | * @return string 49 | */ 50 | public function getToken() 51 | { 52 | return $this->token; 53 | } 54 | 55 | /** 56 | * Set Token 57 | * 58 | * @return Feedback 59 | */ 60 | public function setToken($token) 61 | { 62 | if (! is_scalar($token)) { 63 | throw new Exception\InvalidArgumentException('Token must be a scalar value'); 64 | } 65 | $this->token = $token; 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Get Time 72 | * 73 | * @return int 74 | */ 75 | public function getTime() 76 | { 77 | return $this->time; 78 | } 79 | 80 | /** 81 | * Set Time 82 | * 83 | * @param int $time 84 | * @return Feedback 85 | */ 86 | public function setTime($time) 87 | { 88 | $this->time = (int) $time; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Parse Raw Response 95 | * 96 | * @return Feedback 97 | */ 98 | public function parseRawResponse($rawResponse) 99 | { 100 | $rawResponse = trim($rawResponse); 101 | $token = unpack('Ntime/nlength/H*token', $rawResponse); 102 | $this->setTime($token['time']); 103 | $this->setToken(substr($token['token'], 0, $token['length'] * 2)); 104 | 105 | return $this; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Apns/Response/Message.php: -------------------------------------------------------------------------------- 1 | parseRawResponse($rawResponse); 63 | } 64 | } 65 | 66 | /** 67 | * Get Code 68 | * 69 | * @return int 70 | */ 71 | public function getCode() 72 | { 73 | return $this->code; 74 | } 75 | 76 | /** 77 | * Set Code 78 | * 79 | * @param int $code 80 | * @return Message 81 | */ 82 | public function setCode($code) 83 | { 84 | if (($code < 0 || $code > 8) && $code != 255) { 85 | throw new Exception\InvalidArgumentException('Code must be between 0-8 OR 255'); 86 | } 87 | $this->code = $code; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Get Identifier 94 | * 95 | * @return string 96 | */ 97 | public function getId() 98 | { 99 | return $this->id; 100 | } 101 | 102 | /** 103 | * Set Identifier 104 | * 105 | * @param string $id 106 | * @return Message 107 | */ 108 | public function setId($id) 109 | { 110 | if (! is_scalar($id)) { 111 | throw new Exception\InvalidArgumentException('Identifier must be a scalar value'); 112 | } 113 | $this->id = $id; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Parse Raw Response 120 | * 121 | * @param string $rawResponse 122 | * @return Message 123 | */ 124 | public function parseRawResponse($rawResponse) 125 | { 126 | if (! is_scalar($rawResponse)) { 127 | throw new Exception\InvalidArgumentException('Response must be a scalar value'); 128 | } 129 | 130 | if (strlen($rawResponse) === 0) { 131 | $this->code = self::RESULT_OK; 132 | 133 | return $this; 134 | } 135 | $response = unpack('Ccmd/Cerrno/Nid', $rawResponse); 136 | $this->setId($response['id']); 137 | $this->setCode($response['errno']); 138 | 139 | return $this; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | namespace ZendService\Apple\Exception; 8 | 9 | use ErrorException; 10 | 11 | class StreamSocketClientException extends ErrorException 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /test/Apns/FeedbackClientTest.php: -------------------------------------------------------------------------------- 1 | apns = new FeedbackClient(); 29 | } 30 | 31 | protected function setupValidBase() 32 | { 33 | $this->apns->open(FeedbackClient::SANDBOX_URI, __DIR__ . '/TestAsset/certificate.pem'); 34 | } 35 | 36 | public function testFeedback() 37 | { 38 | $this->setupValidBase(); 39 | $time = time(); 40 | $token = 'abc123'; 41 | $length = strlen($token) / 2; 42 | $this->apns->setReadResponse(pack('NnH*', $time, $length, $token)); 43 | $response = $this->apns->feedback(); 44 | $this->assertCount(1, $response); 45 | $feedback = array_shift($response); 46 | $this->assertEquals($time, $feedback->getTime()); 47 | $this->assertEquals($token, $feedback->getToken()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/Apns/MessageClientTest.php: -------------------------------------------------------------------------------- 1 | apns = new MessageClient(); 33 | $this->message = new Message(); 34 | } 35 | 36 | protected function setupValidBase() 37 | { 38 | $this->apns->open(MessageClient::SANDBOX_URI, __DIR__ . '/TestAsset/certificate.pem'); 39 | $this->message->setToken('662cfe5a69ddc65cdd39a1b8f8690647778204b064df7b264e8c4c254f94fdd8'); 40 | $this->message->setId(time()); 41 | $this->message->setAlert('bar'); 42 | } 43 | 44 | public function testConnectThrowsExceptionOnInvalidEnvironment() 45 | { 46 | $this->expectException('InvalidArgumentException'); 47 | $this->apns->open(5, __DIR__ . '/TestAsset/certificate.pem'); 48 | } 49 | 50 | public function testSetCertificateThrowsExceptionOnNonString() 51 | { 52 | $this->expectException('InvalidArgumentException'); 53 | $this->apns->open(MessageClient::PRODUCTION_URI, ['foo']); 54 | } 55 | 56 | public function testSetCertificateThrowsExceptionOnMissingFile() 57 | { 58 | $this->expectException('InvalidArgumentException'); 59 | $this->apns->open(MessageClient::PRODUCTION_URI, 'foo'); 60 | } 61 | 62 | public function testSetCertificatePassphraseThrowsExceptionOnNonString() 63 | { 64 | $this->expectException('InvalidArgumentException'); 65 | $this->apns->open(MessageClient::PRODUCTION_URI, __DIR__ . '/TestAsset/certificate.pem', ['foo']); 66 | } 67 | 68 | public function testOpen() 69 | { 70 | $ret = $this->apns->open(MessageClient::SANDBOX_URI, __DIR__ . '/TestAsset/certificate.pem'); 71 | $this->assertEquals($this->apns, $ret); 72 | $this->assertTrue($this->apns->isConnected()); 73 | } 74 | 75 | public function testClose() 76 | { 77 | $this->apns->open(MessageClient::SANDBOX_URI, __DIR__ . '/TestAsset/certificate.pem'); 78 | $this->apns->close(); 79 | $this->assertFalse($this->apns->isConnected()); 80 | } 81 | 82 | public function testOpenWhenAlreadyOpenThrowsException() 83 | { 84 | $this->expectException('RuntimeException'); 85 | $this->apns->open(MessageClient::SANDBOX_URI, __DIR__ . '/TestAsset/certificate.pem'); 86 | $this->apns->open(MessageClient::SANDBOX_URI, __DIR__ . '/TestAsset/certificate.pem'); 87 | } 88 | 89 | public function testSendReturnsTrueOnSuccess() 90 | { 91 | $this->setupValidBase(); 92 | $response = $this->apns->send($this->message); 93 | $this->assertEquals(MessageResponse::RESULT_OK, $response->getCode()); 94 | $this->assertNull($response->getId()); 95 | } 96 | 97 | public function testSendResponseOnProcessingError() 98 | { 99 | $this->setupValidBase(); 100 | $this->apns->setReadResponse(pack('CCN*', 1, 1, 12345)); 101 | $response = $this->apns->send($this->message); 102 | $this->assertEquals(MessageResponse::RESULT_PROCESSING_ERROR, $response->getCode()); 103 | $this->assertEquals(12345, $response->getId()); 104 | } 105 | 106 | public function testSendResponseOnMissingToken() 107 | { 108 | $this->setupValidBase(); 109 | $this->apns->setReadResponse(pack('CCN*', 2, 2, 12345)); 110 | $response = $this->apns->send($this->message); 111 | $this->assertEquals(MessageResponse::RESULT_MISSING_TOKEN, $response->getCode()); 112 | $this->assertEquals(12345, $response->getId()); 113 | } 114 | 115 | public function testSendResponseOnMissingTopic() 116 | { 117 | $this->setupValidBase(); 118 | $this->apns->setReadResponse(pack('CCN*', 3, 3, 12345)); 119 | $response = $this->apns->send($this->message); 120 | $this->assertEquals(MessageResponse::RESULT_MISSING_TOPIC, $response->getCode()); 121 | $this->assertEquals(12345, $response->getId()); 122 | } 123 | 124 | public function testSendResponseOnMissingPayload() 125 | { 126 | $this->setupValidBase(); 127 | $this->apns->setReadResponse(pack('CCN*', 4, 4, 12345)); 128 | $response = $this->apns->send($this->message); 129 | $this->assertEquals(MessageResponse::RESULT_MISSING_PAYLOAD, $response->getCode()); 130 | $this->assertEquals(12345, $response->getId()); 131 | } 132 | 133 | public function testSendResponseOnInvalidTokenSize() 134 | { 135 | $this->setupValidBase(); 136 | $this->apns->setReadResponse(pack('CCN*', 5, 5, 12345)); 137 | $response = $this->apns->send($this->message); 138 | $this->assertEquals(MessageResponse::RESULT_INVALID_TOKEN_SIZE, $response->getCode()); 139 | $this->assertEquals(12345, $response->getId()); 140 | } 141 | 142 | public function testSendResponseOnInvalidTopicSize() 143 | { 144 | $this->setupValidBase(); 145 | $this->apns->setReadResponse(pack('CCN*', 6, 6, 12345)); 146 | $response = $this->apns->send($this->message); 147 | $this->assertEquals(MessageResponse::RESULT_INVALID_TOPIC_SIZE, $response->getCode()); 148 | $this->assertEquals(12345, $response->getId()); 149 | } 150 | 151 | public function testSendResponseOnInvalidPayloadSize() 152 | { 153 | $this->setupValidBase(); 154 | $this->apns->setReadResponse(pack('CCN*', 7, 7, 12345)); 155 | $response = $this->apns->send($this->message); 156 | $this->assertEquals(MessageResponse::RESULT_INVALID_PAYLOAD_SIZE, $response->getCode()); 157 | $this->assertEquals(12345, $response->getId()); 158 | } 159 | 160 | public function testSendResponseOnInvalidToken() 161 | { 162 | $this->setupValidBase(); 163 | $this->apns->setReadResponse(pack('CCN*', 8, 8, 12345)); 164 | $response = $this->apns->send($this->message); 165 | $this->assertEquals(MessageResponse::RESULT_INVALID_TOKEN, $response->getCode()); 166 | $this->assertEquals(12345, $response->getId()); 167 | } 168 | 169 | public function testSendResponseOnUnknownError() 170 | { 171 | $this->setupValidBase(); 172 | $this->apns->setReadResponse(pack('CCN*', 255, 255, 12345)); 173 | $response = $this->apns->send($this->message); 174 | $this->assertEquals(MessageResponse::RESULT_UNKNOWN_ERROR, $response->getCode()); 175 | $this->assertEquals(12345, $response->getId()); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /test/Apns/MessageTest.php: -------------------------------------------------------------------------------- 1 | alert = new Alert(); 30 | $this->message = new Message(); 31 | } 32 | 33 | public function testSetAlertTextReturnsCorrectly() 34 | { 35 | $text = 'my alert'; 36 | $ret = $this->message->setAlert($text); 37 | $this->assertInstanceOf('ZendService\Apple\Apns\Message', $ret); 38 | $checkText = $this->message->getAlert(); 39 | $this->assertInstanceOf('ZendService\Apple\Apns\Message\Alert', $checkText); 40 | $this->assertEquals($text, $checkText->getBody()); 41 | } 42 | 43 | public function testSetAlertThrowsExceptionOnTextNonString() 44 | { 45 | $this->expectException(InvalidArgumentException::class); 46 | $this->message->setAlert([]); 47 | } 48 | 49 | public function testSetAlertThrowsExceptionOnActionLocKeyNonString() 50 | { 51 | $this->expectException(InvalidArgumentException::class); 52 | $this->alert->setActionLocKey([]); 53 | } 54 | 55 | public function testSetAlertThrowsExceptionOnLocKeyNonString() 56 | { 57 | $this->expectException(InvalidArgumentException::class); 58 | $this->alert->setLocKey([]); 59 | } 60 | 61 | public function testSetAlertThrowsExceptionOnLaunchImageNonString() 62 | { 63 | $this->expectException(InvalidArgumentException::class); 64 | $this->alert->setLaunchImage([]); 65 | } 66 | 67 | public function testSetAlertThrowsExceptionOnTitleNonString() 68 | { 69 | $this->expectException(InvalidArgumentException::class); 70 | $this->alert->setTitle([]); 71 | } 72 | 73 | public function testSetAlertThrowsExceptionOnTitleLocKeyNonString() 74 | { 75 | $this->expectException(InvalidArgumentException::class); 76 | $this->alert->setTitleLocKey([]); 77 | } 78 | 79 | public function testSetBadgeReturnsCorrectNumber() 80 | { 81 | $num = 5; 82 | $this->message->setBadge($num); 83 | $this->assertEquals($num, $this->message->getBadge()); 84 | } 85 | 86 | public function testSetBadgeNonNumericThrowsException() 87 | { 88 | $this->expectException(InvalidArgumentException::class); 89 | $this->message->setBadge('string!'); 90 | } 91 | 92 | public function testSetBadgeAllowsNull() 93 | { 94 | $this->message->setBadge(null); 95 | $this->assertNull($this->message->getBadge()); 96 | } 97 | 98 | public function testSetExpireReturnsInteger() 99 | { 100 | $expire = 100; 101 | $this->message->setExpire($expire); 102 | $this->assertEquals($expire, $this->message->getExpire()); 103 | } 104 | 105 | public function testSetExpireNonNumericThrowsException() 106 | { 107 | $this->expectException(InvalidArgumentException::class); 108 | $this->message->setExpire('sting!'); 109 | } 110 | 111 | public function testSetSoundReturnsString() 112 | { 113 | $sound = 'test'; 114 | $this->message->setSound($sound); 115 | $this->assertEquals($sound, $this->message->getSound()); 116 | } 117 | 118 | public function testSetSoundThrowsExceptionOnNonScalar() 119 | { 120 | $this->expectException(InvalidArgumentException::class); 121 | $this->message->setSound([]); 122 | } 123 | 124 | public function testSetSoundThrowsExceptionOnNonString() 125 | { 126 | $this->expectException(InvalidArgumentException::class); 127 | $this->message->setSound(12345); 128 | } 129 | 130 | /** 131 | * @dataProvider provideSetMutableContentThrowsExceptionOnNonIntegerOneOrNullData 132 | * 133 | * @param mixed $value 134 | */ 135 | public function testSetMutableContentThrowsExceptionOnNonIntegerOneAndNull($value) 136 | { 137 | $this->expectException(InvalidArgumentException::class); 138 | $this->message->setMutableContent($value); 139 | } 140 | 141 | /** 142 | * @return array 143 | */ 144 | public function provideSetMutableContentThrowsExceptionOnNonIntegerOneOrNullData() 145 | { 146 | return [ 147 | 'unsupported positive integer' => ['value' => 2], 148 | 'zero integer' => ['value' => 0], 149 | 'negative integer' => ['value' => -1], 150 | 'boolean' => ['value' => true], 151 | 'string' => ['value' => 'any string'], 152 | 'float' => ['value' => 123.12], 153 | 'array' => ['value' => []], 154 | 'object' => ['value' => new stdClass()], 155 | ]; 156 | } 157 | 158 | public function testSetMutableContentResultsInCorrectPayloadWithIntegerValue() 159 | { 160 | $value = 1; 161 | $this->message->setMutableContent($value); 162 | $payload = $this->message->getPayload(); 163 | $this->assertEquals($value, $payload['aps']['mutable-content']); 164 | } 165 | 166 | public function testSetMutableContentResultsInCorrectPayloadWithNullValue() 167 | { 168 | $this->message->setMutableContent(null); 169 | $json = $this->message->getPayloadJson(); 170 | $payload = json_decode($json, true); 171 | $this->assertFalse(isset($payload['aps']['mutable-content'])); 172 | } 173 | 174 | public function testSetContentAvailableThrowsExceptionOnNonInteger() 175 | { 176 | $this->expectException(InvalidArgumentException::class); 177 | $this->message->setContentAvailable("string"); 178 | } 179 | 180 | public function testGetContentAvailableReturnsCorrectInteger() 181 | { 182 | $value = 1; 183 | $this->message->setContentAvailable($value); 184 | $this->assertEquals($value, $this->message->getContentAvailable()); 185 | } 186 | 187 | public function testSetContentAvailableResultsInCorrectPayload() 188 | { 189 | $value = 1; 190 | $this->message->setContentAvailable($value); 191 | $payload = $this->message->getPayload(); 192 | $this->assertEquals($value, $payload['aps']['content-available']); 193 | } 194 | 195 | public function testSetCategoryReturnsString() 196 | { 197 | $category = 'test'; 198 | $this->message->setCategory($category); 199 | $this->assertEquals($category, $this->message->getCategory()); 200 | } 201 | 202 | public function testSetCategoryThrowsExceptionOnNonScalar() 203 | { 204 | $this->expectException(InvalidArgumentException::class); 205 | $this->message->setCategory([]); 206 | } 207 | 208 | public function testSetCategoryThrowsExceptionOnNonString() 209 | { 210 | $this->expectException(InvalidArgumentException::class); 211 | $this->message->setCategory(12345); 212 | } 213 | 214 | public function testSetUrlArgsReturnsString() 215 | { 216 | $urlArgs = ['path/to/somewhere']; 217 | $this->message->setUrlArgs($urlArgs); 218 | $this->assertEquals($urlArgs, $this->message->getUrlArgs()); 219 | } 220 | 221 | public function testSetCustomData() 222 | { 223 | $data = ['key' => 'val', 'key2' => [1, 2, 3, 4, 5]]; 224 | $this->message->setCustom($data); 225 | $this->assertEquals($data, $this->message->getCustom()); 226 | } 227 | 228 | public function testAlertConstructor() 229 | { 230 | $alert = new Alert( 231 | 'Foo wants to play Bar!', 232 | 'PLAY', 233 | 'GAME_PLAY_REQUEST_FORMAT', 234 | ['Foo', 'Baz'], 235 | 'Default.png', 236 | 'Alert', 237 | 'ALERT', 238 | ['Foo', 'Baz'] 239 | ); 240 | 241 | $this->assertEquals('Foo wants to play Bar!', $alert->getBody()); 242 | $this->assertEquals('PLAY', $alert->getActionLocKey()); 243 | $this->assertEquals('GAME_PLAY_REQUEST_FORMAT', $alert->getLocKey()); 244 | $this->assertEquals(['Foo', 'Baz'], $alert->getLocArgs()); 245 | $this->assertEquals('Default.png', $alert->getLaunchImage()); 246 | $this->assertEquals('Alert', $alert->getTitle()); 247 | $this->assertEquals('ALERT', $alert->getTitleLocKey()); 248 | $this->assertEquals(['Foo', 'Baz'], $alert->getTitleLocArgs()); 249 | } 250 | 251 | public function testAlertJsonPayload() 252 | { 253 | $alert = new Alert( 254 | 'Foo wants to play Bar!', 255 | 'PLAY', 256 | 'GAME_PLAY_REQUEST_FORMAT', 257 | ['Foo', 'Baz'], 258 | 'Default.png', 259 | 'Alert', 260 | 'ALERT', 261 | ['Foo', 'Baz'] 262 | ); 263 | $payload = $alert->getPayload(); 264 | 265 | $this->assertArrayHasKey('body', $payload); 266 | $this->assertArrayHasKey('action-loc-key', $payload); 267 | $this->assertArrayHasKey('loc-key', $payload); 268 | $this->assertArrayHasKey('loc-args', $payload); 269 | $this->assertArrayHasKey('launch-image', $payload); 270 | $this->assertArrayHasKey('title', $payload); 271 | $this->assertArrayHasKey('title-loc-key', $payload); 272 | $this->assertArrayHasKey('title-loc-args', $payload); 273 | } 274 | 275 | public function testAlertPayloadSendsOnlyBody() 276 | { 277 | $alert = new Alert('Foo wants Bar'); 278 | $payload = $alert->getPayload(); 279 | 280 | $this->assertEquals('Foo wants Bar', $payload); 281 | } 282 | 283 | public function testPayloadJsonFormedCorrectly() 284 | { 285 | $text = 'hi=привет'; 286 | $this->message->setAlert($text); 287 | $this->message->setId(1); 288 | $this->message->setExpire(100); 289 | $this->message->setToken('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'); 290 | $payload = $this->message->getPayload(); 291 | $this->assertEquals($payload, ['aps' => ['alert' => 'hi=привет']]); 292 | if (defined('JSON_UNESCAPED_UNICODE')) { 293 | $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE); 294 | $this->assertEquals($payloadJson, '{"aps":{"alert":"hi=привет"}}'); 295 | $length = 35; // 23 + (2 * 6) because UTF-8 (Russian) "привет" contains 2 bytes per letter 296 | $result = 297 | pack( 298 | 'CNNnH*', 299 | 1, 300 | $this->message->getId(), 301 | $this->message->getExpire(), 302 | 32, 303 | $this->message->getToken() 304 | ) 305 | . pack('n', $length) 306 | . $payloadJson; 307 | $this->assertEquals($this->message->getPayloadJson(), $result); 308 | } else { 309 | $payloadJson = JsonEncoder::encode($payload); 310 | $this->assertEquals($payloadJson, '{"aps":{"alert":"hi=\u043f\u0440\u0438\u0432\u0435\u0442"}}'); 311 | $length = 59; // (23 + (6 * 6) because UTF-8 (Russian) "привет" converts into 6 bytes/letter 312 | $result = 313 | pack( 314 | 'CNNnH*', 315 | 1, 316 | $this->message->getId(), 317 | $this->message->getExpire(), 318 | 32, 319 | $this->message->getToken() 320 | ) 321 | . pack('n', $length) 322 | . $payloadJson; 323 | $this->assertEquals($this->message->getPayloadJson(), $result); 324 | } 325 | } 326 | 327 | public function testCustomDataPayloadIncludesEmptyApsObject() 328 | { 329 | $data = ['custom' => 'data']; 330 | $expected = array_merge($data, ['aps' => (object) []]); 331 | $this->message->setCustom($data); 332 | 333 | $payload = $this->message->getPayload(); 334 | $this->assertEquals($expected, $payload); 335 | } 336 | 337 | public function testTokensAllowUpperCaseHex() 338 | { 339 | $token = str_repeat('abc1234defABCDEF', 4); 340 | $this->message->setToken($token); 341 | $this->assertSame($token, $this->message->getToken()); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /test/Apns/TestAsset/FeedbackClient.php: -------------------------------------------------------------------------------- 1 | readResponse = $str; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Set the write response 55 | * 56 | * @param mixed $resp 57 | * @return FeedbackClient 58 | */ 59 | public function setWriteResponse($resp) 60 | { 61 | $this->writeResponse = $resp; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Connect to Host 68 | * 69 | * @return FeedbackClient 70 | */ 71 | protected function connect($host, array $ssl) 72 | { 73 | return $this; 74 | } 75 | 76 | /** 77 | * Return Response 78 | * 79 | * @param string $length 80 | * @return string 81 | */ 82 | protected function read($length = 1024) 83 | { 84 | if (! $this->isConnected()) { 85 | throw new Exception\RuntimeException('You must open the connection prior to reading data'); 86 | } 87 | $ret = substr($this->readResponse, 0, $length); 88 | $this->readResponse = null; 89 | 90 | return $ret; 91 | } 92 | 93 | /** 94 | * Write and Return Length 95 | * 96 | * @param string $payload 97 | * @return int 98 | */ 99 | protected function write($payload) 100 | { 101 | if (! $this->isConnected()) { 102 | throw new Exception\RuntimeException('You must open the connection prior to writing data'); 103 | } 104 | $ret = $this->writeResponse; 105 | $this->writeResponse = null; 106 | 107 | return (null === $ret) ? strlen($payload) : $ret; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/Apns/TestAsset/MessageClient.php: -------------------------------------------------------------------------------- 1 | readResponse = $str; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Set the write response 55 | * 56 | * @param mixed $resp 57 | * @return MessageClient 58 | */ 59 | public function setWriteResponse($resp) 60 | { 61 | $this->writeResponse = $resp; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Connect to Host 68 | * 69 | * @return MessageClient 70 | */ 71 | protected function connect($host, array $ssl) 72 | { 73 | return $this; 74 | } 75 | 76 | /** 77 | * Return Response 78 | * 79 | * @param string $length 80 | * @return string 81 | */ 82 | protected function read($length = 1024) 83 | { 84 | if (! $this->isConnected()) { 85 | throw new Exception\RuntimeException('You must open the connection prior to reading data'); 86 | } 87 | $ret = substr($this->readResponse, 0, $length); 88 | $this->readResponse = null; 89 | 90 | return $ret; 91 | } 92 | 93 | /** 94 | * Write and Return Length 95 | * 96 | * @param string $payload 97 | * @return int 98 | */ 99 | protected function write($payload) 100 | { 101 | if (! $this->isConnected()) { 102 | throw new Exception\RuntimeException('You must open the connection prior to writing data'); 103 | } 104 | $ret = $this->writeResponse; 105 | $this->writeResponse = null; 106 | 107 | return (null === $ret) ? strlen($payload) : $ret; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/Apns/TestAsset/certificate.pem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendframework/ZendService_Apple_Apns/1e4f36899a3a05419c92f5514ab6c6d1f8f6ebaf/test/Apns/TestAsset/certificate.pem --------------------------------------------------------------------------------