mpociot/botman-tinker
BotMan Tinker adds an interactive REPL to the BotMan chatbot framework, letting you chat with and test your bots straight from the terminal. Send messages, inspect conversation state, and debug bot responses instantly without wiring up a messaging platform first.
What it actually is. A single Artisan command — php artisan botman:tinker — that boots your existing BotMan instance and drops you into a REPL. Every line you type is handed to the bot as an incoming message; replies are printed to the terminal. No webhook, no channel credentials, no HTTP round-trip. Think laravel/tinker, but the object you're poking is a conversation.
Install as a dev dependency, pinned exactly. The last release is 1.2.0 (May 2020) and its entire changelog is a composer.json bump for Laravel 7.x. A floating constraint buys you nothing and risks a silent dev-environment change.
composer require --dev mpociot/botman-tinker:1.2.0
Verify resolution before you commit to it:
composer require --dev mpociot/botman-tinker:1.2.0 --dry-run
composer why-not mpociot/botman-tinker 1.2.0
If the only path to installation involves downgrading Laravel, PHP, or symfony/console, stop and read the escape hatch in Gotchas — this tool is ~40 lines.
Where to look first, in order.
composer.json. It is the most accurate documentation the project ships: the declared bounds on illuminate/*, symfony/console, and php tell you immediately whether you can use it.src/. Read it once. It shows you exactly how it resolves the BotMan instance and which driver it drives — that single fact determines everything about whether tinker output is trustworthy.config/botman.php (or wherever your driver config is assembled). This decides which driver the REPL runs against.First use case. Pick one Conversation class or one hears() listener. Run the command, walk the happy path plus two failure branches. That's the whole adoption story:
php artisan botman:tinker
> hi
> order a pizza
> large
> nonsense
> yes, confirm
If that loop isn't measurably faster than curling your real webhook, don't standardise on it.
Non-negotiable prerequisite: confirm the REPL is pointed at a local driver (BotMan's null/echo driver or a test driver), not Slack or Telegram. See the first Gotcha — this is the one mistake that can page a customer.
Extract first, then tinker. The tool is only as useful as your bot is testable. Conversations belong in app/BotMan/Conversations/* extending BotMan\BotMan\Conversations\Conversation; hears() handlers belong in dedicated listener classes registered from a service provider. Once that's true, your daily loop becomes: edit the conversation class → rerun botman:tinker → replay the same four lines.
Keep that sequence in a scratch file so you're not retyping it from memory:
# notes/pizza-flow.txt
hi
order a pizza
large
yes, confirm
Add a local-only logging middleware to see what BotMan actually matched. With the HTTP boundary gone, tinker is the cleanest place to inspect incoming message shape:
// app/Providers/BotManServiceProvider.php
if (app()->environment('local')) {
$botman->middleware->heard(function ($message, $bot) {
logger()->debug('botman.heard', [
'text' => $message->getText(),
'driver' => $message->getDriver(),
'payload' => $message->getPayload(),
]);
});
}
Gate it on local so it never runs in production. This is how you validate pattern-matching assumptions and confirm reply payload assembly — menus, buttons, and cards as raw arrays.
Exercise stateful flows deliberately. Multi-turn conversations work in a single REPL session because the process keeps the BotMan instance alive between turns. Use that property rather than fighting it:
ask() validation loop.Separate intent routing from rendering. Use tinker to confirm the right branch fires and the right payload is built — not what it looks like. Rich templates get flattened or dumped as arrays in a terminal, so UX review belongs in a real channel. For CI assertions, use BotMan's own tester helpers (BotManTester or a fake driver depending on your BotMan major), not this.
Demo and onboarding prep. Before a stakeholder demo, script the exact prompt sequence into a notes file and rehearse it in tinker with the target channel's driver config copied over. For onboarding, pair a new engineer on it — it's the fastest way to show how a Conversation moves between ask() and reply() without handing out channel credentials.
Keep it out of CI. It expects an interactive TTY, produces free-form output, and makes no assertions. Treat it as a debugger, not a test suite. If someone proposes wiring it into a pipeline, that's the signal to build a proper fake-driver harness instead.
Your "local" REPL can message real customers. Replies echo in your terminal, but the BotMan instance is booted with your real driver config. If config/botman.php selects Slack or Telegram, $bot->reply() may hit the live API from your terminal. Point the command at an echo/test driver, or use a dedicated local config, before typing anything. This is the number one way people accidentally notify a customer from php artisan.
It needs a TTY. Without one it hangs or exits immediately:
docker compose run --rm -it app php artisan botman:tinker # -it is required
echo "hi" | php artisan botman:tinker # exits on EOF; don't rely on it
For arrow-key history and line editing, confirm the PHP readline extension is present in the container. Without it you get raw stdin and the tool feels broken rather than minimal.
Conversation state bleeds between sessions. BotMan persists conversation state through its cache driver. Array/in-memory caches die with the process (fine). Redis and file caches survive, so a flow abandoned yesterday resumes mid-conversation today and looks like a code bug. When a flow behaves impossibly, run php artisan cache:clear before debugging anything else.
Green in tinker does not mean it works on the channel. The REPL bypasses webhook signature verification, HTTP payload shape, retries, and channel templating. Nothing you learn here proves integration. Say so explicitly when reporting "tested" to a reviewer — otherwise you'll be blamed for a Slack formatting bug you never could have seen.
Legacy vendor mismatch. This package is mpociot/botman-tinker while BotMan itself moved to botman/botman. Confirm your app uses botman/botman and that Composer isn't pulling a second, older BotMan alongside it. Two BotMan classes in one autoloader produces confusing "method does not exist" errors that look like your code's fault.
composer show | grep -i botman # one-second check
Framework ceiling. 1.2.0 targets Laravel 7.x. Laravel 8+ and PHP 8.2/8.3 are unverified; expect composer why-not conflicts on illuminate/* or symfony/console. Never downgrade your framework to keep a dev convenience tool.
Provider auto-discovery can be off. If discovery is disabled or you curate providers manually, add the provider class (check extra.laravel.providers in the package's composer.json) to config/app.php, or the command simply won't appear in php artisan list.
Interactive prompts collide with the REPL prompt. ask() inside a conversation consumes lines from the same stdin the REPL loop is reading. Usually fine, but if prompts get swallowed during a long ask() chain, type an empty line to nudge the loop.
Exit cleanly. Use exit / quit or Ctrl+D. Ctrl+C mid-listen() can wedge the process, especially with streaming drivers; if the terminal stops echoing, stty sane restores it.
The escape hatch — own the command. This package is roughly 40 lines you can write in an afternoon, and doing so removes the Laravel version ceiling, the vendor mismatch, and the abandonment risk in one move:
protected $signature = 'botman:chat';
public function handle()
{
$botman = app(\BotMan\BotMan\BotMan::class);
while (($line = readline('> ')) !== false) {
if (trim($line) === 'exit') {
break;
}
// feed $line through a local/test driver, then $botman->listen()
}
}
The exact wiring depends on your BotMan major and how the instance is bound — read the package's command class as the reference implementation and mirror it. For any team on Laravel 8+, writing it is the default recommendation; adopting the package is the exception you justify.
How can I help you explore Laravel packages today?