# Example

This page provides a complete example of implementing Web Push notifications in a Symfony application.

## Complete Working Example

### Step 1: Configuration

First, configure the bundle with VAPID authentication:

{% code title="config/packages/webpush.yaml" %}

```yaml
webpush:
  vapid:
    enabled: true
    subject: 'mailto:admin@example.com'
    web_token:
      enabled: true
      public_key: '%env(WEBPUSH_PUBLIC_KEY)%'
      private_key: '%env(WEBPUSH_PRIVATE_KEY)%'
  payload:
    aes128gcm:
      padding: 'recommended'
    aesgcm:
      padding: 'recommended'
  logger: 'monolog.logger'
```

{% endcode %}

{% code title=".env" %}

```bash
# Generate keys with: openssl ecparam -genkey -name prime256v1 -out private_key.pem
# Extract public key: openssl ec -in private_key.pem -pubout -outform DER|tail -c 65|base64|tr -d '=' |tr '/+' '_-'
# Extract private key: openssl ec -in private_key.pem -outform DER|tail -c +8|head -c 32|base64|tr -d '=' |tr '/+' '_-'
WEBPUSH_PUBLIC_KEY=your-public-key-here
WEBPUSH_PRIVATE_KEY=your-private-key-here
```

{% endcode %}

### Step 2: Create the Subscription Entity

{% code title="src/Entity/PushSubscription.php" %}

```php
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\PushSubscriptionRepository;
use Doctrine\ORM\Mapping as ORM;
use WebPush\Subscription as WebPushSubscription;

#[ORM\Entity(repositoryClass: PushSubscriptionRepository::class)]
#[ORM\Table(name: 'push_subscriptions')]
class PushSubscription extends WebPushSubscription
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(nullable: false)]
    private User $user;

    #[ORM\Column]
    private \DateTimeImmutable $createdAt;

    public function __construct(string $endpoint, User $user)
    {
        parent::__construct($endpoint);
        $this->user = $user;
        $this->createdAt = new \DateTimeImmutable();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getUser(): User
    {
        return $this->user;
    }

    public function getCreatedAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }

    public static function createFromString(string $input): self
    {
        throw new \RuntimeException('Use createFromRequest instead');
    }
}
```

{% endcode %}

### Step 3: Create the Repository

{% code title="src/Repository/PushSubscriptionRepository.php" %}

```php
<?php

declare(strict_types=1);

namespace App\Repository;

use App\Entity\PushSubscription;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class PushSubscriptionRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, PushSubscription::class);
    }

    public function save(PushSubscription $subscription): void
    {
        $this->getEntityManager()->persist($subscription);
        $this->getEntityManager()->flush();
    }

    public function remove(PushSubscription $subscription): void
    {
        $this->getEntityManager()->remove($subscription);
        $this->getEntityManager()->flush();
    }

    /**
     * @return PushSubscription[]
     */
    public function findByUser(User $user): array
    {
        return $this->findBy(['user' => $user]);
    }

    public function findByEndpoint(string $endpoint): ?PushSubscription
    {
        return $this->findOneBy(['endpoint' => $endpoint]);
    }
}
```

{% endcode %}

### Step 4: Create the Subscription Controller

{% code title="src/Controller/PushSubscriptionController.php" %}

```php
<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\PushSubscription;
use App\Repository\PushSubscriptionRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use WebPush\Subscription;

#[Route('/api/push')]
class PushSubscriptionController extends AbstractController
{
    public function __construct(
        private readonly PushSubscriptionRepository $repository
    ) {
    }

    #[Route('/subscribe', name: 'push_subscribe', methods: ['POST'])]
    public function subscribe(Request $request): JsonResponse
    {
        $user = $this->getUser();
        if (!$user) {
            return new JsonResponse(['error' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED);
        }

        $data = json_decode($request->getContent(), true);
        if (!isset($data['endpoint'])) {
            return new JsonResponse(['error' => 'Invalid subscription'], Response::HTTP_BAD_REQUEST);
        }

        // Check if subscription already exists
        $existing = $this->repository->findByEndpoint($data['endpoint']);
        if ($existing) {
            return new JsonResponse(['message' => 'Already subscribed'], Response::HTTP_OK);
        }

        // Create subscription from browser data
        $baseSubscription = Subscription::createFromString(json_encode($data));

        $subscription = new PushSubscription($baseSubscription->getEndpoint(), $user);
        $subscription->withContentEncodings($baseSubscription->getSupportedContentEncodings());

        foreach ($baseSubscription->getKeys() as $key => $value) {
            $subscription->setKey($key, $value);
        }

        $this->repository->save($subscription);

        return new JsonResponse(['message' => 'Subscription saved'], Response::HTTP_CREATED);
    }

    #[Route('/unsubscribe', name: 'push_unsubscribe', methods: ['POST'])]
    public function unsubscribe(Request $request): JsonResponse
    {
        $user = $this->getUser();
        if (!$user) {
            return new JsonResponse(['error' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED);
        }

        $data = json_decode($request->getContent(), true);
        $subscription = $this->repository->findByEndpoint($data['endpoint'] ?? '');

        if (!$subscription || $subscription->getUser() !== $user) {
            return new JsonResponse(['error' => 'Subscription not found'], Response::HTTP_NOT_FOUND);
        }

        $this->repository->remove($subscription);

        return new JsonResponse(['message' => 'Subscription removed'], Response::HTTP_OK);
    }

    #[Route('/public-key', name: 'push_public_key', methods: ['GET'])]
    public function getPublicKey(): JsonResponse
    {
        return new JsonResponse([
            'publicKey' => $this->getParameter('env(WEBPUSH_PUBLIC_KEY)')
        ]);
    }
}
```

