ntcte / max-platform
PHP SDK for the Max messenger Bot API: typed API client, bot framework, webhooks, long polling, Laravel and Symfony integrations
Requires
- php: ^8.3
- ext-json: *
- ext-mbstring: *
- php-http/discovery: ^1.19
- php-http/multipart-stream-builder: ^1.3
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^2.0 || ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- guzzlehttp/guzzle: ^7.9
- laravel/framework: ^12.0 || ^13.0
- mockery/mockery: ^1.6
- nyholm/psr7: ^1.8
- orchestra/testbench-core: ^10.0 || ^11.0
- php-http/mock-client: ^1.6
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11.5 || ^12.0
- symfony/browser-kit: ^7.0 || ^8.0
- symfony/framework-bundle: ^7.0 || ^8.0
Suggests
- guzzlehttp/guzzle: PSR-18 HTTP client implementation
- laravel/framework: To use the Laravel bridge (^12.0 || ^13.0)
- symfony/framework-bundle: To use the Symfony bundle (^7.0 || ^8.0)
- symfony/http-client: PSR-18 HTTP client implementation (with nyholm/psr7)
Provides
None
Conflicts
None
Replaces
None
README
PHP-библиотека для работы с Bot API мессенджера MAX: типизированный клиент всех методов API, фреймворк для ботов (роутинг, middleware, webhook и long polling), загрузка файлов, а также интеграции с Laravel и Symfony.
- PHP 8.3+, строгая типизация, неизменяемые модели (
readonly). - HTTP через PSR-18/PSR-17: подходит Guzzle, Symfony HttpClient или любой другой клиент.
- Все 19 типов событий (
Update), вложения, кнопки и разметка разбираются в конкретные классы. Неизвестные новые типы не ломают бота. - Ограничения схемы (длина текста, кнопок, формат секрета и т. д.) проверяются до отправки запроса.
- Повтор запросов при 429/503, сетевых ошибках и
attachment.not.ready.
Спецификация API: max-messenger/api-schema.
Содержание
- Установка
- Быстрый старт
- API-клиент
- Сообщения, клавиатуры и форматирование
- Загрузка файлов
- Бот: роутинг и обработчики
- Получение событий: webhook и long polling
- Ошибки и повторы
- Laravel
- Symfony
- Расширение
- Разработка
Установка
composer require ntcte/max-platform
Библиотеке нужна реализация PSR-18. Если в проекте её нет, установите, например, Guzzle:
composer require guzzlehttp/guzzle
Токен бота выдаётся на платформе MAX для партнёров после модерации бота: «Чат-боты» → «Перейти» → «Расширенные настройки».
Сертификат Минцифры
Сертификат platform-api2.max.ru выпущен корневым центром Минцифры (Russian Trusted Root CA), которого нет в стандартных хранилищах доверенных сертификатов. Без него запросы падают с TransportException: cURL error 60: SSL certificate problem.
Варианты:
- добавить сертификат в системное хранилище (рекомендуется для серверов):
sudo curl -o /usr/local/share/ca-certificates/russian_trusted_root_ca.crt https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt sudo update-ca-certificates - или передать свой HTTP-клиент с файлом сертификатов, включающим этот корень:
$max = MaxClient::create($token, new \GuzzleHttp\Client(['verify' => '/path/to/bundle-with-russian-root.pem']));
Быстрый старт
use Ntcte\Max\MaxClient;
$max = MaxClient::create(getenv('MAX_BOT_TOKEN'));
$me = $max->bots()->getMe();
echo $me->firstName, ' @', $me->username, PHP_EOL;
$max->messages()->sendToUser(123456789, 'Привет из PHP!');
Простой бот на long polling:
use Ntcte\Max\Bot\Bot;
use Ntcte\Max\Bot\Context;
use Ntcte\Max\Bot\Polling\LongPollingRunner;
$bot = new Bot($max);
$bot->command('start', fn (Context $ctx) => $ctx->reply('Здравствуйте!'));
$bot->message(fn (Context $ctx) => $ctx->reply('Вы написали: ' . $ctx->getText()));
(new LongPollingRunner($max->updates(), $bot))->run();
Готовые сценарии лежат в examples/: бот на polling, webhook без фреймворка, отправка файла.
API-клиент
MaxClient группирует методы по ресурсам. Каждый метод соответствует одному эндпоинту схемы.
| Ресурс | Методы |
|---|---|
bots() | getMe(), setCommands() |
chats() | get(), edit(), sendAction(), getPinnedMessage(), pinMessage(), unpinMessage(), getMembership(), leave() |
members() | list(), all() (ленивая постраничная выборка), add(), remove(), getAdmins(), setAdmins(), revokeAdmin() |
messages() | send(), sendToChat(), sendToUser(), edit(), delete(), get(), list() |
comments() | list(), get(), send(), edit(), delete() |
callbacks() | answer() |
videos() | getDetails() |
subscriptions() | list(), subscribe(), unsubscribe(), unsubscribeAll() |
uploads() | getUploadUrl() |
updates() | get() (long polling) |
uploader() | uploadImage(), uploadVideo(), uploadAudio(), uploadFile() |
Ответы возвращаются как типизированные модели из Ntcte\Max\Model. Время в API передаётся в миллисекундах; для перевода в дату есть Ntcte\Max\Support\Time::fromMilliseconds().
use Ntcte\Max\Enum\ChatAdminPermission;
$chat = $max->chats()->get(-100123);
echo $chat->title, ': ', $chat->participantsCount, ' участников', PHP_EOL;
foreach ($max->members()->all($chat->chatId) as $member) {
if ($member->hasPermission(ChatAdminPermission::PinMessage)) {
echo $member->getFullName(), PHP_EOL;
}
}
Свой HTTP-клиент, логгер и настройки передаются в create():
use Ntcte\Max\Config\ClientConfig;
use Ntcte\Max\Http\RetryPolicy;
$max = MaxClient::create(
new ClientConfig(
token: getenv('MAX_BOT_TOKEN'),
retryPolicy: new RetryPolicy(maxAttempts: 5, initialDelayMs: 1000),
),
httpClient: new \GuzzleHttp\Client(['timeout' => 60]),
logger: $psrLogger,
);
Для long polling таймаут HTTP-клиента должен быть больше таймаута ожидания (по умолчанию 30 секунд).
Сообщения, клавиатуры и форматирование
NewMessageBody неизменяем: методы with*() возвращают новую копию.
use Ntcte\Max\Keyboard\InlineKeyboard;
use Ntcte\Max\Request\Attachment\LocationAttachmentRequest;
use Ntcte\Max\Request\NewMessageBody;
use Ntcte\Max\Text\Markdown;
$keyboard = InlineKeyboard::create()
->callback('Записаться', 'signup:42')->callback('Отмена', 'cancel')
->newRow()
->link('Подробнее', 'https://example.ru/events/42')
->requestContact('Поделиться телефоном')
->build();
$body = NewMessageBody::markdown(
Markdown::bold('День открытых дверей') . "\n" . Markdown::escape($userInput)
)
->withKeyboard($keyboard)
->withAttachment(new LocationAttachmentRequest(57.9194, 59.9650))
->silent();
$message = $max->messages()->sendToChat($chatId, $body);
- Форматы текста:
NewMessageBody::markdown()иNewMessageBody::html(). ПомощникиMarkdownиHtmlэкранируют пользовательский ввод. - Ответ на сообщение:
->replyTo($mid); пересылка:->forward($mid). - При редактировании
attachments = nullсохраняет вложения, а->withoutAttachments()удаляет их. - Упоминание пользователя без username:
Markdown::mention($userId, 'Иван').
Загрузка файлов
Uploader выполняет трёхшаговую загрузку: получает URL, отправляет файл и возвращает готовое вложение.
$photo = $max->uploader()->uploadImage('/path/to/photo.jpg');
$doc = $max->uploader()->uploadFile('/path/to/report.pdf');
$max->messages()->sendToChat($chatId, NewMessageBody::create('Отчёт')->withAttachments($photo, $doc));
Видео и аудио сервер обрабатывает асинхронно. Если файл ещё не готов, API возвращает attachment.not.ready, и клиент повторяет отправку согласно RetryPolicy. Токен бота на сервер загрузки не передаётся.
Бот: роутинг и обработчики
Bot пропускает каждое событие через цепочку middleware и передаёт его в Router. Срабатывает первый подходящий маршрут, остальные пропускаются.
use Ntcte\Max\Bot\Bot;
use Ntcte\Max\Bot\Context;
use Ntcte\Max\Bot\Filter\AllOf;
use Ntcte\Max\Bot\Filter\ChatTypeFilter;
use Ntcte\Max\Bot\Filter\UpdateTypeFilter;
use Ntcte\Max\Enum\ChatType;
use Ntcte\Max\Enum\UpdateType;
$bot = new Bot($max);
$bot->started(fn (Context $ctx) => $ctx->reply('Добро пожаловать!')); // кнопка «Начать» и deep link
$bot->command('help', fn (Context $ctx) => $ctx->reply('Справка')); // /help и /help@bot_name
$bot->command('find', fn (Context $ctx) => search($ctx->getCommandArgs())); // /find текст запроса
$bot->callback('signup:*', function (Context $ctx): void { // * — подстановочный символ
$eventId = $ctx->getMatches()[0];
$ctx->answerCallback('Вы записаны на мероприятие ' . $eventId);
});
$bot->text('привет', fn (Context $ctx) => $ctx->reply('Привет!')); // точное совпадение без учёта регистра
$bot->textRegex('/^заказ (\d+)$/u', fn (Context $ctx) => showOrder($ctx->getMatches()[1]));
$bot->on(UpdateType::BotAdded, fn (Context $ctx) => $ctx->reply('Спасибо, что добавили!'));
$bot->on(new AllOf(new UpdateTypeFilter(UpdateType::MessageCreated), new ChatTypeFilter(ChatType::Dialog)), $handler);
$bot->fallback(fn (Context $ctx) => null); // всё остальное
Меню команд
Чтобы клиент MAX подсказывал команды при вводе /, их нужно опубликовать в меню бота. Само по себе command() только обрабатывает входящую команду. Передайте описание третьим аргументом и вызовите syncCommands():
$bot->command('start', $startHandler, 'Начать работу');
$bot->command('list', $listHandler, 'Мои заявки');
$bot->command('stop', $stopHandler, 'Отписаться от уведомлений');
$bot->command('debug', $debugHandler); // без описания: команда работает, но в меню не показывается
$bot->syncCommands(); // PATCH /me/commands, до 32 команд
Меню хранится на стороне MAX, поэтому синхронизировать его достаточно при деплое или изменении команд, а не на каждый запрос. В Laravel и Symfony для этого есть команда max:commands:sync.
Контекст
Context содержит событие ($ctx->update), API-клиент ($ctx->api) и сокращения:
| Метод | Назначение |
|---|---|
getChatId(), getUser(), getMessage(), getText(), getCallback(), getUserLocale() | Данные события; null, если для этого типа события их нет |
reply($text, quote: false) | Ответ в чат события; если чат неизвестен, ответ уходит пользователю напрямую |
answerCallback($answer) | Ответ на нажатие callback-кнопки |
sendAction(SenderAction::TypingOn) | Индикатор «печатает…» |
setAttribute() / getAttribute() | Передача данных между middleware и обработчиками |
Обработчик — это замыкание или класс, реализующий HandlerInterface. Middleware — замыкание или MiddlewareInterface:
use Ntcte\Max\Bot\HandlerInterface;
use Ntcte\Max\Bot\Middleware\LoggingMiddleware;
$bot->use(new LoggingMiddleware($logger));
$bot->use(function (Context $ctx, HandlerInterface $next) use ($allowList): void {
if (!in_array($ctx->getUser()?->userId, $allowList, true)) {
return; // не вызываем $next — обработка прекращается
}
$next->handle($ctx);
});
Исключения обработчиков по умолчанию логирует LoggingErrorHandler, после чего бот продолжает работу. Свою стратегию можно передать в new Bot($max, errorHandler: ...). В тестах удобен RethrowErrorHandler.
Конкретные типы событий лежат в Ntcte\Max\Model\Update\*. Общие данные доступны через интерфейсы HasChatId, HasUser, HasMessage, HasUserLocale.
Получение событий: webhook и long polling
MAX поддерживает два способа, но одновременно работает только один. В production используйте webhook: long polling ограничен по скорости и сроку хранения событий.
Webhook
use Ntcte\Max\Bot\Webhook\WebhookHandler;
use Ntcte\Max\Enum\UpdateType;
use Ntcte\Max\Request\SubscriptionRequest;
// Один раз: подписка (только HTTPS, порт 443, доверенный сертификат)
$max->subscriptions()->subscribe(new SubscriptionRequest(
'https://bot.example.ru/max/webhook',
secret: getenv('MAX_WEBHOOK_SECRET'),
updateTypes: [UpdateType::MessageCreated, UpdateType::MessageCallback, UpdateType::BotStarted],
));
// В обработчике запроса
$handler = new WebhookHandler($bot, $max->getHydrator(), getenv('MAX_WEBHOOK_SECRET'));
$handler->handleGlobals(); // обычный PHP
// $response = $handler->handle($psr7Request, $responseFactory); // PSR-7
// $handler->handleRaw($body, $secretHeader); // любой фреймворк
Заголовок X-Max-Bot-Api-Secret сравнивается за постоянное время. Ответы: 200 — событие принято, 403 — неверный секрет, 400 — некорректное тело запроса. MAX повторяет доставку до получения 200 в течение 8 часов, поэтому ошибки обработчиков не превращаются в ответ 500.
Long polling
$runner = new LongPollingRunner($max->updates(), $bot, timeout: 30, types: [UpdateType::MessageCreated]);
$runner->run(); // остановка по Ctrl+C (SIGINT/SIGTERM) или $runner->stop()
Long polling недоступен, пока у бота есть webhook-подписка. Удалить подписки: $max->subscriptions()->unsubscribeAll().
Ошибки и повторы
Все исключения библиотеки реализуют Ntcte\Max\Exception\MaxExceptionInterface.
| Исключение | Когда |
|---|---|
ApiException и наследники (BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException, MethodNotAllowedException, TooManyRequestsException, InternalServerErrorException, ServiceUnavailableException) | API вернул ошибку; доступны getHttpStatus(), getErrorCode() (например, chat.denied), getErrorMessage() |
TransportException | Сетевая ошибка |
SerializationException | Некорректный JSON или неожиданная структура ответа |
ValidationException | Значение нарушает ограничения схемы; запрос не отправлялся |
UploadException | Ошибка сервера загрузки файлов |
RetryPolicy по умолчанию делает до 3 попыток с экспоненциальной задержкой (0,5 с, 1 с, …, максимум 10 с) и учитывает Retry-After. Отключить повторы: RetryPolicy::disabled().
Laravel
Пакет регистрируется автоматически (package discovery). Поддерживаются Laravel 12–13.
MAX_BOT_TOKEN=...
MAX_WEBHOOK_SECRET=...
MAX_WEBHOOK_URL=https://bot.example.ru/max/webhook
php artisan vendor:publish --tag=max-config # config/max.php (необязательно)
Маршруты бота описываются в классе-конфигураторе, который подключается в config/max.php → configurators:
namespace App\Max;
use Ntcte\Max\Bot\Bot;
use Ntcte\Max\Bot\BotConfiguratorInterface;
use Ntcte\Max\Bot\Context;
final class MaxBotRoutes implements BotConfiguratorInterface
{
public function __construct(private readonly StudentService $students) {}
public function configure(Bot $bot): void
{
$bot->command('start', fn (Context $ctx) => $ctx->reply('Здравствуйте!'), 'Начать работу');
$bot->command('schedule', fn (Context $ctx) => $ctx->reply($this->students->scheduleFor($ctx->getUser())), 'Расписание');
}
}
Что регистрирует пакет:
- сервисы
MaxClient(алиасmax),Bot,WebhookHandler,LongPollingRunnerв контейнере; - фасад
Max:Max::messages()->sendToUser($id, 'Текст'); - маршрут
POST /max/webhook(путь задаётся вmax.webhook.route,nullотключает маршрут); - команды:
php artisan max:webhook:set # подписка на URL из конфига (или передайте URL аргументом)
php artisan max:webhook:info # бот и активные подписки
php artisan max:webhook:delete [url] # удалить одну или все подписки
php artisan max:polling [--force] # long polling для разработки
php artisan max:commands:sync # опубликовать меню команд из зарегистрированных command()
Если в контейнере зарегистрирован Psr\Http\Client\ClientInterface, используется он.
Symfony
Поддерживаются Symfony 7–8. Подключите бандл:
// config/bundles.php
return [
// ...
Ntcte\Max\Bridge\Symfony\MaxBundle::class => ['all' => true],
];
# config/packages/max.yaml
max:
token: '%env(MAX_BOT_TOKEN)%'
# http_client: psr18.http_client # id PSR-18 сервиса; по умолчанию discovery
retry: { max_attempts: 3 }
webhook:
url: '%env(MAX_WEBHOOK_URL)%'
secret: '%env(MAX_WEBHOOK_SECRET)%'
update_types: [message_created, message_callback, bot_started]
polling: { timeout: 30 }
// config/routes/max.php
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
return static function (RoutingConfigurator $routes): void {
$routes->import('@MaxBundle/Resources/config/routes.php'); // POST /max/webhook
};
Сервисы, реализующие BotConfiguratorInterface, получают тег max.bot_configurator автоматически (autoconfigure) и настраивают бота. Консольные команды совпадают с Laravel: max:webhook:set, max:webhook:info, max:webhook:delete, max:polling, max:commands:sync.
Расширение
Новый тип события или вложения. Неизвестные типы приходят как UnknownUpdate, UnknownAttachment, UnknownButton или UnknownMarkup с сырыми данными. Свой класс можно зарегистрировать без изменения библиотеки:
use Ntcte\Max\Model\Update\Update;
use Ntcte\Max\Serializer\DefaultDiscriminatorMap;
use Ntcte\Max\Serializer\Hydrator;
$registry = DefaultDiscriminatorMap::create()->add(Update::class, 'message_reaction', MessageReactionUpdate::class);
$max = MaxClient::create($token, hydrator: new Hydrator($registry));
Свой транспорт. Реализуйте Ntcte\Max\Http\TransportInterface (например, для записи запросов в тестах) и передайте его в new MaxClient($transport).
Разработка
composer install
composer check # php-cs-fixer (dry-run) + phpstan (level max) + phpunit
composer test
composer analyse
composer cs:fix
Интеграционные тесты обращаются к реальному API и запускаются отдельно:
MAX_BOT_TOKEN=... MAX_TEST_CHAT_ID=... [MAX_CA_BUNDLE=...] composer test:integration
Правила внесения изменений описаны в CONTRIBUTING.md, история версий — в CHANGELOG.md.
Лицензия
MIT © 2026 ГАПОУ СО «Нижнетагильский торгово-экономический колледж»