Skip to content

Commit 292a7ff

Browse files
committed
Correct CHANGELOG and add more detail to README
1 parent 68413cf commit 292a7ff

3 files changed

Lines changed: 197 additions & 10 deletions

File tree

.idea/php.xml

Lines changed: 35 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [1.0.0] - 2026-07-12
8+
## [Unreleased]
99

1010
### Added
1111

1212
- Added support for the `collapseId` field (iOS + Android) (#60)
1313
- Added support for the `tag` field (Android) (#61)
14+
- Added art, summary and changelog section to README
1415

1516
### Changed
1617

17-
- Added art, summary and changelog section to README
1818
- Cached SDK version in memory to avoid repeated disk reads (#62)
1919

2020
### Fixed
@@ -116,7 +116,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
116116
- README (#16)
117117
- CHANGELOG (#16)
118118

119-
[1.0.0]: https://github.com/dru1x/expo-server-sdk-php/compare/v0.8.2...v1.0.0
119+
[Unreleased]: https://github.com/dru1x/expo-server-sdk-php/compare/v0.8.2...HEAD
120120
[0.8.2]: https://github.com/dru1x/expo-server-sdk-php/compare/v0.8.1...v0.8.2
121121
[0.8.1]: https://github.com/dru1x/expo-server-sdk-php/compare/v0.8.0...v0.8.1
122122
[0.8.0]: https://github.com/dru1x/expo-server-sdk-php/compare/v0.7.0...v0.8.0

README.md

Lines changed: 159 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<p align="center">
2-
<img src="art/banner.svg" alt="Laravel Expo Push" width="100%">
2+
<img src="art/banner.svg" alt="Expo Push Server SDK (PHP)" width="100%">
33
</p>
44

55
# Server-side library for Expo's Push Server
66

77
[![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md)
8-
![Test Workflow Status](https://github.com/Dru1X/expo-server-sdk-php/workflows/Test/badge.svg)
8+
![Test Workflow Status](https://github.com/dru1x/expo-server-sdk-php/workflows/Test/badge.svg)
99

1010
This is a PHP 8.2+ SDK for working with
1111
[Expo's Push Notification service](https://docs.expo.dev/push-notifications/overview/). It provides a clean, typed
@@ -39,6 +39,7 @@ have to.
3939

4040
- [PHP 8.2+](https://php.net/releases)
4141
- [PHP Zlib extension](https://www.php.net/manual/en/book.zlib.php)
42+
- [PHP JSON extension](https://www.php.net/manual/en/book.json.php)
4243

4344
### Instructions
4445

@@ -109,6 +110,27 @@ $expoPush = new ExpoPush(retryConfig: RetryConfig::disabled());
109110
Client errors (e.g. `4xx` responses) are never retried, since retrying is unlikely to change the
110111
outcome. If all retries are exhausted, the failure is recorded as a `PushError`.
111112

113+
This can also be restored explicitly with the `default()` helper, which is useful when switching between presets:
114+
115+
```php
116+
$expoPush = new ExpoPush(retryConfig: RetryConfig::default());
117+
```
118+
119+
### Sharing Rate Limit State
120+
121+
By default, the rate limiting used to stay within Expo's 600 notifications/second limit is tracked in memory, scoped
122+
to the current process. If your application sends notifications from multiple processes or servers concurrently
123+
(e.g. multiple queue workers), this can be shared between them by supplying a `RateLimitStore` from
124+
[`saloonphp/rate-limit-plugin`](https://github.com/saloonphp/rate-limit-plugin) — such as its `RedisStore`,
125+
`PredisStore`, `LaravelCacheStore`, `PsrStore` or `FileStore` — as the second constructor argument:
126+
127+
```php
128+
use Dru1x\ExpoPush\ExpoPush;
129+
use Saloon\RateLimitPlugin\Stores\RedisStore;
130+
131+
$expoPush = new ExpoPush(rateLimitStore: new RedisStore(new Redis()));
132+
```
133+
112134
### Sending Push Notifications
113135

114136
Push notifications can be sent by supplying a `PushMessageCollection`, or an array of `PushMessage` objects, to the
@@ -141,18 +163,72 @@ $tickets = $result->tickets;
141163
/** @var PushErrorCollection|null $errors */
142164
$errors = $result->errors;
143165
```
166+
167+
#### Message Fields
168+
169+
`PushMessage` supports the full set of fields defined in
170+
[Expo's push message request format](https://docs.expo.dev/push-notifications/sending-notifications/#message-request-format):
171+
172+
| Field | Type | Description |
173+
|---------------------|------------------------------------|-------------------------------------------------------------------------------------------------|
174+
| `to` | `PushToken\|PushTokenCollection` | The recipient(s) of this notification. |
175+
| `title` | `?string` | The notification title. |
176+
| `subtitle` | `?string` | The notification subtitle (iOS only). |
177+
| `body` | `?string` | The notification body. |
178+
| `ttl` | `?int` | Time-to-live, in seconds, before the notification expires. |
179+
| `data` | `array\|object\|null` | Arbitrary JSON-serialisable data to attach to the notification (max 4096 bytes once encoded). |
180+
| `expiration` | `?int` | A UNIX timestamp after which the notification should no longer be delivered. |
181+
| `priority` | `?Priority` | Delivery priority: `Priority::Default`, `Priority::Normal` or `Priority::High`. |
182+
| `sound` | `?string` | The notification sound to play (iOS only). |
183+
| `badge` | `?int` | The app icon badge count (iOS only). |
184+
| `interruptionLevel` | `?InterruptionLevel` | iOS interruption level: `Active`, `Critical`, `Passive` or `TimeSensitive`. |
185+
| `channelId` | `?string` | The Android notification channel to deliver to. |
186+
| `icon` | `?string` | The notification icon (Android only). |
187+
| `richContent` | `?RichContent` | An image to display with the notification. |
188+
| `categoryId` | `?string` | The ID of a registered notification category, used for interactive notifications. |
189+
| `collapseId` | `?string` | Notifications sharing a `collapseId` replace one another instead of stacking (iOS + Android). |
190+
| `tag` | `?string` | Notifications sharing a `tag` replace one another instead of stacking (Android only). |
191+
| `mutableContent` | `?bool` | Allows a Notification Service Extension to modify the notification before delivery (iOS only). |
192+
| `_contentAvailable` | `?bool` | Delivers the notification silently, waking the app in the background (iOS only). |
193+
194+
```php
195+
use Dru1x\ExpoPush\PushMessage\InterruptionLevel;
196+
use Dru1x\ExpoPush\PushMessage\PushMessage;
197+
use Dru1x\ExpoPush\PushMessage\Priority;
198+
use Dru1x\ExpoPush\PushMessage\RichContent;
199+
use Dru1x\ExpoPush\PushToken\PushToken;
200+
201+
$message = new PushMessage(
202+
to: new PushToken('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]'),
203+
title: 'Your order has shipped',
204+
body: 'Order #1234 is on its way',
205+
data: ['orderId' => 1234],
206+
priority: Priority::High,
207+
sound: 'default',
208+
badge: 1,
209+
interruptionLevel: InterruptionLevel::TimeSensitive,
210+
channelId: 'orders',
211+
richContent: new RichContent(image: 'https://example.com/order-1234.png'),
212+
collapseId: 'order-1234-update',
213+
tag: 'order-1234',
214+
);
215+
```
216+
144217
The `SendNotificationsResult` object returned by `sendNotifications()` contains a collection of all the resulting
145218
`PushTicket` objects, as well as a collection of `PushError` objects representing any
146219
[request-level errors](https://docs.expo.dev/push-notifications/sending-notifications/#request-errors) encountered while
147220
sending the given batch of notifications.
148221

149222
The `PushTicketCollection` is ordered according to the order of the `PushMessage` objects passed in to
150223
`sendNotifications()`. Each `PushTicket` will either be a `SuccessfulPushTicket` or a `FailedPushTicket`, the latter
151-
representing a ticket that was returned with a status of "error".
224+
representing a ticket that was returned with a status of "error". Either can be distinguished with `$ticket->isSuccessful()`/`$ticket->isFailed()`, in addition to `instanceof`.
152225

153226
If errors were encountered, they will be present in the `PushErrorCollection`, and the `PushTicketCollection` will have
154227
a gap in its keys that corresponds to the failed chunk of notifications. Inspect the errors to find out what went wrong.
155228

229+
`SendNotificationsResult` also provides a set of helper methods for quickly checking the outcome without inspecting
230+
the collections directly: `hasTickets()`, `hasSuccessfulTickets()`, `hasFailedTickets()` and `hasErrors()`.
231+
156232
If only a single notification needs to be sent, the `sendNotification()` method can be used instead. This accepts a
157233
single `PushMessage` and, like `sendNotifications()`, returns a `SendNotificationsResult`:
158234

@@ -205,11 +281,50 @@ getting the given batch of receipts.
205281

206282
The `PushReceiptCollection` respects the order of receipts returned by the Expo Push API. To find a specific receipt in
207283
the collection, the `getById()` method can be used. Each `PushReceipt` with either be a `SuccessfulPushReceipt` or a
208-
`FailedPushReceipt`, the latter representing a receipt that was returned with a status of "error".
284+
`FailedPushReceipt`, the latter representing a receipt that was returned with a status of "error". Either can be
285+
distinguished with `$receipt->isSuccessful()`/`$receipt->isFailed()`, in addition to `instanceof`.
209286

210287
If errors were encountered, they will be present in the `PushErrorCollection`, and the `PushReceiptCollection` will have
211288
a gap in its keys that corresponds to the failed chunk of notifications. Inspect the errors to find out what went wrong.
212289

290+
`GetReceiptsResult` also provides a set of helper methods for quickly checking the outcome without inspecting the
291+
collections directly: `hasReceipts()`, `hasSuccessfulReceipts()`, `hasFailedReceipts()` and `hasErrors()`.
292+
293+
### Serialisation
294+
295+
`PushMessage`, `PushToken`, and every collection class (`PushMessageCollection`, `PushTokenCollection`,
296+
`PushTicketCollection`, `PushReceiptCollection`, `PushReceiptIdCollection` and `PushErrorCollection`) can be converted
297+
to and from arrays or JSON strings via `toJson()`/`toArray()`/`fromJson()`/`fromArray()`, which is useful for queuing
298+
messages or storing them between requests:
299+
300+
```php
301+
use Dru1x\ExpoPush\PushMessage\PushMessage;
302+
303+
$message = new PushMessage(/* ... */);
304+
305+
$json = $message->toJson(); // string
306+
$array = $message->toArray(); // array<string, mixed>
307+
308+
$message = PushMessage::fromJson($json);
309+
$message = PushMessage::fromArray($array);
310+
```
311+
312+
Note that `PushToken` serialises to/from a bare JSON string (e.g. `"ExponentPushToken[...]"`) via `toJson()`/`fromJson()`,
313+
but to/from an array shaped as `['value' => '...']` via `toArray()`/`fromArray()` — match the form to the method you're
314+
using:
315+
316+
```php
317+
use Dru1x\ExpoPush\PushToken\PushToken;
318+
319+
$token = new PushToken('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]');
320+
321+
$token->toJson(); // "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
322+
$token->toArray(); // ['value' => 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]']
323+
324+
PushToken::fromJson('"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"');
325+
PushToken::fromArray(['value' => 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]']);
326+
```
327+
213328
### Checking the SDK Version
214329

215330
The installed version of this SDK, as reported by composer, can be retrieved via the `sdkVersion()` method:
@@ -218,21 +333,59 @@ The installed version of this SDK, as reported by composer, can be retrieved via
218333
$version = $expoPush->sdkVersion();
219334
```
220335

336+
### Error Handling
337+
338+
Two distinct kinds of failure can occur when using this library:
339+
340+
- **Request-level errors** are surfaced within a `PushErrorCollection` (see above) and represent failures Expo
341+
reported for an entire batch of notifications or receipts. These do not throw exceptions.
342+
- **PHP exceptions** are thrown directly by the SDK, and generally indicate a programming error or an unrecoverable
343+
failure:
344+
345+
| Exception | Thrown when |
346+
|-----------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
347+
| `InvalidArgumentException` | A `PushToken` is constructed with an invalid token string, or a `PushMessage`'s `data` cannot be encoded as JSON. |
348+
| `OverflowException` | A single `PushMessage`'s `data` field exceeds 4096 bytes once encoded. (Notification/receipt *counts* per request are chunked automatically and won't trigger this.) |
349+
| `Saloon\Exceptions\InvalidPoolItemException` | An item yielded to the internal request pool is not a valid request — this would indicate a bug in the library. |
350+
| `Saloon\Exceptions\Request\RequestException` | An HTTP request ultimately failed (e.g. a persistent `4xx`/`5xx` response) after all retries were exhausted, with `throwOnMaxTries` enabled (the default). |
351+
| `Saloon\Exceptions\Request\FatalRequestException` | A network-level failure (e.g. connection refused, timeout) after all retries were exhausted, with `throwOnMaxTries` enabled (the default). |
352+
353+
If `throwOnMaxTries` is set to `false` on the `RetryConfig`, the last failed response is returned as a `PushError`
354+
instead of throwing `RequestException`/`FatalRequestException`.
355+
356+
```php
357+
use Dru1x\ExpoPush\ExpoPush;
358+
use Dru1x\ExpoPush\PushMessage\PushMessage;
359+
use Dru1x\ExpoPush\PushToken\PushToken;
360+
use Saloon\Exceptions\Request\FatalRequestException;
361+
use Saloon\Exceptions\Request\RequestException;
362+
363+
$expoPush = new ExpoPush();
364+
365+
try {
366+
$result = $expoPush->sendNotification(
367+
new PushMessage(to: new PushToken('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]'), body: 'Hello'),
368+
);
369+
} catch (RequestException|FatalRequestException $exception) {
370+
// All retries were exhausted - log and handle as appropriate
371+
}
372+
```
373+
221374
### Further Information
222375

223376
More detailed information about Expo's Push API can be found on their
224377
[documentation website](https://docs.expo.dev/push-notifications/sending-notifications/).
225378

226379
## 💬 Support
227380

228-
Please report any problems by submitting an [issue](https://github.com/Dru1X/expo-server-sdk-php/issues). Ensure that
381+
Please report any problems by submitting an [issue](https://github.com/dru1x/expo-server-sdk-php/issues). Ensure that
229382
the problem is well-described and can be replicated by others. All issues will be reviewed as soon as is reasonably
230383
possible.
231384

232385
## 🤝 Contributing
233386

234387
Thank you for considering contributing! Please open a
235-
[pull request](https://github.com/Dru1X/expo-server-sdk-php/pulls), ensuring that test coverage is maintained or
388+
[pull request](https://github.com/dru1x/expo-server-sdk-php/pulls), ensuring that test coverage is maintained or
236389
increased with any proposed changes. All pull requests will be reviewed as soon as is reasonably possible.
237390

238391
## 🔒 Security

0 commit comments

Comments
 (0)