bootdesk / chat-sdk-laravel
Laravel integration for the PHP Chat SDK
Requires
- php: >=8.2
- bootdesk/chat-sdk-core: ^0.2.4
- 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
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'), // ], ], // Cache store used for state persistence. Any Laravel cache store // works: file, redis, database, memcached, array. Configure the // store in config/cache.php as usual. 'state' => [ 'store' => env('CHAT_STATE_STORE', 'file'), 'prefix' => env('CHAT_STATE_PREFIX', 'chat:'), ], // Classes that register message handlers on the Chat instance. // Each class must implement a register($chat) method. 'handlers' => [ // \App\Chat\ChatHandlers::class, ], // How to handle concurrent messages for the same thread: // - drop: Discard new messages while one is being processed // - queue: Queue messages and process sequentially // - debounce: Reset timer, process only the latest // - concurrent: Process all messages simultaneously '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 // identity resolver bound to 'chat.identity' in a service provider. '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:
'handlers' => [\App\Chat\ChatHandlers::class],
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
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.
Facade
use BootDesk\ChatSDK\Laravel\ChatFacade as Chat; Chat::thread('slack:C123')->post('Hello!');
Artisan Commands
| Command | Description |
|---|---|
php artisan chat:list |
List configured adapters |
php artisan chat:install |
Publish config file |
Queue Processing
Incoming messages are processed asynchronously via ProcessMessageJob. Make sure your Laravel queue worker is running:
php artisan queue:work
The job dispatches automatically when webhook requests arrive. No manual setup is needed beyond configuring your queue driver in config/queue.php.
State
State persistence uses Laravel's cache system. Set CHAT_STATE_STORE to any Laravel cache driver (file, redis, database, memcached, array). The cache store is configured 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
License
MIT