bushlanov-dev/max-bot-api-client-php
bushlanov-dev/max-bot-api-client-php is a Composer-ready PHP client for the MAX Bot API, making it easy to build bots for the MAX messenger. It offers convenient methods for messages, media, keyboards, updates, and webhooks, with a clean, framework-agnostic design.
1. Install and discover the entry point
composer require bushlanov-dev/max-bot-api-client-php
There is no Laravel service provider in this package — it's a plain PHP SDK. Read, in this order:
README.md — the constructor signature and the "quick start" snippet tell you the auth model (a bot token, plus the base URI https://botapi.max.ru by default).src/ — find the client class (namespace is BushlanovDev\MaxBotApi\…) and the sibling directory holding the request/response DTOs. That DTO directory is the API surface; skim it before writing anything.composer.json — check the PHP constraint and whether it pulls Guzzle, PSR-18, or both. This determines how you test it later.2. Get a token
Create the bot in MAX itself (the @MasterBot flow) and copy the access token. Put it in .env, never in a committed config:
MAX_BOT_TOKEN=xxxxxxxx
MAX_BOT_API_BASE_URI=https://botapi.max.ru
3. Wire it into the container
// config/services.php
'max_bot' => [
'token' => env('MAX_BOT_TOKEN'),
'base_uri' => env('MAX_BOT_API_BASE_URI', 'https://botapi.max.ru'),
],
// app/Providers/AppServiceProvider.php (register() or boot())
$this->app->singleton(Client::class, fn () => new Client(
token: config('services.max_bot.token'),
baseUri: config('services.max_bot.base_uri'),
));
The client is stateless HTTP, so a singleton is correct and gives you connection reuse.
4. First useful call
$me = app(Client::class)->getMe(); // sanity check: confirms the token works
$client->sendMessage(chatId: $chatId, text: 'Hello from Laravel');
If getMe() returns, everything else is just picking the right DTO from src/. Start with an echo bot (receive an update → send the same text back) before building anything with keyboards or attachments.
Wrap it, don't sprinkle it. Because there is no framework integration, define a thin seam so your app doesn't depend on the SDK's method names everywhere:
interface MaxBot
{
public function send(int $chatId, string $text, array $opts = []): void;
}
final class SdkMaxBot implements MaxBot
{
public function __construct(private Client $client) {}
public function send(int $chatId, string $text, array $opts = []): void
{
$this->client->sendMessage(chatId: $chatId, text: $text, ...$opts);
}
}
Bind MaxBot to SdkMaxBot. This pays for itself the first time you need to swap a transport, add retry logic, or fake the bot in a feature test.
Two ingestion strategies — pick one per bot:
| Strategy | Fits | Laravel shape |
|---|---|---|
Long polling (getUpdates in a loop) |
Local dev, single-server bots, no public HTTPS | php artisan max:listen command, run under Supervisor |
| Webhook (subscription to a callback URL) | Production, horizontal scale | Route + controller, VerifyCsrfToken excepted, registered once via the client's subscription method |
The polling command is the classic shape:
public function handle(Client $client): int
{
$marker = Cache::get('max:marker');
while (true) {
$updates = $client->getUpdates(marker: $marker, timeout: 30);
foreach ($updates as $update) {
ProcessMaxUpdate::dispatch($update);
$marker = $update->marker ?? $marker;
}
Cache::forever('max:marker', $marker);
}
}
Persisting the marker to the cache (or DB) matters — in-process state disappears on every deploy.
Never send from the request/update handler. Dispatch a job. The API has per-chat and global rate limits, and a slow send inside a webhook handler will make MAX retry the update and duplicate your message.
final class SendMaxMessage implements ShouldQueue
{
use Queueable;
public $tries = 5;
public $backoff = [1, 5, 15, 60];
public function handle(MaxBot $bot): void
{
$bot->send($this->chatId, $this->text);
}
}
Laravel notifications. A custom channel is the highest-leverage integration — it makes the bot usable from anywhere in the app:
final class MaxChannel
{
public function send($notifiable, Notification $notification): void
{
$message = $notification->toMax($notifiable);
SendMaxMessage::dispatch($notifiable->max_chat_id, $message->text);
}
}
Add a routeNotificationForMax() on your User model returning the stored chat_id. Note that a MAX bot can only message a chat the user has already started — collect the chat_id during onboarding via a deep link / start payload, not by guessing a phone number.
Multi-bot / multi-tenant. If you run more than one bot, don't use a singleton. Use a factory keyed by tenant and request-scoped binding, or Client instances constructed on demand from a bots table. Cache nothing tenant-specific in the singleton.
Attachments and keyboards. These are DTO objects, not arrays. Build them once in a small MaxKeyboard::main() helper rather than inline in every job — the payloads are nested and easy to get subtly wrong, and the error you get back will be a generic 400.
composer.json ("bushlanov-dev/max-bot-api-client-php": "1.*" or exact) and read CHANGELOG/git tags before composer update. Treat the SDK's DTO properties as semi-public API and adapt at your MaxBot seam.vendor/bushlanov-dev/…/src/ — you'll find the exact request body and endpoint in under a minute, which is faster than guessing.numprocs=1 and --timeout=0. Running it as a queued job or inside a schedule:run will either time out or spawn overlapping pollers, and two pollers with the same token fight over the marker and silently drop updates.max_update_id unique index and insertOrIgnore before processing; the alternative is users seeing the same reply twice.$e->getMessage() only after stripping query strings, and never Log::info($request) in the client wrapper. Also exclude the token from config:cache output and from Horizon/Telescope payloads if you pass DTOs around.getUpdates long polls). The default Guzzle timeout of 0 will hang a worker forever on a half-open connection, and that looks exactly like "the bot stopped responding."retry(3). MAX returns rate-limit errors under load. Use backoff() in the job, and make sends idempotent-ish by only retrying on transport/5xx/429 errors — retrying on a 4xx (bad chat id, bot blocked) burns your queue.hash_equals. If it doesn't, restrict the endpoint by path secret plus IP allowlist, and return 200 quickly — a non-2xx response triggers redelivery storms.Http facade, so Http::fake() won't intercept it. Fake MaxBot in feature tests (assert a job was dispatched, or use a recording fake) and keep one thin integration test that hits the real getMe() in a nightly/manual job.ext constraints. Check composer.json for ext-curl/ext-json and the minimum PHP version before adding it to a legacy app; the failure mode is a composer require conflict that looks like a dependency-resolution mess rather than a version constraint.How can I help you explore Laravel packages today?