opwoco/rms-push-notifications-bundle
A Symfony bundle for integrating RMS push notifications into your application. It provides configuration and delivery support for connecting app events to RMS push services, simplifying notification setup.
Read this first: it is a Symfony bundle, not a Laravel package. opwoco/rms-push-notifications-bundle depends on symfony/framework-bundle, symfony/dependency-injection and symfony/http-kernel. There is no ServiceProvider, no facade, no publishable config. Dropping it into composer require on a Laravel app will either fail dependency resolution or give you a bundle class Laravel never boots.
The practical path for a Laravel developer is to use the underlying library directly — richsage/rms-push-notifications — and wrap it in a thin Laravel service yourself. The bundle is only DI glue plus a YAML config tree; everything useful (APNSMessage, GCMessage, the senders) lives in the library.
composer require richsage/rms-push-notifications
Where to look first, in this order:
DependencyInjection/RMSPushNotificationsExtension.php and Resources/config/services.xml in the bundle repo — this is your only accurate source of truth for the config keys (apns.certificate, apns.passphrase, apns.environment, gcm.api.key) and the service id (rms_push_notifications).Service/ directory — getAPNS() and getGCM() are the two entry points you will actually call.Message/ directory — APNSMessage, GCMessage, and the MessageInterface they implement.First use case — send an APNS alert to one token. Translating the bundle's Symfony YAML into config/services.php:
// config/services.php
'apns' => [
'certificate' => env('APNS_CERT'), // absolute path to .pem
'passphrase' => env('APNS_PASSPHRASE'),
'environment' => env('APNS_ENV', 'sandbox'),
],
'gcm' => [
'key' => env('GCM_API_KEY'),
],
// app/Services/PushNotifier.php
namespace App\Services;
use RMS\PushNotificationsBundle\Message\APNSMessage;
use RMS\PushNotificationsBundle\Service\NotificationService;
class PushNotifier
{
public function __construct(private NotificationService $rms) {}
public function alert(string $token, string $body, array $extra = []): bool
{
$message = new APNSMessage();
$message->setDeviceIdentifier($token);
$message->setMessage($body);
$message->setCustomData($extra); // must be an array; becomes "aps" siblings
return $this->rms->getAPNS()->send($message);
}
}
Bind it in a provider:
$this->app->singleton(NotificationService::class, function () {
return new NotificationService([
'apns' => config('services.apns'),
'gcm' => config('services.gcm'),
]);
});
Do a sandbox APNS send to one hard-coded device token before building anything else. If that does not work, stop — the rest of this package's value is zero.
Wrap, don't leak. Keep RMS\... classes out of your controllers, jobs and notifications. One PushNotifier (or separate ApnsSender/GcmSender) with sendAlert(), sendSilent(), and sendBatch(). When (not if) you migrate to kreait/laravel-firebase or laravel-notification-channels/apn, you change one class.
Channel-per-platform, not one channel. APNS and FCM payloads diverge sharply — APNS nests everything under aps, GCM/FCM takes a flat data map. Model them as two Laravel notification channels:
// app/Notifications/Channels/ApnsChannel.php
public function send($notifiable, Notification $notification): void
{
foreach ($notifiable->routeNotificationForApns() as $token) {
$this->apns->sendAlert($token, ...$notification->toApns($notifiable));
}
}
Then via() returns [ApnsChannel::class, GcmChannel::class] and the notification stays platform-agnostic.
Always queue. RMS has no bulk send — send() is one blocking socket round-trip per device. Looping 5k tokens inline in a request will time out. Pattern: one SendPushNotification job dispatched per chunk of ~200 tokens, tries = 3, backoff = [10, 60, 300], and a ShouldBeUnique key per campaign so a retry doesn't double-notify.
Token table, not a column. A push_tokens table with (user_id, platform, token, last_seen_at, active) and a unique index on token. Users have multiple devices, tokens rotate, and you need to prune. Store the platform as an enum so the fan-out logic is a simple where('platform', 'ios').
Feedback loop. APNS exposes a feedback endpoint that tells you which tokens are dead. In RMS this surfaces as getAPNSFeedback(), returning objects with getDeviceIdentifier() and getTime(). Schedule a daily command:
foreach ($rms->getAPNSFeedback() as $response) {
PushToken::where('token', $response->getDeviceIdentifier())->delete();
}
Without this, your token table rots and deliverability metrics look broken within a month.
Payload builders with hard size caps. APNS caps the payload (256 bytes on the legacy protocol, 4 KB on HTTP/2) and FCM caps at 4 KB. Trim aggressively and treat mb_strlen(json_encode($payload)) as an assertion, not a comment.
Config in .env, certs outside the repo. APNS_CERT=/var/secrets/apns.pem, APNS_PASSPHRASE=.... Never commit .pem files; a combined cert+key file is a production credential.
Swappable for tests. Bind PushNotifier to a FakePushNotifier that records messages in an array, then assert assertPushedTo('ios', fn ($m) => $m['body'] === 'Order shipped'). Since RMS returns bare booleans, this is also the only place you will get useful assertions.
Kill criteria — check these before writing any code. (a) The package has 0 stars, 0 dependents and a 0.03 score; there is no community to help you. (b) It is Symfony 2/3-era: expect composer install on PHP 8.2 + Laravel 11 to fail on symfony/http-kernel: ~2.x and php: >=5.3 constraints. (c) If the version you pull uses APNS's binary protocol on port 2195, it is dead — Apple retired that endpoint in March 2021. Verify the transport before committing. Any of these failing means the answer is "use edamov/pushok or kreait/laravel-firebase instead."
GCM is a ghost town. Google shut down the GCM HTTP legacy API and FCM legacy endpoints. getGCM() may still construct a request, but it will 404 or 401 against Google forever. Anything you write against the GCM path is throwaway — go straight to FCM HTTP v1 with OAuth2 service-account credentials (kreait/laravel-firebase handles this well). Only the APNS side of this package has a plausible reason to exist.
send() returns a boolean and swallows detail. A failure gives you false with no status code, no token attribution, no reason. Wrap every call in a try/catch and check the return; log the token, the platform and the timestamp so you can correlate with Apple/Google. Do not expect exceptions.
Certificate mismatches fail silently. Sandbox cert + production device token (or the reverse) typically yields a connection-level error, not a meaningful message. Sanity-check the cert out of band:
openssl s_client -connect gateway.sandbox.push.apple.com:2195 \
-cert apns.pem -key apns.pem 2>&1 | head
If that handshake fails, the package will too — don't debug PHP for it.
Clean your device tokens. iOS tokens copied from a device log often arrive wrapped in < > and spaces; strip them and lowercase the hex. FCM tokens are a different shape entirely (~150+ chars) — never let one column accept both without a platform discriminator.
Silent/background pushes have extra requirements. APNS background delivery needs content-available: 1 (RMS: setContentAvailable(true)) and, on modern Apple, the apns-push-type: background header. If you are on an old RMS fork, that header may not exist — another reason the APNS transport may be a dead end.
Extension points. If you must fork, the clean seams are: implement MessageInterface for custom message types, subclass APNSMessage to inject headers the library doesn't expose, and swap the sender via your own container binding (getAPNS() just resolves a registered service). Vendor the fork under a VCS repository in composer.json and pin a tag — with 0 stars and 0 dependents, upstream is not going to fix anything for you.
Debugging checklist, in order: does composer show richsage/rms-push-notifications reveal the transport version → does the openssl s_client handshake pass → does a single sandbox send with a freshly generated dev token return true → does a token you know is unregistered also return false (confirming errors aren't swallowed) → only then integrate.
How can I help you explore Laravel packages today?