Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Voluntary Application Server Identification
Web Notifications made easy
openssl ecparam -genkey -name prime256v1 -out private_key.pem
openssl ec -in private_key.pem -pubout -outform DER|tail -c 65|base64|tr -d '=' |tr '/+' '_-' >> public_key.txt
openssl ec -in private_key.pem -outform DER|tail -c +8|head -c 32|base64|tr -d '=' |tr '/+' '_-' >> private_key.txtcomposer require nyholm/psr7 symfony/http-client spomky-labs/web-push-libwebpush:
doctrine_mapping: true<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use WebPush\Subscription as WebPushSubscription;
/**
* @ORM\Table(name="subscriptions")
* @ORM\Entity
*/
class Subscription extends WebPushSubscription
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private ?int $id = null;
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="subscriptions", cascade={"persist"})
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable="true")
*/
private ?User $user;
public function getId(): ?int
{
return $this->id;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
// We need to override this method as it returns a WebPush\Subscription and we want an entity
public static function createFromString(string $input): self
{
$base = BaseSubscription::createFromString($input);
$object = new self($base->getEndpoint());
$object->withContentEncodings($base->getSupportedContentEncodings());
foreach ($base->getKeys()->all() as $k => $v) {
$object->getKeys()->set($k, $v);
}
return $object;
}
}<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="users")
* @ORM\Entity
*/
class User //Usual interface here
{
//Usual user stuff here
/**
* @ORM\OneToMany(targetEntity="Subscription", mappedBy="user")
*/
private Collection $subscriptions;
public function __construct()
{
$this->notifications = new ArrayCollection();
}
/**
* @return Notification[]
*/
public function getSubscriptions(): array
{
return $this->notifications->toArray();
}
public function addSubscription(Subscription $subscription): self
{
$subscription->setUser($this);
$this->subscriptions->add($subscription);
return $this;
}
public function removeSubscription(Subscription $subscription): self
{
$child->setUser(null);
$this->subscriptions->removeElement($subscription);
return $this;
}
}$subscriptions = $user->getSubscriptions();
foreach ($subscriptions as $subscription) {
$report = $this->webPush->send($notification, $subscription);
if ($report->subscriptionExpired()) {
//...Remove this subscription
}
}<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use WebPush\Subscription as WebPushSubscription;
/**
* @ORM\Table(name="subscriptions")
* @ORM\Entity
*/
class Subscription
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private ?int $id = null;
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="subscriptions", cascade={"persist"})
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable="true")
*/
private ?User $user;
/**
* @ORM\Column(type="webpush_subscription")
*/
private WebPushSubscription $subscription;
public function getId(): ?int
{
return $this->id;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
public function getSubscription(): WebPushSubscription
{
return $this->subscription;
}
}<?php
declare(strict_types=1);
namespace App\Entity;
use function array_key_exists;
use Assert\Assertion;
use DateTimeInterface;
use Safe\DateTimeImmutable;
use function Safe\json_decode;
class Subscription implements SubscriptionInterface
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private ?int $id = null;
/**
* @ORM\Column(type="integer")
*/
private string $endpoint;
/**
* @ORM\Column(type="array")
*
* @var string[]
*/
private array $keys = [];
/**
* @ORM\Column(type="array")
*
* @var string[]
*/
private array $supportedContentEncodings = ['aesgcm'];
/**
* @ORM\Column(type="integer", nullable=true)
*/
private ?int $expirationTime = null;
public function __construct(string $endpoint)
{
$this->endpoint = $endpoint;
}
/**
* @param string[] $contentEncodings
*/
public function setContentEncodings(array $contentEncodings): self
{
$this->supportedContentEncodings = $contentEncodings;
return $this;
}
public function getKeys(): array
{
return $this->keys;
}
public function hasKey(string $key): bool
{
return isset($this->keys[$key]);
}
/**
* @return array<string, string>
*/
public function getKey(string $key): string
{
Assertion::keyExists($this->keys, $key, 'The key does not exist');
return $this->keys[$key];
}
public function setKeys(array $keys): self
{
$this->keys = $keys;
return $this;
}
public function getExpirationTime(): ?int
{
return $this->expirationTime;
}
public function setExpirationTime(?int $expirationTime): self
{
$this->expirationTime = $expirationTime;
return $this;
}
public function getEndpoint(): string
{
return $this->endpoint;
}
/**
* @return string[]
*/
public function getSupportedContentEncodings(): array
{
return $this->supportedContentEncodings;
}
/**
* @return array<string, string|string[]>
*/
public function jsonSerialize(): array
{
return [
'endpoint' => $this->endpoint,
'supportedContentEncodings' => $this->supportedContentEncodings,
'keys' => $this->keys,
];
}
}<?php
use WebPush\Notification;
$notification = Notification::create();<?php
use WebPush\Notification;
$notification = Notification::create()
->withPayload('Hello world')
;<?php
use WebPush\Notification;
$notification = Notification::create()
->withTTL(3600)
;<?php
use WebPush\Notification;
$notification = Notification::create()
->withTopic('user-account-updated')
;<?php
use WebPush\Notification;
$notification = Notification::create()
->veryLowUrgency()
->lowUrgency()
->normalUrgency()
->highUrgency()
;<?php
use WebPush\Notification;
$notification = Notification::create()
->async() // Prefer async response
->sync() // Prefer sync response (default)
;<?php
use WebPush\Action;
use WebPush\Message;
use WebPush\Notification;
$message = Message::create('This is the title', null, true)
->mute() // Silent
->unmute() // Not silent (default)
->auto() //Direction = auto (default)
->ltr() //Direction = left to right
->rtl() //Direction = right to left
->addAction(Action::create('alert', 'Click me!'))
->interactionRequired()
->noInteraction()
->renotify()
->doNotRenotify() // Default
->withBody('Hello World!')
->withIcon('https://…')
->withImage('https://…')
->withData(['foo' => 'BAR']) // Arbitrary data
->withBadge('badge1')
->withLang('fr-FR')
->withTimestamp(time())
->withTag('foo')
->vibrate(300, 100, 400)
->toString() // Converts the Message object into a string
;
$notification = Notification::create()
->withPayload($message)
;{
"title":"This is the title",
"options":{
"actions":[
{
"action":"alert",
"title":"Click me!"
}
],
"badge":"badge1",
"body":"Hello World!",
"data":{
"foo":"BAR"
},
"dir":"rtl",
"icon":"https://…",
"image":"https://…",
"l ang":"fr-FR",
"renotify":false,
"requireInteraction":false,
"silent":false,
"tag":"foo",
"timestamp":1629145424,
"vibrate":[
300,
100,
400
]
}
} const {title, options} = payload;
const notification = new Notification(title, options);<?php
use WebPush\Payload\AES128GCM;
use WebPush\Payload\AESGCM;
use WebPush\Payload\PayloadExtension;
$payloadExtension = PayloadExtension::create()
->addContentEncoding(AESGCM::create()->maxPadding())
->addContentEncoding(AES128GCM::create()->maxPadding())
;<?php
use WebPush\Payload\AES128GCM;
use WebPush\Payload\AESGCM;
use WebPush\Payload\PayloadExtension;
$aesgcm = new AESGCM();
$aesgcm->maxPadding();
$aes128gcm = new AES128GCM();
$aes128gcm->maxPadding();
$payloadExtension = new PayloadExtension();
$payloadExtension->addContentEncoding($aesgcm);
$payloadExtension->addContentEncoding($aes128gcm);composer require nyholm/psr7 symfony/http-client spomky-labs/web-push-bundleuse Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Component\HttpClient\Psr18Client;
use WebPush\WebPush;
$client = new Psr18Client();
$requestFactory = new Psr17Factory();
$service = new WebPush($client, $requestFactory, $extensionManager);<?php
use WebPush\Subscription;
use WebPush\Notification;
$subscription = Subscription::createFromString('{"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/AAAAAAAA[…]AAAAAAAAA","keys":{"auth":"XXXXXXXXXXXXXX","p256dh":"YYYYYYYY[…]YYYYYYYYYYYYY"}}');
$notification = Notification::create()
->withPayload('Hello world')
;
$statusReport = $service->send($notification, $subscription);<?php
use WebPush\Subscription;
use WebPush\Notification;
use WebPush\WebPushService;
/** @var Notification $notification */
/** @var Subscription $subscription */
/** @var WebPushService $webPushService */
$statusReport = $webPushService->send($notification, $subscription);
if(!$statusReport->isSuccess()) {
//Something went wrong
} else {
$statusReport->getLocation();
$statusReport->getLinks();
}<?php
if($statusReport->hasExpired()) {
$this->subscriptionRepository->remove($subscription);
}The MIT License (MIT)
{
"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/AAAAAAAA[…]AAAAAAAAA",
"keys":{
"auth":"XXXXXXXXXXXXXX",
"p256dh":"YYYYYYYY[…]YYYYYYYYYYYYY"
}
}<?php
use WebPush\Subscription;
$subscription = Subscription::createFromString('{"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/AAAAAAAA[…]AAAAAAAAA","keys":{"auth":"XXXXXXXXXXXXXX","p256dh":"YYYYYYYY[…]YYYYYYYYYYYYY"}}');// Retreive the supported content encodings
const supportedContentEncodings = PushManager.supportedContentEncodings || ['aesgcm'];
// Assign the encodings to the subscription object
const jsonSubscription = Object.assign(
subscription.toJSON(),
{ supportedContentEncodings }
);
// Send the subscription object to the application server
fetch('/subscription/add', {
method: 'POST',
body: JSON.stringify(jsonSubscription),
});{
"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/AAAAAAAA[…]AAAAAAAAA",
"keys":{
"auth":"XXXXXXXXXXXXXX",
"p256dh":"YYYYYYYY[…]YYYYYYYYYYYYY",
"supportedContentEncodings":["aes128gcm","aesgcm"]
}
}use WebPush\ExtensionManager;
use WebPush\PreferAsyncExtension;
use WebPush\TopicExtension;
use WebPush\TTLExtension;
use WebPush\UrgencyExtension;
$extensionManager = ExtensionManager::create()
->add(TTLExtension::create())
->add(UrgencyExtension::create())
->add(TopicExtension::create())
->add(PreferAsyncExtension::create())
;$payloadExtension = PayloadExtension::create()
->addContentEncoding(AESGCM::create())
->addContentEncoding(AES128GCM::create())
;
$extensionManager = ExtensionManager::create()
->add($payloadExtension)
;use WebPush\VAPID\WebTokenProvider;
use WebPush\VAPID\LcobucciProvider;
// Web-Token
$jwsProvider = WebTokenProvider::create(
'BB4W1qfBi7MF_Lnrc6i2oL-glAuKF4kevy9T0k2vyKV4qvuBrN3T6o9-7-NR3mKHwzDXzD3fe7XvIqIU1iADpGQ', // Public key
'C40jLFSa5UWxstkFvdwzT3eHONE2FIJSEsVIncSCAqU' // Private key
);
// lcobucci/jwt
$jwsProvider = LcobucciProvider::create(
'BB4W1qfBi7MF_Lnrc6i2oL-glAuKF4kevy9T0k2vyKV4qvuBrN3T6o9-7-NR3mKHwzDXzD3fe7XvIqIU1iADpGQ',
'C40jLFSa5UWxstkFvdwzT3eHONE2FIJSEsVIncSCAqU'
);
$extensionManager = ExtensionManager::create()
->add(VAPIDExtension::create('http://my-service.com', $jwsProvider)
);webpush:
vapid:
enabled: true # Enable the feature
subject: 'https://my-service.com:8000' # An URL or an email address
web_token:
enabled: true # We use web-token in this example
public_key: 'BB4W1qfBi7MF_Lnrc6i2oL-glAuKF4kevy9T0k2vyKV4qvuBrN3T6o9-7-NR3mKHwzDXzD3fe7XvIqIU1iADpGQ'
private_key: 'C40jLFSa5UWxstkFvdwzT3eHONE2FIJSEsVIncSCAqU'webpush:
vapid:
enabled: true
subject: 'https://my-service.com:8000'
lcobucci:
enabled: true # We use web-token in this example
public_key: 'BB4W1qfBi7MF_Lnrc6i2oL-glAuKF4kevy9T0k2vyKV4qvuBrN3T6o9-7-NR3mKHwzDXzD3fe7XvIqIU1iADpGQ'
private_key: 'C40jLFSa5UWxstkFvdwzT3eHONE2FIJSEsVIncSCAqU'webpush:
vapid:
enabled: true
token_lifetime: 'now +2 hours'webpush:
payload:
aesgcm:
padding: 'none' # "none", "recommended", "max" or an integer (0 to 4078)
aes128gcm:
padding: 'none' # "none", "recommended", "max" or an integer (0 to 3993)webpush:
payload:
aesgcm:
cache: Psr\Cache\CacheItemPoolInterface
cache_lifetime: '+1 hour' #Default: now +30min
aes128gcm:
cache: Psr\Cache\CacheItemPoolInterface
cache_lifetime: '+1 hour' #Default: now +30minwebpush:
logger: Psr\Log\LoggerInterface<?php
declare(strict_types=1);
namespace App\MessageHandler;
use App\Message\SubscriptionExpired;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use WebPush\Notification;
use WebPush\WebPush;
final class SendPushNotifications implements MessageHandlerInterface
{
private MessageBusInterface $messageBus;
private SubscriptionRepository $repository;
private WebPush $webPush;
public function __construct(MessageBusInterface $messageBus, SubscriptionRepository $repository, WebPush $webPush)
{
$this->messageBus = $messageBus;
$this->repository = $repository;
$this->webPush = $webPush;
}
public function __invoke(Notification $notification): void
{
// Fetch all subscriptions
$subscriptions = $this->repository->fetchAllSubscriptions();
foreach ($subscriptions as $subscription) {
//Sends the notification to the subscriber
$report = $this->webPush->send($notification, $subscription);
//If the subscription expired
if ($report->subscriptionExpired()) {
//We dispatch a new message and expect for
// the subscription to be deleted
$this->messageBus->dispatch(
new SubscriptionExpired($subscription)
);
}
}
}
}