mollsoft/laravel-telegram-bot
Laravel package for building Telegram bots with a framework-native API. It handles webhooks, commands, and message routing so you can define bot logic using familiar Laravel conventions, configuration, and service-container bindings.
1. Verify the gate before installing. The package targets Laravel 11+, and there is no changelog detail in the v1.1.0 release notes. Read composer.json in the repo directly for the php and illuminate/* constraints and compare against your composer.lock — this is the real compatibility check, not the README.
composer require mollsoft/laravel-telegram-bot
2. Publish and inspect. Laravel-native packages follow the same shape, so expect the usual suspects:
php artisan vendor:publish --provider="Mollsoft\TelegramBot\TelegramBotServiceProvider"
Diff the published config against vendor/mollsoft/laravel-telegram-bot/config/*.php rather than assuming the file is complete — with thin release notes, the vendor copy is the source of truth. Add the bot token to .env and to your secrets manager for deployed environments; never commit it.
3. Where to look first. Spend the first hour in three places, in this order:
vendor/mollsoft/laravel-telegram-bot/src/ — the service provider (what it binds, what it registers) and the update-dispatch path (how an incoming update becomes your code).routes/ or the provider's boot() — whether the package registers a webhook route for you or expects you to declare one.4. Register the webhook. Telegram must be told where to POST. If the package ships an artisan command for this, use it; otherwise call setWebhook through the package's API wrapper. Point it at an HTTPS URL — Telegram will not deliver to plain HTTP or to a self-signed cert.
5. First use case: a /start + echo bot. The smallest end-to-end loop that proves config, routing, dispatch, and outbound send all work:
/start from a private test bot and confirm the reply lands.Once that round-trips, everything else is business logic.
Webhook in production, polling locally. Webhooks need a public HTTPS endpoint, which is friction on a laptop. Use a tunnel (ngrok, Expose, or Laravel Herd/Valet's exposed mode) for local webhook testing so the code path matches production exactly. If the package supports long polling, treat it as a dev-only convenience — switching modes also means calling deleteWebhook first, since Telegram refuses getUpdates while a webhook is set.
One handler per command, thin handlers. The package advertises command routing and extensible handlers. Keep each handler to: parse the update, validate, enqueue or delegate, return. Push anything with I/O or latency into a queued job. This is the single highest-leverage pattern — it keeps handlers testable and keeps your webhook response fast.
Respond fast, work later. Telegram retries an update if your endpoint is slow or returns non-2xx. Aim to return 200 in well under a second: acknowledge the update, dispatch a job, exit. Long-running work inline is the most common cause of duplicated messages and mysterious double-charges in bot integrations.
Deduplicate on update_id. Telegram delivery is at-least-once. Store the update_id (unique index, or a short-TTL cache key) and skip updates you have already processed. This is cheap insurance and the difference between "occasionally sends the same message twice" and "reliably exactly-once" from the user's perspective.
Model chats and users with Eloquent. The package is Eloquent-friendly by design. Keep your own tables for chat/user state rather than relying on package tables for domain data — that keeps the package replaceable. Typical schema: telegram_chats (chat_id, type, title, last_seen_at), telegram_users (telegram_id, username, first_name, locale), and a conversation_state column or table for multi-step flows.
Conversations as explicit state machines. Bots drift into if ($step === 'awaiting_email') sprawl. Model state as a small enum plus a transition map keyed by the last asked question, persisted per chat. It survives deploys and restarts, unlike in-memory state.
Wrap the package behind one interface. Define TelegramGateway in your app (sendMessage, sendPhoto, setWebhook, answerCallbackQuery) and implement it with the package. Every handler depends on your interface, not the package's client. This is a day-one cost of about an hour and makes a future swap or a second bot provider a contained change.
Test with fakes, not the network. Use Http::fake() to stub api.telegram.org responses, and feed recorded update payloads (JSON fixtures) into your handlers. Keep a fixtures directory of real updates: /start, plain text, a callback query, a photo, an edited message. These are the cheapest regression tests you will own.
Multi-bot and multi-tenant. If you need more than one bot, resolve the bot identity from the incoming request (different webhook path per bot, or a token lookup) and scope config accordingly. Do not cache a single token in a singleton if you run several.
Exclude the webhook route properly. The route must be outside auth, web, and CSRF middleware. A 419 or 302 in your access log means Telegram is being bounced to a login page or rejected on token mismatch — the bot will appear to "do nothing" while the app looks healthy.
Validate the webhook secret. Set a secret_token when registering the webhook and check the X-Telegram-Bot-Api-Secret-Token header on every request. Without it, your endpoint is an unauthenticated write path into your application.
Config cache is a deploy trap. If the package reads a config key that you never published or never merged into your deployed config/, it resolves to null — and this fails only in environments with stale published config, not in CI that regenerates. After every deploy: php artisan config:clear && php artisan config:cache, and after every package upgrade: diff vendor config against published config and merge intentionally.
Do not adopt new 1.1.0 features in the same deploy as the version bump. Minor-version releases can carry silent behavior changes behind unchanged signatures. Ship the bump alone, verify, then adopt features.
Escaping in parse_mode. If you send Markdown or HTML, user-supplied text (names, message content) must be escaped or the API returns 400 Bad Request: can't parse entities. Prefer HTML with e()-style escaping over legacy Markdown — it is more forgiving. When a send fails with a 400, log the description field; it names the offending entity offset.
Telegram's own limits are your limits. Roughly 30 messages/second globally and about 1/second per chat; 4096 characters per message; 20 MB for files fetched via getFile. Anything user-triggered at scale needs a rate limiter in front of the outbound client, or you will silently drop messages on 429s. Handle 429 with the retry_after value.
Duplicate/absent messages after deploy. Two culprits, in order of likelihood: slow webhook responses causing retries (fixed by queueing), and the webhook being re-registered to a stale URL. getWebhookInfo returns the current URL, pending update count, and last error — make it your first debugging command, always.
Log the raw update. On any handler exception, log the full update JSON with the update_id. You cannot reproduce Telegram deliveries on demand, and without the payload a bug report is unactionable.
Migrations: read before running. Confirm what, if anything, the package publishes as migrations and whether those tables overlap your own. Run them on a scratch database first. A package that owns tables in your domain model is the one category of dependency that is genuinely expensive to remove later.
Local debugging hygiene. Use a dedicated test bot with a token separate from production, and never point a dev tunnel at the production bot — webhook registration is global, so a dev machine can hijack production traffic in one command.
Known risk, stated plainly. This is a 5-star, single-maintainer package with an empty v1.1.0 changelog and no dependents. Pin the version in composer.json (not ^1.0), verify against src/ rather than docs, and keep the dependency behind your own interface so removal is a one-file change. Assign one engineer as the named owner for dependency review so the knowledge does not reset every upgrade cycle.
How can I help you explore Laravel packages today?