yaroslavche/tdlib-bundle
Symfony bundle for integrating TDLib into PHP applications, with service configuration and a convenient wrapper for building Telegram clients. It helps wire TDLib dependencies and use Telegram’s API from Symfony services.
The bundle is a thin Symfony integration over the yaroslavche/tdlib-php PHP extension, which itself wraps Telegram's TDLib. That means the hardest dependency is not the bundle — it is the native TDLib library and the PHP extension compiled against your PHP version.
yaroslavche/tdlib-php. Confirm with php -m | grep -i tdlib and php -r "var_dump(extension_loaded('tdlib'));". If class_exists(...)/function_exists('tdCreate') is false, the bundle will fail at container build, not at runtime.composer require yaroslavche/tdlib-bundle, then confirm the bundle is registered in config/bundles.php (Flex usually handles it; without Flex add it manually).config/packages/tdlib.yaml. At minimum you need api_id and api_hash from https://my.telegram.org → API development tools (these are per-application, not per-account), plus a writable database directory. Feed them via env vars: %env(TDLIB_API_ID)%, %env(TDLIB_API_HASH)%.DependencyInjection/Configuration.php (the real config tree — the README is likely partial), Resources/config/* (actual service IDs and whether a default client is autowired or you must build one from a factory), and the README's usage example.TdlibParameters and GetAuthorizationState, then walks the auth flow interactively (phone number → code → optional 2FA password). Run it once, complete auth, and TDLib persists the session to the database directory. Every subsequent run reuses it.Auth is a one-shot, stateful command. Treat "login" as an interactive CLI command, not a web endpoint. After the session exists on disk, normal code just loads the client. Keep the auth command in your app so you can re-run it when a session is revoked.
Long-running worker as the update loop. TDLib delivers updates via a blocking receive call. Structure it as:
while (true) {
$update = $client->receive(1.0); // block up to 1s
if ($update === null) { continue; }
$dispatcher->dispatch($update, TdlibUpdateEvent::class);
}
Run it under Supervisor/systemd. Never call receive() inside an HTTP request — it will hang the worker until timeout.
Route update types through Symfony's EventDispatcher. Map updateNewMessage, updateMessageSendSucceeded, updateMessageContent, updateConnectionState, etc. onto your own event/listener classes. This is the natural extension point and keeps application code free of TDLib type-switching.
Keep outbound sends on a queue. send() is asynchronous — it returns immediately and results arrive as update objects. Pair tdSend with Messenger: a handler consumes SendMessage messages and issues the TDLib call; a separate listener reconciles the updateMessageSendSucceeded/updateMessageSendFailed. This gives you retries and backpressure for free.
Wrap the client in your own service. The bundle likely exposes a default client service (check Resources/config for the exact ID). Inject your own TelegramClient wrapper around it so controllers, commands, and listeners do not touch the raw extension API — this is what makes mocking and a future swap to Bot API feasible.
Multiple accounts/bots = multiple clients. Each TDLib client needs its own database directory. Define one service per account (or a client factory keyed by account ID) and keep them in separate workers; do not multiplex two accounts on one client handle.
Thin controllers, fat commands. Controllers should validate input + dispatch a SendMessage message; a CLI worker does the TDLib work. This is the only pattern that scales when TDLib is blocking and rate-limited.
receive() blocks and is not interruptible in a friendly way. Use a short timeout so your loop can check a shutdown flag, or signal the process and let Supervisor restart. Don't wrap it in PHP set_time_limit logic — this is a daemon.send() returns nothing useful; the response to a request arrives as an update keyed by @extra. If you need request/response semantics, stash the @extra in a pending-request map and resolve it in the update listener. This is the single most common source of "why is my message missing" bugs.error 429 / FLOOD_WAIT_* in your listener with exponential backoff. Do not let a Messenger retry policy hammer TDLib.Configuration.php. Service autowiring may or may not work out of the box; be prepared to alias or explicitly define the client service.composer.lock and pin the extension in your Dockerfile.extension_loaded('tdlib') in phpunit.xml skip groups), and program against an interface so unit tests never touch TDLib. Integration tests that actually talk to Telegram should be opt-in and run against a throwaway account.How can I help you explore Laravel packages today?