Неупорядоченные webhooks в PHP — это проблема согласованности, а не только связности. Провайдер может повторно отправить доставку, поскольку не получил корректный ответ, очередь может задержать сообщение, либо два события одной и той же сущности могут пройти разными маршрутами. Если приложение предполагает, что каждое событие приходит один раз и последовательно, старое подтверждение может перезаписать последующую отмену, а повтор может дважды выполнить необратимую операцию.
Исходное правило простое: webhook — это уведомление о том, что что-то могло измениться в другой системе. Сам по себе он не является надёжной инструкцией для изменения локального состояния без проверок. Проектирование должно сохранять полученные данные, определять, какие события допустимы, и применять изменения идемпотентно и упорядоченно в соответствии с правилами домена.
Разделяйте приём, валидацию и применение к домену

HTTP endpoint должен делать немногое и предсказуемо. Его задача — принять запрос, проверить его, сохранить неизменяемую запись и ответить в срок, ожидаемый отправителем. Работа, изменяющая заказы, подписки, инвентарь или любую другую бизнес-сущность, должна выполняться позднее, как правило, через асинхронный процесс.
Разделение этапов не позволяет временной недоступности внутреннего API превратить корректную доставку в неоднозначную повторную попытку. Оно также позволяет возобновить обработку, не запрашивая у провайдера повторную отправку старых событий.
- Приём: захватить заголовки, неизменённое тело, момент получения и идентифицированный источник.
- Валидация входных данных: проверить подпись, формат, размер, тип содержимого и минимально необходимые поля.
- Сохранение: сохранить событие и его начальное состояние в короткой транзакции.
- Постановка в очередь: отметить наличие ожидающей работы, не завися от её обработки в рамках HTTP-ответа.
- Применение: worker интерпретирует событие, получает необходимое состояние и выполняет контролируемый бизнес-переход.
Важно различать доставку и событие. Одна и та же доставка может повторяться, а некоторые провайдеры присваивают разный идентификатор каждой попытке доставки. При наличии стабильного идентификатора события он обычно является лучшей основой для дедупликации. Если его нет, потребуется определить ключ, включающий источник, внешнюю сущность, тип и версию либо временную метку с известной семантикой.
Что записывать для аудита и повторной обработки
Таблица событий не должна хранить только интерпретированный JSON. Сохраняйте исходное тело, поскольку его нормализация перед записью может удалить информацию, необходимую для проверки подписи, расследования инцидента или адаптации последующего parser.
Как минимум запись должна содержать:
- Источник или провайдер и окружение интеграции.
- Внешний идентификатор события и, при наличии, идентификатор доставки.
- Тип события, идентификатор внешней сущности и версию, последовательность или дату вступления в силу.
- Значимые заголовки и исходную полезную нагрузку, защищённую от изменений.
- Локальный момент получения и отдельно временную метку, заявленную отправителем.
- Криптографический отпечаток полезной нагрузки для диагностики и вспомогательной дедупликации.
- Статус обработки: получено, валидировано, ожидает обработки, применено, проигнорировано, с ошибкой или требует проверки.
- Число попыток, краткое описание ошибки, момент последней попытки и ссылку на затронутую локальную сущность.
Уникальное ограничение для (origen, external_event_id) устраняет повторения, когда провайдер предоставляет стабильный ID. Сначала выполните вставку и рассматривайте конфликт как уже известную доставку, а не как бизнес-ошибку. Ответ всё равно может быть успешным, чтобы остановить повторные попытки.
Однако дедупликации сообщения недостаточно для гарантии идемпотентности. Например, два разных события могут выражать одно и то же подтверждение, и оба могут попытаться создать бухгалтерскую проводку. Бизнес-операция должна иметь собственную защиту: ключ идемпотентности, уникальное ограничение на эффект или переход, проверяющий, существует ли результат уже.
Проверяйте подлинность и ограничивайте поверхность входа
Не принимайте webhook только потому, что он поступил с ожидаемого IP-адреса или содержит поле, которое выглядит как секрет. Когда провайдер это допускает, проверяйте подпись, рассчитанную по необработанному телу и временной метке. Сравнение должно выполняться за постоянное время, а временное окно должно ограничивать воспроизведения с учётом разумного расхождения часов.
До сохранения установите операционные ограничения: максимальный размер тела, время чтения, допустимые форматы и минимальную схему. Корректный JSON не обязательно является корректным событием. Отклоняйте неизвестные типы, если нет явной политики архивировать их без применения эффектов.
Секреты подписи требуют ротации. Во время изменения может потребоваться принимать прежний и новый ключи в течение ограниченного периода, записывая, какой из них подтвердил доставку. Не включайте полные тела, токены или ненужные персональные данные в логи приложения. Журнал аудита должен иметь средства контроля доступа и политику хранения, соответствующую чувствительности данных.
Определяйте логический порядок, а не доверяйте сетевому
Время получения не определяет, что произошло раньше. Дата в payload также не всегда достаточна: она может быть приблизительной, относиться к созданию события, а не к переходу, либо зависеть от несинхронизированных часов. Наилучший сигнал — монотонная версия или номер последовательности для каждой сущности, предоставляемый исходной системой.
При наличии версии сохраняйте последнюю применённую версию в локальной сущности. Worker может применить событие только в том случае, если его версия больше сохранённой; равная версия означает повтор, а меньшая — запоздалое событие. При пробелах в последовательности не придумывайте промежуточное состояние: отметьте сущность для сверки или запросите исходный API, если этот API является источником истины.
Если нет ни последовательности, ни версии, правила должны определяться доменом. Явная машина состояний безопаснее прямого присваивания полученного текста. Например, отменённая сущность может не допускать возврата к подтверждённой, кроме как через документированный и разрешённый переход. Модель должна определять действия для каждой комбинации текущего состояния и входящего события.
if ($eventVersion <= $entity->lastExternalVersion) {
markIgnored($event, 'version_no_mas_reciente');
return;
}
applyAllowedTransition($entity, $event);
$entity->lastExternalVersion = $eventVersion;Код иллюстрирует критерий, но не заменяет транзакцию или правила переходов. Для событий без версии сравнение дат приемлемо только в том случае, если контракт отправителя гарантирует их семантику и точность.
Обрабатывайте запоздалые события с учётом цены ошибки
Не все задержанные события заслуживают одинаковой реакции. Выбор между игнорированием, регистрацией, пересчётом или компенсацией зависит от того, может ли событие изменить реальное обязательство, и от того, что является источником истины.
- Игнорировать: подходит для старой версии, эффект которой уже включён в проверяемое последующее состояние.
- Зарегистрировать и оповестить: полезно, если последовательность противоречива или для решения без вмешательства недостаточно информации.
- Пересчитать: запросить текущее состояние во внешней системе и обновить локальное зеркало, когда внешняя система имеет приоритет.
- Компенсировать: создать отслеживаемое корректирующее действие, когда предыдущий эффект уже привёл к последствиям и его нельзя безопасно удалить.
Рассмотрим гипотетический случай внешней операции. Приходит подтверждение с версией 12, затем отмена с версией 13 и позднее повторно отправляется подтверждение 12. При контроле версий повтор не возобновляет операцию. Если отмена приходит первой и системе известно, что отсутствует версия 12, она может применить отмену, если это допускает машина состояний, или запросить сверку до создания чувствительного эффекта.
Внутренняя конкуренция, очереди и блокировки по сущности
Асинхронная обработка улучшает отзывчивость, но создаёт внутренние гонки: два worker могут прочитать одно и то же состояние до того, как один из них выполнит запись. Дедупликация события не предотвращает это состояние.
Для чувствительных сущностей сериализуйте обработку по ключу внешней или локальной сущности. Это можно реализовать через разделы очереди на основе этого ключа, распределённую блокировку с тщательно спроектированным сроком истечения или блокировку строки внутри короткой транзакции. Другой вариант — оптимистический контроль: обновлять, только если сохранённая версия всё ещё соответствует ожидаемой, и повторять попытку при обнаружении конфликта.
Избегайте удержания открытой транзакции во время вызовов удалённых сервисов. Сначала зарезервируйте или последовательно прочитайте состояние; затем выполните вызов с идемпотентным ключом, когда это возможно; наконец, зафиксируйте результат. Если процесс завершится сбоем между шагами, повторная попытка должна уметь различать ожидающую операцию и уже завершённую.
Эксплуатация, наблюдаемость и тестирование до публикации

