symfony/telegram-notifier
Provides Telegram integration for Symfony Notifier. Configure via a TELEGRAM_DSN with token and chat ID, send ChatMessages with TelegramOptions for parse mode, inline keyboards, and other Bot API settings, and optionally target a local API server.
Install the package:
composer require symfony/telegram-notifier
Configure the DSN in .env:
TELEGRAM_DSN=telegram://BOT_TOKEN@default?channel=CHAT_ID
BOT_TOKEN with your Telegram bot token (from @BotFather).CHAT_ID with your target chat ID (e.g., -1001234567890 for a group or @username for a private chat).Send your first message in a Laravel controller or command:
use Symfony\Component\Notifier\Notifier;
use Symfony\Component\Notifier\Message\ChatMessage;
$notifier = new Notifier();
$message = new ChatMessage('Hello from Laravel!');
$notifier->send($message);
Use Telegram Notifier for real-time alerts (e.g., deployment notifications, errors):
$message = new ChatMessage('🚀 Deployment successful!');
$message->options((new TelegramOptions())->parseMode('MarkdownV2'));
$notifier->send($message);
Since v8.1.6: pass plain, unescaped text — the transport now escapes MarkdownV2 markup for you (see Gotchas #3).
Leverage TelegramOptions for rich messages:
$options = (new TelegramOptions())
->parseMode('MarkdownV2')
->disableWebPagePreview(true)
->replyMarkup((new InlineKeyboardMarkup())
->inlineKeyboard([
(new InlineKeyboardButton('View Logs'))->url('/logs'),
(new InlineKeyboardButton('Retry'))->callbackData('retry'),
])
);
$message = new ChatMessage('⚠️ Task failed!');
$message->options($options);
$notifier->send($message);
Send logs, screenshots, or documents:
// Public URL
$options = (new TelegramOptions())->document('https://example.com/report.pdf');
// Local file (uploaded via multipart)
$options = (new TelegramOptions())->uploadDocument(storage_path('logs/error.log'));
// File ID (from a previously uploaded file)
$options = (new TelegramOptions())->document('ABC123XYZ');
Handle callback queries (e.g., buttons) in a Laravel route:
use Symfony\Component\Notifier\Bridge\Telegram\TelegramOptions;
public function handleCallback(Request $request)
{
$callbackQueryId = $request->input('callback_query_id');
$options = (new TelegramOptions())
->answerCallbackQuery($callbackQueryId, showAlert: true);
$message = new ChatMessage('Action confirmed!');
$message->options($options);
$notifier->send($message);
}
Organize messages in threads (Symfony 7.3+):
$options = (new TelegramOptions())->messageThreadId('thread_123');
$message = new ChatMessage('Threaded update');
$message->options($options);
$notifier->send($message);
Use Laravel’s service container to dynamically configure notifications:
// config/services.php
'telegram' => [
'default_chat' => env('TELEGRAM_DEFAULT_CHAT', '-1001234567890'),
'chats' => [
'admin' => '-1009876543210',
'support' => '@support_bot',
],
],
// In a service
public function notifyAdmins(string $message)
{
$options = (new TelegramOptions())->chatId(config('services.telegram.chats.admin'));
$notifier->send(new ChatMessage($message)->options($options));
}
Integrate with Laravel events (e.g., Deployed, JobFailed):
// In EventServiceProvider
protected $listen = [
JobFailed::class => [TelegramNotifier::class, 'handleJobFailure'],
];
// In TelegramNotifier
public function handleJobFailure(JobFailed $event)
{
$message = new ChatMessage("Job [{$event->job}] failed: {$event->exception}");
$message->options((new TelegramOptions())->parseMode('MarkdownV2'));
$notifier->send($message);
}
Because the transport escapes markup as of v8.1.6, it is now safe to interpolate untrusted strings (usernames, exception messages containing _, *, ., -) into a MarkdownV2 message — no manual addslashes()-style pre-escaping:
$message = new ChatMessage("User {$user->name} triggered rule {$rule->slug}");
$message->options((new TelegramOptions())->parseMode('MarkdownV2'));
-1001234567890).@username) or positive IDs.getUpdates to discover chat IDs.symfony/telegram-notifier only registers the Telegram transport. In Laravel you must wire it into a Notifier yourself (or rely on symfony/notifier's notifier.transport_factory services), otherwise Notifier::send() silently has no transport to use.upload* methods for local files (e.g., uploadDocument).file_id. Use uploadSticker or a public URL.v8.1.6 ships bug #65644 — "Fix escaping of MarkdownV2 markup in TelegramTransport". The transport now performs the MarkdownV2 escaping of the message body before sending.
Practical consequences:
User \*John\* \_Doe\_ will now render with literal backslashes in Telegram (double-escaping). Pass User *John* _Doe_ instead.v8.1.4 / v8.1.5, the fix is absent and you still need to escape reserved characters (_ * [ ] ( ) ~ > # + - = | { } . !) yourself. Set "symfony/telegram-notifier": "^8.1.6"incomposer.json` for consistent behaviour across environments.\_, \., \- literally, or conversely Telegram rejecting with 400 Bad Request: can't parse entities — both point at an escaping mismatch between your code and the installed transport version.parseMode() entirely; plain text has no escaping rules and is the safest default for user-generated strings.MarkdownV2 fixture. Add a feature test that sends a message containing _ * . - ( ) and asserts the outbound payload, since this is exactly the code path that changed.// Correct on >= v8.1.6 — raw text, transport escapes it
$message = new ChatMessage('User *John* _Doe_ — score 9.5');
$message->options((new TelegramOptions())->parseMode('MarkdownV2'));
TransportExceptionInterface (Symfony\Component\Notifier\Exception\TransportExceptionInterface) gracefully:
use Symfony\Component\Notifier\Exception\TransportExceptionInterface;
try {
$notifier->send($message);
} catch (TransportExceptionInterface $e) {
Log::error('Telegram send failed', ['error' => $e->getMessage()]);
// Retry logic or fallback (e.g., email)
}
storage/logs/laravel.log for the outgoing sendMessage request body — the text field shows exactly what the escaping fix produced.400 with can't parse entities is a formatting problem; a 403/400 chat not found is a chat ID or bot-membership problem.InlineKeyboardMarkup or ReplyKeyboardMarkup for reusable buttons:
class AdminKeyboardMarkup extends InlineKeyboardMarkup
{
public function __construct()
{
$this->inlineKeyboard([
new InlineKeyboardButton('Approve')->callbackData('approve'),
new InlineKeyboardButton('Reject')->callbackData('reject'),
]);
}
}
class LoggingTelegramTransport implements TransportInterface { /* delegate + log */ }
This is also the right seam if you need to disable the new automatic escaping for a specific message (e.g., you already produce fully-escaped output).$notifier = new Notifier([$mockTransport]);
$notifier->send($message);
$this->assertCount(1, $mockTransport->getSentMessages());
text field — the best way to regression-test the MarkdownV2 escaping fix..env and validate input (e.g., chat IDs).TelegramOptions::protectContent(true) for sensitive updates.dispatch(new TelegramNotificationJob($message));
^8.1.6 so every environment gets the MarkdownV2 escaping fix (#65644) — mixing 8.1.4/8.1.5 between local and production causes messages to render differently for the same code.preg_replace, addcslashes, or manual \* / \_ sequences applied to Telegram message bodies and remove them after upgrading, or you will double-escape.How can I help you explore Laravel packages today?