PHPackages                             bootdesk/chat-sdk-core - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. bootdesk/chat-sdk-core

ActiveLibrary[API Development](/categories/api)

bootdesk/chat-sdk-core
======================

Core package for the PHP Chat SDK

0.4.44(2w ago)031913MITPHPPHP &gt;=8.2

Since Jun 4Pushed 1w agoCompare

[ Source](https://github.com/bootdesk/chat-sdk-core)[ Packagist](https://packagist.org/packages/bootdesk/chat-sdk-core)[ RSS](/packages/bootdesk-chat-sdk-core/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (31)Versions (38)Used By (13)

bootdesk/chat-sdk-core
======================

[](#bootdeskchat-sdk-core)

Framework-agnostic core SDK for building chat bots in PHP.

Installation
------------

[](#installation)

```
composer require bootdesk/chat-sdk-core
```

Chat class
----------

[](#chat-class)

The main entry point. Accepts a state adapter, adapters, configuration, and optional `ConcurrencyHandler`.

```
use BootDesk\ChatSDK\Core\Chat;
use BootDesk\ChatSDK\Core\State\MemoryStateAdapter;
use BootDesk\ChatSDK\Core\Concurrency\DefaultConcurrencyHandler;

$chat = new Chat(
    state: new MemoryStateAdapter(),
    userName: 'MyBot',
    config: [],
    concurrencyHandler: new DefaultConcurrencyHandler($state, ['concurrency' => 'drop']),
);

// Register handlers
$chat->onNewMessage('/^hello$/i', function (MessageContext $ctx) {
    $ctx->thread->post('Hey there!');
});

$chat->onDirectMessage(function (MessageContext $ctx) {
    $ctx->thread->post('You DMd me!');
});

$chat->onNewMention(function (MessageContext $ctx) {
    $ctx->thread->post('You mentioned me!');
});
```

Thread
------

[](#thread)

Represents a conversation thread on any platform. Retrieved by platform-specific identifier.

```
$thread = $chat->thread('slack:C12345');

$thread->post('Hello!');
$thread->edit('msg-id', 'Updated text');
$thread->delete('msg-id');
$thread->subscribe();
$thread->startTyping();

$thread->setState(['step' => 2]);
$state = $thread->getState();
```

Direct Messages (DM)
--------------------

[](#direct-messages-dm)

Open a DM with a user. Adapter is inferred from userId format or explicit `adapter:userId` prefix.

```
// Infer adapter from userId format:
$thread = $chat->openDM('U1234567');          // Slack (U prefix)
$thread = $chat->openDM('users/12345');       // Google Chat
$thread = $chat->openDM('29:base64string...');// Teams
$thread = $chat->openDM('8f1f3c7e-d4e1-...');// Linear (UUID v4)
$thread = $chat->openDM('1234567890');        // Discord/Telegram/GitHub (numeric)

// Explicit adapter prefix:
$thread = $chat->openDM('slack:U1234567');
$thread = $chat->openDM('telegram:123456789');

// Or pass an Author object:
$author = new Author(id: 'U1234567');
$thread = $chat->openDM($author);

$thread->post('Hello in DM!');
```

Throws `RuntimeException` when adapter can't be inferred or doesn't support DMs.

Cards
-----

[](#cards)

Build rich, platform-adaptive message cards with text, tables, dividers, links, buttons, and more.

### Element types

[](#element-types)

Builder methodTypeDescription`header(string)`—Card title (rendered as bold header)`section(callable)``Section`Grouped text + fields`text(string, TextStyle)``Text`Styled text (`Bold`, `Muted`, `Plain`)`divider()``Divider`Horizontal separator (`---`)`link(label, url)``Link`Inline hyperlink`table(headers, rows, align)``Table`Data table with optional column alignment`image(url, alt)``Image`Embedded image`actions(Button[])``Button`Action buttons (triggers `onAction`)`linkButton(label, url, style)``LinkButton`URL button (opens link)### Text styles

[](#text-styles)

```
use BootDesk\ChatSDK\Core\Cards\TextStyle;

TextStyle::Plain;  // default
TextStyle::Bold;   // rendered as **bold** or  depending on platform
TextStyle::Muted;  // rendered as _italic_ or muted color
```

### Buttons

[](#buttons)

```
use BootDesk\ChatSDK\Core\Cards\Button;
use BootDesk\ChatSDK\Core\Cards\ButtonStyle;
use BootDesk\ChatSDK\Core\Cards\LinkButton;

// Action button — triggers onAction handler when clicked
Button::primary('Confirm', 'order_confirm', ['item' => 'Pizza']);
Button::danger('Cancel', 'order_cancel');
Button::secondary('Maybe', 'order_maybe');

// Link button — opens a URL in the client
LinkButton::primary('Open Dashboard', 'https://dash.example.com');
LinkButton::danger('Report Issue', 'https://github.com/org/repo/issues');
```

### Table with alignment

[](#table-with-alignment)

```
use BootDesk\ChatSDK\Core\Cards\TableAlignment;

$card = Card::make()
    ->table(
        ['Service', 'Status', 'Uptime'],
        [
            ['API', '✅ Healthy', '99.9%'],
            ['Database', '✅ Connected', '99.8%'],
            ['Queue', '✅ Running', '100%'],
        ],
        [TableAlignment::Left, TableAlignment::Center, TableAlignment::Right],
    );
```

### Full example

[](#full-example)

```
use BootDesk\ChatSDK\Core\Cards\Card;
use BootDesk\ChatSDK\Core\Cards\Button;
use BootDesk\ChatSDK\Core\Cards\ButtonStyle;
use BootDesk\ChatSDK\Core\Cards\TextStyle;
use BootDesk\ChatSDK\Core\Cards\TableAlignment;

$card = Card::make()
    ->header('System Status')
    ->text('All services are operational.', TextStyle::Bold)
    ->divider()
    ->table(
        ['Service', 'Status', 'Uptime'],
        [
            ['API', '✅ Healthy', '99.9%'],
            ['Database', '✅ Connected', '99.8%'],
            ['Queue', '✅ Running', '100%'],
        ],
        [TableAlignment::Left, TableAlignment::Center, TableAlignment::Right],
    )
    ->divider()
    ->link('View details', 'https://status.example.com')
    ->linkButton('Dashboard', 'https://dash.example.com', ButtonStyle::Primary)
    ->actions([Button::primary('Refresh', 'refresh_status')]);

$thread->post($card);
```

### Card imageUrl

[](#card-imageurl)

Set a header image that renders as a native image block on supported platforms:

```
$card = Card::make()
    ->imageUrl('https://picsum.photos/seed/demo/800/200', 'Demo banner')
    ->header('Status')
    ->text('All good!');
```

- **Slack**: renders as a `type: image` Block Kit block before the header
- **Telegram**: uses `sendPhoto` with the card text as caption
- **Discord**: renders as `embed.image.url`

### Sections with fields

[](#sections-with-fields)

```
$card = Card::make()
    ->header('Deploy Ready')
    ->section(fn ($s) => $s
        ->text('Build passed on main')
        ->fields(['Branch' => 'main', 'Status' => 'passing'])
    )
    ->actions([Button::primary('Deploy', 'deploy')]);
```

### Platform rendering

[](#platform-rendering)

Each adapter converts cards to its native format:

AdapterFormatSlackBlock Kit (header, section, divider, image, actions)DiscordEmbed + Action Row componentsTelegramHTML text + inline keyboardWhatsAppInteractive reply buttons or text fallbackMessengerGeneric/Button templateGitHubMarkdown (headings, pipe tables, links)LinearMarkdown (same as GitHub)Attachments &amp; File Uploads
------------------------------

[](#attachments--file-uploads)

Send URL-based media attachments or binary file uploads with any message.

### URL-based Attachments

[](#url-based-attachments)

```
use BootDesk\ChatSDK\Core\Attachment;
use BootDesk\ChatSDK\Core\PostableMessage;

$message = new PostableMessage(
    content: 'Here is a photo:',
    attachments: [
        new Attachment('image', 'https://picsum.photos/seed/foo/800/600', 'Photo', 'image/jpeg'),
    ],
);
$thread->post($message);
```

All adapters support URL-based attachments. Platforms without native attachment rendering fall back to text links.

### Binary File Uploads

[](#binary-file-uploads)

```
use BootDesk\ChatSDK\Core\FileUpload;
use BootDesk\ChatSDK\Core\PostableMessage;

// From file path
$upload = FileUpload::fromFilename('/path/to/document.pdf');

// From string data
$upload = new FileUpload(file_get_contents($url), 'photo.jpg', 'image/jpeg');

$message = new PostableMessage(
    content: 'Here is your file:',
    files: [$upload],
);
$thread->post($message);
```

**Native support:** Slack (3-step API), Telegram (`sendDocument`), Discord (`files[N]` multipart).

**Other platforms:** Binary files are converted to URL-based attachments via a `FileUploadConverter`. If no converter is registered, `AdapterException` is thrown.

### FileUploadConverter

[](#fileuploadconverter)

```
use BootDesk\ChatSDK\Core\Contracts\FileUploadConverter;
use BootDesk\ChatSDK\Core\Attachment;
use BootDesk\ChatSDK\Core\FileUpload;

class S3FileUploader implements FileUploadConverter
{
    public function upload(FileUpload $file, Adapter $adapter): Attachment
    {
        $url = $this->s3->upload($file->getData(), $file->filename);
        return new Attachment('file', $url, $file->filename, $file->mimeType);
    }
}
```

In Laravel, bind it in your service provider:

```
$this->app->bind(FileUploadConverter::class, S3FileUploader::class);
```

Conversations
-------------

[](#conversations)

Define multi-turn dialog flows by extending the `Conversation` class.

```
use BootDesk\ChatSDK\Core\Conversations\Conversation;
use BootDesk\ChatSDK\Core\Thread;
use BootDesk\ChatSDK\Core\Message;

class OrderConversation extends Conversation
{
    public function start(Thread $thread, Message $message): void
    {
        $this->ask($thread, 'What would you like to order?', 'handleOrder');
    }

    public function handleOrder(Thread $thread, Message $message): void
    {
        $this->say($thread, "You ordered: {$message->text}");
        $this->end($thread);
    }
}
```

Start a conversation:

```
$chat->conversationManager->start(OrderConversation::class, $thread, $message);
```

Emoji Resolver
--------------

[](#emoji-resolver)

Cross-platform emoji normalization for reactions and message placeholders.

### EmojiValue

[](#emojivalue)

Immutable singleton representing a normalized emoji name.

```
use BootDesk\ChatSDK\Core\Support\EmojiValue;

$e = EmojiValue::get('thumbs_up');
$e->name;       // "thumbs_up"
(string) $e;    // "{{emoji:thumbs_up}}"

// Singleton — same name always returns same object
EmojiValue::get('thumbs_up') === EmojiValue::get('thumbs_up'); // true
```

### EmojiResolver

[](#emojiresolver)

Converts between platform formats and normalized names.

```
use BootDesk\ChatSDK\Core\Support\EmojiResolver;

$resolver = new EmojiResolver;

// Normalize incoming reactions
$resolver->fromSlack('+1');       // "thumbs_up"
$resolver->fromGChat('👍');       // "thumbs_up"
$resolver->fromTeams('like');     // "thumbs_up"
$resolver->fromGithub('+1');      // "thumbs_up"

// Convert to platform format for outgoing reactions
$resolver->toSlack('thumbs_up');   // "+1"
$resolver->toGChat('thumbs_up');   // "👍"
$resolver->toDiscord('thumbs_up'); // "👍"
$resolver->toGithub('thumbs_up');  // "+1"

// Match across formats
$resolver->matches('+1', 'thumbs_up');  // true
$resolver->matches('👍', 'thumbs_up');  // true
```

### Emoji Placeholders in Messages

[](#emoji-placeholders-in-messages)

Use `{{emoji:name}}` in message text. They convert automatically to the correct platform format.

```
EmojiResolver::convertPlaceholders(
    'Great work! {{emoji:fire}} {{emoji:rocket}}',
    'slack',
);
// Result: "Great work! :fire: :rocket:"

EmojiResolver::convertPlaceholders(
    'Great work! {{emoji:fire}} {{emoji:rocket}}',
    'gchat',
);
// Result: "Great work! 🔥 🚀"
```

Adapters with `EmojiResolver` injection auto-convert placeholders in outgoing messages.

### Reaction Events

[](#reaction-events)

Reaction events provide both the normalized name and the raw platform string:

```
$chat->onReaction(function (ReactionEvent $event) {
    $event->emoji;      // Normalized: "thumbs_up"
    $event->rawEmoji;   // Platform raw: "+1" (Slack) or "👍" (Telegram)
});
```

See the [Emoji System guide](https://bootdesk.github.io/chat-sdk/guide/14-emoji.html) for the full emoji map and platform details.

Middleware
----------

[](#middleware)

Six middleware interfaces for intercepting different stages:

- **ReceivingMiddleware** -- Intercept inbound messages before handlers run
- **SendingMiddleware** -- Intercept outbound messages before they are delivered
- **WebhookMiddleware** -- Intercept raw webhook payloads before parsing
- **SentMiddleware** -- Act after a message has been sent
- **HeardMiddleware** -- Fire after a pattern matches, before the handler runs
- **WebhookEventMiddleware** -- Swap adapter per-event in batched webhooks

All `add*Middleware()` methods accept an optional `int $priority` parameter (default `0`). Higher values execute earlier. When omitted or equal, insertion order is preserved.

```
$chat->addReceivingMiddleware($auditLog, priority: 100);  // runs first
$chat->addReceivingMiddleware($transform);                  // runs second (priority 0)
```

Extending Adapters
------------------

[](#extending-adapters)

All adapters use `protected` members for extensibility. Extend any adapter to customize behavior:

```
use BootDesk\ChatSDK\Telegram\TelegramAdapter;

class MyTelegramAdapter extends TelegramAdapter
{
    protected function apiCall(string $method, array $params): array
    {
        // Add custom logging, retry logic, etc.
        return parent::apiCall($method, $params);
    }

    protected function buildMessageParams(PostableMessage $message): array
    {
        $params = parent::buildMessageParams($message);

        // Add custom parameters
        $params['disable_web_page_preview'] = true;

        return $params;
    }
}
```

Register your custom adapter via `AdapterRegistry`:

```
use BootDesk\ChatSDK\Core\Support\AdapterRegistry;

// Register in a service provider or bootstrap file

// Replace an existing adapter
AdapterRegistry::register('telegram', MyTelegramAdapter::class);

// Or register as a new adapter
AdapterRegistry::register('telegram-custom', MyTelegramAdapter::class);
```

**With AdapterResolver:** Dynamic resolution tries resolver first (tenant-specific), then falls back to static adapters from config (global default). This allows tenants to override specific adapters while using global defaults for others.

StateAdapter interface
----------------------

[](#stateadapter-interface)

The state adapter handles persistence, pub/sub, locking, and queuing. Methods:

MethodPurpose`connect`Establish connection to state store`disconnect`Close connection`subscribe`Subscribe to a channel`unsubscribe`Unsubscribe from a channel`acquireLock`Acquire a named lock with TTL (returns `Lock` or `null`)`releaseLock`Release a previously acquired lock`extendLock`Extend a lock's TTL (returns `bool`)`get`Retrieve a value by key`set`Store a value by key with optional TTL`delete`Remove a value by key`enqueue`Add item to a queue (max size configurable)`dequeue`Remove and return item from a queue`queueDepth`Get current queue size**Locks** are used for concurrency control (drop/queue/debounce strategies). **Queues** store pending messages when `concurrency: queue` is set.

Concurrency is pluggable via `ConcurrencyHandler`. The core provides `DefaultConcurrencyHandler` (sync/blocking with `usleep` for debounce). Framework packages (e.g., Laravel) replace it with async implementations that dispatch jobs to workers. `ConcurrencyHandler::process()` accepts an optional `?ServerRequestInterface $request` — framework packages can serialize it for jobs so `AdapterResolver` receives the original request in queued context. Adapters can declare sync/async preference via `RequiresSyncResponse` and `RequiresAsyncResponse` marker interfaces.

MessageContext
--------------

[](#messagecontext)

Passed to every event handler.

- **Properties:** `thread`, `message`, `transcripts`
- **Methods:** `skip()`, `setState()`, `getState()`

Transcripts
-----------

[](#transcripts)

Per-user message history. Configure via `$transcripts` param on `Chat` constructor. Requires an `IdentityResolver`.

```
use BootDesk\ChatSDK\Core\Contracts\IdentityResolver;
use BootDesk\ChatSDK\Core\Contracts\TranscriptsApi;

// Pass an IdentityResolver + config array — creates DefaultTranscriptsApi
$chat = new Chat(
    state: $state,
    identity: new class implements IdentityResolver {
        public function resolve(Author $author): ?string { return $author->id; }
    },
    transcripts: ['max_messages' => 100, 'ttl_ms' => 2592000000],
);

// Or pass a pre-built TranscriptsApi instance (for custom impls)
$chat = new Chat(
    state: $state,
    identity: $resolver,
    transcripts: new MyCustomTranscriptsApi($state),
);
```

- Incoming messages auto-recorded in `dispatchIncomingMessage()`
- Outgoing replies auto-recorded via `TranscriptSentMiddleware` (wired automatically)
- Access via `$chat->getTranscripts()` or `$context->transcripts` in handlers
- Methods: `append(userKey, Message)`, `appendOutgoing(userKey, SentMessage, text)`, `list(userKey)`, `count(userKey)`, `delete(userKey)`

Event handlers
--------------

[](#event-handlers)

MethodPatternDescription`onNewMessage`regexMatch text messages`onDirectMessage`-DM-only messages`onNewMention`-Bot was mentioned`onSubscribedMessage`-Subscribed thread messages`onReaction`emojiReaction added/removed`onAction`actionIdButton/action triggered`onSlashCommand`commandSlash command`onModalSubmit`callbackIdModal form submitted`onModalClose`callbackIdModal form closed`onOptionsLoad`actionIdExternal select options requested`onAssistantThreadStarted`-Slack assistant thread created`onAssistantContextChanged`-Slack assistant context changed`onAppHomeOpened`-Slack App Home tab opened`onMemberJoinedChannel`-User joined a channelModals
------

[](#modals)

Build and open platform-agnostic modal forms.

### Value Objects

[](#value-objects)

```
use BootDesk\ChatSDK\Core\Modals\Modal;
use BootDesk\ChatSDK\Core\Modals\TextInput;
use BootDesk\ChatSDK\Core\Modals\Select;
use BootDesk\ChatSDK\Core\Modals\ExternalSelect;
use BootDesk\ChatSDK\Core\Modals\RadioSelect;
use BootDesk\ChatSDK\Core\Modals\SelectOption;

$modal = new Modal(
    callbackId: 'feedback',
    title: 'Submit Feedback',
    submitLabel: 'Send',
    closeLabel: 'Cancel',
    notifyOnClose: true,
    children: [
        new TextInput(
            id: 'comment',
            label: 'Comment',
            placeholder: 'Enter your feedback...',
            multiline: true,
        ),
        new ExternalSelect(
            id: 'category',
            label: 'Category',
            placeholder: 'Start typing...',
            minQueryLength: 1,
        ),
    ],
);
```

### Opening Modals from Handlers

[](#opening-modals-from-handlers)

Both `ActionEvent` and `SlashCommandEvent` expose `openModal()` via the `OpensModals` trait:

```
use BootDesk\ChatSDK\Core\Modals\Modal;
use BootDesk\ChatSDK\Core\Modals\TextInput;

$chat->onAction('feedback', function (ActionEvent $event) {
    $event->openModal(new Modal(
        callbackId: 'feedback',
        title: 'Submit Feedback',
        submitLabel: 'Send',
        children: [
            new TextInput(id: 'comment', label: 'Comment', multiline: true),
        ],
    ));
});
```

Modal context (thread info) is stored server-side and restored when the modal is submitted or closed, so handlers have access to `$event->relatedThread`.

### External Selects / Options Load

[](#external-selects--options-load)

When using `ExternalSelect`, Slack sends `block_suggestion` events as the user types. Handle them with `onOptionsLoad`:

```
$chat->onOptionsLoad(function (OptionsLoadEvent $event) {
    $prefix = strtolower($event->query);
    $all = [
        ['text' => 'Bug Report', 'value' => 'bug'],
        ['text' => 'Feature Request', 'value' => 'feature'],
    ];

    return $prefix === ''
        ? $all
        : array_values(array_filter($all, fn ($o) => str_starts_with(strtolower($o['text']), $prefix)));
});
```

The return value must be an array of `['text' => string, 'value' => string]`. The adapter converts to platform format.

### Modal Events

[](#modal-events)

```
$chat->onModalSubmit(function (ModalSubmitEvent $event) {
    $event->relatedThread?->post("Form submitted: " . json_encode($event->values));
});

$chat->onModalClose(function (ModalCloseEvent $event) {
    $event->relatedThread?->post("Form closed without submitting.");
});
```

- `ModalSubmitEvent`: `callbackId`, `values` (map of actionId → value), `user`, `viewId`, `relatedThread`
- `ModalCloseEvent`: `callbackId`, `user`, `viewId`, `relatedThread`

Documentationn
--------------

[](#documentationn)

Full API documentation:

License
-------

[](#license)

MIT

###  Health Score

47

—

FairBetter than 93% of packages

Maintenance97

Actively maintained with recent releases

Popularity17

Limited adoption so far

Community19

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

37

Last Release

14d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3874705?v=4)[Vin](/maintainers/tryvin)[@tryvin](https://github.com/tryvin)

---

Top Contributors

[![tryvin](https://avatars.githubusercontent.com/u/3874705?v=4)](https://github.com/tryvin "tryvin (1 commits)")

### Embed Badge

![Health badge](/badges/bootdesk-chat-sdk-core/health.svg)

```
[![Health](https://phpackages.com/badges/bootdesk-chat-sdk-core/health.svg)](https://phpackages.com/packages/bootdesk-chat-sdk-core)
```

###  Alternatives

[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k16](/packages/tempest-framework)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M600](/packages/shopware-core)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

85036.3k](/packages/flow-php-flow)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M754](/packages/sylius-sylius)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[mollie/mollie-api-php

Mollie API client library for PHP. Mollie is a European Payment Service provider and offers international payment methods such as Mastercard, VISA, American Express and PayPal, and local payment methods such as iDEAL, Bancontact, SOFORT Banking, SEPA direct debit, Belfius Direct Net, KBC Payment Button and various gift cards such as Podiumcadeaukaart and fashioncheque.

60316.0M89](/packages/mollie-mollie-api-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