{% endcode %}

### Step 5: Create the Notification Service

{% code title="src/Service/PushNotificationService.php" %}

```php
<?php

declare(strict_types=1);

namespace App\Service;

use App\Entity\PushSubscription;
use App\Entity\User;
use App\Repository\PushSubscriptionRepository;
use Psr\Log\LoggerInterface;
use WebPush\Message;
use WebPush\Notification;
use WebPush\WebPushService;

final readonly class PushNotificationService
{
    public function __construct(
        private WebPushService $webPush,
        private PushSubscriptionRepository $repository,
        private LoggerInterface $logger
    ) {
    }

    public function sendToUser(User $user, string $title, string $body, array $data = []): void
    {
        $subscriptions = $this->repository->findByUser($user);

        if (empty($subscriptions)) {
            $this->logger->info('No subscriptions found for user', ['user_id' => $user->getId()]);
            return;
        }

        $message = Message::create($title)
            ->withBody($body)
            ->withData($data)
            ->withIcon('/icon-192.png')
            ->withBadge('/badge-72.png');

        $notification = Notification::create()
            ->withPayload($message->toString())
            ->withTTL(3600);

        foreach ($subscriptions as $subscription) {
            $this->sendNotification($notification, $subscription);
        }
    }

    private function sendNotification(Notification $notification, PushSubscription $subscription): void
    {
        try {
            $report = $this->webPush->send($notification, $subscription);

            if ($report->isSubscriptionExpired()) {
                $this->logger->info('Subscription expired, removing', [
                    'endpoint' => $subscription->getEndpoint()
                ]);
                $this->repository->remove($subscription);
            } elseif (!$report->isSuccess()) {
                $this->logger->error('Failed to send notification', [
                    'endpoint' => $subscription->getEndpoint()
                ]);
            }
        } catch (\Throwable $e) {
            $this->logger->error('Exception while sending notification', [
                'endpoint' => $subscription->getEndpoint(),
                'error' => $e->getMessage()
            ]);
        }
    }
}
```

{% endcode %}

### Step 6: Client-side JavaScript

{% code title="public/js/push-notifications.js" %}

```javascript
// Request notification permission
async function requestNotificationPermission() {
    const permission = await Notification.requestPermission();
    if (permission !== 'granted') {
        console.log('Notification permission denied');
        return false;
    }
    return true;
}

// Subscribe to push notifications
async function subscribeToPush() {
    if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
        console.error('Push notifications are not supported');
        return;
    }

    try {
        // Register service worker
        const registration = await navigator.serviceWorker.register('/service-worker.js');
        await navigator.serviceWorker.ready;

        // Get public key from server
        const response = await fetch('/api/push/public-key');
        const { publicKey } = await response.json();

        // Subscribe
        const subscription = await registration.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey: urlBase64ToUint8Array(publicKey)
        });

        // Get supported content encodings
        const supportedContentEncodings = PushManager.supportedContentEncodings || ['aesgcm'];

        // Send subscription to server
        const subscriptionData = {
            endpoint: subscription.endpoint,
            keys: {
                p256dh: arrayBufferToBase64(subscription.getKey('p256dh')),
                auth: arrayBufferToBase64(subscription.getKey('auth'))
            },
            supportedContentEncodings: supportedContentEncodings
        };

        await fetch('/api/push/subscribe', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(subscriptionData)
        });

        console.log('Successfully subscribed to push notifications');
    } catch (error) {
        console.error('Failed to subscribe:', error);
    }
}

// Unsubscribe from push notifications
async function unsubscribeFromPush() {
    try {
        const registration = await navigator.serviceWorker.ready;
        const subscription = await registration.pushManager.getSubscription();

        if (!subscription) {
            return;
        }

        await fetch('/api/push/unsubscribe', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ endpoint: subscription.endpoint })
        });

        await subscription.unsubscribe();
        console.log('Successfully unsubscribed from push notifications');
    } catch (error) {
        console.error('Failed to unsubscribe:', error);
    }
}

// Helper functions
function urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

function arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    for (let i = 0; i < bytes.byteLength; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return window.btoa(binary)
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
}
```

{% endcode %}

{% code title="public/service-worker.js" %}

```javascript
self.addEventListener('push', function(event) {
    if (!event.data) {
        return;
    }

    const data = event.data.json();
    const { title, options } = data;

    event.waitUntil(
        self.registration.showNotification(title, options)
    );
});

self.addEventListener('notificationclick', function(event) {
    event.notification.close();

    event.waitUntil(
        clients.openWindow('/')
    );
});
```

{% endcode %}

### Step 7: Usage Example

{% code title="src/Controller/NotificationTestController.php" %}

```php
<?php

declare(strict_types=1);

namespace App\Controller;

use App\Service\PushNotificationService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class NotificationTestController extends AbstractController
{
    #[Route('/test/notification', name: 'test_notification')]
    public function sendTestNotification(PushNotificationService $pushService): Response
    {
        $user = $this->getUser();

        $pushService->sendToUser(
            $user,
            'Test Notification',
            'This is a test notification from your Symfony app!',
            ['url' => '/dashboard']
        );

        return new Response('Notification sent!');
    }
}
```

{% endcode %}

## Demo Application

For a complete working demo application, please visit: <https://github.com/Spomky-Labs/web-push-demo>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://web-push.spomky-labs.com/the-symfony-bundle/example.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
