minishlink/web-push
PHP library to send Web Push notifications to browser push endpoints (RFC 8030). Handles VAPID and payload encryption, supports batching and reporting, and works with modern PHP (8.2+) via Composer for integrating push into your backend.
Installation:
composer require minishlink/web-push
Ensure your PHP environment meets requirements: PHP 8.2+, bcmath/gmp, mbstring, curl, and openssl (with elliptic curve support).
Generate VAPID Keys (one-time setup):
openssl ecparam -genkey -name prime256v1 -out vapid_private.pem
openssl ec -in vapid_private.pem -pubout -outform DER | tail -c 65 | base64 | tr -d '=' | tr '/+' '_-' > vapid_public.txt
openssl ec -in vapid_private.pem -outform DER | tail -c +8 | head -c 32 | base64 | tr -d '=' | tr '/+' '_-' > vapid_private.txt
Or use PHP:
use Minishlink\WebPush\VAPID;
$keys = VAPID::createVapidKeys(); // Store $keys['publicKey'] and $keys['privateKey']
Initialize WebPush:
use Minishlink\WebPush\WebPush;
$webPush = new WebPush([
'VAPID' => [
'subject' => 'mailto:your-email@example.com',
'publicKey' => file_get_contents('vapid_public.txt'),
'privateKey' => file_get_contents('vapid_private.txt'),
]
]);
First Push:
PushSubscription (from subscription.toJSON()) in your database.$subscription = \Minishlink\WebPush\Subscription::create(json_decode($storedSubscription, true));
$webPush->sendOneNotification($subscription, json_encode(['message' => 'Hello!']));
Subscription Management:
PushSubscription data (from subscription.toJSON()) in your database (e.g., subscriptions table with endpoint, keys, and contentEncoding).Subscription object:
$subscription = Subscription::create($dbSubscriptionData);
Batch Processing:
$webPush->queueNotification($subscription, $payload);
foreach ($webPush->flush() as $report) {
if ($report->isSuccess()) {
log("Sent to {$report->getEndpoint()}");
} else {
log("Failed: {$report->getReason()}");
}
}
Event-Driven Triggers:
sent, failed):
event(new PushSent($subscription, $payload));
public function handle(PushSent $event) {
$webPush->queueNotification($event->subscription, $event->payload);
}
Service Worker Integration:
navigator.serviceWorker.register('/sw.js').then(reg => {
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
}).then(sub => {
fetch('/push/subscribe', {
method: 'POST',
body: JSON.stringify(sub),
headers: { 'Content-Type': 'application/json' }
});
});
});
API Endpoints:
Route::post('/push/subscribe', function (Request $request) {
$subscription = Subscription::create($request->json()->all());
// Store in DB and return success
});
Route::post('/push/unsubscribe', function (Request $request) {
$endpoint = $request->input('endpoint');
// Delete from DB
});
Dynamic Payloads:
$payload = json_encode([
'title' => 'New Update',
'body' => 'Check your dashboard!',
'icon' => asset('images/icon.png'),
'data' => ['url' => route('dashboard')]
]);
Retry Logic:
if (!$report->isSuccess()) {
$retryAfter = $report->getRetryAfter();
if ($retryAfter) {
sleep($retryAfter);
$webPush->sendOneNotification($subscription, $payload);
}
}
Analytics:
$stats = [
'total' => 0,
'success' => 0,
'failed' => 0,
];
foreach ($webPush->flush() as $report) {
$stats['total']++;
if ($report->isSuccess()) $stats['success']++;
else $stats['failed']++;
}
// Store $stats in DB
Queue Integration:
PushNotification::dispatch($subscription, $payload)
->onQueue('push-notifications');
class PushNotification implements ShouldQueue {
public function handle() {
$webPush = app(WebPush::class);
$webPush->sendOneNotification($this->subscription, $this->payload);
}
}
Testing:
WebPush in tests:
$mockWebPush = Mockery::mock(WebPush::class);
$mockWebPush->shouldReceive('sendOneNotification')
->once()
->andReturn(new MessageSentReport(true, null, null, null));
$this->app->instance(WebPush::class, $mockWebPush);
VAPID Key Management:
config or .env for keys:
VAPID_PUBLIC_KEY=your_public_key_here
VAPID_PRIVATE_KEY=your_private_key_here
Load in config/webpush.php:
'vapid' => [
'subject' => env('VAPID_SUBJECT', 'mailto:your-email@example.com'),
'publicKey' => env('VAPID_PUBLIC_KEY'),
'privateKey' => env('VAPID_PRIVATE_KEY'),
],
Payload Size:
$payload = json_encode(['data' => str_repeat('a', 3000)]); // Safe
$compressed = gzcompress($payload, 9);
$webPush->sendOneNotification($subscription, $compressed, [
'contentEncoding' => 'aesgcm',
'contentType' => 'application/octet-stream',
]);
Subscription Expiry:
isSubscriptionExpired():
if ($report->isSubscriptionExpired()) {
// Delete from DB and re-subscribe user
}
$webPush = new WebPush();
$subscriptions = Subscription::query()->limit(100)->get();
foreach ($subscriptions as $sub) {
$report = $webPush->sendOneNotification($sub, null);
if (!$report->isSuccess()) {
$sub->delete();
}
}
HTTPS Requirement:
ngrok:
ngrok http 80
Update your subject in VAPID to match the ngrok URL (e.g., https://your-subdomain.ngrok.io).Browser-Specific Quirks:
payload: null for some endpoints.urgency field (default to 'normal'):
$webPush->set
How can I help you explore Laravel packages today?