torgodly / messenger-bot
Facebook Messenger webhooks and Page comments for Laravel (BotMan-style API, no BotMan dependency).
Requires
- php: ^8.3
- ext-json: *
- illuminate/bus: ^12.0|^13.0
- illuminate/cache: ^12.0|^13.0
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/events: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/queue: ^12.0|^13.0
- illuminate/routing: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.27
- orchestra/testbench: ^10.48|^11.0
- pestphp/pest: ^4.0
- phpunit/phpunit: ^11.5|^12.0
README
Facebook Messenger webhooks for Laravel (messages, postbacks, quick replies, feed comments). BotMan-style API (hears, payload, onComment) without BotMan.
PHP 8.3+ · Laravel 12 or 13 · Composer ^2.2
Quick start
Single Facebook Page (default)
composer require torgodly/messenger-bot:^2.2 php artisan vendor:publish --tag=messenger-bot-config php artisan messenger-bot:install
Register handlers in AppServiceProvider::boot(). No tenant config.
Multi-tenant (many Pages)
php artisan messenger-bot:install --tenant --model=App\Models\YourFacebookPage
Set in .env (no quotes around the FQCN):
MESSENGER_BOT_TENANCY_ENABLED=true MESSENGER_BOT_TENANCY_CONNECTION_MODEL=App\Models\YourFacebookPage MESSENGER_BOT_TENANCY_PAGE_ID_COLUMN=page_id MESSENGER_BOT_AUTO_SUBSCRIBE_AFTER_OAUTH=true MESSENGER_BOT_AUTO_SYNC_MENU_AFTER_OAUTH=true MESSENGER_BOT_OAUTH_PENDING_PAGES_URL=https://YOUR-DOMAIN/meta/pick-page MESSENGER_BOT_VALIDATES_PAGE_LINK=App\\Messenger\\YourPageLinkValidator
Your model must extend Eloquent Model and implement MessengerBot\Contracts\MessengerConnectable (trait InteractsWithMessengerConnection is recommended). Wrong model class fails fast in local / testing.
Business rules (enforced by your app via ValidatesMessengerPageLink):
- One Graph Page ID → at most one tenant at a time.
- One tenant → at most one linked Page at a time (reconnecting the same
page_idis allowed).
Meta Developer checklist
| Step | What to do |
|---|---|
| App | Messenger product + Facebook Login; add your Page. |
| OAuth redirect | Exact match: https://YOUR-DOMAIN/messenger-bot/oauth/facebook/callback (or MESSENGER_BOT_OAUTH_REDIRECT_URI). Production needs HTTPS; APP_URL must match. |
| Webhook | Callback https://YOUR-DOMAIN/webhook/messenger, verify token = MESSENGER_BOT_VERIFY_TOKEN. |
| Fields | Enable fields in config webhook_fields — include feed for Page comments. messenger-bot:install / sync-page can subscribe via Graph. |
| Tokens | Production: MESSENGER_BOT_PAGE_TOKEN_CACHE_STORE and/or MESSENGER_BOT_CONNECTION_TOKEN_CACHE_STORE = redis or database. |
Routes register outside the web middleware group (avoids CSRF 419 on Meta POSTs).
Handlers
use MessengerBot\Facades\MessengerBot; MessengerBot::hears('hi', fn ($bot, $message) => $bot->reply('Hello!')); MessengerBot::payload('GET_STARTED', fn ($bot, $postback) => $bot->reply('Welcome!')); MessengerBot::onComment(function ($bot, $comment) { $bot->replyToComment($comment->id, 'Thanks!'); }); MessengerBot::fallback(fn ($bot, $message) => $bot->reply('Use the menu.'));
- Priority:
MessengerBot::hears('pattern', $handler, priority: 10) - Get Started:
payload()forMESSENGER_BOT_GET_STARTED_PAYLOADorMESSENGER_BOT_GET_STARTED_REPLY - Comments:
CommentCreatedis dispatched beforeonCommenthandlers (see Events)
Optional host config comment_handlers.queue — infrastructure only; the package does not ship DB comment rules. See README-EXTERNAL-RULES.md.
Multi-tenant
| Config / env | Purpose |
|---|---|
tenancy.connection_model / MESSENGER_BOT_TENANCY_CONNECTION_MODEL |
Eloquent class implementing MessengerConnectable |
tenancy.connection_page_id_column / MESSENGER_BOT_TENANCY_PAGE_ID_COLUMN |
Column matched to Meta Page ID (default facebook_page_id; many apps use page_id) |
Model messengerFacebookPageIdColumn() |
Override column name on the model |
OAuth redirect:
use MessengerBot\Facades\MessengerOAuth; return MessengerOAuth::redirectToFacebook($pageModel);
Multi-Page OAuth flow (no package UI)
Facebook may return 0, 1, or many managed Pages.
| Count | Behaviour |
|---|---|
| 0 | Error 400 |
| 1 | CompleteOAuthPageLink stores token → redirect success |
| 2+ | Not an error. No token stored. All Pages cached → redirect MESSENGER_BOT_OAUTH_PENDING_PAGES_URL?token=<opaque> |
Your app shows a Page picker (Filament, Livewire, etc.). After the user chooses:
use MessengerBot\OAuth\CompleteOAuthPageLink; use MessengerBot\OAuth\PendingOAuthPages; $payload = PendingOAuthPages::pull($request->query('token')); // $payload['pages'], $payload['mt'] app(CompleteOAuthPageLink::class)->complete($chosenPage, $payload['mt']);
ValidatesMessengerPageLink — implement in your app:
use MessengerBot\Contracts\ValidatesMessengerPageLink; final class YourPageLinkValidator implements ValidatesMessengerPageLink { public function assertMayLinkPage(array $page, array $mt): void { // Block if page_id belongs to another tenant // Block if tenant already has a different page_id // Allow reconnect when page_id unchanged } }
On rejection, OAuth redirect uses session flash key messenger_bot_oauth_error.
Optional: MESSENGER_BOT_OAUTH_PREFERRED_PAGE_ID — when exactly one Page in the list matches, link immediately (skip pending redirect).
| Env | Purpose |
|---|---|
MESSENGER_BOT_OAUTH_PENDING_PAGES_URL |
Required when tenancy enabled |
MESSENGER_BOT_OAUTH_PENDING_PAGES_TTL |
Cache minutes (default 10) |
MESSENGER_BOT_OAUTH_PENDING_PAGES_CACHE_PREFIX |
Default messenger_bot:oauth_pages: |
MESSENGER_BOT_VALIDATES_PAGE_LINK |
FQCN for validator (required when tenancy enabled) |
Webhook context:
app(\MessengerBot\Laravel\MessengerCurrentConnection::class)->resolution();
Resolver behaviour: looks up the model by Page ID column; if missing, falls back to the connection token page index and fires ConnectablePageIdSynced so your app can persist page_id on the row.
Advanced: MESSENGER_BOT_TENANCY_RESOLVER for a custom TenantResolver; optional EloquentMessengerTenantResolver subclass (see stubs/eloquent_messenger_tenant_resolver.php.stub).
After OAuth automation
When multi-tenant OAuth stores a token, ConnectionTokenStored runs. Enable automatic webhook subscribe + persistent menu sync:
MESSENGER_BOT_AUTO_SUBSCRIBE_AFTER_OAUTH=true MESSENGER_BOT_AUTO_SYNC_MENU_AFTER_OAUTH=true MESSENGER_BOT_LINK_SKIP_TOKEN_CHECK=false MESSENGER_BOT_AFTER_OAUTH_QUEUE=false
Config block: messenger-bot.after_connection_token_stored. Replaces most host wrappers around ConnectionTokenRepository::put().
On failure, the package logs and may queue SyncPageProfileAfterOAuthJob when queue_retry_on_failure is true.
Events
| Event | When |
|---|---|
WebhookReceived |
Raw webhook payload |
MessageReceived |
Incoming message |
PostbackReceived |
Postback |
CommentCreated |
Top-level feed comment |
ConnectionTokenStored |
After ConnectionTokenRepository::put() |
ConnectablePageIdSynced |
Tenant resolved from token index (DB page_id empty) |
PostsSynced, PostsCacheHit, PostsCacheMiss |
Posts sync |
Outgoing OutgoingMessage* |
Send pipeline |
DB-driven rules (your app)
The package does not store tenant rules. Load rules from your database inside handlers or a small engine — no optimize:clear needed.
Guide: README-EXTERNAL-RULES.md
Reference implementation: Matager
Matager (not included in this package) shows a production pattern:
CommentRuleEngine— readsMetaCommentRulerows per tenantMetaCommentPostId— normalizes Graph post IDs foronlyForPostIdsMessengerBot::onComment→ engine or queuedProcessMetaCommentRuleJobPersistFacebookPageAfterOAuthListeneronConnectionTokenStored— savespage_idonFacebookPage- Filament admin for rules — stays in the host app
Use Matager as a reference; copy patterns, not the package.
Page posts, Artisan, troubleshooting
Posts: SyncsFacebookPagePosts, PostsSyncRequest::forConnectable(), RefreshPostsCacheJob::forConnectable() — see previous examples in CHANGELOG / v2.0 notes.
Artisan: messenger-bot:install, install --tenant --model=, sync-page, token-status, clear-page-token.
Troubleshooting
| Symptom | Check |
|---|---|
Comments ignored / tenant_unresolved |
feed subscribed; MESSENGER_BOT_TENANCY_CONNECTION_MODEL + MESSENGER_BOT_TENANCY_PAGE_ID_COLUMN; Page linked via OAuth; token cache has page index |
| Webhook 403 | MESSENGER_BOT_APP_SECRET matches Meta |
| OAuth / webhook 419 | Routes must not use web CSRF; package registers outside web by default |
| Invalid tenancy model | Exception in local/testing; production logs critical and disables configurable resolver |
Upgrade 2.1 → 2.2
composer require torgodly/messenger-bot:^2.2- Set
MESSENGER_BOT_OAUTH_PENDING_PAGES_URLandMESSENGER_BOT_VALIDATES_PAGE_LINKwhen tenancy is on - Implement
ValidatesMessengerPageLinkfor Page/tenant uniqueness - Build a Page picker route that reads
?token=, callsPendingOAuthPages::pull(), thenCompleteOAuthPageLink::complete() - Breaking: multiple Pages from OAuth are no longer auto-linked to the first Page
Upgrade 2.0 → 2.1
composer require torgodly/messenger-bot:^2.1- Publish / merge config for
after_connection_token_storedandcomment_handlers - Replace host
put()wrappers withMESSENGER_BOT_AUTO_SUBSCRIBE_AFTER_OAUTH/MESSENGER_BOT_AUTO_SYNC_MENU_AFTER_OAUTH - Listen to
ConnectionTokenStoredfor DB hydration (page_id) instead of wrapping the repository - Remove custom resolvers that only duplicated token page-index lookup — use package fallback +
ConnectablePageIdSynced