mpociot/botman-laravel-starter
A Laravel starter kit for the BotMan chatbot framework. It scaffolds a ready-to-run bot project with controllers, configuration, and example conversation logic, so you can quickly build messaging bots that respond across channels like Slack, Telegram, Facebook Messenger, and the web without wiring u...
Know what you're scaffolding. This is not a package you composer require into an existing app — it's a complete Laravel skeleton with BotMan 2.x pre-wired, distributed via composer create-project. The package name is the legacy one; the repo is botman/studio. If you try to add it to a running Laravel 10/11 service you will lose a day to Composer and learn nothing.
composer create-project mpociot/botman-laravel-starter mybot
cd mybot && cp .env.example .env && php artisan key:generate
php artisan serve
Pin the runtime before anything else. The last release is 2018-10-01, so expect PHP 7.2–7.4 and a Laravel 5.x-era constraint tree on Symfony 3/4. The fastest path to a working loop is a disposable container (php:7.4-cli + the extensions BotMan drivers want: curl, mbstring, json) rather than arguing with your host PHP. Run composer why-not laravel/framework 10 early — if that resolves cleanly you're on a newer snapshot than expected; if it explodes, that's the spike's real answer and you should record it, not fight it.
Where to look first, in this order:
routes/botman.php — this is the conversation surface. Every intent lives here or is dispatched from here.app/Http/Controllers/BotManController.php — the webhook entry point; it loads the driver and calls listen(). Confirm the route with php artisan route:list --path=botman.config/botman/ — one file per driver (web.php, slack.php, telegram.php, facebook.php, …) plus shared settings. Credentials go here, never in code.app/Providers/BotMan/ — driver registration, middleware and listener wiring, and where you add a channel or a custom driver.app/Conversations/ — multi-turn flows.First use case (an afternoon): open routes/botman.php, add one intent, refresh the bundled chat widget, type.
$botman->hears('hello', function ($bot) {
$bot->reply('Hi! Ask me about holidays or expenses.');
});
The starter ships a web driver and a local chat widget at /botman/tinker, so your first demo needs zero channel credentials — no Slack app, no Facebook page review, no public HTTPS. Exploit that: build the whole flow on the web driver, then swap in a real channel once the design holds up.
Keep routes/botman.php thin. The highest-leverage habit with this skeleton is treating the routes file as a routing table, not a logic file:
$botman->hears('holidays', [HolidayIntent::class, 'handle']);
$botman->hears('expenses', [ExpenseIntent::class, 'handle']);
$botman->hears('{message}', FallbackIntent::class); // always last
Intent classes stay plain PHP with a handle(BotMan $bot, ...$params) signature. This is what makes the eventual port onto current BotMan core mechanical instead of archaeological — you move handlers, not a Laravel 5 app shell.
Use named pattern parameters for anything structured. hears() accepts {placeholder} segments that arrive as callback arguments, which gets you a long way without an NLU:
$botman->hears('book {room} for {people} people', function ($bot, $room, $people) { ... });
Promote multi-step flows into Conversation classes the moment a handler needs a second question. One method per step, entry point is run(), and the next step is invoked from inside the ask() callback:
class HolidayConversation extends Conversation
{
public function run()
{
$this->ask('How many days do you need?', function (Answer $answer) {
if (! is_numeric($answer->getText())) {
$this->say('Numbers only, please.');
return $this->repeat();
}
$this->days = (int) $answer->getText();
$this->askStartDate();
});
}
}
Start it with $bot->startConversation(new HolidayConversation); end with $this->stop() when you want the flow to terminate. $this->bot is available inside a conversation, so $this->bot->reply() / ->say() work for out-of-band messages.
Interactive replies need getValue(), not getText(). Buttons and quick replies come back as payload answers:
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use BotMan\BotMan\Messages\Outgoing\Question;
$this->ask(Question::create('Which team?')->addButton(Button::create('HR')->value('hr')), function (Answer $answer) {
$team = $answer->isInteractiveReply() ? $answer->getValue() : strtolower($answer->getText());
});
Push cross-cutting concerns into middleware. Register on $bot->middleware->received(), ->heard(), ->captured(), ->sending(); each implements the matching interface (Received, Heard, Captured, Sending) and takes ($message, $next, BotMan $bot). This is where you put request logging, PII redaction, allow-listing (internal tools), and input normalisation. It's also where NLU belongs — BotMan core ships middleware for API.AI/Dialogflow and Wit.ai; add it as received middleware incrementally alongside your hears() patterns rather than rewriting intents:
$bot->middleware->received(new ApiAi(config('services.dialogflow.token')));
Scope handlers per channel with the third argument to hears() when a flow only makes sense on one surface (Slack block actions, Telegram keyboards) — it keeps the shared routes file honest about what is portable.
Acknowledge fast, work later. Slack, Telegram and Messenger all have tight timeouts. Reply immediately from the handler, dispatch a job, and use proactive messaging for the follow-up where the driver supports it ($bot->say('Report ready', $userId)), or accept the two-step "type status" pattern for drivers that don't.
Test the logic, not the loop. Extract decision logic from handler closures into plain classes you can unit test, and only exercise the bot end-to-end through the web widget. BotMan core does ship a test/fake driver and tester helper for asserting replies — check the class names in vendor/botman/botman/src/ for your pinned version, since they drifted across 2.x — but for a two-week spike, handler-level tests give you most of the value for a fraction of the setup.
Composer will fail before your first commit. composer create-project against PHP 8.1+ typically dies on Symfony/Illuminate constraints. Don't paper over it with --ignore-platform-reqs; the runtime incompatibilities are real. Pin the container, commit composer.lock, and build an image — otherwise a rebuild in six months will resolve a different tree and you'll never reconstruct the demo.
The /botman/tinker route is a public endpoint. It's fine locally and a bad idea anywhere else. Gate it behind an environment check or auth middleware before anything leaves your laptop, and confirm CSRF handling — app/Http/Middleware/VerifyCsrfToken::$except must include the botman path, so if you rename the webhook route, add it there or every web-widget POST will 419.
Conversation state lives in the Laravel cache, and that's a scaling decision. Check config/botman/config.php for conversation_cache_time and user_cache_time against the cache store in use. file or array stores silently break multi-node deployments — conversations "forget" mid-flow depending on which container answers. Use Redis the moment you have more than one instance. Too-long timeouts leave users stuck in an abandoned flow; too-short ones truncate legitimate ones. Always ship a cancel / start over intent that begins a fresh conversation, because that's the only reset users will trust.
The catch-all fallback eats things. hears('{message}') registered before your real intents swallows everything, and a broad fallback can intercept button payloads before the conversation sees them. Register it last, and make the fallback reply point at the menu rather than apologising vaguely.
Channel SDKs have drifted since 2018. Facebook Graph API versions, Telegram Bot API changes, and Slack's Events API (including the url_verification challenge handshake) have all moved. Treat each non-web driver as unverified until you've posted a real payload at it. If Slack's challenge isn't answered automatically, a tiny controller branch that echoes challenge back is the usual fix. Verify webhook authenticity yourself in middleware (X-Hub-Signature for Facebook, signing secret for Slack) — don't assume the driver does it, and don't put an unauthenticated webhook on the internet.
Debugging is quiet by default. When a handler "doesn't fire", the cause is usually one of: intent order (first match wins), a channel-scoped hears() that excludes the driver you're testing on, or middleware short-circuiting by returning a message instead of $next($message). Add a received middleware that logs the IncomingMessage text, driver, and sender ID on the way in — five lines that save hours. Errors inside handlers can surface to the user as a generic reply, so check storage/logs, not the chat window.
Extending is straightforward but version-specific. New channel = config file in config/botman/ + a service provider entry in app/Providers/BotMan/. Scaffolding generators exist under the botman: command namespace and have been renamed between releases — run php artisan list | grep botman rather than trusting any tutorial's command name, then read what it generated before editing.
Treat it as a fork, not a dependency, and plan the exit on day one. Zero tracked dependents and no release since October 2018 means no upstream fixes. Before writing handler code, write down the reuse list — conversation classes, intent definitions, middleware, routes — and keep those files free of starter-specific glue (deferred providers, old facades, Studio-only config paths). If the spike graduates, you rebuild the shell on current Laravel plus BotMan core and port that list. If it doesn't, you delete the container and keep the evidence. Either way the decision stays cheap, provided the starter never entered the dependency tree of anything you have to patch for security.
How can I help you explore Laravel packages today?