Flags
A flag is a per-handler metadata tag — a name plus an optional value — that middlewares and filters read at dispatch time to decide whether to apply per-handler behaviour (auth, throttling, chat actions, …).
How it works
The Flag primitive
Flag is a small readonly value object plus a PHP attribute. As an attribute it can be repeated on a method, function, or class, so a single handler can carry #[Flag('admin_only')]
, #[Flag('chat_action', 'typing')]
, and #[Flag('throttle', 0.5)]
simultaneously. As a value object the same Flag
instance ends up in the handler's $flags
list at registration time. The dual role lets the same primitive serve both the declarative (#[Flag(...)]
on the method) and imperative ($obs->register($cb, flags: [...])
) styles. The attribute's #[Attribute(...IS_REPEATABLE...)]
tagging is what makes "stack any number of flags" work; aiogram uses a dict[str, Any]
because Python has no equivalent attribute mechanism.
Attaching flags at registration time
Both the attribute style and the imperative (array) style are available. The imperative flags:
argument is the simpler option for closures, where PHP attributes cannot be applied:
use Gruven\PhpBotGram\Dispatcher\Dispatcher;
use Gruven\PhpBotGram\Dispatcher\Flags\Flag;
use Gruven\PhpBotGram\Types\Message;
$dispatcher = new Dispatcher();
// Imperative style: pass a flags array at registration time.
$dispatcher->message->register(
static function (Message $event): void {
$event->answer('hello')->emit();
},
flags: ['chat_action' => 'upload_photo'],
);
// Attribute style: annotate the handler function with #[Flag].
$dispatcher->message->register(
#[Flag('admin_only')]
static function (Message $event): void {
$event->answer('admin only')->emit();
},
);
Storage and lookup
Attribute-driven flags are read via PHP reflection. Imperative flags are stored in a process-wide FlagDecorator WeakMap
keyed by the target closure or object. The weak-map trick exists because PHP cannot mutate arbitrary properties on a Closure
— Python's cb.aiogram_flag = {...}
translates to a side-table here. The weak-map evicts entries automatically when the closure is garbage-collected, so the storage is bounded by live handlers — there is no manual cleanup or registry maintenance needed even when the dispatcher rebuilds its handler graph (e.g. when a scene re-registers its method handlers).
Flags is the read API. Flags::extractFlags($target)
returns both attachment styles concatenated (imperative first, then attribute-driven). Flags::getFlag($target, 'admin_only')
returns the first match or null
. Middlewares that read flags use these helpers — they never touch the WeakMap directly. The extractFlagsFromObject
helper on the HandlerObject
itself bakes the read into the dispatch hot path so a per-event lookup costs one getFlag
call against a typically-small list.
Reading flags inside a middleware
The handler's resolved flags are available in $data['handler']->flags
(an array<string, mixed>
) inside any inner middleware. A middleware that gates on admin_only
would look like:
use Gruven\PhpBotGram\Dispatcher\Dispatcher;
use Gruven\PhpBotGram\Dispatcher\Event\HandlerObject;
use Gruven\PhpBotGram\Dispatcher\Middlewares\BaseMiddleware;
final class AdminGateMiddleware extends BaseMiddleware
{
public function __invoke(\Closure $handler, object $event, array $data): mixed
{
$handlerObj = $data['handler'] ?? null;
if (!$handlerObj instanceof HandlerObject) {
return $handler($event, $data);
}
$flag = $handlerObj->flags['admin_only'] ?? null;
if ($flag === true && !$this->isAdmin($event)) {
return null; // short-circuit — do not invoke the handler
}
return $handler($event, $data);
}
private function isAdmin(object $event): bool
{
// your admin-check logic here
return false;
}
}
$dispatcher = new Dispatcher();
$dispatcher->message->innerMiddleware(new AdminGateMiddleware());
Framework-shipped flag-aware utilities
The framework ships two flag-aware utilities. CallbackAnswerMiddleware
(in Utils/CallbackAnswer/
) reads the callback_answer
flag and auto-acknowledges the callback query before the handler runs, with the parameters from the flag's value shaping the acknowledgement text. ChatActionMiddleware
(in Utils/ChatAction/
) reads the chat_action
flag and sends a chat_action
API call that loops in the background until the handler returns — visible to the user as the "typing..." indicator during long-running operations. Both follow the same pattern: read the flag, optionally do prep work, delegate to the handler, then run cleanup.
The chat_action
flag supports several forms. Absent means default 'typing'
; a string overrides the action; false
opts the handler out entirely:
use Gruven\PhpBotGram\Dispatcher\Dispatcher;
use Gruven\PhpBotGram\Types\Message;
use Gruven\PhpBotGram\Utils\ChatAction\ChatActionMiddleware;
$dispatcher = new Dispatcher();
// Flag-aware middleware must be INNER — per-handler flags live on the
// HandlerObject ($data['handler']), which only inner middleware can see.
$dispatcher->message->innerMiddleware(new ChatActionMiddleware());
// Default: typing indicator sent automatically.
$dispatcher->message->register(static function (Message $event): void {
$event->answer('hello')->emit();
});
// Custom action for a slow photo handler.
$dispatcher->message->register(
static function (Message $event): void {
$event->answer('photo coming')->emit();
},
flags: ['chat_action' => 'upload_photo'],
);
// Opt out entirely — no indicator for this handler.
$dispatcher->message->register(
static function (Message $event): void {
$event->answer('instant')->emit();
},
flags: ['chat_action' => false],
);
FlagGenerator is the helper that converts a flag list into a normalised form for inspection. The dispatcher does not use it on the hot path — it's exposed for tooling and tests that want to assert "this handler has exactly these flags". Production middlewares should use the Flags::getFlag
shortcut instead.
Trade-offs
Flags are unstructured by design. A flag's value
is mixed
— any type, any shape — because middlewares own the interpretation. This is flexible but means a typo in a flag name ('admin_onyl'
) fails silently. There is no central registry of legal flag names; the contract is (middleware, flag-name)
pairs documented at the middleware's call site. We considered an enum-typed flag-name surface and rejected it: middlewares ship in user code as often as in the framework, and forcing every user middleware to extend a framework enum would not scale.
The WeakMap trick depends on the target being an object. String callables or [$obj, 'method']
arrays cannot carry flags this way — the registration adapter lifts them via Closure::fromCallable(...)
first. For attribute-driven flags this matters only when the attribute is on the method itself; the attribute path is independent of WeakMap storage. The lift to a closure is automatic and idempotent; users do not normally see it. PHP's closure equality is identity-based, so the WeakMap correctly distinguishes two closures over the same function.
Flag values are read at dispatch time, every dispatch. There is no cache. A flag-aware middleware running on a hot observer pays a reflection-and-WeakMap lookup per event. The cost is small (one getFlag
against a typically-empty list) but real — if you find yourself adding flags to every handler, consider moving the policy into the middleware itself, where it can short-circuit faster. The profile-driven choice is to keep the lookup hot-path simple and let heavy middlewares opt out by checking a per-instance toggle rather than caching the flag read.
Two attachment styles also mean two read paths. Flags::extractFlags
concatenates them in a deterministic order (imperative first), so imperative flags override attribute-driven ones on getFlag
lookups. This matters when a middleware-level imperative flag should win
against a class-level attribute default — a common pattern for per-route overrides.