phpbotgram

Bot and Session

The Bot facade is the thin entry point users hold; the Session is the HTTP transport that turns a typed method DTO into a wire call.

How it works

Constructing a Bot and making calls

Bot is a codegen-produced facade with one typed method per Telegram Bot API endpoint (sendMessage , getChat , editMessageText , … plus the managed-bot extensions). Each method allocates a small DTO from Methods/ and forwards it to the session via $bot($method) . The DTO carries no I/O logic — it is a pure value object with a ::ApiMethod class constant naming the wire endpoint and a ReturnsType PHPDoc anchor that types the response. The codegen runs during make regenerate and writes both the Bot.php facade and the Methods/*.php shape classes, so a Bot API schema bump is one regenerate cycle away from a typed PHP surface.

The simplest usage is constructing a Bot from a token and handing it to a Dispatcher :

use Gruven\PhpBotGram\Bot;
use Gruven\PhpBotGram\Dispatcher\Dispatcher;
use Gruven\PhpBotGram\Dispatcher\PollingOptions;
use Gruven\PhpBotGram\Types\Message;

$bot = new Bot(getenv('BOT_TOKEN') ?: '123:abc');
$dispatcher = new Dispatcher();

$dispatcher->message->register(static function (Message $event): void {
    $text = $event->text ?? '';
    if ($text === '') {
        return;
    }
    $event->answer($text)->emit();
});

$dispatcher->runPolling(new PollingOptions(), $bot);

When you need raw access without a dispatcher — for scripts, probes, or migrations — call $bot(new MethodDTO(...)) directly:

use Gruven\PhpBotGram\Bot;
use Gruven\PhpBotGram\Methods\GetUpdates;
use Gruven\PhpBotGram\Methods\SendMessage;

$bot = new Bot(getenv('BOT_TOKEN') ?: '123:abc');
$offset = null;

while (true) {
    $updates = $bot(new GetUpdates(offset: $offset, timeout: 10));

    foreach ($updates as $update) {
        $offset = $update->updateId + 1;
        $message = $update->message;
        if ($message === null || $message->text === null) {
            continue;
        }
        $bot(new SendMessage(
            chatId: $message->chat->id,
            text: "You said: {$message->text}",
        ));
    }
}

The session layer

The default session is AmphpSession, which inherits from BaseSession. BaseSession owns the middleware chain (request middlewares run around makeRequest ), JSON encoder/decoder injection, the TelegramApiServer host configuration, and the prepareValue / checkResponse serialization seams. The concrete amphp implementation only contributes the actual HTTP call — amphp/http-client v5 with form-urlencoded bodies. The bot constructor accepts a custom session by argument, so testing substitutes a recording session and production sites with a local Bot API server can point at a self-hosted host through TelegramApiServer::fromBase('http://localhost:8081', isLocal: true) .

The $event->answer(...) shortcut

The Bot::setCurrent FiberLocal is what makes $message->answer($text) work without the caller threading a $bot parameter through every handler. The dispatcher binds the current bot at feedUpdate entry and clears the slot in a finally block, so a handler exception never leaves a stale binding. Nested TelegramObject s also carry an explicit ?Bot $bot constructor parameter — the serializer threads the bot when hydrating the Update , so shortcuts on nested types (e.g. CallbackQuery::message->answer(...) ) work without a global lookup.

Revolt's FiberLocal is per-fiber storage; a per-update concurrent dispatch (PollingOptions::$handleAsTasks > 0 ) gets its own bound slot in its own fiber, so cross-bot contamination is structurally impossible.

Sharing a session across multiple bots

Sessions are designed to be reusable. Multiple bots can share the same session — for example, a multi-tenant deployment hands one AmphpSession to every Bot so they pool the underlying HttpClient 's connection pool:

use Gruven\PhpBotGram\Bot;
use Gruven\PhpBotGram\Client\Session\AmphpSession;
use Gruven\PhpBotGram\Dispatcher\Dispatcher;
use Gruven\PhpBotGram\Dispatcher\PollingOptions;
use Gruven\PhpBotGram\Types\Message;

// Share one session — pools the HTTP connection across both tokens.
$session = new AmphpSession();
$bot1 = new Bot(getenv('BOT_TOKEN') ?: '111:aaa', $session);
$bot2 = new Bot(getenv('BOT_TOKEN_2') ?: '222:bbb', $session);

$dispatcher = new Dispatcher();

$dispatcher->message->register(static function (Message $event, Bot $bot): void {
    $tokenId = explode(':', $bot->token)[0];
    $event->answer("Bot #{$tokenId} received: " . ($event->text ?? ''))->emit();
});

// runPolling accepts variadic bots; each gets its own polling fiber.
$dispatcher->runPolling(new PollingOptions(), $bot1, $bot2);

Bots are not coupled to their session beyond the constructor handshake; calling $bot->session returns the attached instance and tests can swap it via reflection if necessary.

Shortcut generation

The shortcut layer is generated alongside the facade. Codegen reads the Bot API schema's response types and emits Message::answer , CallbackQuery::answer , ChatJoinRequest::approve , etc. The shortcut constructs the appropriate method DTO bound to the calling event's chat/message identifiers and returns it, so handlers write $event->answer($text)->emit() instead of building a SendMessage DTO explicitly. The emit() call routes back through the bot bound by the dispatcher's FiberLocal — the same path a manual $bot(new SendMessage(...)) would take.

Trade-offs

The serializer is reflection-based, not codegen-output. Each request walks the DTO's public properties to build the snake_case wire payload; each response walks the typed ReturnsType to hydrate the result. This costs one ReflectionClass per shape per request but trades the runtime cost for codegen simplicity — Phase 2's generator produces the Methods/Types tree without also having to emit per-method serializers. The serializer reflects on every dump /load rather than caching type metadata; only the camelCase→snake_case name conversion is memoised (Serializer::$camelToSnakeCache ).

BaseSession::__invoke always runs the request through the middleware manager, even when the chain is empty. That single closure compose is the price of letting users insert telemetry, retry, or rate-limit middleware without changing the call site. For the empty chain the middleware manager short-circuits to the bare makeRequest closure, so the overhead is one extra function call per request — negligible against the network round-trip but worth knowing if you wrap the bot in a tight loop.

There is no built-in retry-on-network-error. RestartingTelegram and TelegramNetworkException propagate to the caller; the dispatcher's polling loop has its own backoff. Custom retry policies belong in a request middleware so the bot itself stays predictable. We chose not to ship a default retry middleware because policy varies wildly across deployments — exponential backoff with jitter is right for some bots, fail-fast with alerting is right for others.

The bot is not thread-safe in the traditional sense — PHP has no threads — but it is fiber-safe through the session's __invoke contract. Two fibers calling the same bot concurrently each get their own makeRequest closure invocation; the underlying HTTP client handles connection-pool concurrency. The Bot::setCurrent slot is per-fiber via FiberLocal , so concurrent dispatch on multiple bots each see their own binding even when the dispatcher fans out to a semaphore-bounded pool.

See also

Search results