Гид по технологиям

Web Push на PHP — реализация серверной части

8 min read Серверная разработка Обновлено 01 Dec 2025
Web Push на PHP: серверная реализация
Web Push на PHP: серверная реализация

Логотип PHP

Web Push требует серверной части для подписи и доставки сообщений; на PHP это удобно реализовать с помощью библиотеки minishlink/web-push. В статье пошагово показаны генерация VAPID-ключей, хранение подписок, отправка одиночных и пакетных уведомлений, обработка ошибок и рекомендации по безопасности и хранению данных.

Быстрые ссылки

  • Пререквизиты
  • Настройка проекта
  • Генерация VAPID-ключей
  • Регистрация подписок
  • Подготовка подписок
  • Отправка уведомления
  • Пакетная отправка
  • Опции уведомлений
  • Отладка и ошибки
  • Безопасность и конфиденциальность
  • Критерии приёмки
  • Чек-листы ролей
  • Краткий анонс

Пререквизиты

Коротко:

  • Уверенные знания в создании HTTP API на PHP.
  • В проекте должен быть endpoint, который принимает JSON с PushSubscription из браузера.
  • На клиенте должен быть service worker, который обрабатывает событие push и показывает уведомление. Эта статья не покрывает клиентскую часть подробно.

Определение: PushSubscription — объект, который браузер генерирует при подписке и отправляет серверу. Он содержит endpoint и ключи, необходимые для шифрования.

Важно: сервер отвечает только за формирование и отправку полезной нагрузки (payload) и за подпись сообщений VAPID. Доставка осуществляется платформами браузеров (FCM для Chrome, Mozilla Push Service для Firefox и др.).

Настройка проекта

Рекомендуемая библиотека: minishlink/web-push (доступна через Packagist). Она абстрагирует различия между платформами доставки.

Установите пакет через Composer:

composer require minishlink/web-push

Требования: PHP 7.2+ и расширения gmp, mbstring, curl, openssl.

Дальше приведены примеры и советы по конфигурации и использованию библиотеки.

Генерация VAPID-ключей

VAPID — ключи для идентификации и аутентификации сервера перед платформами доставки уведомлений. Публичный ключ передаётся клиенту (через API) и используется браузером при создании подписки. Приватный ключ храните в секрете.

Пример генерации ключей с помощью библиотеки (правильные неймспейсы):

Рекомендации по хранению ключей:

  • Приватный ключ в секретном хранилище (Vault, AWS Secrets Manager) или хотя бы в файле с правами доступа 600.
  • Публичный ключ можно кэшировать в CDN или отдавать через API-ендпоинт.
  • Обновляйте ключи только при явной необходимости; ротация требует обновления подписок на клиентах.

Регистрация подписок

Клиент (браузер) формирует PushSubscription и отправляет JSON на ваш backend. На сервере сохраняйте структуру минимум со следующими полями:

  • user_id — идентификатор пользователя в вашей системе
  • endpoint — URL конечной точки (например, FCM)
  • keys.p256dh — публичный ключ клиента
  • keys.auth — авторизационный токен
  • content_encoding — например, aesgcm или aes128gcm
  • created_at, last_seen — метаданные для управления жизненным циклом

Пример простой схемы таблицы (SQL-строка для примера):

CREATE TABLE push_subscriptions (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  endpoint TEXT NOT NULL,
  p256dh VARCHAR(255),
  auth_token VARCHAR(255),
  content_encoding VARCHAR(50),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  last_seen DATETIME DEFAULT NULL,
  UNIQUE KEY unique_endpoint (endpoint(255))
);

На сервере реализуйте CRUD API для подписок:

  • POST /push/subscribe — сохранить новую подписку
  • PUT /push/subscribe/:id — обновить подписку
  • DELETE /push/subscribe/:id — удалить подписку
  • GET /push/public-key — вернуть публичный VAPID-ключ

Совет: при создании подписки валидируйте структуру JSON и сохраняйте связанные данные с идентификатором пользователя.

Подготовка подписок в серверном коде

Создайте экземпляр WebPush, указав VAPID-настройки. Пример корректного использования библиотеки:

 'mailto:admin@example.com', // или URL вашего сайта
  'publicKey' => 'VAPID_Public_Key_Here',
  'privateKey' => 'VAPID_Private_Key_Here',
];

$webPush = new WebPush(['VAPID' => $vapid]);

// Получаем подписку из БД и создаём объект Subscription
$subscription = Subscription::create([
  'endpoint' => 'https://fcm.googleapis.com/fcm/send/....',
  'contentEncoding' => 'aesgcm',
  'keys' => [
    'p256dh' => '',
    'auth' => ''
  ]
]);

Пояснения:

  • subject — идентифицирует ваш сервис (URL или mailto:)
  • publicKey/privateKey — ключи VAPID в Base64; если вы сгенерировали ключи библиотекой, перекодирование обычно не требуется.

Отправка одиночного уведомления

