geekcodev / max-php-client
Framework-agnostic PHP client for MAX Messenger Bot API
Requires
- php: ^8.4
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1|^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- guzzlehttp/guzzle: ^7.15
- guzzlehttp/psr7: ^2.7
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.5
Suggests
- guzzlehttp/guzzle: PSR-18 HTTP client (recommended implementation, ships with Laravel)
- symfony/http-client: PSR-18 HTTP client adapter for Symfony projects
README
Универсальный, 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-client
Быстрый старт
use 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.
Rate limit
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_-].
Long polling
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