laraxgram/laraquest
LaraQuest is a lightweight Laravel package for adding quests and gamification to your app. Define objectives, track user progress, and award rewards with a clean, configurable API that integrates smoothly with Eloquent and existing Laravel projects.
1. Install and publish
composer require laraxgram/laraquest
php artisan vendor:publish --tag=laraquest-config # verify the tag with `php artisan vendor:publish` if this one misses
2. Configure the bot
Set the token in .env and confirm the config reads it:
TELEGRAM_BOT_TOKEN=123456:AA...
TELEGRAM_WEBHOOK_SECRET=some-long-random-string
// config/laraquest.php (shape may differ — read the published file before editing)
return [
'bots' => [
'default' => [
'token' => env('TELEGRAM_BOT_TOKEN'),
'secret' => env('TELEGRAM_WEBHOOK_SECRET'),
],
],
];
3. Where to look first
config/laraquest.php — bot registry, base URI, timeout, webhook path. This file tells you more about the package's real API surface than any README.vendor/laraxgram/laraquest/src/ — check what is bound as singleton vs. factory. That determines whether you can safely mutate config at runtime.src/ for the client/request builder class and any Events/ or Handlers/ directory — that is where "receiving updates" is wired.php artisan route:list | grep -i laraquest) to see if the webhook endpoint is auto-registered or expected in your own routes/api.php.Because the upstream repository is not linked on Packagist, treat the vendor directory as the documentation. Read the source once, then pin the version in composer.json.
4. First use case: an echo bot
Register the webhook, then respond to the simplest possible update:
php artisan laraquest:webhook set https://example.com/telegram/webhook
# or, if no command exists:
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook?url=..."
// routes/api.php
Route::post('/telegram/webhook', TelegramWebhookController::class)
->middleware('laraquest.webhook'); // if such middleware ships; otherwise verify the secret yourself
public function __invoke(Request $request)
{
$update = $request->all();
if ($message = $update['message'] ?? null) {
Laraquest::sendMessage(
chatId: $message['chat']['id'],
text: 'You said: '.($message['text'] ?? '(not text)'),
);
}
return response()->noContent();
}
Once that round-trip works you have validated token, transport, webhook secret, and response serialisation in one shot — everything else is variations on it.
Wrap outgoing calls in a thin application service. Do not scatter facade calls through controllers. A TelegramNotifier class that takes the package client via constructor injection gives you one place to add retries, logging, and a fake in tests. This matters more than usual here because the package is small and its client contract may change across minor versions.
Queue everything that isn't a direct reply. A webhook reply (answering the incoming message) can be synchronous. Anything triggered by a domain event — order shipped, invoice paid, new signup — should go through a queued job:
class SendTelegramMessage implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public array $backoff = [1, 5, 15];
public function __construct(
public int|string $chatId,
public string $text,
) {}
public function handle(TelegramClient $client): void
{
$client->sendMessage($this->chatId, $this->text);
}
}
This is the single highest-value pattern with a package like this: it turns Telegram's rate limits and downtime into ordinary queue behaviour instead of user-facing 429s.
Dispatch on update type, not on raw array shape. Telegram updates are a union type (message, edited_message, callback_query, inline_query, chat_member, …). Route them:
match (true) {
isset($update['message']) => $this->onMessage($update['message']),
isset($update['callback_query']) => $this->onCallback($update['callback_query']),
isset($update['chat_member']) => $this->onMembership($update['chat_member']),
default => null,
};
If the package ships its own event classes, use those instead — but keep the dispatch table in your code, not in a giant switch inside the package callback.
Use a state machine for multi-step conversations. Telegram has no session. Store the user's current step and partial data in the database or cache keyed by chat_id + user_id, and have your message handler read/resolve that state. Do not try to derive intent from chat history.
Multi-bot layouts. If config/laraquest.php exposes a bots map, resolve a client per bot rather than assuming the default:
$client = Laraquest::bot('alerts'); // confirm the accessor name in source
This lets you run a user-facing support bot and an ops-alert bot from one app with separate tokens and separate rate-limit budgets.
Testing. Telegram's API is HTTP, so Http::fake() is almost always enough — no need to boot the package's transport:
Http::fake(['api.telegram.org/*' => Http::response(['ok' => true, 'result' => []])]);
$this->postJson('/telegram/webhook', $update)->assertNoContent();
Http::assertSent(fn ($req) => str_contains($req->url(), 'sendMessage'));
For incoming updates, keep a fixture per update type in tests/Fixtures/telegram/. Copy them verbatim from real updates — hand-written fixtures miss fields like entities, from.is_bot, and message_thread_id that cause subtle branching bugs.
Integration tip: reuse the same handler from a Laravel Notification channel or an artisan command. A php artisan telegram:poll local-command path against a dev bot is the fastest debug loop; you get to iterate without redeploying a public HTTPS endpoint.
The repository being unlinked is the biggest risk. 12 stars and no visible source means no issue tracker, no changelog, and no guarantee of a timely security fix. Before adopting: read the whole src/ tree (it is small), check the last commit date against the release date, and pin "laraxgram/laraquest": "^x.y" with a committed composer.lock. If you find anything alarming in the client — disabled TLS verification, logging of request bodies containing the token — fork it into your own namespace rather than relying on upstream.
Answer the webhook with 200 fast. Telegram retries non-2xx responses and will re-deliver the same update. If your handler does real work inline, it will eventually exceed the timeout and you will process duplicates. Return 200 (or 204) immediately and push the work to the queue — or, at minimum, persist the raw update first.
Deduplicate on update_id. Even with a fast 200, retries happen. Store processed update_ids (a unique index with a short TTL, or Cache::add("tg:{$update->update_id}", true, 3600)) and bail early. This is cheap and saves you from double-sent confirmations.
Reject requests without the secret header. Set secret_token when registering the webhook, and verify X-Telegram-Bot-Api-Secret-Token on every request with a constant-time comparison (hash_equals). If the package's middleware does this, confirm it in source — do not assume. Without it, anyone who learns your endpoint URL can forge updates.
Webhook requirements: HTTPS only, valid publicly trusted certificate (self-signed is rejected), and the port must be one of 443, 80, 88, or 8443. A local dev tunnel is fine; http:// or :8000 is not. Also remember only one webhook URL exists per token — running staging and production against the same bot token means they fight over it. Use separate bots.
Markdown/HTML parse mode will reject unescaped user content. Any _, *, [, or < in a user-supplied string breaks the send with 400 can't parse entities. Either escape per parse mode or send with parse_mode omitted. Related: the hard limit is 4096 characters per message — chunk long output on paragraph boundaries rather than letting the API 400 on you.
Rate limits are real and asymmetric. Roughly 30 messages/second globally, but about 20 messages/minute into a single group chat. Bulk-broadcast features must be queued with a throttle; the package will not do this for you. 429 responses carry retry_after in parameters — honour it in the job's backoff() or release().
Run php artisan config:cache once after editing config, and again after deploy. A stale cached config with an old token produces 401 Unauthorized that looks like a code bug. Also note env() calls inside package config files return null once config is cached — keep env reads in the config file only, never in your own app/ code.
Extension points to reach for before forking: the container binding for the HTTP client (swap in a client with custom Guzzle middleware for logging or metrics), the base URI config (point at a local mock server in tests), and any event dispatched after a successful send (hook an audit log there). If none of these are exposed, a thin wrapper class of your own around the package is cheaper than patching vendor code — and survives composer update.
Debugging order for a silent bot: token validity (getMe) → webhook status (getWebhookInfo, which surfaces last_error_message and pending update count) → whether your endpoint returns 200 → whether the secret header matches → queue worker actually running. getWebhookInfo alone resolves the majority of "the bot stopped responding" reports.
How can I help you explore Laravel packages today?