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

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
Автор
Редакция

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

RDP: полный гид по настройке и безопасности
Инфраструктура

RDP: полный гид по настройке и безопасности

Android как клавиатура и трекпад для Windows
Гайды

Android как клавиатура и трекпад для Windows

Советы и приёмы для работы с PDF
Документы

Советы и приёмы для работы с PDF

Calibration в Lightroom Classic: как и когда использовать
Фото

Calibration в Lightroom Classic: как и когда использовать

Отключить Siri Suggestions на iPhone
iOS

Отключить Siri Suggestions на iPhone

Рисование таблиц в Microsoft Word — руководство
Office

Рисование таблиц в Microsoft Word — руководство