Search by

ntcte / max-platform

ega22a

PHP SDK for the Max messenger Bot API: typed API client, bot framework, webhooks, long polling, Laravel and Symfony integrations

Package info

gitverse.ru/ntcte/max-platform

pkg:composer/ntcte/max-platform

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

v0.2.0 2026-10-16 05:38 UTC

This package is auto-updated.

Last update: 2026-10-16 05:40:11 UTC


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.

Содержание

Установка

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 ГАПОУ СО «Нижнетагильский торгово-экономический колледж»