Notifications
Push notifications through Firebase or OneSignal, backed by a per-user queue that doubles as an in-app inbox — with unread counts, read state and localized copy.
Sending
The Notification class pushes to devices directly. sendToAll() broadcasts to every
registered device, while sendToSome() targets specific device player ids. Each carries a
title, a message, a url to open, a dataType / dataID pair the app can
route on. A send to some devices also takes an optional badge, the number the app icon shows,
which a broadcast cannot, as no one number is right for everyone. Each answers with a NotificationResult and the id the provider
gave the push, which is empty when it did not go out:
use Framework\Notification\Notification;
// Broadcast to every registered device
[ $result, $externalID ] = Notification::sendToAll(
title: "New release",
message: "Version 2 is out",
url: "changelog",
dataType: "release",
dataID: 42,
);
// Or target specific devices by their player id
[ $result, $externalID ] = Notification::sendToSome(
title: "Order shipped",
message: "Your order is on its way",
url: "orders/7",
dataType: "order",
dataID: 7,
playerIDs: [ "a1b2…", "c3d4…" ],
badge: 3,
);
Send results
Delivery records a NotificationResult per user — Sent, or the reason it was skipped:
| Result | Meaning |
|---|---|
Sent | Handed off to the provider. |
InactiveSend | Notifications are turned off (NOTIFICATION_ACTIVE). |
NoDevices | The user has no registered devices. |
NoProvider | No NOTIFICATION_PROVIDER is set, so there is nothing to push through. |
ProviderError | The provider rejected the push. |
Configuration
As with email, everything is set through environment values:
| Key | Controls |
|---|---|
NOTIFICATION_ACTIVE | The master switch — off means nothing is sent. |
NOTIFICATION_PROVIDER | Which provider to push through (see below). |
NOTIFICATION_ICON | The default icon shown on a push. |
NOTIFICATION_LIMIT | Maximum notifications delivered per run. |
NOTIFICATION_DELETE_DAYS | How long delivered notifications are kept. |
Providers
NOTIFICATION_PROVIDER names a NotificationProvider. When you call a send, the
Notification class works out the icon and the full url, hands the push
to the matching provider, and answers Sent with the id the provider gave it,
or ProviderError with an empty one if it would not take it.
There is no provider to fall back on. A send with NOTIFICATION_PROVIDER unset returns
NoProvider, so a setup that was never finished says so.
Each device records the provider that reaches it, and a push only goes to the devices of the provider of the config. So when one replaces another, the devices of the old one are kept rather than pushed to through the new one, and are there should the config go back.
Firebase
Pushes are delivered through Firebase Cloud Messaging,
addressed by the registration token that each device registers when
a user signs in. A push to everyone goes to the all topic, which every device subscribes to on its
own, and a token Firebase reports as no longer registered takes its device off the credential. The keys are the
project_id, client_email and private_key of the service account file the
Firebase console gives you, pasted as they are:
NOTIFICATION_ACTIVE = true
NOTIFICATION_PROVIDER = "Firebase"
FIREBASE_PROJECT_ID = "…"
FIREBASE_CLIENT_EMAIL = "…"
FIREBASE_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n…\n-----END PRIVATE KEY-----\n"OneSignal
Pushes are delivered through OneSignal's REST API, addressed by the device player ids that each device registers when a user signs in. Point it at your OneSignal app with its app id and REST key:
NOTIFICATION_ACTIVE = true
NOTIFICATION_PROVIDER = "OneSignal"
ONESIGNAL_APP_ID = "…"
ONESIGNAL_REST_KEY = "…"
ONESIGNAL_USE_ALIAS = falseONESIGNAL_USE_ALIAS addresses the devices by their OneSignal alias rather than by
subscription id.
Writing your own
The enum is not a list you edit. A provider is a class implementing
NotificationSender, and the build finds
every one of them — in the framework and in your app — and writes the NotificationProvider enum from
their names, the same way email does. The class name is the case, so a
Pushy class is NOTIFICATION_PROVIDER = "Pushy", and a Name
constant on the class overrides that.
Its keys are yours to name: add them to the .env and the
build turns each into a typed Config getter. Both sends answer with the id the provider gave the
push, or an empty string when it would not take it:
namespace MyApp\Provider;
use Framework\Notification\NotificationSender;
class Pushy implements NotificationSender {
public static function sendToAll(
string $title,
string $message,
string $url,
string $icon,
string $dataType,
int $dataID,
): string {
return "the-id-it-gave-back";
}
public static function sendToSome(
string $title,
string $message,
string $url,
string $icon,
string $dataType,
int $dataID,
array $playerIDs,
int $badge = 0,
): string {
return "the-id-it-gave-back";
}
}Pushing in a test
As with email, write a sender that keeps what it is handed and give it
to Notification::setSender(). The send then runs the whole way and stops there:
use Framework\Notification\Notification;
Notification::setSender(TestNotificationSender::class);
[ $result, $externalID ] = Notification::sendToAll(
"New release", "Version 2 is out", "changelog", "release", 42,
);
Notification::setSender(); // back to the one of the config
The queue & inbox
Most notifications go through the NotificationQueue rather than pushing straight away. It stores one
row per user, which makes it two things at once: a delivery buffer that a cron flushes to each user's devices,
and an in-app inbox with read state and unread counts. Queue one for a credential:
use Framework\Notification\NotificationQueue;
NotificationQueue::add(
credentialID: 42,
currentUser: 1,
title: "Order shipped",
message: "Your order is on its way",
url: "orders/7",
dataType: "order",
dataID: 7,
);
A scheduled sendAll() then looks up each user's devices and pushes the pending rows, each with
the user's unread amount as the badge, while the
rest of the API drives an inbox UI:
| Method | Does |
|---|---|
sendAll() | Cron — push every pending notification to its user's devices. |
getAllForCredential(…) | A user's notifications, for rendering the inbox. |
getUnreadAmount(…) | The unread badge count. |
markAsRead(id) / discard(id) | Mark one as read, or dismiss it. |
deleteOld() | Cron — prune rows past NOTIFICATION_DELETE_DAYS. |
Run the scheduled methods — sendAll() and deleteOld() — from your server's cron.
First expose each as a console command:
use Framework\Discovery\Attr\ConsoleCommand;
use Framework\Notification\NotificationQueue;
class NotificationCommands {
#[ConsoleCommand("sendNotifications")]
public static function send(): void {
NotificationQueue::sendAll();
}
#[ConsoleCommand("cleanNotifications")]
public static function clean(): void {
NotificationQueue::deleteOld();
}
}
Then add the crontab entries:
# crontab
* * * * * cd /srv/app && ./framework sendNotifications
0 4 * * * cd /srv/app && ./framework cleanNotifications
Localized content
Repeated notifications keep their copy in the translation files, exactly like
email content. Each message lives per language under
nls/notifications, keyed by a code, with a title, a message, and a
description — a note for whoever edits the copy:
{
"OrderShipped": {
"description": "Sent when an order ships",
"title": "Order shipped",
"message": "Your order {{orderID}} is on its way"
}
}Add the matching es.json, with the same keys in the same order —
nlsCheck says whether they still match. At build time the
NotificationBuilder turns each key into a typed NotificationCode, while a
migration loads the copy into the database. Fetch it by language:
use Framework\Notification\NotificationContent;
use Framework\System\NotificationCode;
$content = NotificationContent::get(NotificationCode::OrderShipped, "en");
NotificationContent::render() fills its {{placeholders}} — like
{{orderID}} above — with per-send data before the title and message are queued.