Artikel

Telegram-Nachrichten mit Symfony: der einfache, robuste Weg

PHPSymfony

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“.

  1. Telegram öffnen
  2. In der Suche: BotFather
  3. Chat mit BotFather starten
  4. Befehl: /newbot
  5. Bot-Namen vergeben (Anzeige-Name)
  6. Bot-Usernamen vergeben (muss auf bot enden, z. B. MeinProjektAlertBot)
  7. 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:

  1. Den gerade angelegten Bot in Telegram öffnen
  2. Im Chat mit dem Bot: /start senden
    (Wichtig: Ohne das ist der Bot in vielen Fällen „nicht initialisiert“ und Updates fehlen.)
  3. 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:

  1. Bot zur Gruppe hinzufügen
  2. In der Gruppe eine Nachricht senden (beliebig)
  3. 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, .env nicht 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=HTML und 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