sly/pushover
sly/pushover is a Laravel package for sending Pushover push notifications from your PHP applications. It offers a simple integration for delivering real-time alerts to iOS and Android devices via Pushover’s API, with configuration for your application token and user key.
This is a thin, framework-agnostic PHP client for the Pushover REST API — not a batteries-included Laravel package. There is no service provider, no facade, no config/pushover.php to publish. You get two classes and you wire them up yourself. Plan for ~15 minutes to first send.
1. Install and pin
composer require sly/pushover
Pin the exact version in composer.json (34 stars, zero dependents — treat it as vendored code, not a moving dependency). Run composer audit and confirm the PHP/Laravel constraints before merging.
2. Provision two separate credentials at pushover.net
They are not interchangeable. Swapping them returns a 400 with a vague message that costs ten minutes to diagnose. Store them as secrets, not in code.
3. Put them in config and .env
// config/services.php
'pushover' => [
'token' => env('PUSHOVER_TOKEN'),
'user' => env('PUSHOVER_USER'), // default recipient, e.g. ops team
],
4. Where to look first
README.md — the entire API surface fits on one screen.src/Sly/PushOver/PushOver.php — the client; holds the token, performs the HTTP call.src/Sly/PushOver/Model/Push.php — the fluent DTO: setUser, setMessage, setTitle, setSound, setDevice, setUrl, setUrlTitle, setTimestamp, setPriority, and emergency-only setRetry / setExpire / setCallback.5. First use case — prove it in tinker before writing app code
use Sly\PushOver\PushOver;
use Sly\PushOver\Model\Push;
$push = (new Push())
->setUser(config('services.pushover.user'))
->setTitle('Deploy finished')
->setMessage('prod is green')
->setSound('pushover');
$client = new PushOver();
$client->setToken(config('services.pushover.token'));
$client->send($push);
If the phone buzzes, everything after this is plumbing. The canonical day-one use case is an internal ops alert (deploy finished, cron failed, incident opened) to on-call — not user-facing notifications.
Bind the client as a singleton in AppServiceProvider::register(). No controller should ever construct PushOver:
$this->app->singleton(PushOver::class, function () {
$client = new PushOver();
$client->setToken(config('services.pushover.token'));
return $client;
});
Wrap it in an application-owned PushoverNotifier. This is the single most important pattern — it gives you an exit hatch when (not if) the package goes stale:
class PushoverNotifier
{
public function __construct(private PushOver $client) {}
public function send(string $message, array $opts = []): void
{
$push = (new Push())
->setUser($opts['user'] ?? config('services.pushover.user'))
->setMessage($message)
->setTitle($opts['title'] ?? config('app.name'));
if (isset($opts['priority'])) $push->setPriority($opts['priority']);
if (isset($opts['sound'])) $push->setSound($opts['sound']);
if (isset($opts['url'])) $push->setUrl($opts['url']);
$this->client->send($push);
}
}
Callers depend on PushoverNotifier; only one file knows about sly/pushover. Swapping to Laravel's Http client later is a one-file change.
Always queue. A synchronous Pushover call inside a request or model observer blocks — and Pushover's own latency is out of your hands. Two idiomatic options:
ShouldQueue job (SendPushoverAlertJob).class PushoverChannel
{
public function __construct(private PushoverNotifier $notifier) {}
public function send($notifiable, Notification $notification): void
{
$payload = $notification->toPushover($notifiable);
$this->notifier->send($payload['message'], $payload);
}
}
// In the Notification:
public function via($notifiable): array { return [PushoverChannel::class]; }
public function toPushover($notifiable): array
{
return ['title' => 'Order shipped', 'message' => "Order #{$this->order->id} is on its way"];
}
Route by user key, not device. Store pushover_user_key on users (a notification route) and pass it into setUser() per recipient. Reserve setDevice() for the rare case where one user has multiple registered devices and you need to target a specific one. For fan-out to several people, loop in the job — the library sends one Push per call.
Use priority deliberately — it's the feature that earns this library its keep:
| Priority | Meaning | Use for |
|---|---|---|
-2 |
silent, no notification | background confirmations |
-1 |
quiet | low-signal info (deploy succeeded) |
0 |
normal | default |
1 |
high, bypasses quiet hours | paging-adjacent alerts |
2 |
emergency, requires ack | true on-call incidents |
Capture the request id. Pushover returns JSON with status and request — log both. The request id is what you quote to Pushover support and what lets you reconcile a send with a delivery report afterwards.
Test against a mock, not the real API. The client's HTTP layer is small; bind a fake PushOver (or Http::fake() if you later swap to the Laravel client) in a test service provider so CI never burns your Pushover quota or fires real phones.
Pushover facade or a pushover config file, it's describing the separate Symfony bundle (sly/pushover-bundle) or a different package. You register the binding yourself.2) is rejected without setRetry() and setExpire(). Retry is minimum 30 seconds; expire is maximum 10800 (3 hours). Add setCallback() if you want acknowledgement to come back to your app.setToken() on a singleton is set once at boot — if you ever need multi-app support, don't rely on the singleton; construct per-send.send() in try/catch, log at error with the exception message, and fail soft — a failed push must never take down the request/job that triggered it.request id or a local send log and accept that duplicate emergency alerts are worse than a missed quiet one — pick the trade-off deliberately per priority tier.retry() helper or RateLimited job middleware around the notifier, and monitor failure rate.1 bypasses quiet hours on the recipient's device; -1 and 0 don't. This is a product decision, not a code one — get ops to agree who gets woken up before you ship.PushoverNotifier boundary and a documented fallback channel (email/Slack) so a swap is a one-file change when the package rots or Pushover's free tier limits change.message, title, priority, and the request id — not the payload's credentials.How can I help you explore Laravel packages today?