Простейший сценарий — отправить одно уведомление на одну подписку:

 'Demo notification',
  'type' => 'demo',
]);

$result = $webPush->sendOneNotification($subscription, $payload);

if ($result->isSuccess()) {
  // Уведомление доставлено или принято к доставке
} else {
  error_log('Push error: ' . $result->getReason());
  error_log('HTTP response: ' . $result->getResponse());

  if ($result->isSubscriptionExpired()) {
    // Удалите запись подписки из БД — конечная точка больше не действует
  }
}

Примечания по полезной нагрузке:

  • payload — произвольный JSON или строка. Service worker получит его и самостоятельно решит, как показать уведомление.
  • Размер полезной нагрузки ограничен платформой доставки (обычно несколько килобайт). Для больших объёмов используйте короткие сообщения и подтягивайте данные по API при клике.

Пакетная отправка

Для массовой отправки используйте очередь и flush для оптимальной доставки:

queueNotification($subscription, json_encode(['msg' => 'first']));
$webPush->queueNotification($subscription, json_encode(['msg' => 'second']));

foreach ($webPush->flush() as $i => $result) {
  echo "Notification $i was " . ($result->isSuccess() ? "sent" : "not sent") . "\n";
}

// Ограничение пакета
$webPush->flush(100); // отправить 100 сообщений в этом вызове

Советы для масштабирования:

  • Бейчинг уменьшает накладные расходы по установлению TLS-сессий и API-вызовов.
  • Старайтесь группировать сообщения по провайдерам (FCM vs Mozilla) если это возможно.
  • Ограничьте concurrency на уровне процесса или очереди задач, чтобы не превысить лимиты поставщиков.

Опции уведомлений

Методы sendOneNotification и queueNotification принимают третий аргумент — массив опций:

  • TTL — время жизни в секундах. Платформы по умолчанию могут хранить уведомления до 4 недель. Установите адекватный TTL, чтобы не доставлять устаревший контент.
  • urgency — normal, low или very-low. Может влиять на приоритет доставки и энергопотребление устройства.
  • batchSize — влияет на поведение flush().

Пример установки дефолтных значений:

$webPush = new WebPush(['VAPID' => $vapid], ['TTL' => 3600]);

Отладка и распространённые причины отказов

Когда отправка неуспешна, проверьте:

  • Ошибки сетевого уровня (timeout, DNS). Провайдеры доставки возвращают HTTP-статусы и тела ответов, доступные через getResponse().
  • Истёкшие подписки. result->isSubscriptionExpired() поможет определить это.
  • Неверные ключи (p256dh/auth) или несовместимое contentEncoding.
  • Неверные VAPID-ключи или subject.

Поведение при ошибках:

  • Удаляйте или помечайте неактивные подписки при получении 410 / истечении.
  • На кратковременные ошибки (5xx) используйте ретрай с экспоненциальной задержкой.
  • Логируйте тело ответа провайдера для диагностики.

Когда этот подход не подходит

Контрпример: если вам нужна единая облачная система пушей с GUI, аналитикой и A/B-тестами, лучше использовать коммерческие сервисы (OneSignal, Firebase Cloud Messaging Management, Pushy). Серверная реализация хороша для контроля данных, гибкости и приватности.

Альтернативные подходы

  • Использовать FCM HTTP v1 напрямую (если все клиенты — Android/Chrome): нужен отдельный код для подписи и токенов.
  • Использовать сторонние провайдеры уведомлений (OneSignal, Airship): избавляют от части серверной логики, но вводят зависимость и возможные расходы.
  • Серверless-функции (AWS Lambda) для отправки по расписанию или по событиям — уменьшает поддержuвание серверов.

Безопасность и конфиденциальность

Рекомендации:

  • Приватный VAPID-ключ храните в секретном менеджере. Ограничьте доступ по ролям.
  • Не логируйте целые payloads с личными данными.
  • Минимизируйте хранимые данные подписки: храните только то, что нужно для доставки.
  • Реализуйте согласие пользователя (opt-in) и механизм удаления подписки по запросу.

GDPR и хранение данных:

  • PushSubscription может рассматриваться как персональные данные, если связана с ID пользователя. Обеспечьте возможность удаления данных по запросу.
  • Сроки хранения подписок описывайте в политике конфиденциальности.

Чек-листы ролей

Разработчик:

  • Реализовать эндпоинты CRUD подписок
  • Вернуть публичный VAPID-ключ через API
  • Использовать подписанную полезную нагрузку и проверять формат JSON на входе

DevOps:

  • Хранить приватный ключ в секретном хранилище
  • Настроить мониторинг ошибок и очередей
  • Ограничить concurrency и настроить ретраи

Продукт/PM:

  • Обозначить TTL для разных типов уведомлений
  • Решить политику хранения подписок и срока действия
  • Описать UX для отказа от уведомлений

