Framework
Subsystems

Notifications

Push notifications through 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, and a dataType / dataID pair the app can route on:

use Framework\Notification\Notification;

// Broadcast to every registered device
Notification::sendToAll(
    title:    "New release",
    message:  "Version 2 is out",
    url:      "changelog",
    dataType: "release",
    dataID:   42,
);

// Or target specific devices by their player id
Notification::sendToSome(
    title:    "Order shipped",
    message:  "Your order is on its way",
    url:      "orders/7",
    dataType: "order",
    dataID:   7,
    playerIDs: [ "a1b2…", "c3d4…" ],
);

Send results

Delivery records a NotificationResult per user — Sent, or the reason it was skipped:

ResultMeaning
SentHanded off to the provider.
InactiveSendNotifications are turned off (NOTIFICATION_ACTIVE).
NoDevicesThe user has no registered devices.
ProviderErrorThe provider rejected the push.

Configuration

As with email, everything is set through environment values:

KeyControls
NOTIFICATION_ACTIVEThe master switch — off means nothing is sent.
NOTIFICATION_ICONThe default icon shown on a push.
NOTIFICATION_USE_ALIASAddress devices by alias instead of player id.
NOTIFICATION_LIMITMaximum notifications delivered per run.
NOTIFICATION_DELETE_DAYSHow long delivered notifications are kept.

The OneSignal provider

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:

.env
NOTIFICATION_ACTIVE = true
ONESIGNAL_APP_ID    = "…"
ONESIGNAL_REST_KEY  = "…"

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, while the rest of the API drives an inbox UI:

MethodDoes
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:

nls/notifications/en.json
{
    "OrderShipped": {
        "description": "Sent when an order ships",
        "title": "Order shipped",
        "message": "Your order {{orderID}} is on its way"
    }
}

Add the matching es.json, and 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.