bootdesk / chat-sdk-laravel
Laravel integration for the PHP Chat SDK
Requires
- php: >=8.2
- bootdesk/chat-sdk-core: 0.4.45
- illuminate/cache: ^10.0|^11.0|^12.0|^13.0
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- nyholm/psr7: ^1.0
- symfony/psr-http-message-bridge: ^5.4|^6.0|^6.4|^7.0|^8.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
This package is auto-updated.
Last update: 2026-07-14 21:24:56 UTC
README
Laravel integration for laravel-bootdesk.
Install
composer require bootdesk/chat-sdk-laravel
Setup
php artisan chat:install
This publishes config/chat.php to your application.
Configuration
The published config/chat.php file contains the following sections:
return [ // The display name your bot uses when posting messages. 'user_name' => env('BOT_USERNAME', 'Bot'), // Platform adapters to enable. Only adapters whose Composer package // is installed (class_exists) will be loaded. For multi-tenant // setups, omit the platform here and use an AdapterResolver instead. 'adapters' => [ // 'slack' => [ // 'bot_token' => env('SLACK_BOT_TOKEN'), // 'signing_secret' => env('SLACK_SIGNING_SECRET'), // ], // 'telegram' => [ // 'bot_token' => env('TELEGRAM_BOT_TOKEN'), // ], // 'whatsapp' => [ // 'access_token' => env('WHATSAPP_ACCESS_TOKEN'), // 'app_secret' => env('WHATSAPP_APP_SECRET'), // 'phone_number_id' => env('WHATSAPP_PHONE_NUMBER_ID'), // 'verify_token' => env('WHATSAPP_VERIFY_TOKEN'), // ], // 'discord' => [ // 'bot_token' => env('DISCORD_BOT_TOKEN'), // 'application_id' => env('DISCORD_APPLICATION_ID'), // 'public_key' => env('DISCORD_PUBLIC_KEY'), // ], // 'messenger' => [ // 'page_access_token' => env('MESSENGER_PAGE_ACCESS_TOKEN'), // 'app_secret' => env('MESSENGER_APP_SECRET'), // 'verify_token' => env('MESSENGER_VERIFY_TOKEN'), // ], // 'web' => [ // 'user_name' => env('BOT_USERNAME', 'Bot'), // ], // 'github' => [ // 'auth_token' => env('GITHUB_TOKEN'), // 'webhook_secret' => env('GITHUB_WEBHOOK_SECRET'), // ], // 'linear' => [ // 'api_key' => env('LINEAR_API_KEY'), // 'webhook_secret' => env('LINEAR_WEBHOOK_SECRET'), // ], ], // State persistence backed by the Laravel Cache facade. The cache // store is resolved from the facade at runtime — configure it in // config/cache.php as usual. 'state' => [ 'prefix' => env('CHAT_STATE_PREFIX', 'chat:'), ], // Global handler classes registered on every Chat instance regardless // of adapter. Each class must implement a register($chat) method. 'handlers' => [ // \App\Chat\GlobalHandlers::class, ], // Adapter-specific handler groups. Only the matching group is // registered per webhook request, alongside global handlers above. 'handler_groups' => [ // 'slack' => [ // \App\Chat\SlackHandler::class, // ], // 'telegram' => [ // \App\Chat\TelegramHandler::class, // ], ], // How to handle concurrent messages for the same thread. // Core strategies: drop (default), queue, debounce, concurrent. // Laravel uses QueueConcurrencyHandler to dispatch jobs for async processing. 'concurrency' => env('CHAT_CONCURRENCY', 'drop'), // Scope for distributed locks: 'thread' (default) or 'channel'. // Use 'channel' for platforms like WhatsApp/Telegram where // conversations are per-channel (one conversation per phone number). 'lock_scope' => env('CHAT_LOCK_SCOPE', 'thread'), // Cross-platform per-user message persistence. Requires an // IdentityResolver bound to the container (IdentityResolver::class). 'transcripts' => null, ];
Webhook Routes
Register a webhook route for incoming platform events:
// routes/web.php or routes/api.php use BootDesk\ChatSDK\Laravel\Http\Controllers\WebhookController; Route::match(['get', 'post'], '/api/webhooks/{adapter}', WebhookController::class);
The {adapter} segment matches the keys in your config/chat.php adapters array (e.g. slack, telegram, discord).
Handlers
Create a handler class to respond to messages:
// app/Chat/ChatHandlers.php namespace App\Chat; use BootDesk\ChatSDK\Core\Chat; use BootDesk\ChatSDK\Core\MessageContext; use BootDesk\ChatSDK\Laravel\Contracts\ChatHandler as ChatHandlerContract; class ChatHandlers implements ChatHandlerContract { public function register(Chat $chat): void { $chat->onNewMessage('/^hello$/i', function (MessageContext $ctx) { $ctx->thread->post('Hey!'); }); $chat->fallback(function (MessageContext $ctx) { $ctx->thread->post("I don't understand that."); }); } }
Register it in config/chat.php:
// Global — fires for every adapter 'handlers' => [\App\Chat\ChatHandlers::class],
Or scoped to a specific adapter group:
'handler_groups' => [ 'slack' => [\App\Chat\SlackHandlers::class], 'telegram' => [\App\Chat\TelegramHandlers::class], ],
When a webhook arrives for slack, both global and slack group handlers are registered. telegram group handlers are skipped.
Multiple Groups per Channel
Override resolveGroups on the WebhookController to route channels to different handler groups — even combining multiple groups:
use Illuminate\Http\Request; use Psr\Http\Message\ServerRequestInterface; class ChannelAwareController extends WebhookController { protected function resolveGroups(string $adapter, Request $request, ServerRequestInterface $psrRequest): array { $channel = $request->input('channel_id'); return match ($channel) { 'C001' => ['slack', 'internal-support'], 'C002' => ['slack', 'customer-support'], default => [$adapter], }; } }
Then ChatFactory::forGroups(['slack', 'internal-support']) merges global handlers + handlers from both groups. Groups are serialized as the chat_groups PSR-7 attribute — they survive into async queue jobs automatically.
Handlers that Inspect the Request
Implement ChatHandlerWithRequest to access the PSR-7 request during registration:
use BootDesk\ChatSDK\Laravel\Contracts\ChatHandlerWithRequest; class TenantAwareHandler implements ChatHandlerWithRequest { public function register(Chat $chat, ?ServerRequestInterface $request = null): void { $tenant = $request?->getHeaderLine('X-Tenant') ?? 'default'; $chat->onNewMessage('/bill/', function (MessageContext $ctx) use ($tenant) { // tenant-specific billing flow }); } }
The factory auto-detects which interface the handler implements — existing ChatHandler implementations continue working unchanged.
Middleware
Middleware intercept messages at different stages. Register in your handler class:
use BootDesk\ChatSDK\Core\Contracts\WebhookMiddleware; use BootDesk\ChatSDK\Core\Contracts\ReceivingMiddleware; use BootDesk\ChatSDK\Core\Contracts\SendingMiddleware; class ChatHandlers { public function register(Chat $chat): void { // Intercept raw webhook before parsing $chat->addWebhookMiddleware(new class implements WebhookMiddleware { public function handle(ServerRequestInterface $request, callable $next): ResponseInterface { logger()->info('Webhook received', ['path' => $request->getUri()->getPath()]); return $next($request); } }); // Transform inbound messages before handlers $chat->addReceivingMiddleware(new class implements ReceivingMiddleware { public function handle(Message $message, Adapter $adapter, callable $next): ?Message { // Return null to drop the message if (str_contains($message->text, 'blocked')) { return null; } return $next($message); } }); // Transform outbound messages before delivery $chat->addSendingMiddleware(new class implements SendingMiddleware { public function handle(string $threadId, PostableMessage $message, Adapter $adapter, string $operation, callable $next): ?SentMessage { logger()->info('Sending message', ['thread' => $threadId, 'operation' => $operation]); return $next($message); } }); } }
Operations: post, edit, postEphemeral
Transcripts
Per-user message persistence stored via the Laravel cache. Incoming and outgoing messages are auto-recorded when configured.
Setup
Bind an IdentityResolver and set transcripts config:
// AppServiceProvider::register() use BootDesk\ChatSDK\Core\Contracts\IdentityResolver; use BootDesk\ChatSDK\Core\Author; $this->app->bind(IdentityResolver::class, fn () => new class implements IdentityResolver { public function resolve(Author $author): ?string { return $author->id; } });
// config/chat.php 'transcripts' => ['max_messages' => 100, 'ttl_ms' => 2592000000],
Custom implementation
Override the TranscriptsApi binding in any service provider:
$this->app->bind(TranscriptsApi::class, function ($app) { return new MyRedisTranscriptsApi( state: $app->make(StateAdapter::class), config: ['max_messages' => 200], ); });
Usage
$transcripts = $chat->getTranscripts(); // List history for a user $entries = $transcripts->list('user:U123'); // Each entry has: id, text, authorId, threadId, timestamp, direction // Count $count = $transcripts->count('user:U123'); // Delete $transcripts->delete('user:U123');
Multi-Tenant Adapter Resolution
For multi-tenant applications where each tenant has their own bot credentials, use an AdapterResolver:
// app/Chat/MultiTenantAdapterResolver.php namespace App\Chat; use BootDesk\ChatSDK\Core\Contracts\Adapter; use BootDesk\ChatSDK\Core\Contracts\AdapterResolver; use BootDesk\ChatSDK\Slack\SlackAdapter; use BootDesk\ChatSDK\Telegram\TelegramAdapter; use Illuminate\Support\Facades\DB; use Psr\Http\Message\ServerRequestInterface; class MultiTenantAdapterResolver implements AdapterResolver { public function resolve(string $name, ?ServerRequestInterface $request): ?Adapter { // Extract tenant from request (header, subdomain, route param, etc.) // When called from a job, $request is null - use other context (job payload, auth, etc.) $tenantId = $request?->getHeaderLine('X-Tenant-ID') ?? $this->getTenantFromContext(); if ($tenantId === null || $tenantId === '') { return null; } // Load tenant-specific credentials from database $config = DB::table('tenant_chat_configs') ->where('tenant_id', $tenantId) ->where('adapter', $name) ->first(); if (! $config) { return null; } // Instantiate adapter with tenant credentials return match ($name) { 'slack' => new SlackAdapter( botToken: $config->credentials['bot_token'], httpClient: app(\Psr\Http\Client\ClientInterface::class), signingSecret: $config->credentials['signing_secret'] ?? null, ), 'telegram' => new TelegramAdapter( botToken: $config->credentials['bot_token'], httpClient: app(\Psr\Http\Client\ClientInterface::class), secretToken: $config->credentials['secret_token'] ?? null, ), default => null, }; } }
Register the resolver in a service provider:
// app/Providers/AppServiceProvider.php use BootDesk\ChatSDK\Core\Contracts\AdapterResolver; public function register(): void { $this->app->bind( AdapterResolver::class, \App\Chat\MultiTenantAdapterResolver::class ); }
Resolution order: Tenant-specific (resolver) → Global (config). Tenants can override specific adapters while falling back to global defaults for others.
Injecting ChatFactory
To send messages programmatically, inject ChatFactory and get a Chat instance:
use BootDesk\ChatSDK\Laravel\ChatFactory; class MessageController { public function __construct( private ChatFactory $chatFactory, ) {} public function send() { $chat = $this->chatFactory->default(); // global handlers only $chat->thread('slack:C123')->post('Hello!'); } }
Or for adapter-specific handlers:
$chat = $this->chatFactory->forGroup('slack'); // global + slack handlers $chat->handleWebhook('slack', $psrRequest);
For multiple groups:
$chat = $this->chatFactory->forGroups(['slack', 'internal-support']); // global + both groups
Artisan Commands
| Command | Description |
|---|---|
php artisan chat:list |
List configured adapters |
php artisan chat:install |
Publish config file |
Queue Processing
The package binds QueueConcurrencyHandler as the default ConcurrencyHandler. It dispatches jobs as follows: drop acquires a lock during the webhook (dispatches ProcessMessageJob if acquired, drops silently if held — lock released when job finishes); queue and concurrent dispatch ProcessMessageJob; debounce dispatches ProcessDebouncedMessageJob (unique delayed job). The debounce job caches the latest message and a :last timestamp; on re-dispatch it does not restore :last — preventing infinite re-dispatch loops. :latest and :skipped restoration is guarded against overwriting concurrent webhook data. RequiresSyncResponse adapters always process inline (within the HTTP request) regardless of strategy.
When the original PSR-7 webhook request is available, QueueConcurrencyHandler serializes it into a RequestContext value object (method, URI, headers, body, query/parsed/server params, cookies, version, requestAttributes) and passes it to every dispatched job. Both ProcessMessageJob and ProcessDebouncedMessageJob reconstruct the PSR-7 request and pass it to Chat::resolveAdapter() — so AdapterResolver::resolve($name, $request) receives the original request in both sync and queued contexts. The requestAttributes field captures PSR-7 getAttributes() — extend WebhookController to add tenant/context attributes that survive into jobs.
Make sure your Laravel queue worker is running:
php artisan queue:work
No manual setup is needed beyond configuring your queue driver in config/queue.php.
State
State persistence uses the Laravel Cache facade. The cache store is resolved from the facade at runtime — configure it in config/cache.php as usual.
Error Handling
Adapter exceptions bubble up to Laravel's exception handler. Register custom handlers in app/Exceptions/Handler.php:
use BootDesk\ChatSDK\Core\Exceptions\AdapterException; use BootDesk\ChatSDK\Core\Exceptions\AuthenticationException; use BootDesk\ChatSDK\Core\Exceptions\RateLimitException; use Illuminate\Http\Request; public function register(): void { $this->renderable(function (AuthenticationException $e, Request $request) { return response()->json(['error' => 'Unauthorized'], 401); }); $this->renderable(function (RateLimitException $e, Request $request) { return response()->json(['error' => 'Rate limited'], 429); }); $this->renderable(function (AdapterException $e, Request $request) { Log::error('Chat adapter error', [ 'message' => $e->getMessage(), 'adapter' => $request->route('adapter'), ]); return response()->json(['error' => 'Adapter failed'], 500); }); }
Exception types:
AuthenticationException— Invalid credentials/tokensRateLimitException— Platform rate limit exceededAdapterException— Generic adapter errorsResourceNotFoundException— Adapter/thread not foundValidationException— Invalid input data
Documentation
Full API documentation: https://bootdesk.github.io/chat-sdk
License
MIT