Keyboards & Action Binding
InteractiveFlow decouples callback routing from tedious manual string checking using modern PHP 8 attributes: #[Action] and #[Input].
🎯 The #[Action] Attribute
Methods annotated with #[Action('pattern')] are automatically executed when the user clicks an inline keyboard button matching the specified pattern.
1. Literal Action Patterns
use Tueen\Telegram\Flow\InteractiveFlow;
use Tueen\Telegram\Flow\Screen;
use Tueen\Telegram\Flow\Attributes\Action;
use Tueen\Telegram\Keyboards\InlineKeyboard;
class SettingsFlow extends InteractiveFlow
{
public function render(): Screen
{
return Screen::make('Settings Menu')
->inline(fn (InlineKeyboard $k) => $k
->action('Toggle Dark Mode', 'toggle_dark')
->action('Language', 'language')
);
}
#[Action('toggle_dark')]
public function toggleDarkMode(): void
{
$current = $this->get('dark', false);
$this->set('dark', !$current);
$this->refresh();
}
}2. Parameterized Action Patterns
You can capture dynamic URL-style parameters in callback data using {param} placeholders:
public function render(): Screen
{
return Screen::make('Select an Item:')
->inline(fn (InlineKeyboard $k) => $k
->action('Item 10', 'item_10')
->action('Item 25', 'item_25')
);
}
#[Action('item_{id}')]
public function selectItem(int $id): void
{
$this->set('selected_id', $id);
$this->push(ItemDetailsFlow::class, ['item_id' => $id]);
}The parameter is automatically converted to the type hinted in the method signature (int, string, etc.).
✍️ Free-Text Inputs with #[Input]
If a screen needs to accept free-form text from the user while keeping the interactive screen active:
use Tueen\Telegram\Flow\Attributes\Input;
use Tueen\Telegram\Types\Update;
#[Input]
public function handleTextInput(Update $update): void
{
$query = trim($update->findAnyText() ?? '');
$this->set('search_query', $query);
$this->refresh();
}🎛️ Switching Keyboard Types Seamlessly
Screen lets you switch seamlessly between inline keyboards, native reply keyboards, and removing keyboards:
// Switch to a native contact sharing keyboard:
Screen::make('Please share your phone number:')
->reply(fn (ReplyKeyboard $k) => $k
->requestContact('📱 Share Contact')
->resize()
->oneTime()
);
// Remove any existing reply keyboards:
Screen::make('Processing your request...')
->removeKeyboard();NOTE
Telegram's Bot API does not allow converting an inline message into a native reply keyboard via editMessage. When transitioning from an inline keyboard to a ReplyKeyboard, InteractiveFlow automatically sends a new message with the reply keyboard and tracks the new $this->messageId.
📄 Automatic Pagination Bar
When rendering paginated items or catalogues, add an automatic pagination control row directly using withPagination():
Screen::make("Products (Page {$page} of {$totalPages}):")
->inline(...)
->withPagination(
currentPage: $page,
totalPages: $totalPages,
actionPattern: 'page_{page}', // Generates [◀️] [2/5] [▶️]
prevLabel: '◀️',
nextLabel: '▶️'
)
->withNavigation(back: true, home: true);Then bind the page action with a parameterized #[Action]:
#[Action('page_{page}')]
public function changePage(int $page): void
{
$this->set('currentPage', $page);
$this->refresh();
}⏱️ Action Rate-Limiting & Debouncing (#[Debounce])
Users in Telegram often double-tap buttons impulsively, which can cause duplicate orders, race conditions, or duplicate payment requests.
Annotate any #[Action] method with #[Debounce] to lock rapid button clicks:
use Tueen\Telegram\Flow\Attributes\Action;
use Tueen\Telegram\Flow\Attributes\Debounce;
#[Action('checkout')]
#[Debounce(seconds: 3.0, notice: '⏳ Please wait, processing your payment...', showAlert: false)]
public function handleCheckout(): void
{
// Executes at most once every 3 seconds per user
$this->processPayment();
}seconds: Debounce window in seconds (supports fractional seconds like1.5).notice: The toast notification or alert message shown to the user when throttled.showAlert: Iftrue, pops up a modal alert dialog; iffalse(default), shows a non-intrusive toast popup.
🛡️ Channel Membership Guard (#[RequireMember])
Gate actions or screens behind mandatory Telegram channel or supergroup subscriptions. The #[RequireMember] attribute verifies that the user is currently a member, administrator, or creator:
use Tueen\Telegram\Flow\Attributes\Action;
use Tueen\Telegram\Flow\Attributes\RequireMember;
#[Action('claim_bonus')]
#[RequireMember(
channel: '@tueen_channel',
fallbackMessage: '🔒 You must join our official channel to claim this daily reward!',
joinUrl: 'https://t.me/tueen_channel'
)]
public function claimDailyBonus(): void
{
// Executed ONLY if the user is a member/admin/creator of @tueen_channel
$this->grantBonus(100);
$this->toast('🎁 Bonus claimed!');
$this->refresh();
}Guard Workflow:
InteractiveFlowqueries Telegram'sgetChatMember(chat_id, user_id).- If member status is
creator,administrator, ormember, execution proceeds to the action method. - If not joined (or
left,kicked,restricted), execution is halted. IfjoinUrlis specified, an inline screen with a direct "📢 Join Channel" button and a "🔄 Check Again" refresh button is automatically rendered for the user.