# Примеры интеграции

Рабочий код, а не псевдокод: пути в нём сверяются со спекой тестом
`examples/contract_test.go`, синтаксис PHP — линтером в CI. Пример, разошедшийся
с API, ломает сборку здесь, а не интеграцию у читателя.

| Файл | Что показывает |
|---|---|
| [php/HubClient.php](php/HubClient.php) | клиент на Guzzle: идемпотентность, различение отказа приёма и сбоя сети, `Retry-After` |
| [php/webhook_receiver.php](php/webhook_receiver.php) | приёмник вебхуков: проверка подписи по сырым байтам, дедупликация батча и события |
| [laravel/SmsHubChannel.php](laravel/SmsHubChannel.php) | канал уведомлений: ключ идемпотентности из `$notification->id` |
| [laravel/HubWebhookController.php](laravel/HubWebhookController.php) | контроллер приёмника: подпись, отбрасывание виденного, постановка в очередь |
| [laravel/ProcessHubBatch.php](laravel/ProcessHubBatch.php) | обработка батча в Horizon: дедупликация и защита от отставшего события |
| [postman/sms-hub.postman_collection.json](postman/sms-hub.postman_collection.json) | коллекция Postman: двенадцать запросов от аккаунта до песочницы |

## Что в этих примерах главное

Три вещи, на которых ломаются интеграции, и все три видны в коде:

**Ключ идемпотентности — на попытку отправки, а не на сообщение.** Он
генерируется там, где принимается решение отправить, и переживает перезапуск
процесса. Свежий ключ на каждом повторе очереди означает второе сообщение
живому человеку; в Laravel-канале поэтому берётся `$notification->id`, который
Laravel сохраняет вместе с джобой.

**Отказ приёма и сбой сети — разные вещи.** `4xx` повторять бессмысленно: тот же
запрос получит тот же ответ, и двадцать попыток только спрячут дефект. Не
дошедший до хаба запрос повторять нужно — с тем же ключом идемпотентности,
чтобы повтор был безопасен, даже если запрос всё-таки дошёл.

**Подпись считается по сырым байтам тела.** Пересобранный из разобранного
массива JSON даёт другие байты: порядок ключей и пробелы не обещаны. В Laravel
это `$request->getContent()`, а не `$request->all()`.

## Как запустить

Клиент и приёмник — обычные PHP-файлы без зависимостей, кроме Guzzle:

```sh
composer require guzzlehttp/guzzle
```

Коллекция Postman импортируется как есть; перед первым запросом заполните
`base_url` и `api_key` в переменных коллекции.

Полное описание API — [руководства для интеграторов](../docs/public/README.md).
