Download the PHP package wekser/laragram without Composer
On this page you can find all versions of the php package wekser/laragram. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package laragram
Laragram
A Laravel package for building Telegram bots in MVC style — routing, controllers, views, and a station-based state machine, all wired into the Laravel ecosystem.
- Requirements: PHP ^8.3 · Laravel ^12|^13 · ext-curl
- Documentation: Wiki · Changelog
What's in the box: routing on event / station / role / chat type / forum topic · multi-step scenes with validation, conditional steps, back navigation and timeouts · views as message components on disk, with auto-escaping, keyboards and albums · groups & forum topics with per-(user, chat, topic) state · payments in Telegram Stars and fiat · inline mode · file upload and download · reactions · broadcasting with auto-pruning of unreachable users · a bundled admin panel with its own login · optional queue offload · a testing trait for whole bot flows without HTTP.
Contents: Commands
Installation
Publish the config and run migrations:
Add your bot credentials to .env:
Register the webhook:
laragram:install leaves you with blank route and scene files. To start from a working bot instead, publish the runnable demo — views, lang files, demo controllers, and demo routes appended to your route file (idempotent, safe to re-run):
No public URL yet? Skip the webhook and long-poll instead — same routing, same controllers:
Upgrading from 1.x
2.0 is a rewrite. Read the changelog in full before upgrading; the essentials:
Renamed classes and methods
| 1.x | 2.0 |
|---|---|
BotRouteCollection · BotRouter |
Routing\RouteCollection · Routing\Router |
Support\FormRequest · Support\FormResponse |
Http\RequestTransformer · Http\ResponseTransformer |
Exceptions\BotException |
Exceptions\ExceptionHandler (a static utility, not an exception) |
BotResponse::user() |
BotResponse::setUser() |
BotClient::sendMessage() · getUserStatus() · isUserActive() |
Services\TelegramErrorHandler |
config/config.php |
config/laragram.php |
Behaviour changes to check
- Replies are now outbound API calls. The webhook always answers
200 OKwith an empty body; nothing rides in it. Each message costs one extra round-trip, in exchange for a single delivery path that also supports several messages per update. BotAPIlost its ~40 hand-written wrappers in favour of a__call()proxy — every Bot API method now works, including ones added after this release.- Route files moved to
routes/laragram/routes.php(plusscenes.php). Setpaths.routeif you keep yours elsewhere. - Lumen support and
src/Examples/are gone; PHP ^8.3 and Laravel ^12 are the floor.
Database migration. The laragram_users and laragram_sessions tables gained columns. Fresh installs get them from laragram:install; an existing app needs its own migration — without thread_id the session upsert fails on every update:
thread_idisNOT NULL DEFAULT 0(0= no topic) because SQL treatsNULLs as distinct, which would defeat the unique key the upsert is keyed on.- Also widen
laragram_users.uidtounsignedBigIntegerif you are still on the 1.xintegercolumn — Telegram ids have outgrown 32 bits. User::isActive()now reads theis_activecolumn, notsettings.active. The redundantactivesettings key is no longer written.- The opt-in
laragram_paymentstable ships separately:php artisan vendor:publish --tag=laragram-migrations.
How It Works
Telegram sends a POST request to /{prefix}/{secret}. Laragram authenticates the sender, resolves the user's current station (state), matches a route, and calls your controller. The controller returns one or several BotResponse objects; Laragram delivers each as an outbound Telegram Bot API call and answers the webhook with 200 OK.
Routes
Bot routes live in routes/laragram/routes.php. Use the injected $collection variable or the BotRoute facade — both are equivalent.
DSL reference:
| Method | Description |
|---|---|
->get('event') |
Telegram update type (message, callback_query, inline_query, etc.) |
->from('station') |
Only match when user is at this station |
->contains('/cmd') |
Command, exact text, or {param} pattern |
->role('admin') |
Restrict to users with a specific role |
->chat('supergroup') |
Restrict to chat type(s) — ->inGroups() / ->inPrivate() are shortcuts |
->thread(42) |
Restrict to forum topic(s) — see Group Chats & Forum Topics |
->name('route_name') |
Assign a name (shown in route:list) |
->call([Ctrl::class, 'method']) |
Controller action or closure |
->fallback() |
Catch-all — matches anything not matched above |
->group() applies a shared station, role, chat type, and forum topic to multiple routes:
Controllers
Controllers are resolved through Laravel's IoC container — constructor injection works out of the box.
BotRequest wraps the incoming update:
BotResponse builds the reply:
Text is auto-escaped for the active parse mode — do not manually escape, it will double-escape. Pass null as the format to send pre-formatted text.
Multiple messages
Return an array of responses to send several messages in reply to one update. Each is delivered as a separate Bot API call, in order:
- The next station is taken from the last response that calls
->redirect()(last-write-wins); if none do, the user stays at the current station. - Delivery is resilient: a failed message is logged and the batch continues — unless the user is unreachable (blocked the bot, deactivated, chat gone), in which case the remaining messages are skipped.
- Each
BotResponse::text(),view(),photo(), etc. returns a fresh instance, so collecting several of them into an array always produces distinct messages — even when built through theBotResponsefacade.
Views
Views are directories under resources/laragram/ (dot notation → subdirectories). Each component of the message is a separate PHP file:
text.php — write plain text plus your own HTML markup (default parse mode is HTML); {{ }} escapes a value, {!! !!} emits it raw, {{-- --}} is a comment:
Static markup (<b>…</b>) renders as-is. {{ }} values are auto-escaped (safe for user data); {!! !!} values are emitted raw (use for trusted, pre-formatted content like translation strings). Variables from $data are extracted into scope, so $name works directly. $user (the authenticated User model) is also available.
A translation with placeholders is mixed-trust — __() substitutes :placeholder values without escaping them — so escape each one with e(): {!! __('laragram.order.placed', ['address' => e($address)]) !!}.
inline_keyboard.php — use global helper functions:
The full InlineKeyboardButton API is available as helpers: button(), href(), web_app(), login_url(), switch_inline(), switch_inline_chosen(), switch_inline_chosen_chat(), copy_text(), pay(), callback_game(). Each one takes optional trailing style: (primary/success/danger) and icon: (custom emoji) attributes — e.g. button('Delete', 'rm', style: 'danger') (Bot API 9.4+).
reply_keyboard.php:
Buttons may sit behind conditions — a keyboard component that adds none simply sends the message without a keyboard (a view with no content at all still throws). Since an omitted markup leaves the user's current reply keyboard in place, use remove_keyboard() to clear it.
media.php — for sendMediaGroup:
For single media, add a photo.php (or video.php, document.php, etc.) containing just the file_id or URL.
Render with:
Scaffold a new view directory:
Keyboards (programmatic)
For building keyboards in controllers without view files:
InlineKeyboard covers the full button API (switchInline(), switchInlineChosen(), switchInlineChosenChat(), loginUrl(), copyText(), pay(), callbackGame(), plus a paginate() helper); ReplyKeyboard adds one-tap requestContact() / requestLocation() / requestPoll() / requestUser() / requestChat() buttons. Every button method on both builders accepts optional trailing style: (primary/success/danger) and icon: (custom emoji) attributes — e.g. ->button('Delete', 'rm', style: 'danger') (Bot API 9.4+).
Station (State Machine)
Each user has a station — a string stored in laragram_sessions.station. Routes match only when the user is at the declared station. Use ->redirect() to move users between steps:
Debug routing in your terminal:
Group Chats & Forum Topics
The bot works in private chats and in groups/supergroups — private (1-on-1) behaviour is unchanged.
- Commands like
/startarrive as/start@YourBotin groups — the mention is stripped automatically. SetLARAGRAM_BOT_USERNAME(your bot's @username, without@) so only your bot's mention matches. - State is per-(user, chat, topic): each member has independent station + scene state in each chat — and in each forum topic within it — so wizards never collide between a group, a topic, and a private chat.
- Group privacy is ON by default in @BotFather: the bot only sees commands aimed at it, replies to its own messages, and @mentions. To receive all group messages, disable privacy via @BotFather (
/setprivacy→ Disable) and re-add the bot.
Forum topics
In a supergroup with topics enabled, a reply is sent back to the topic it came from — nothing to configure. Beyond that:
To push a notification into a topic with no incoming update to reply to, call the API directly — BotAPI is a __call() proxy, so any parameter passes straight through:
- A topic is detected from
is_topic_message, not from the bare presence ofmessage_thread_id— Telegram also sets that field on plain replies inside a non-forum supergroup, where the id is not a valid send target. A forum's General topic behaves like any non-forum chat (no topic). message_thread_idis injected only onto methods that accept it (anysend*, pluscopyMessage/forwardMessage) — never ontoanswer*,edit*,delete*, orsetMessageReaction.- The bot needs the Manage Topics admin right to post in a closed topic.
Upgrading? Per-(user, chat, topic) state adds
chat_idandthread_idcolumns tolaragram_sessions. Fresh installs get them fromlaragram:install; an existing app must add them, or the session upsert fails on every update — see Upgrading from 1.x.
Scenes (Wizards)
For multi-step forms — registration, an order, a survey — a scene manages the whole flow for you: it asks each step's question, validates the answer, stores it, and hands all answers to a completion handler. It's built on top of stations, but you don't wire them by hand.
Define scenes in routes/laragram/scenes.php:
Enter the scene from any route handler, and handle the result when it finishes:
More step/scene options: ->when(fn ($ctx) => …) (conditional steps), ->allowBack('/back'), ->timeout($minutes) + ->onTimeout(), ->onInvalid($view|$closure) (custom error message), and typed extractors ->expectContact()/expectLocation()/expectPhoto()/expectCallback(). A SceneContext exposes get()/all()/has()/request()/user(). Back navigation drops any answers a changed earlier answer makes irrelevant (so onComplete never sees inconsistent data), and an invalid reply re-asks the step without resetting the timeout() clock.
Scenes require the
databaseauth driver (step state is persisted between updates inlaragram_sessions.payload). Scaffold one withphp artisan laragram:make:scene order --steps=size,address.
See the Scenes wiki page for the full reference.
Payments (Telegram Stars & fiat)
Send invoices, answer the checkout steps, and handle completed payments — for both Telegram Stars (XTR) and fiat currencies (Telegram Payments 2.0):
- Amounts are in the currency's smallest unit (cents; whole stars for
XTR). approveShipping(array $options)/declineShipping($reason)cover flexible-price shipping.BotRequestaccessors:preCheckoutQuery(),shippingQuery(),successfulPayment().Events\PaymentReceivedfires for every completed payment — independently of routing — so you can grant the entitlement from a single listener (invoicePayload(),chargeId(),totalAmount(),isStars()).- Opt-in payment history: set
LARAGRAM_PAYMENTS_STORE=true(plus the publishedlaragram_paymentsmigration) and every payment is persisted idempotently. - Outbound actions live on the
Services\Paymentsservice:invoiceLink()(createInvoiceLink),refund($userId, $chargeId)(refundStarPayment),starTransactions().
Inline Mode
Answer inline_query updates — the results users get typing @yourbot query in any chat:
Result builders: article(), photo(), gif(), video(), document(), cachedPhoto(), sticker(), and raw(array) for any other InlineQueryResult type. Answer-level options: cache(), personal(), nextOffset() (pagination), button(). The inline_query_id is injected automatically.
Files
Sending a local file or URL
BotResponse speaks JSON, so it can send media only by file_id or URL — it cannot upload bytes. MediaUploader does that in one outbound call and hands you the permanent file_id Telegram assigns:
Types: photo, document, audio, video, voice, animation, video_note, sticker. Telegram requires a destination chat even for an upload, so the media is delivered to $chatId as a side effect — use the recipient's id, or any chat the bot can reach. The source is trusted server-side input: a local path is read straight off disk, and only http/https URLs are accepted (file://, ftp://, … are rejected). Never pass unvalidated user input.
Receiving a file
Turn a file a user sent to the bot into bytes or a stored file — the mirror image of MediaUploader:
BotRequest::fileId() finds the file across the common media fields (photo → largest size, document, video, audio, voice, animation, video_note, sticker). Downloads are SSRF-hardened and size-capped:
Message Reactions
React to messages and handle users' reactions:
react() accepts an emoji string, a list of them, raw ReactionType arrays (custom emoji), or [] to clear the bot's reaction.
Telegram delivers
message_reaction/message_reaction_countupdates only whenallowed_updatesexplicitly includes them — pass it when callingsetWebhook.
Queue (optional, for scale)
By default Laragram processes each update inside the webhook request. Under bursts of concurrent users you can offload processing to a queue: the webhook validates the update, dispatches a job, and answers 200 OK immediately, while routing and the outbound Bot API calls run on a worker.
Enable it in .env:
Run a worker:
- The four webhook middleware (verify → auth → dedup → throttle) still run synchronously, so only verified, non-bot, non-duplicate, rate-limited updates are ever queued.
- Per-user ordering: jobs are serialized per sender (
WithoutOverlapping), avoiding session races. This is mutual exclusion, not strict FIFO — run a single worker per queue if a step-by-step station flow must never reorder. - Throughput: a named
laragramrate limiter caps global execution to stay under Telegram's ~30 msg/sec outbound limit. - Privacy: the job implements
ShouldBeEncrypted, so the payload (which carries user PII) is encrypted at rest in the queue store.
Use Redis in production — the rate limiter and the per-user lock need a shared cache store to be accurate across multiple workers. When disabled (the default), behaviour is fully synchronous and unchanged.
Broadcasting (mass messaging)
Send one message to your whole user base — announcements, promos, downtime notices — with the BotBroadcast facade or the laragram:broadcast command. Requires the database auth driver (there are no persisted users under array).
Three ways to compose a broadcast — all can carry full formatting, inline/reply keyboards, and media:
| Method | Rendered | Use for |
|---|---|---|
text($text) |
once | a plain announcement |
view($name, $data) |
per recipient (localized, $user in scope) |
a reusable, translated message; buttons/media come from the view's component files |
message(BotResponse) |
once | an ad-hoc rich message (keyboard/media built inline) — throws on an empty BotResponse |
From the CLI:
- Delivery scales with your setup. With the queue enabled, a broadcast dispatches one job per recipient, throttled by the same
laragramrate limiter as incoming updates; otherwise it sends synchronously, paced just under Telegram's ~30 msg/sec limit.text()/view()messages are rendered per recipient (so views localize to each user); amessage()payload is rendered once at compose time. - Unreachable users self-prune. The first time a send fails because a user blocked the bot, deactivated, or the chat is gone, that user is marked inactive (
User::deactivate()) so future broadcasts skip them. This runs for every send, not just broadcasts, via theBotExceptionHandledevent — toggle withLARAGRAM_BROADCAST_DEACTIVATE_UNREACHABLE.
Admin Panel
A bundled, server-rendered dashboard for your bot's user base — metrics, users & roles, sessions, and a broadcast composer. Self-contained (own routes + Blade views, no build step, no external dependencies), mounted at /laragram/admin by default. Requires the database auth driver.
The panel is protected by its own login page backed by the laragram_admins table — no host-app web auth is required, and it works in production from any IP. Run the migration (laragram:install scaffolds it), then create an account:
Browse to /laragram/admin and you'll be redirected to the login page. Passwords are hashed automatically by Models\Admin (a dedicated Authenticatable, distinct from the Telegram User), which logs in through a self-registered laragram_admin session guard — so no config/auth.php edits are needed.
Escape hatch: if you'd rather reuse your host app's own web auth, define a viewLaragram Gate ability — it overrides the login and decides access itself (a denying gate is a hard 403):
Pages: Dashboard (total/active users, new today/week, roles, active sessions) · Users (set role, activate/deactivate) · Sessions (browse, prune) · Broadcast — compose a plain-text message or pick an on-disk view (with optional JSON data) so an operator can send a fully-formatted, per-recipient-localized message with buttons/media, not just text; dry-run count or send, honouring the queue/sync path.
Observability
Laragram never lets an exception escape update processing — routing, delivery, and the queued job all funnel their errors through ExceptionHandler, which logs reportable ones and silences user-unreachable ones. That makes silently-handled failures invisible (they never reach failed_jobs). The BotExceptionHandled event is the seam for surfacing them:
Listening is optional (no listener = near-zero-cost no-op); dispatch is guarded, so a faulty listener can never break exception handling.
Network failures are retried. A call that fails before reaching Telegram — DNS, connect, TLS handshake, or a timeout during the handshake — is retried with a jittered exponential back-off (telegram.retries, default 2). Telegram API errors are never retried, and neither is an attempt whose request body already went out, so a retry can never deliver the same message twice. When the retries run out you get a TransportException; the rest of that message batch is skipped rather than burning a connect timeout each.
A recurring cURL error: SSL connection timeout (cURL errno 28) is a network problem on your server, not a bot bug — most often a blackholed IPv6 route to api.telegram.org. Compare curl -4 -o /dev/null -w '%{time_appconnect}\n' https://api.telegram.org/ with the same command without -4; if -4 is healthy, set LARAGRAM_API_IP_VERSION=4. If the hosting blocks Telegram outright, LARAGRAM_API_PROXY routes calls through a proxy.
Testing
Feature-test whole bot flows in-process — no HTTP, no Telegram. botReceives() runs the real auth → router → session → delivery pipeline against a recording API double, so assertions inspect the messages your bot actually sends:
- Updates:
message(),groupMessage(),topicMessage(),callbackQuery(),inlineQuery(),chosenInlineResult(),editedMessage(),channelPost(),preCheckoutQuery(),shippingQuery(),successfulPaymentMessage(),messageReaction(). - Assertions:
assertBotRepliedWith()/assertBotRepliedText()/assertResponseContains()(the first message),assertBotRepliedTimes()/assertNthReplyWith()/assertNthReplyText()(a multi-message batch),assertBotRepliedInThread(),assertUserRedirectedTo(),assertNoResponse(), and the scene setassertInScene()/assertSceneStep()/assertSceneData()/assertNotInScene(). - The webhook middleware (secret verification, dedup, throttling) is not run — test those as ordinary Laravel middleware.
Configuration
laragram:install publishes config/laragram.php. Everything has a working default; the keys you are most likely to touch:
| Key | Default | Purpose |
|---|---|---|
auth.driver |
database |
database persists users and sessions; array keeps a user in memory with no DB I/O |
auth.session.lifetime |
10080 |
Minutes before a session (and its station) expires |
telegram.username |
— | Your bot's @username, so /cmd@YourBot matches in groups |
paths.route · paths.scenes |
laragram/routes · laragram/scenes |
File names under routes/; a subdirectory is allowed |
paths.views |
laragram |
View directory under resources/ |
bot.languages |
['en'] |
Locales your views are translated into |
rate.max_attempts · rate.decay_seconds |
60 · 60 |
Per-user inbound rate limit |
security.verify_secret |
true |
Validate the X-Telegram-Bot-Api-Secret-Token header |
telegram.retries · telegram.retry_delay |
2 · 300 |
Retries on a network-level failure talking to Telegram, and the base back-off in ms |
telegram.timeout · telegram.connect_timeout |
30 · 10 |
Seconds for a whole API call, and for the connect + TLS handshake alone |
telegram.ip_version |
— | Pin outgoing calls to 4 or 6; set 4 when the host's IPv6 route to Telegram blackholes |
Auth drivers. The database driver is the default and the one to use: it persists the User, the station, and scene state, and it is required by scenes, broadcasting, the admin panel, roles, and update deduplication. The array driver skips all database I/O — useful for a stateless bot or a fast test suite, but every user is permanently at station start.
Roles. The role column is never written by the auth drivers — assign it yourself:
Then gate routes with ->role('admin'), or check $user->hasRole('admin') / $user->isAdmin() in a handler.
See the Configuration wiki page for every key.
Artisan Commands
| Command | Description |
|---|---|
laragram:install |
Bootstraps the host app: config, migrations, blank route/scene files, .env variables |
laragram:publish |
Publishes the runnable demo: views, lang, demo controllers + routes (incl. Stars payments, inline mode, file receiving) |
laragram:webhook:set |
Register the webhook with Telegram |
laragram:webhook:remove |
Remove the webhook |
laragram:getMe |
Display bot info (getMe) |
laragram:webhook:info |
Display current webhook state |
laragram:poll |
Start long-polling (dev without a public URL) |
laragram:route:list |
List all registered bot routes |
laragram:route:match {event} {text} |
Debug: show which route matches |
laragram:session:prune |
Delete expired sessions |
laragram:make:controller |
Scaffold a new bot controller |
laragram:make:view |
Scaffold a new bot view directory |
laragram:make:scene |
Scaffold a new scene (wizard) in the scenes file |
laragram:scene:list |
List all registered scenes |
laragram:set-role {uid} {role} |
Assign a role to a user |
laragram:broadcast {message?} |
Mass-message users (--view, --data, --role=*, --include-inactive, --dry-run, --no-confirm) |
laragram:admin:create {username?} |
Create (or reset the password of) an admin-panel login account (--name, --password) |
laragram:admin:delete {username} |
Delete an admin-panel login account |
Supported Update Types
| Event | Matched against |
|---|---|
message / edited_message / channel_post / edited_channel_post |
text |
callback_query |
data |
inline_query |
query |
chosen_inline_result |
result_id |
shipping_query / pre_checkout_query |
invoice_payload |
poll |
question |
poll_answer |
option_ids |
my_chat_member / chat_member / chat_join_request |
from |
message_reaction |
user |
message_reaction_count |
reactions |
Changelog
See CHANGELOG for release notes.
License
MIT — see LICENSE.