sgomez/botman-bundle
Symfony bundle that integrates the BotMan chatbot framework into Symfony applications, making it easy to configure messaging platforms, register conversation handlers, and build conversational interfaces using familiar Symfony services and configuration.
Install and wire it up
composer require sgomez/botman-bundle
Register the bundle (Symfony Flex usually does this automatically):
// config/bundles.php
return [
// ...
Sgomez\BotManBundle\BotManBundle::class => ['all' => true],
];
Configure at least one driver in config/packages/botman.yaml. The Web driver is the fastest path to a first working bot because you can exercise it with the botman/web widget without provisioning a Slack or Telegram token:
botman:
drivers:
web: ~ # or slack: { token: '%env(SLACK_TOKEN)%' }
Define a controller that boots a conversation and calls listen():
namespace App\Controller;
use BotMan\BotMan\BotMan;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BotController extends AbstractController
{
#[Route('/botman', name: 'botman')]
public function listen(BotMan $botman): Response
{
$botman->hears('hello', fn (BotMan $bot) => $bot->reply('Hi there!'));
$botman->hears('.*', fn (BotMan $bot) => $bot->reply('Sorry, I did not understand.'));
$botman->listen();
return new Response('', Response::HTTP_NO_CONTENT);
}
}
Where to look first
bin/console config:dump-reference botman – the authoritative list of driver keys for the installed version.bin/console debug:container --tag=botman.driver – confirm which drivers the bundle has registered.vendor/sgomez/botman-bundle/ – its README is thin, so the DI extension and driver compiler pass are the real documentation.First real use case: a hears()-based Q&A handler behind a Slack slash command or web hook. Get the Web driver working locally first, then swap in the Slack/Telegram driver by changing only the botman.drivers block — application code stays identical.
Registering conversations — Conversations are ordinary services. Tag or autoconfigure them and hand the FQCN to $botman->hears() or a middleware:
$botman->hears('order {id}', function (BotMan $bot) {
$bot->startConversation($bot->getContainer()->make(OrderStatusConversation::class));
});
Because the bundle bridges to BotMan's container, make sure OrderStatusConversation is a public (or autowired) service, or pass a closure that does $this->get(OrderStatusConversation::class) from the controller.
Middleware via service tags — Cross-cutting concerns (logging, user lookup, typing indicators) belong in middleware tagged botman.middleware. Autoconfigure them:
services:
_defaults:
autowire: true
autoconfigure: true
tags: ['botman.middleware']
App\BotMan\Middleware\:
resource: '../src/BotMan/Middleware/'
Middleware can also be attached per-driver or per-hears() at runtime with $botman->middleware->heard(...) when you need it scoped rather than global.
Controller-per-platform vs. shared controller — For multi-channel bots, prefer one controller per platform route that shares a single "bot definition" service:
final class BotRegistrar
{
public function __construct(private BotMan $botman) {}
public function register(): void
{
$this->botman->hears('help', [HelpHandler::class, 'handle']);
// ...
}
}
Then Slack and Telegram controllers both call $registrar->register(); $botman->listen();. This keeps route wiring platform-specific but the conversation logic centralised and unit-testable.
Async / queue integration — If the bundle's botman.async settings are enabled, long-running handlers (LLM calls, third-party lookups) should delegate to a Messenger message and reply later via the stored BotMan instance ID. Configure the timeout in botman.async.timeout to be shorter than your HTTP timeout so the driver doesn't hang the webhook.
Testing — BotMan ships a FakeDriver (or you can subclass a driver) that lets you assert on outgoing replies without hitting a real API:
$bot = $this->createMock(BotMan::class);
// or use BotMan's FakeDriver and $bot->getDriver()->messages()
For end-to-end coverage, POST to /botman with the Web driver payload shape and assert on the returned JSON.
listen() writes to the response, it doesn't return one. BotMan's drivers call sendResponse() (typically exit or echo) internally. In Symfony this means a controller that returns a Response after listen() may already have output buffered. For the Web driver, capture the JSON with ob_start()/ob_get_clean() if you need to manipulate headers or wrap it; for messaging platforms just return new Response() and let BotMan handle the reply.
Registering two drivers makes them both listen. listen() iterates every registered driver and dispatches to whichever matches the request. If a payload accidentally matches two drivers you get duplicated replies. Remove unused drivers from botman.drivers rather than leaving them configured with empty tokens.
Conversation cache is required for multi-turn flows. BotMan persists conversation state between requests via a cache driver. If you don't configure one (or point it at Symfony's cache pool), startConversation() silently restarts on every message. Set botman.config.conversation_cache_time and ensure the underlying cache survives between requests — the default array cache does not.
Token configuration should always be env-backed. Use %env(SLACK_TOKEN)% in botman.drivers.slack.token and never commit real tokens. Verify with bin/console debug:container --env-vars that the variable is loaded in the target environment; a missing token surfaces as a generic InvalidArgumentException from the driver, not the bundle.
Version drift with BotMan core. The bundle pins a range of botman/botman versions and the driver class names/config keys change between 2.x releases. If a config key is rejected, run composer show botman/botman and cross-check the driver's __construct and getConfig() against vendor/botman/botman/src/Drivers/<X>/<X>Driver.php.
Debugging with no visible output. When a bot "does nothing", the cause is almost always (a) no driver matching the incoming request, or (b) the hears() pattern not matching. Enable the botman.middleware LoggingMiddleware (or your own tagged middleware) and log $bot->getMessage()->getText() plus $bot->getDriver()->getName() at the top of every handler.
Stateful BotMan service. The botman service is effectively request-scoped. Don't inject it into long-lived workers (Messenger consumers, RoadRunner/Swoole) and reuse it across jobs — construct a fresh instance per message or you'll get leaking conversation state.
Extension points worth knowing:
BotMan\BotMan\Interfaces\DriverInterface and tag with botman.driver.BotMan\BotMan\Interfaces\MiddlewareInterface and tag botman.middleware.BotMan\BotMan\Interfaces\CacheInterface and pointing the bundle config at it.Maintenance signal. With 12 stars, this bundle is a thin wrapper — read BotManExtension::load() before trusting a config key from a blog post, and be prepared to vendor-patch if a Symfony minor upgrade breaks service decoration. The alternative is to drop the bundle and wire BotMan manually in services.yaml, which is roughly 30 lines of DI and often the pragmatic call for long-term maintenance.
How can I help you explore Laravel packages today?