defstudio/telegraph
Telegraph is a Laravel package for building Telegram bots with a fluent, testable API. It handles keyboards, media, conversations, webhooks, and polls, so you can ship interactive bots quickly using familiar Laravel patterns.
Telegraph is the practical Laravel wrapper for the Telegram Bot API: it gives you an eloquent-ish fluent builder for outgoing messages, a webhook endpoint + handler for incoming updates, and DB-backed models for bots and chats.
composer require defstudio/telegraph
php artisan vendor:publish --tag="telegraph-migrations"
php artisan vendor:publish --tag="telegraph-config"
php artisan migrate
php artisan telegraph:new-bot
php artisan telegraph:set-webhook
# inspect / debug with the webhook-info command shipped by your version
use DefStudio\Telegraph\Facades\Telegraph;
Telegraph::message('🚀 Deployed ' . config('app.env'))->send();
Where to look first, in this order:
config/telegraph.php — token/bot resolution, webhook URL, queue, HTTP timeouts, logging channel.src/Models/TelegraphBot.php and TelegraphChat.php — how bots/chats persist and how you address a chat.src/Telegraph.php + the Telegraph facade — the fluent API surface (message(), photo(), keyboard(), edit(), delete(), chatAction()).Outbound: always go through a chat record. Keep TelegraphChat rows for anything you message repeatedly (users, groups, alert channels), then:
$chat->message("Build #{$build->id} failed")->markdown()->send();
This gives you a stable chat_id you can look up by your own domain model instead of scattering IDs in config.
Rich messages, keyboards, edits. The builder composes cleanly — formatting (->markdown(), ->html()), media (->photo(), ->document(), ->location()), and keyboards/buttons for approvals:
$chat->message('Deploy to prod?')
->keyboard([[$approve, $reject]]) // Button::make('Approve')->action('approve')
->send();
Editing after a button press (->edit()) is the standard pattern for "click to acknowledge" UIs; delete for cleanup.
Queue the sends. Synchronous send() blocks your request/job on a Telegram round-trip. Use the package's queued dispatch (or wrap in your own job) for anything user-facing or fan-out. Ops script: one message, sync is fine. Notification fan-out: queue, always.
Inbound: webhook → idempotent handler. Telegram retries on any non-2xx or timeout, so key your handling on the update payload (update_id) and make handlers replay-safe. Persist whatever the bot needs from the update (chat, user, message intent) before doing slow work, and return 200 fast. Heavy processing belongs in a job dispatched from the handler.
Multi-bot. Bots live in the DB, so adding a second bot is an artisan command, not a deploy. Switch per-request with the bot scoping on the facade; this is the pattern for "public bot + internal ops bot" in one app.
Custom request URL (new in 1.73.0). Requests can now carry an explicit full URL instead of the generated one. Two immediate wins:
// self-hosted / proxied Bot API endpoint, or pinning an absolute webhook URL
Telegraph::message('hello')->url('https://bot-api.internal/...')->send();
Use it for local Bot API servers, egress proxies, and when your webhook URL can't be inferred from APP_URL (containers, blue/green hosts, multi-domain apps). Keep the override in config and read it from one place.
Deferred chat bot (new in 1.73.0). Bot/chat resolution can now be deferred rather than resolved eagerly. Day-to-day, this means cheap request construction — build the message, let the bot bind at send time — and fewer DB hits in flows that build several candidate messages but only send one. Check the PR for exact semantics before restructuring existing code; the safe migration is: build as you always did, resolve explicitly anywhere you read bot metadata ($bot->token, $bot->name) before send().
Testing. Use the package's fake so tests never hit the network, and assert on the outgoing payload (assertSent / assertSentData-style helpers). For webhook handlers, post a fixture update array to the webhook route and assert the DB/job side effects — don't try to unit-test the fluent builder.
APP_KEY rotation kills stored bots. Tokens are encrypted with your app key. Rotating it means re-entering every bot token. Treat bot rows as secrets in backups.php artisan serve won't cut it — use a tunnel (Expose/ngrok/Valet share) and re-run telegraph:set-webhook every time the tunnel URL changes. This is the #1 "why is nothing arriving" cause.ok: false. Telegram returns HTTP 200 with ok: false for many logical errors (bad chat_id, unescaped Markdown, file too large). Turn on the package's debug logging channel early; don't assume a 200 means delivered._, *, [ from user data breaks entities and the whole message fails. Prefer ->html() with e(), or escape interpolated values.429 with retry_after; Telegraph surfaces these as exceptions. Throttle your jobs and back off — don't retry immediately.send() rather than at build time. Anything that inspects bot metadata mid-pipeline should force resolution first; tests should assert on the send, not the construction.rtrim, validate with filter_var), and remember the override applies only to that request chain — don't assume it sticks for subsequent sends.SerializesModels semantics and make sure a worker is actually running. A "message never arrived" bug is often a stalled worker, not the API.update_id (unique index) — Telegram will redeliver on any hiccup, and duplicate "Approve deploy" actions are expensive.TelegraphChat to add your own relations is the cleanest way to integrate with existing domain models.telegraph:webhook-info tells you what Telegram thinks your URL is — check that before debugging your handler.How can I help you explore Laravel packages today?