Telegram-Nachrichten mit Symfony: der einfache, robuste Weg
Wenn ich in Symfony etwas „einfach nur zuverlässig“ haben will, greife ich ungern zu großen Abstraktionsschichten. Telegram ist ein gutes Beispiel: Die Bot-API ist im Kern ein HTTP-POST. Mehr braucht es nicht.
In diesem Artikel halte ich die Lösung absichtlich schlank:
- Symfony HttpClient (ist ohnehin Standard)
- Telegram Bot API direkt (kein Telegram-SDK, kein Notifier-Overhead)
- Konfiguration über
.env - Fehlerfälle sauber geloggt, aber ohne den Hauptprozess zu stören
Das Ergebnis ist eine kleine Service-Klasse, die ich überall aus meinem Symfony-Projekt heraus verwenden kann.
Voraussetzungen
- Symfony-Projekt (ab 5.x, läuft auch in 6/7)
symfony/http-client(ist in vielen Projekten bereits drin)- Ein Telegram-Bot (Token) und eine Chat-ID
Schritt 1: Bot anlegen und Token bekommen
Der Token kommt immer über den Telegram-Bot „BotFather“.
- Telegram öffnen
- In der Suche: BotFather
- Chat mit BotFather starten
- Befehl:
/newbot - Bot-Namen vergeben (Anzeige-Name)
- Bot-Usernamen vergeben (muss auf
botenden, z. B.MeinProjektAlertBot) - BotFather gibt anschließend den API Token aus
Dieser Token ist der Schlüssel, mit dem dein Server Telegram-Requests signiert. Wer den Token hat, kann als Bot Nachrichten senden. Entsprechend gehört er in .env und niemals ins Repo.
Schritt 2: Chat-ID herausfinden (Privatchat)
Die Chat-ID ist das Ziel, an das Telegram die Nachricht sendet. Für den Privatchat (du willst dir selbst Nachrichten schicken) geht das so:
- Den gerade angelegten Bot in Telegram öffnen
- Im Chat mit dem Bot:
/startsenden
(Wichtig: Ohne das ist der Bot in vielen Fällen „nicht initialisiert“ und Updates fehlen.) - Jetzt per HTTP die Updates abfragen.
Du kannst dafür die Telegram Bot API direkt im Browser oder per curl nutzen:
curl -s "https://api.telegram.org/bot<DEIN_TOKEN>/getUpdates"
In der JSON-Antwort findest du typischerweise:
result[0].message.chat.id
Das ist deine chat_id.
Beispiel (vereinfacht):
{
"ok": true,
"result": [
{
"message": {
"chat": {
"id": 8382328232,
"type": "private"
}
}
}
]
}
Hier wäre die Chat-ID: 8382328232.
Schritt 3: Chat-ID in Gruppen herausfinden
Wenn du an eine Gruppe senden willst:
- Bot zur Gruppe hinzufügen
- In der Gruppe eine Nachricht senden (beliebig)
- Wieder:
curl -s "https://api.telegram.org/bot<DEIN_TOKEN>/getUpdates"
Dann findest du die Gruppen-Chat-ID ebenfalls in message.chat.id.
Wichtig: Gruppen-IDs sind häufig negativ und beginnen oft mit -100….
Schritt 4: Kurztest (ohne Symfony)
Bevor ich Symfony anfasse, mache ich immer einen direkten Test. So weiß ich, dass Token und Chat-ID stimmen.
curl -X POST "https://api.telegram.org/bot<DEIN_TOKEN>/sendMessage" \
-H "Content-Type: application/json" \
-d '{"chat_id":"<CHAT_ID>","text":"TEST OK"}'
Wenn {"ok":true,...} zurückkommt, ist alles korrekt.
Schritt 5: Symfony-Service zum Senden
Jetzt kommt der eigentliche Symfony-Teil. Ich implementiere einen kleinen Service, der:
- per HTTP POST sendet
- HTML als parse_mode nutzt (robust, solange man HTML-escaped)
- Timeout setzt
- Fehler loggt, aber nicht den Hauptprozess kaputt macht
.env
TELEGRAM_BOT_TOKEN=yyyyy
TELEGRAM_CHAT_ID=xxxxx
TelegramService.php
<?php
// backend/src/Service/TelegramService.php
declare(strict_types=1);
namespace App\Service;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class TelegramService
{
private const string API_BASE = 'https://api.telegram.org';
private const float HTTP_TIMEOUT_SECONDS = 5.0;
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly LoggerInterface $logger,
private readonly string $botToken,
private readonly string $chatId
) {
if ($this->botToken === '') {
throw new RuntimeException('Telegram bot token is empty');
}
if ($this->chatId === '') {
throw new RuntimeException('Telegram chat id is empty');
}
}
public function send(string $text): void
{
$url = self::API_BASE . '/bot' . $this->botToken . '/sendMessage';
try {
$response = $this->httpClient->request('POST', $url, [
'timeout' => self::HTTP_TIMEOUT_SECONDS,
'json' => [
'chat_id' => $this->chatId,
'text' => $this->escape($text),
'parse_mode' => 'HTML',
'disable_web_page_preview' => true,
],
]);
$statusCode = $response->getStatusCode();
if ($statusCode !== 200) {
$this->logger->warning('Telegram API returned non-200 status', [
'status' => $statusCode,
'body' => $response->getContent(false),
]);
}
} catch (TransportExceptionInterface $exception) {
$this->logger->error('Telegram transport error', [
'exception' => $exception,
]);
} catch (\Throwable $exception) {
$this->logger->error('Telegram unexpected error', [
'exception' => $exception,
]);
}
}
private function escape(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
Schritt 6: Service konfigurieren (Env-Injection)
Symfony kann Strings aus .env nicht „magisch“ autowiren, dafür ist eine explizite Konfiguration nötig.
config/services.yaml
services:
App\Service\TelegramService:
arguments:
$botToken: '%env(TELEGRAM_BOT_TOKEN)%'
$chatId: '%env(TELEGRAM_CHAT_ID)%'
Schritt 7: Verwendung im Code
Jetzt kannst du überall (Controller, Handler, Middleware, Cron-Job) den Service injizieren und senden:
$telegramService->send('Job fertig: Kapitel 12');
In einem Messenger-Handler ist das besonders praktisch, weil du bei „fertig“ oder „failed“ sofort pingst.
Typische Fehler und schnelle Diagnose
401 Unauthorized
- Token falsch
- Bot wurde erneuert,
.envnicht aktualisiert
Test:
curl -s "https://api.telegram.org/bot<TOKEN>/getMe"
400 chat_id is empty
- Chat-ID fehlt oder ist falsch
- In Gruppen: falsche ID (muss meist
-100...sein)
400 can’t parse entities
- Du nutzt
parse_mode=HTMLund sendest ungefiltertes HTML - Lösung:
htmlspecialchars()wie oben
Fazit
Telegram in Symfony muss kein eigenes Ökosystem sein. Wenn ich das Ziel „zuverlässig, wartbar, keine Magie“ ernst nehme, gewinnt die direkte Bot-API per Symfony HttpClient:
- schneller implementiert
- besser debugbar
- kein „Abstraktions-Layer“, der in Randfällen dazwischenfunkt
- produktionsstabil
Wer danach mehr will (z. B. verschiedene Chats, Routing, Prioritäten), kann immer noch erweitern. Aber der Einstieg ist: ein HTTP-POST, sauber verpackt.
0 Kommentare