Navigation Stack & Root Flow โ
InteractiveFlow features a hierarchical navigation stack that models menus and multi-layered screens just like modern UI frameworks.
flowchart TD
subgraph StackRepresentation ["๐ฅ Navigation Stack Hierarchy ($state->flowStack)"]
direction TB
TopScreen["๐ NotificationsFlow<br/><b>[Active Screen]</b><br/><i>Rendered on message_id: 420</i>"]
MidScreen["โ๏ธ SettingsFlow<br/><b>[Suspended Frame 1]</b><br/><i>Saved state & step memory</i>"]
RootScreen["๐ MainMenuFlow<br/><b>[Root Frame 0]</b><br/><i>Application Entry Point</i>"]
TopScreen -->|"$this->pop('result')"| MidScreen
MidScreen -->|"$this->pop()"| RootScreen
TopScreen -.->|"Click ๐ Home (Stack Reset)"| RootScreen
end
subgraph Transitions ["Navigation Operations"]
direction LR
PushOp["<b>push(Child::class, data)</b><br/>โข Suspends current flow<br/>โข Pushes state to stack<br/>โข Reuses same message_id"]
PopOp["<b>pop(result)</b><br/>โข Drops top frame<br/>โข Resumes parent flow<br/>โข Calls onResume(result)"]
HomeOp["<b>home() / ๐ </b><br/>โข Purges entire stack<br/>โข Restores rootFlow<br/>โข Clean state slate"]
end
๐ฅ Stack Navigation: push() and pop() โ
$this->push(string $childFlowClass, array $data = []): void โ
Suspends the active flow, pushes its state onto $state->flowStack, and opens the child flow on the exact same message ID for zero-flicker transitions.
#[Action('open_profile')]
public function openProfile(): void
{
$this->push(UserProfileFlow::class, ['user_id' => 123]);
}$this->pop(mixed $result = null): void โ
Pops the active flow off the stack and restores the parent flow on the same message ID, executing the parent's onResume($result) hook with any returned data.
#[Action('save_selection')]
public function saveSelection(): void
{
$this->pop('English (US)');
}๐งญ Navigation Buttons: ๐ Back and ๐ Home โ
Screen includes a navigation bar with language-neutral emoji defaults:
- Back Button:
๐(triggers$this->pop()) - Home Button:
๐(resets flow stack and jumps torootFlow)
// Enable both Back and Home:
Screen::make('Select an option:')
->withNavigation(back: true, home: true);
// Enable only Back:
Screen::make('Select an option:')
->withNavigation(back: true, home: false);
// Customizing labels:
Screen::make('Select an option:')
->withNavigation(
back: true,
home: true,
backLabel: '๐ Return',
homeLabel: '๐ Main Menu'
);TIP
Navigation buttons work seamlessly on both Inline Keyboards (via callback query) and Reply Keyboards (via emoji text message ๐ or ๐ ).
๐ Configuring rootFlow & Handling /start โ
You can configure a global root flow on the client or FlowManager:
$bot->setRootFlow(MainMenuFlow::class);
// Or through Config builder:
$config = Config::builder()
->token('...')
->rootFlow(MainMenuFlow::class)
->build();Global /start Behavior โ
When a user issues /start while in an active flow:
FlowManagerchecks$activeFlow->canInterrupt($update).- If
true(default), the entire flow stack is cleared, and the user is redirected torootFlow. - If
false(e.g. during checkout or an active form), the/startcommand is blocked, and the user remains safely in the active step.
๐ Breadcrumbs Trail (withBreadcrumbs) โ
When nesting multi-level menus (e.g. Catalog ยป Electronics ยป Headphones), orient the user with an automatic breadcrumbs trail:
use Tueen\Telegram\Flow\Attributes\Breadcrumb;
#[Breadcrumb('Headphones')]
class HeadphoneDetailsFlow extends InteractiveFlow
{
public function render(): Screen
{
return Screen::make("Sony WH-1000XM5 Specs...")
->withBreadcrumbs(separator: ' ยป ')
->inline(...)
->withNavigation(back: true, home: true);
}
}This prefixes your screen text with:
Catalog ยป Electronics ยป Headphones
Sony WH-1000XM5 Specs...You can also customize the breadcrumb label dynamically via PHP 8.4 property hook:
public string $breadcrumbTitle {
get => $this->get('product_name', 'Item');
}๐ก๏ธ Resilience & Missing Class Fallback โ
In long-running or distributed systems, code deployments may remove, rename, or refactor a Flow class while active users still have that Flow recorded in their persistent storage (e.g. storage/flow/*.json or Redis/database).
tueen/telegram features an intelligent, multi-layered resilience algorithm to ensure your bot never crashes or leaves users stranded:
flowchart TD
Update(["Incoming User Update"]) --> ValidateClass{"Flow Class Exists<br/>in Codebase?"}
subgraph HappyPath ["Normal Execution"]
ValidateClass -->|Yes| ValidateStep{"Step Method<br/>Exists?"}
ValidateStep -->|Yes| ExecStep["โถ๏ธ Execute Flow Step"]
end
subgraph ClassRecovery ["Class Missing (Deleted / Renamed in Deploy)"]
ValidateClass -->|No| HasParent{"Parent Flow<br/>in Stack?"}
HasParent -->|Yes| UnwindParent["โช Pop & Resume Nearest Valid Parent"]
HasParent -->|No| HasRoot1{"rootFlow<br/>Configured?"}
end
subgraph StepRecovery ["Step Missing (Refactored Method)"]
ValidateStep -->|No| TriggerHook["๐ Trigger onMissingStep()"]
TriggerHook --> HasHistory{"History Stack<br/>Available?"}
HasHistory -->|Yes| StepBack["๐ Auto Fallback: $this->back()"]
HasHistory -->|No| HasStart{"start()<br/>Available?"}
HasStart -->|Yes| StepStart["๐ Reset to: $this->start()"]
HasStart -->|No| HasRoot2{"rootFlow<br/>Configured?"}
end
subgraph TerminalFallback ["Root Fallback & Cleanup"]
HasRoot1 -->|Yes| JumpRoot["๐ Reset to rootFlow"]
HasRoot2 -->|Yes| JumpRoot
HasRoot1 -->|No| PurgeState["๐งน Purge Orphaned State<br/>Pass to Bot Router"]
HasRoot2 -->|No| PurgeState
end
1. Unwinding Navigation Stack to Parent Flows โ
If the user was inside a child flow (pushed via $this->push(...)) whose class was removed, FlowManager automatically inspects $state->flowStack and pops back to the nearest parent Flow that still exists, resuming it smoothly with its original state and messageId.
2. Fallback to rootFlow (Default Flow) โ
If the stack is empty (or parent classes were also removed), FlowManager checks if a rootFlow (or defaultFlow) is configured in Config:
$config = Telegram::create('TOKEN')
->withRootFlow(MainMenuFlow::class) // or withDefaultFlow(MainMenuFlow::class)
->build();When configured, the user's corrupted state is cleared and they are seamlessly redirected to the entry step of the root flow.
3. Missing Step Recovery (onMissingStep) โ
If a Flow class exists, but a specific step method was removed or renamed in code, Flow::onMissingStep($step, $update) is automatically invoked:
- It checks
$state->historyand navigates back one step. - If history is empty, it attempts to return to the
start($update)method. - If a custom
$this->fallbackFlowis set on the flow, it transitions there. - Or it redirects to
rootFlow.
You can also customize missing step handling directly on any Flow:
class CustomFlow extends Flow
{
protected ?string $fallbackFlow = HomeFlow::class;
public function onMissingStep(string $step, Update $update): void
{
$this->bot->sendMessage(
chatId: $this->chatId,
text: "This section was recently updated. Taking you back..."
);
$this->back();
}
}4. Graceful Router Fallthrough โ
If no parent flows, root flows, or step recovery paths are available, FlowManager purges the orphaned session and returns false. This ensures the user's message is not discarded and is immediately handled by your regular command handlers, router attributes, or fallback routes.