├── .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 [](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
--------------------------------------------------------------------------------