Универсальный, production-grade PHP-клиент для MAX Messenger Bot API. Framework-agnostic ядро (PSR-7/PSR-17/PSR-18), которое может быть использовано как основа для модулей интеграции в разные фреймворки (Laravel, Symfony и др.).
- PHP >= 8.4
- PSR-18 HTTP-клиент (например,
guzzlehttp/guzzle) - PSR-17 фабрики запросов/стримов/URI
composer require geekcodev/max-php-clientuse GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;
use GeekCo\MaxPhpClient\ApiClient;
use GeekCo\MaxPhpClient\Dto\Recipient;
use GeekCo\MaxPhpClient\Dto\NewMessageBody;
$psrFactory = new HttpFactory();
$client = ApiClient::create(
httpClient: new GuzzleClient(),
requestFactory: $psrFactory,
streamFactory: $psrFactory,
uriFactory: $psrFactory,
accessToken: getenv('MAX_API_TOKEN'),
);
$me = $client->getMe();
$message = $client->sendMessage(
new Recipient(chatId: 123456789),
new NewMessageBody(text: 'Привет из PHP!'),
);Токен передаётся в заголовке Authorization без префикса Bearer. Передача токена через query-параметры не
поддерживается.
| Переменная | Обязательная | Назначение |
|---|---|---|
MAX_API_TOKEN |
да | Access token бота; передаётся в заголовке Authorization без префикса Bearer |
MAX_WEBHOOK_SECRET |
нет | Секрет вебхука (заголовок X-Max-Bot-Api-Secret); 5–256 символов [a-zA-Z0-9_-] |
Пример .env:
MAX_API_TOKEN=your-bot-access-token
MAX_WEBHOOK_SECRET=your-webhook-secret- Все эндпоинты API (chats, messages, members/admins, subscriptions, updates, uploads, answers, me)
- Типизированные DTO для всех объектов спеки
- Ретраи с экспоненциальным бэкоффом (в т.ч.
attachment.not.ready,429,503, сетевые ошибки) - Локальный rate limiter 2 req/s на диалог/чат/канал
- Загрузка медиа в несколько шагов (запрос upload → загрузка файла → отправка сообщения)
- Обработка вебхуков с верификацией секрета
X-Max-Bot-Api-Secret - Long polling runner
- Верификация контакта из кнопки
request_contact - Верификация стартовых данных мини-приложения (
WebAppDataValidator) - Типизированные исключения
Полностью рабочие примеры — в каталоге examples/. Требуются переменные из .env (MAX_API_TOKEN, для вебхука —
MAX_WEBHOOK_SECRET).
Запуск через Docker (рекомендуется; PHP/Composer на машине не нужны, зависимости ставятся автоматически, переменные
берутся из .env). Примеры запускаются через сервис examples из docker-compose.yml (docker compose run):
./examples/run.sh echo-bot-long-polling.php
./examples/run.sh echo-bot-webhook.php # слушает http://localhost:8080Для любого другого примера укажите его имя: ./examples/run.sh send-to-user.php.
Вебхук-пример использует встроенный PHP-сервер на localhost:8080. API принимает вебхуки только по HTTPS на публичном
адресе, поэтому для локального тестирования нужен туннель до этого порта, например
cloudflared tunnel --url http://localhost:8080 или ngrok http 8080, — полученный https://... адрес укажите в
createSubscription(). Порт можно переопределить: MAX_EXAMPLES_PORT=9090 ./examples/run.sh echo-bot-webhook.php.
Запуск локально (PHP >= 8.4 + Composer):
source .env
composer install
php examples/echo-bot-long-polling.php
php -S 0.0.0.0:8080 examples/echo-bot-webhook.phpСписок примеров:
| Файл | Что показывает |
|---|---|
examples/echo-bot-webhook.php |
Вебхук-бот: верификация секрета, разбор апдейтов, эхо, ответ на колбэки (php -S) |
examples/echo-bot-long-polling.php |
Long polling-бот с тем же обработчиком апдейтов |
examples/inline-keyboard.php |
Отправка inline-клавиатуры (кнопки callback/link) |
examples/send-media.php |
Загрузка медиа и отправка с подписью, форматированием и disable_link_preview |
examples/send-to-user.php |
Идентификация пользователя и отправка ему личного сообщения |
examples/set-commands.php |
Установка команд бота (editBotCommands) |
examples/verify-contact.php |
Верификация контакта из кнопки request_contact |
examples/verify-webapp-data.php |
Верификация стартовых данных мини-приложения (WebAppDataValidator) |
Как определить пользователя и отправить ему сообщение. Бот получает user_id и chat_id диалога из любого апдейта
($update->user->userId, $update->chatId). Сообщение пользователю отправляется через Recipient(userId: ...);
подробная информация о пользователе — через getChatMembers($chatId, [$userId]) (аватар, описание, роль админа) или
getChat($chatId)->dialogWithUser.
use GeekCo\MaxPhpClient\Retry\RetryStrategy;
$client = ApiClient::create(
// ...
retryStrategy: new RetryStrategy(
maxAttempts: 5,
baseDelaySeconds: 1.0,
maxDelaySeconds: 30.0,
factor: 2.0,
),
);По умолчанию ретраятся только идемпотентные методы (GET/PUT/DELETE), а также
AttachmentNotReadyException — всегда. Для ретраев неидемпотентных методов включите retryOnNonIdempotent: true или
задайте customShouldRetry.
RateLimiter — локальный token bucket (2 req/s, бакет на 2) для каждого chat_id. Используется автоматически при
вызовах, связанных с чатом. При исчерпании бакета выбрасывается RateLimitException.
use GeekCo\MaxPhpClient\Dto\AttachmentRequest;
use GeekCo\MaxPhpClient\Enum\AttachmentType;
use GeekCo\MaxPhpClient\Enum\UploadType;
$upload = $client->uploadMedia(UploadType::Image, '/path/to/photo.jpg');
$message = $client->sendMessage(
new Recipient(chatId: 123456789),
new NewMessageBody(attachments: [new AttachmentRequest(type: AttachmentType::Image, token: $upload->token)]),
);Клиент сам ждёт готовности вложения (attachment.not.ready) с экспоненциальными повторами перед отправкой сообщения.
use GeekCo\MaxPhpClient\Webhook\WebhookHandler;
use Psr\Http\Message\ServerRequestInterface;
$handler = new WebhookHandler(secret: getenv('MAX_WEBHOOK_SECRET'));
if (!$handler->verify($request)) {
// 401
}
/** @var GeekCo\MaxPhpClient\Dto\Update|list<GeekCo\MaxPhpClient\Dto\Update> $updates */
$updates = $handler->decode($request);
if ($updates instanceof GeekCo\MaxPhpClient\Dto\Update) {
$updates = [$updates];
}Создание подписки:
$client->createSubscription(
url: 'https://example.com/webhook',
updateTypes: ['message_created', 'message_callback'],
secret: 'my-secret',
);Секрет подписки: 5–256 символов [a-zA-Z0-9_-].
use GeekCo\MaxPhpClient\LongPolling\LongPollingRunner;
$runner = new LongPollingRunner(
api: $client,
handler: static function (Update $update): bool {
// обработать событие
return true; // false — остановить цикл
},
);
$lastMarker = $runner->run();Long polling ограничен по скорости и хранению событий — подходит для разработки и тестирования, но не для production.
| Исключение | Когда возникает |
|---|---|
RateLimitException |
HTTP 429 или локальный rate limiter |
AttachmentNotReadyException |
код ошибки attachment.not.ready |
ApiException |
любой ответ 4xx/5xx с ErrorResponse |
NetworkException |
транспортная ошибка PSR-18 |
InvalidResponseException |
невалидный JSON / структура ответа |
InvalidArgumentException |
некорректные аргументы вызова |
use GeekCo\MaxPhpClient\Exception\MaxApiException;
try {
$client->getChat($chatId);
} catch (MaxApiException $e) {
if ($e instanceof ApiException) {
printf('[%d] %s', $e->statusCode, $e->getError()?->message);
}
}- Верификация контакта:
hash_equals(hash_hmac('sha256', $normalizedVcf, $accessToken), $hash),\r\nвvcf_infoзаменяются на реальные переносы строк. - Верификация стартовых данных мини-приложения (
WebAppDataValidator):secret_key = HMAC-SHA256('WebAppData', token), подписьlaunch_paramsпо алгоритму https://dev.max.ru/docs/webapps/validation. - Секреты и токены никогда не логируются.
- Все URL валидируются (
https://, без SSRF). - Постоянновременное сравнение секретов через
hash_equals.
Ядро framework-agnostic (PSR-7/17/18). Адаптация сводится к регистрации ApiClient как синглтона в DI-контейнере и
пробросу WebhookHandler в контроллер:
// Регистрация в DI-контейнере (Laravel ServiceProvider / Symfony service).
// Клиент создаётся один раз и внедряется в сервисы и контроллеры.
$psrFactory = new HttpFactory();
$container->singleton(ApiClient::class, static fn (): ApiClient => ApiClient::create(
httpClient: $container->get(ClientInterface::class), // PSR-18 клиент фреймворка
requestFactory: $psrFactory,
streamFactory: $psrFactory,
uriFactory: $psrFactory,
accessToken: $config['api_token'],
));
// Контроллер вебхука. Секрет проверяется через verify() (иначе 401),
// невалидный payload — 400. Ответ 200 обязателен в течение 30 сек,
// иначе API повторит доставку по экспоненте.
public function webhook(ServerRequestInterface $request): ResponseInterface
{
if (!$this->handler->verify($request)) {
return new Response(401);
}
try {
$updates = $this->handler->decode($request);
} catch (InvalidResponseException) {
return new Response(400);
}
if ($updates instanceof Update) {
$updates = [$updates];
}
foreach ($updates as $update) {
$this->dispatch($update);
}
return new Response(200);
}Практики для production:
- Храните
user_id($update->user->userId) иchat_id($update->chatId) в своей БД; личные сообщения отправляйте черезRecipient(userId: ...). - Загрузка медиа выполняется клиентом целиком (
uploadMedia), включая ожидание готовности вложения — не дублируйте этот код в проекте. - Для long polling есть готовый
LongPollingRunner; для production используйте вебхуки. - Обработку апдейтов держите асинхронной (очередь), чтобы укладываться в лимит ответа webhook 30 сек.
docker compose run --rm app composer install
docker compose run --rm app vendor/bin/phpunit
docker compose run --rm app vendor/bin/phpstan analyse --no-progress
docker compose run --rm app composer run lint # php-cs-fixer: проверка форматирования
docker compose run --rm app composer run format # php-cs-fixer: авто-исправление
docker compose run --rm app composer run coverage # phpunit + порог покрытия (95% строк)Покрытие кода тестами — 100% строк/методов (проверяется гейтом composer run coverage, порог 95%). Этот же набор
проверок прогоняется в CI на каждый push и PR (см. .github/workflows/ci.yml).
Смоук-тесты против реального API (tests/Integration/SmokeTest.php, группа integration) выполняют read-only вызовы
(getMe, getSubscriptions, getUpdates, getMessages) и запускаются отдельно:
MAX_API_TOKEN=<token> docker run --rm --network host \
-v "$(pwd)":/var/www/html -w /var/www/html \
-e MAX_API_TOKEN="$MAX_API_TOKEN" \
ghcr.io/geekcodev/php:8.4-bookworm vendor/bin/phpunit --group integration- Требуется переменная окружения
MAX_API_TOKEN(из.env). - Цепочка сертификатов Минцифры лежит в
tests/Fixtures/max-ca-chain.pemи используется для проверки TLS. - В Docker-сети (
docker compose run) TLS доplatform-api2.max.ruможет блокироваться — используйте--network host. - Без доступа/токена тесты пропускаются, а не падают.
OpenAPI-спецификация API: https://github.com/geekcodev/max-openapi