phpbotgram

Dispatcher

The dispatcher is the heart of a phpbotgram bot. It owns the polling loop, the update-type observer map, and the router cascade.

How it works

Dispatcher extends Router , so the same registration API (->message->register , ->callbackQuery->register , …) is available at the top level. When you call runPolling , the dispatcher opens an AmphpSession on the bot, then enters a fiber that calls getUpdates in a loop. Each returned update is fed through feedUpdate , which walks the 25-observer map and resolves the correct observer (message , callbackQuery , chatMember , etc.) by attribute presence on the incoming Update payload.

For each observer the dispatcher applies the global filter chain, then per-handler filters, then enters the middleware stack (outerMiddleware → handler → innerMiddleware ). The handler's return value is ignored; side effects ($event->answer(...)) are the contract.

The 25 observers map one-to-one onto Telegram's Update fields: message , editedMessage , channelPost , editedChannelPost , businessConnection , businessMessage , editedBusinessMessage , deletedBusinessMessages , messageReaction , messageReactionCount , inlineQuery , chosenInlineResult , callbackQuery , shippingQuery , preCheckoutQuery , purchasedPaidMedia , poll , pollAnswer , myChatMember , chatMember , chatJoinRequest , chatBoost , removedChatBoost , plus the synthetic errors and update observers. Resolving by attribute presence means a single getUpdates poll can fan out into multiple handlers on the same update — for instance, the update observer always fires alongside the type-specific one.

Routing inside the cascade is depth-first. The dispatcher itself is the root Router; included child routers ($dispatcher->includeRouter($shopRouter) ) form a tree. A handler match in a deeper router short-circuits the walk; a no-match continues up the tree. Filter resolution uses fiber-local context so each update has its own observer / router / handler stack without thread-safety concerns.

The polling-loop shape is straightforward:

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

$bot = new Bot(getenv('BOT_TOKEN'));
$dispatcher = new Dispatcher();
// register handlers on $dispatcher->message, $dispatcher->callbackQuery, …
$dispatcher->runPolling(new PollingOptions(), $bot);

The variadic Bot ...$bots parameter lets a single dispatcher drive several bot tokens from one process — handy for shared admin UIs or for fanning out an alerting service across customer-specific tokens.

Graceful shutdown: the dispatcher registers SIGINT /SIGTERM handlers on runPolling . On signal it stops fetching new updates, lets in-flight handlers finish, and exits with code 0. This means production bots running under systemd can be restarted without losing updates already delivered to the loop. pcntl is required — on builds without it the dispatcher swallows the unsupported-feature exception and falls back to "exit immediately on Ctrl-C" semantics.

Trade-offs

The dispatcher is the only update-fetching entry point in the framework. Webhook mode also goes through feedUpdate , just from a different fiber driven by amphp/http-server instead of the polling loop. The duplication aiogram has (Dispatcher vs. Bot.start_webhook ) is collapsed; this trades flexibility (you can't have a separate "command bot" object that bypasses the router cascade) for a single source of truth and a single integration point for outer middlewares.

runPolling is blocking. If you need to mix the bot with other amphp services in the same fiber, use the lower-level startPolling which returns an amphp/future you can join with the rest of the loop. The blocking variant is the recommended default because production bots almost always are the only thing in their process.

Backoff on TelegramRetryAfter is built into the polling loop rather than living in the session. The trade-off is that the session can't self-throttle on its own; bots that need rate limiting must compose Backoff into their own send paths or reach for the CallbackAnswerMiddleware and ChatActionSender helpers when the use-case matches. See the rate-limiting recipe for the cookbook version.

A handful of decisions are deliberately tight to keep the runtime predictable: the dispatcher does not spawn worker fibers for each update (handlers run inline in the polling fiber), there is no priority queue between observers (registration order is the only sort), and feedUpdate does not retry on handler exceptions (errors observer is the only failure surface). Bots that need worker-pool semantics can drive their own queue in front of the dispatcher; the framework deliberately ships only the simple shape.

See also

Search results