Критерии приёмки

  • Публичный VAPID-ключ доступен через API и корректно применяется в клиенте
  • Сервер успешно отправляет уведомления на тестовую подписку в Chrome и Firefox
  • Сценарий удаления подписки работает, и при удалении сообщения больше не отправляются на этот endpoint
  • Логи показывают успешную доставку и понятные ошибки при неуспехе

Мини-плейбук внедрения (SOP)

  1. Сгенерируйте VAPID-ключи и сохраните их в секрете.
  2. Реализуйте API: GET /push/public-key, POST /push/subscribe, DELETE /push/subscribe/:id.
  3. На клиенте запросите публичный ключ и создайте PushSubscription.
  4. На сервере сохраните подписку и отправьте тестовое уведомление.
  5. Наблюдайте за ответами провайдеров и удаляйте просроченные подписки.

Модель принятия решений (когда использовать серверную реализацию)

  • Если важно хранить ключи и данные на своих серверах — используйте собственный сервер.
  • Если важна скорость внедрения и аналитика из коробки — рассмотрите SaaS-провайдеров.

Простые шаблоны и сниппеты

Пример payload для уведомления:

{
  "title": "Новая заметка",
  "body": "У вас появилась новая заметка в приложении",
  "icon": "/images/icon-192.png",
  "url": "https://example.com/notes/123"
}

Пример обработчика service worker (клиент, для полноты картины):

self.addEventListener('push', function(event) {
  let data = event.data ? event.data.json() : {};
  const title = data.title || 'Уведомление';
  const options = {
    body: data.body,
    icon: data.icon
  };
  event.waitUntil(self.registration.showNotification(title, options));
});

Примеры ошибок и способы решения

  • 401 / 403 при отправке — проверьте VAPID-ключи и subject.
  • 410 Gone — удалите подписку из БД.
  • 5xx от провайдера — логируйте и ретрайте с экспоненциальной задержкой.

1‑строчный глоссарий

  • VAPID — механизм подписи запросов Web Push для идентификации отправителя.
  • PushSubscription — объект подписки браузера: endpoint + ключи для шифрования.
  • TTL — время жизни уведомления на стороне платформы доставки.

Диаграмма принятия решения для обработки результата отправки

flowchart TD
  A[Отправили уведомление] --> B{Результат}
  B -->|Успех| C[Лог: успешно]
  B -->|Ошибка 410| D[Удалить подписку]
  B -->|Ошибка 401/403| E[Проверить VAPID и ключи, алерт]
  B -->|Ошибка 5xx| F[Ретрай с backoff]
  F --> G{Повторы < N}
  G -->|Да| A
  G -->|Нет| H[Оповещение SRE]

Краткий анонс для команды (100–200 слов)

Мы внедрили серверную реализацию Web Push на PHP с использованием библиотеки minishlink/web-push. Сервер генерирует и хранит VAPID-ключи, принимает PushSubscription от клиента и отправляет одиночные и пакетные уведомления. Реализованы эндпоинты для получения публичного ключа, регистрации и удаления подписок. Добавлены механизмы обработки ошибок: удаление устаревших подписок, ретраи для временных ошибок и логирование ответов провайдеров. При внедрении уделено внимание безопасности: приватный VAPID-ключ хранится в секретном хранилище, а данные подписок минимизированы и доступны для удаления по запросу.

Краткое резюме

  • Web Push требует серверной подписи и отправки; minishlink/web-push упрощает это на PHP.
  • Храните приватный VAPID-ключ в секрете; публичный ключ выдавайте клиенту.
  • Обрабатывайте устаревшие подписки и ошибки доставки.
  • Для массовых отправок используйте бэчинг и ограничьте concurrency.

FAQ

Нужно ли использовать отдельный сервис для отправки уведомлений?

Нет, вы можете управлять отправкой полностью на своём сервере. Но если важна аналитика, сегментация пользователей и быстрый старт, можно рассмотреть SaaS-решения.

Как часто нужно обновлять VAPID-ключи?

Ротация ключей не требуется по расписанию — только при компрометации или изменении политики. Ротация усложняет поддержку, так как клиентам нужно пересоздать подписки.

Что хранить в базе данных о подписке?

Минимально: user_id, endpoint, p256dh, auth_token, content_encoding, created_at, last_seen.


Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

OpenMediaVault на Raspberry Pi — быстрый домашний сервер
Домашний сервер

OpenMediaVault на Raspberry Pi — быстрый домашний сервер

Как научить ребёнка программировать во время изоляции
Образование

Как научить ребёнка программировать во время изоляции

Управление плагинами Docker Engine
Docker

Управление плагинами Docker Engine

Как исправить «PUBG Lite недоступен в вашем регионе»
Игры

Как исправить «PUBG Lite недоступен в вашем регионе»

Вернуть классическое контекстное меню Windows 11
Windows

Вернуть классическое контекстное меню Windows 11

PAT для GitHub: создать и настроить
GIT

PAT для GitHub: создать и настроить