mpociot/botman
BotMan is a framework-agnostic PHP chatbot library for building conversational bots across Slack, Telegram, Facebook Messenger, and more. It offers a fluent API, drivers, middleware, and conversation patterns to create interactive chat experiences in Laravel or any PHP app.
Install the right package name. This repo is botman/botman; mpociot/botman is the pre-rename identity and is abandoned on Packagist. Always use the botman/* names.
composer require botman/botman
composer require botman/driver-web --dev # local chat widget, development only
composer require botman/driver-slack # one per channel you actually ship
Check what Composer resolves before you write a line of code. The latest release is 2.6.1 (Sept 2020) and it tops out at Laravel 7/8. On Laravel 9+ Composer will refuse to resolve; if you force it, you inherit PHP 8.x deprecation warnings and runtime surprises. Decide up front whether the bot lives in the main app or in an isolated, pinned deployable — this is the first real decision, not an afterthought.
Auto-discovery and config. BotMan\BotMan\BotManServiceProvider and the BotMan facade are auto-discovered. Publish config and look at exactly two things: conversation_cache_time / user_cache_time, and the per-driver credential files under config/botman/.
php artisan vendor:publish --provider="BotMan\BotMan\BotManServiceProvider"
BotMan Studio (composer require botman/studio --dev, then php artisan botman:install) scaffolds routes/botman.php, the config files, and a /botman/chat test widget. Use it locally; strip or gate it before production.
Register the webhook and get CSRF out of the way. BotMan auto-detects the driver from the incoming request, so a single route can serve several platforms:
use BotMan\BotMan\BotManController;
Route::match(['get', 'post'], 'botman', [BotManController::class, 'handle'])
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
Alternatively add botman to VerifyCsrfToken::$except. Forgetting this is the classic "works locally, 419s in production" failure.
First use case: an internal ops bot on Slack or Telegram. Lowest blast radius, no customer SLA, and it exercises the parts you actually need — credentials, webhook verification, outbound sends — without conversation-design risk.
$botman->hears('status(.*)', function ($bot) {
$bot->reply('All services nominal.');
});
$botman->fallback(function ($bot) {
$bot->reply("I don't know that one. Try `status`.");
});
Hit the Studio widget until you get a reply, then wire the real platform app and credentials. The effort is in app registration and public HTTPS ingress, not in the package.
Where to look first: config/botman/ for credentials, routes/botman.php for the webhook route, then the hears() handlers. Nothing else matters until one flow answers in staging.
Pattern + arguments. Plain strings become anchored, case-insensitive regexes; capture groups are injected after the type-hinted $bot, in order.
$botman->hears('deploy (.+) to (staging|production)', function ($bot, $branch, $env) {
$bot->reply("Queuing {$branch} → {$env}");
DeployJob::dispatch($branch, $env); // keep heavy work out of the request
});
Use non-capturing groups (?:...) for alternation you don't want passed as an argument, and prefer hears('hi(.*)') over hears('hi') when you want it to fire on "hi there".
Rich replies. Buttons, images, and quick replies are first-class and map to Slack blocks, Telegram inline keyboards, and Messenger templates via the driver:
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
$bot->reply(Question::create('Ship it?')
->addButtons([
Button::create('Yes')->value('ship'),
Button::create('No')->value('abort'),
]));
Conversations for anything multi-turn. This is the main reason to use BotMan over raw SDKs — state lives in Laravel's cache, so resumable dialogues come free.
class OnboardingConversation extends Conversation
{
public function run()
{
$this->ask('What is your GitHub handle?', function (Answer $answer) {
$this->bot->userStorage()->save(['github' => $answer->getText()]);
$this->ask('Which team?', function (Answer $team) {
$this->say("Added to {$team->getText()}.");
});
});
}
}
// $bot->startConversation(new OnboardingConversation);
Reach for askForImages(), askForLocation(), askForEmail() when the answer needs validation you'd otherwise hand-roll. $bot->hears('stop')->skipsConversation() is the standard escape hatch so a global command can interrupt mid-flow.
Middleware is where cross-cutting concerns live. Logging, allow-listing, rate limiting, tenant resolution, NLP — all as middleware on received, heard, sending, captured. Write once, applies to every channel, survives a driver swap.
$botman->middleware->received(new LogInbound);
$botman->middleware->sending(new StampTenant);
Proactive outbound sends. A webhook reply has no context inside a queued job or scheduler. Use say() with the driver explicit:
app('botman')->say('Nightly backup finished', $slackUserId, SlackDriver::class);
Wrap this in your own Notifier service so callers never touch the facade directly.
Adapter boundary. Treat BotMan as a substitutable edge layer, not core domain logic. One BotMessenger interface with notify(User, string) / ask(...), implemented by a BotMan class, means replacing the package later is a class swap rather than a grep-and-refactor.
Testing without HTTP. Register a fake or web driver, build an IncomingMessage, call the bot directly, assert on captured outbound messages.
DriverManager::loadDriver(WebDriver::class);
$botman = BotManFactory::create(config('botman'));
$botman->hears('ping', fn ($bot) => $bot->reply('pong'));
$botman->call(new IncomingMessage('ping', 'user-1', 'channel-1'));
Confirm exact helper names against your installed version; the shape — process a fake message, inspect the outgoing set — is stable and enough to cover conversation branches in CI.
The Laravel version cap is the headline risk. 2.6.1 is a compatibility patch that stops at Laravel 7/8, both EOL. Do not let this package silently cap your framework upgrade. Pin the bot to an isolated deployable, fork under MIT, or set an explicit trigger date for replacement.
Never mix a Laravel upgrade with a BotMan change. If something breaks you will not know which side caused it.
Conversation cache must be shared and serializable. Local file cache breaks the moment you run two workers — dialogues bounce between servers and go incoherent. Use Redis or DB. And because conversations serialize between turns, store IDs, never Eloquent models, closures, or resources. A non-serializable property fails on the next message, which is a miserable debugging session.
Watch the TTL. conversation_cache_time defaults to tens of minutes; long onboarding flows silently restart. Raise it deliberately. Cache eviction under memory pressure looks identical to a user abandoning the flow.
Verify webhook signatures yourself. The Facebook driver handles X-Hub-Signature, but Telegram's secret-token header and newer platform verification mechanisms aren't covered by a 2020-era driver. Add route middleware that validates before BotMan sees the request, and fail closed.
Platform retries are normal. BotMan does not dedupe deliveries. Key handlers on a platform message ID in Redis/DB or you'll send duplicate replies and double-charge side effects.
Pattern matching surprises. Anchored regex, so hears('hello') won't fire on "hello, bot". Case-insensitivity is built in; whitespace normalization is not. When a handler "doesn't fire", dump $bot->getMessage()->getText() from the fallback before touching anything else.
Swallowed exceptions look like health. A throwing handler or middleware can degrade into a silent non-response while your uptime check stays green. Log inside every middleware and make the fallback report errors, not just a friendly nudge.
Config caching and credential rotation. After changing driver credentials: php artisan config:clear (plus cache:clear if you run one). Rotated tokens that "didn't take" are almost always cached config.
Studio is dev tooling. /botman/chat is a great local harness and a bad thing to expose unauthenticated in production. Gate it or strip it from the production route file.
Driver decay is your real production risk. HipChat is dead; Telegram, Messenger, Slack and WeChat have all changed payloads, limits, and auth flows since 2020. Budget a quarterly check of each in-scope driver against the platform changelog, and keep $bot->getMessage()->getPayload() as the escape hatch for platform-specific features the driver doesn't model.
Pin ^2.6, keep a tested lockfile, and name an owner. MIT makes forking legal, not free. Write the exit criteria down — "if there's no maintained Laravel 9+ path by , replace behind the adapter" — so the decision is made on evidence rather than discovered mid-upgrade.
How can I help you explore Laravel packages today?