PHPackages                             wekser/laragram - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. wekser/laragram

ActiveLibrary[API Development](/categories/api)

wekser/laragram
===============

Laravel package for easy develop Telegram Bot.

v1.6(1y ago)798[2 PRs](https://github.com/wekser/laragram/pulls)MITPHPPHP ^8.0CI passing

Since Feb 5Pushed 5d ago3 watchersCompare

[ Source](https://github.com/wekser/laragram)[ Packagist](https://packagist.org/packages/wekser/laragram)[ Docs](https://github.com/wekser/laragram)[ RSS](/packages/wekser-laragram/feed)WikiDiscussions master Synced 2w ago

READMEChangelog (10)Dependencies (4)Versions (30)Used By (0)

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

---

Installation
------------

[](#installation)

```
composer require wekser/laragram
```

Publish the config and run migrations:

```
php artisan laragram:install
php artisan migrate
```

Add your bot credentials to `.env`:

```
LARAGRAM_BOT_TOKEN=your-telegram-bot-token
LARAGRAM_WEBHOOK_PREFIX=laragram
LARAGRAM_WEBHOOK_SECRET=generated-secret
```

Register the webhook:

```
php artisan laragram:webhook:set
```

---

How It Works
------------

[](#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
------

[](#routes)

Bot routes live in `routes/laragram.php`. Use the injected `$collection` variable or the `BotRoute` facade — both are equivalent.

```
use Wekser\Laragram\Facades\BotRoute;

// Match /start from any station
BotRoute::get('message')
    ->contains('/start')
    ->call([StartController::class, 'index']);

// Match any text when user is at 'ask_name' station
BotRoute::get('message')
    ->from('ask_name')
    ->call([OnboardingController::class, 'saveName']);

// Match callback with a named param, admin only
BotRoute::get('callback_query')
    ->from('home')
    ->contains('/action {id}')
    ->role('admin')
    ->call([AdminController::class, 'action']);

// Catch-all fallback
BotRoute::fallback()->call([StartController::class, 'fallback']);
```

**DSL reference:**

MethodDescription`->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`->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 shared station and role to multiple routes:

```
BotRoute::group(function ($c) {
    $c->get('message')->contains('/users')->call([AdminController::class, 'users']);
    $c->get('callback_query')->call([AdminController::class, 'callback']);
}, from: 'admin_panel', roles: 'admin');
```

---

Controllers
-----------

[](#controllers)

Controllers are resolved through Laravel's IoC container — constructor injection works out of the box.

```
use Wekser\Laragram\BotRequest;
use Wekser\Laragram\BotResponse;
use Wekser\Laragram\Models\User;

class StartController extends Controller
{
    public function __construct(protected BotResponse $response) {}

    public function index(BotRequest $request, User $user): BotResponse
    {
        return $this->response
            ->text("Hello, {$user->first_name}!")
            ->redirect('home');
    }
}
```

**`BotRequest`** wraps the incoming update:

```
$request->get('text');          // dot-notation access to any update field
$request->input('id');          // named {param} from the matched route pattern
$request->message();            // the message sub-object
$request->callbackQuery();      // the callback_query sub-object
$request->validate([...]);      // Laravel validation on the update payload
```

**`BotResponse`** builds the reply:

```
$response->text('Hello!')                          // sendMessage (HTML by default)
$response->text('Hello!', 'MarkdownV2')            // MarkdownV2 parse mode
$response->text('Hello!', null)                    // no escaping
$response->view('welcome', ['name' => 'Alice'])    // render a view directory
$response->photo($fileId, caption: 'A photo')      // sendPhoto
$response->document($fileId)                       // sendDocument
$response->edit('Updated text')                    // editMessageText
$response->answer('Done!', showAlert: true)        // answerCallbackQuery
$response->delete()                                // deleteMessage
$response->action('typing')                        // sendChatAction
$response->keyboard([...])                         // attach reply_markup (call after content)
$response->redirect('next_station')                // move user to a new station
```

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

[](#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:

```
public function welcome(): array
{
    return [
        $this->response->view('greeting'),
        $this->response->text('Here is a quick tip 👇'),
        $this->response->photo($fileId, caption: 'And a picture')->redirect('home'),
    ];
}
```

- 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 the `BotResponse` facade.

---

Views
-----

[](#views)

Views are **directories** under `resources/laragram/` (dot notation → subdirectories). Each component of the message is a separate PHP file:

```
resources/laragram/
└── welcome/
    ├── text.php               ← message text — use {{ expr }} for dynamic values
    ├── inline_keyboard.php    ← call button() / href() / row()
    └── reply_keyboard.php     ← call reply() / row() / resize() / one_time()

```

**`text.php`** — write plain text plus your own HTML markup (default parse mode is `HTML`); `{{ }}` escapes a value, `{!! !!}` emits it raw:

```
Hello, {{ $first_name }}!
{!! __('laragram.welcome.body') !!}

```

Static markup (`…`) 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.

**`inline_keyboard.php`** — use global helper functions:

```
button('Click me', 'action_1');
href('Open site', 'https://example.com');
web_app('Open Mini App', 'https://example.com/app');
row();
button('Row 2', 'action_2');
```

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`:**

```
resize();
reply('Option A'); reply('Option B');
row(); reply('Help');
```

**`media.php`** — for `sendMediaGroup`:

```
photo($data['photo_id'], caption: 'First');
video($data['video_id']);
```

For single media, add a `photo.php` (or `video.php`, `document.php`, etc.) containing just the file\_id or URL.

Render with:

```
$response->view('welcome', ['first_name' => $user->first_name]);
```

Scaffold a new view directory:

```
php artisan laragram:make:view welcome
```

---

Keyboards (programmatic)
------------------------

[](#keyboards-programmatic)

For building keyboards in controllers without view files:

```
use Wekser\Laragram\Telegram\Keyboards\InlineKeyboard;
use Wekser\Laragram\Telegram\Keyboards\ReplyKeyboard;
use Wekser\Laragram\Telegram\Keyboards\ForceReply;

$response->text('Choose:')->keyboard(
    InlineKeyboard::make()
        ->button('Yes', 'confirm')
        ->button('No', 'cancel')
        ->row()
        ->href('Open site', 'https://example.com')
        ->webApp('Open Mini App', 'https://example.com/app')
        ->toArray()
);

$response->text('Choose:')->keyboard(
    ReplyKeyboard::make()
        ->button('Option A')->button('Option B')
        ->row()->button('Help')
        ->resize()->oneTime()
        ->toArray()
);

ReplyKeyboard::remove();   // ['remove_keyboard' => true]
ForceReply::make()->placeholder('Type here…')->toArray();
```

`InlineKeyboard` covers the full button API (`switchInline()`, `switchInlineChosen()`, `switchInlineChosenChat()`, `loginUrl()`, `copyText()`, `pay()`, `callbackGame()`, plus a `paginate()` helper). 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)
-----------------------

[](#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:

```
// routes/laragram.php
BotRoute::get('message')->contains('/start')->call([Ctrl::class, 'start']);
BotRoute::get('message')->from('ask_name')->call([Ctrl::class, 'saveName']);
BotRoute::get('message')->from('ask_email')->call([Ctrl::class, 'saveEmail']);

// controller
public function start(): array
{
    // Send several messages at once; the next station comes from the last
    // response that calls redirect() (here, the question).
    return [
        $this->response->text('Welcome! 👋'),
        $this->response->text("What's your name?")->redirect('ask_name'),
    ];
}

public function saveName(BotRequest $request): BotResponse
{
    // store name ...
    return $this->response->text('Now your email:')->redirect('ask_email');
}
```

Debug routing in your terminal:

```
php artisan laragram:route:match message "/start"
php artisan laragram:route:match message "hello" --station=ask_name
```

---

Queue (optional, for scale)
---------------------------

[](#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`:

```
LARAGRAM_QUEUE_ENABLED=true
LARAGRAM_QUEUE_CONNECTION=redis   # leave unset to use your default connection
LARAGRAM_QUEUE_NAME=default
LARAGRAM_QUEUE_RATE_LIMIT=25      # max update jobs/sec across all workers
```

Run a worker:

```
php artisan queue:work --queue=default
```

- 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 `laragram` rate 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.

---

Observability
-------------

[](#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:

```
use Illuminate\Support\Facades\Event;
use Wekser\Laragram\Events\BotExceptionHandled;

Event::listen(function (BotExceptionHandled $e) {
    // $e->exception, $e->reportable (was it logged?), $e->terminal (user unreachable?)
    if ($e->terminal) {
        // e.g. count how many users blocked the bot
    }
    // push to Sentry / StatsD / Horizon tags…
});
```

Listening is optional (no listener = near-zero-cost no-op); dispatch is guarded, so a faulty listener can never break exception handling.

---

Artisan Commands
----------------

[](#artisan-commands)

CommandDescription`laragram:install`Publish all package assets`laragram:publish`Selective publish (config / migrations / views / routes)`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:set-role {uid} {role}`Assign a role to a user---

Supported Update Types
----------------------

[](#supported-update-types)

EventMatched 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`---

Changelog
---------

[](#changelog)

See [CHANGELOG](CHANGELOG.md) for release notes.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

48

—

FairBetter than 94% of packages

Maintenance76

Regular maintenance activity

Popularity15

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity77

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~84 days

Recently: every ~207 days

Total

28

Last Release

409d ago

PHP version history (4 changes)v1.0.0PHP ^7.1.3

v1.2.1PHP ^7.3|^8.0

v1.5.0PHP ^8.0

v1.5.12PHP ^8.1

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/17425956?v=4)[Sergey Lapin](/maintainers/wekser)[@wekser](https://github.com/wekser)

---

Top Contributors

[![wekser](https://avatars.githubusercontent.com/u/17425956?v=4)](https://github.com/wekser "wekser (108 commits)")

---

Tags

laravellaravel-telegramtelegramtelegram-bottelegram-bot-apiapilaravellumenbottelegramchatbot

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/wekser-laragram/health.svg)

```
[![Health](https://phpackages.com/badges/wekser-laragram/health.svg)](https://phpackages.com/packages/wekser-laragram)
```

###  Alternatives

[mollie/laravel-mollie

Mollie API client wrapper for Laravel &amp; Mollie Connect provider for Laravel Socialite

3634.4M33](/packages/mollie-laravel-mollie)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
