westacks/telebot-laravel
A Laravel integration for the Telegram Bot API that simplifies building bots with a fluent interface, webhook and polling support, and framework-friendly configuration for sending messages, handling updates, and managing bot commands in PHP applications.
Two commands and you're running:
composer require westacks/telebot-laravel
php artisan telebot:install
telebot:install publishes config/telebot.php and appends TELEGRAM_BOT_TOKEN / TELEGRAM_BOT_NAME to .env. Fill the token in before anything else — if either value is missing or invalid, stop and fix it rather than running commands against an unconfigured bot.
Read config/telebot.php first. It is the whole mental model of the package:
default — which named bot the facade resolves tobots.<name>.token / name / api_url / http — transport configbots.<name>.storage — where per-user conversation state lives (FileStorage by default)bots.<name>.webhook / poll — mode-specific optionsbots.<name>.kernel — the handler pipeline classmiddleware — applied to the webhook routenamespace — base namespace for generated classes (Telegram → App\Telegram)First use case — scaffold a kernel and a /start handler:
php artisan make:telebot:kernel # App\Telegram\Kernel
php artisan make:telebot:command-handler Start # App\Telegram\Handlers\StartCommandHandler
Register the handler in the kernel's constructor array, then pick a run mode:
# development
php artisan telebot:polling --once # verify one update round-trips
php artisan telebot:polling # then leave it running
# production (publicly reachable host)
php artisan telebot:webhook --setup
Webhook routes are registered automatically per bot at /telebot/webhook/{bot}/{token}.
For a one-off send without any handler at all, the facade is enough:
use WeStacks\TeleBot\Laravel\TeleBot;
TeleBot::sendMessage(['chat_id' => $id, 'text' => 'ping']);
Kernel as an ordered pipeline. Handlers are a chain of responsibility: a handler whose handle() returns a value stops the chain, one that returns nothing falls through. That makes ordering a design decision, not an implementation detail.
class Kernel extends BaseKernel
{
public function __construct()
{
parent::__construct([
\App\Telegram\Handlers\StartCommandHandler::class, // narrow triggers first
\App\Telegram\Handlers\ButtonPressHandler::class,
\App\Telegram\Handlers\EchoHandler::class, // catch-all last
]);
}
}
Trigger narrow, handle small. UpdateHandler::trigger() is where you decide whether the handler owns the update ($this->update->type('message')); handle() does the work and returns the API result to end the chain.
Commands are self-documenting. CommandHandler exposes aliases() and description(). The description is what telebot:commands --setup pushes to Telegram's command menu, so wrap it in trans() and you get localized autocompletion for free. Treat the handler class as the single source of truth for a command — aliases, description, and behavior in one file.
Callbacks: match, branch, acknowledge. CallbackHandler with a $match regex plus $this->arguments() is the standard inline-keyboard router:
protected string $match = "/^test:(delete|update):(\d+)$/";
public function handle()
{
[$action, $id] = $this->arguments();
// ... act ...
$this->answerCallbackQuery(); // always
}
The regex doubles as your route table — keep it readable (resource:action:id) and the handler scales to dozens of buttons.
Multi-step conversations via RequestInputHandler. request($bot, $update->user()) parks the user in an input state; the next message hits handle(), you validate, call accept() to clear state, and reply. This is the package's answer to "ask the user a question and wait" — don't build your own conversation table on top of it.
Swap state storage before you scale. Default FileStorage is fine for one box. For containers, multiple app servers, or state that must survive a deploy, implement StorageContract (get/set/delete) against your DB and point bots.bot.storage at it. Keys are user identifiers, so a users.telegram_id → input_state column is the natural mapping.
Notifications as an ops channel. TelegramNotification is a fluent message builder returned from toTelegram(), and chaining multiple sendMessage() calls in one notification is supported:
public function via($notifiable) { return ['telegram']; }
public function toTelegram($notifiable)
{
return (new TelegramNotification)->bot('bot')
->sendMessage(['chat_id' => $notifiable->telegram_chat_id, 'text' => 'Deploy finished'])
->sendMessage(['chat_id' => $notifiable->telegram_chat_id, 'text' => '2 migrations run']);
}
Fan-out with promises. Anything broadcast-shaped (N recipients) should use _promise: true and collect:
$promise = $bot->getMe(_promise: true);
$promise->then(fn ($user) => /* ... */)->wait();
Rescue instead of try/catch. _rescue keeps error policy at the call site without unwinding the pipeline — return a value to swallow, rethrow to surface.
Multi-bot by name. TeleBot::bot('bot2') for a specific bot, BotManager (bots, add, remove, default) for runtime registration, and --all on the webhook/polling/commands commands to operate across every configured bot at once.
A handler that returns something ends the pipeline. The classic bug: a broad message handler placed above a specific one, returning a sendMessage result, silently starving every handler below it. Order narrow → broad, and return null/nothing from handlers that are only inspecting updates.
Unregistered handlers fail silently. Generating a class with make:telebot:* does not wire it up. If your bot "ignores" a command, the first thing to check is the Kernel constructor array.
namespace is a scaffold-time setting. Change config('telebot.namespace') before generating classes; otherwise make:telebot:* emits into one namespace while your Kernel reference points at another.
Webhook validation is opinionated. UpdateRequest enforces POST + JSON, that the token segment in the URL matches config, that X-Telegram-Bot-Api-Secret-Token matches when a secret is configured, and that the payload contains exactly one update type. Symptom of a secret mismatch is a 4xx on every update — check the secret in config against what was registered with telebot:webhook --info.
Re-run --setup after any URL/token change. The registered webhook and the route /telebot/webhook/{bot}/{token} are two halves of one contract; editing .env alone leaves Telegram pointing at the old target. Use --remove when decommissioning, --info to diff reality against expectation.
Polling is a blocking process. Run it under supervisor/systemd, not in a terminal you'll close. It handles SIGINT/SIGTERM/SIGQUIT for graceful shutdown, and --once is the right debugging mode — it proves credentials and handler wiring without a long-lived worker.
Default storage is per-machine. FileStorage breaks the moment you run two app servers or redeploy into a fresh container. Conversation state vanishes and users get stuck mid-flow. Move to a StorageContract DB implementation before horizontal scaling.
Error policy per status class. 401 (bad/revoked token) and 403 (blocked by user / missing rights) are not retryable — log and move on. 429 is retryable but only after retry_after; inspect $e->getCode() inside _rescue. Network timeouts deserve exactly one short retry, then surface.
'_rescue' => function (\Throwable $e) {
if ($e->getCode() === 429) { report($e); return null; }
throw $e;
},
_rescue swallowing silently. It returns the exception in place of throwing — if you pass a closure that returns something and never logs, failures disappear. Always report() or return the exception to a caller that checks.
Promises do nothing until wait(). An un-awaited promise is a fire-and-forget request that may never be sent before the worker exits.
DTO access is forgiving, which cuts both ways. $update->get('message.from.is_bot') with dot notation is pleasant, but a typo returns null instead of erroring — no exception to catch. Prefer typed accessors ($update->message()->from->id) in handlers where a null would propagate into an API call.
The shipped http_errors => false default means Guzzle won't throw on its own; don't expect Guzzle-level exceptions to surface API failures. Route error handling through _rescue and your own logging.
Don't invent methods. The facade proxies the Telegram Bot API methods the installed version exposes plus the BotManager methods. If a method isn't in the docs for your pinned version, check the installed source before assuming it exists — and pin the version you tested against.
The advertised log driver isn't documented in this material. The package description mentions a log driver, but there's no config or usage contract here. Read the published config/telebot.php and the package README before wiring it into a logging stack.
How can I help you explore Laravel packages today?