Операционная панель должна показывать, сколько событий остаются в ожидании, неоднократно завершаются ошибкой, игнорируются из-за устаревания, отклоняются из-за подписи и имеют пробелы в последовательности. Также измеряйте возраст очереди и время от получения до применения. Эти сигналы позволяют обнаружить деградацию интеграции до того, как рассинхронизация станет бизнес-проблемой.
Сохраняйте механизмы повторной обработки, работающие от исходного события и явной версии parser или handler. Повторная обработка не означает слепого выполнения: ограничивайте охват, записывайте, кто её запросил, и сохраняйте те же гарантии идемпотентности.
Контрольный список
- Отправить одно и то же событие несколько раз, в том числе конкурентно.
- Доставить отмену до связанного с ней подтверждения.
- Задержать старое событие до момента после события с более высокой версией.
- Внести пробелы, неизвестные типы, усечённые полезные нагрузки и недействительные подписи.
- Смоделировать сбой worker после создания внешнего эффекта и до отметки события как применённого.
- Проверить, что два worker для одной сущности не создают невозможный переход.
- Убедиться, что повторная обработка сохраняет аудит и не дублирует эффекты.
Надёжная интеграция не пытается заставить сеть доставлять события по порядку. Она проектирует надёжную границу: сохраняет каждый проверяемый вход, применяет идемпотентные бизнес-правила, использует логический порядок при его наличии и выполняет сверку, когда не может достоверно определить состояние.



