Skip to content

Commands ​

Since essence v0.10.0, /command messages are routed through CommandBus (TelegramBotEssentials\Essence\Telegram\Commands\CommandBus) — the same dependency-injected, auto-discovered handler pattern used by CallbackQueries and StateAnswers, in place of the underlying telegram-bot-sdk's own command bus.

Defining a command ​

php
namespace App\Telegram\Commands\Member;

use TelegramBotEssentials\Essence\Enums\Roles;
use TelegramBotEssentials\Essence\Telegram\Commands\Command;

class StartCommand extends Command
{
    protected string $name = 'start';
    protected int $perm = Roles::MEMBER->value;

    // Since v0.12.0: an overridable method calling __() directly, not a
    // stored property — one instance renders in every bot's locale.
    protected function description(): ?string
    {
        return __('app.telegram.start.description');
    }

    public function handle(): ?bool
    {
        // ...

        return true;
    }
}

Scaffold one with:

bash
php artisan tbe:make:command StartCommand --command-name=start --description="Start using the bot"
php artisan tbe:make:command BanUser --admin

--admin puts the class under app/Telegram/Commands/Admin (default: Member) and pre-fills $perm accordingly; --command-name= sets the Telegram-facing name (default: snake_case of the class name).

Auto-discovery ​

Same convention as CallbackQueries, StateAnswers, and ReplyKeys — commands are picked up automatically, no manual registration needed. Since v0.12.0 TelegramBotServiceProvider scans, via loadCommands($path), once per worker boot (not on every request):

  • app/Telegram/Commands/Member/*.php and app/Telegram/Commands/Admin/*.php in the host app
  • Essence's own built-in commands (currently just HelpCommand)

config('tbe-essence.commands') — handler registration vs. the command menu

Earlier versions registered command handlers by listing SDK Telegram\Bot\Commands\Command subclasses in config/tbe-essence.php's commands array, wired via wHook()->api()->addCommand()/processCommand(). That path is gone — handlers are now auto-discovered from the directories above.

The commands key itself still exists, repurposed: it is the list tbe:set-webhook sends to Telegram via setMyCommands() to populate the client's command menu (the / autocomplete list). Its entries must now be TelegramBotEssentials\Essence\Telegram\Commands\Command subclasses (essence resolves each through the container and reads getName()/getDescription()). Keep it populated with the commands you want listed; it does not affect which commands actually work — an auto-discovered command handles messages whether or not it appears here.

Param binding ​

Reflection-based, same as CallbackQuery::handle() — not the SDK's pattern/$this->argument() style. A real /command arg1 arg2 message is whitespace-split and the parts passed positionally into handle()'s parameters:

php
public function handle(string $arg1, ?string $arg2 = null): ?bool

Typed Eloquent model parameters are resolved the same way CallbackQueries resolve them — looked up by id unless the command declares $bindings:

php
protected array $bindings = [
    BotUser::class => 'telegram_user_peer_id',
];

public function handle(BotUser $botUser): ?bool

Triggering a command programmatically — commandBus()->route() ​

Any other bus handler (a CallbackQuery, StateAnswer, ReplyKey — even another Command) can dispatch into a registered command exactly as if the user had typed it:

php
commandBus()->route('start');
commandBus()->route('start', ['some_arg']);

This is how, since Settings v0.0.14, ChannelLockQuery re-runs /start right after a user clears the channel-join gate — the user never has to retype the command themselves.

route():

  • Throws TbeLogicException on an unknown command name/alias — same fail-fast convention as CallbackQueryBus's unregistered-type handling. This also applies to real, unmatched /whatever messages (unified through processCommands()), which is a behavior change from the old SDK bus's silent fallback.
  • Fires BotCommandHandled (see Events & Listeners) whether the trigger was a real message or a programmatic call — the controller no longer fires this event itself.
  • Does not cancel any pending state/process on its own — that stays the caller's responsibility. The real-message path (processCommands(), called from TelegramWebhookController) still cancels state/process at the controller level before routing, unchanged.

The built-in /help command ​

HelpCommand (alias /listcommands) lists every currently registered, enabled, permission-visible command by reading commandBus()->getCommands() — it stays accurate automatically as you add or remove commands, no maintenance required.

Reference ​

MethodDescription
commandBus()Resolve the CommandBus singleton
addCommand(CommandInterface|string $command)Register one command (FQCN or instance)
addCommands(iterable $commands)Register several
removeCommand(string $name) / removeCommands(array $names)Unregister
getCommands()array<string, CommandInterface> keyed by command name
route(string $name, array $params = [])Dispatch a command as if the user typed it
processCommands()Parses the current update's message text and calls route(); used internally by TelegramWebhookController

Source: src/Telegram/Commands/{CommandInterface,Command,CommandBus}.php