Web Push на 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)
- Сгенерируйте VAPID-ключи и сохраните их в секрете.
- Реализуйте API: GET /push/public-key, POST /push/subscribe, DELETE /push/subscribe/:id.
- На клиенте запросите публичный ключ и создайте PushSubscription.
- На сервере сохраните подписку и отправьте тестовое уведомление.
- Наблюдайте за ответами провайдеров и удаляйте просроченные подписки.
Модель принятия решений (когда использовать серверную реализацию)
- Если важно хранить ключи и данные на своих серверах — используйте собственный сервер.
- Если важна скорость внедрения и аналитика из коробки — рассмотрите 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.
Похожие материалы
OpenMediaVault на Raspberry Pi — быстрый домашний сервер
Как научить ребёнка программировать во время изоляции
Управление плагинами Docker Engine
Как исправить «PUBG Lite недоступен в вашем регионе»
Вернуть классическое контекстное меню Windows 11