salesrender / plugin-core-chat
SalesRender plugin chat core
Installs: 224
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 2
Forks: 0
Open Issues: 0
pkg:composer/salesrender/plugin-core-chat
Requires
- php: >=7.4.0
- ext-json: *
- salesrender/plugin-component-queue: ^0.3.0
- salesrender/plugin-core: ^0.4.1
- xakepehok/enum-helper: ^0.1.1
README
Type-specific core framework for SalesRender CHAT plugins
Overview
salesrender/plugin-core-chat is a specialized core library that extends the base salesrender/plugin-core to build Chat-type plugins. Chat plugins handle sending and receiving messages to/from customers via various messaging platforms -- WhatsApp, Telegram, SMS, email, and other gateways.
This core provides:
- A queue-based message sending architecture (
ChatSendTask,ChatSendQueueCommand,ChatSendQueueHandleCommand) - A
ChatSenderInterfacethat the developer must implement with actual gateway logic - An HTTP endpoint (
POST /special/send) for receiving outgoing messages from SalesRender - A
Chat::send()method for forwarding incoming messages back to SalesRender CRM - A
MessageStatusSenderfor reporting delivery statuses (sent, delivered, read, error) - Automatic cron scheduling for processing the send queue
Installation
composer require salesrender/plugin-core-chat
Requirements
- PHP >= 7.4
- ext-json
salesrender/plugin-core^0.4.1 (installed automatically)salesrender/plugin-component-queue^0.3.0 (installed automatically)xakepehok/enum-helper^0.1.1 (installed automatically)
Architecture
How This Core Extends plugin-core
plugin-core-chat overrides both factory classes from the base plugin-core:
WebAppFactory (extends \SalesRender\Plugin\Core\Factories\WebAppFactory):
- Adds CORS support
- Registers the
SenderActionas a special request action, creating the routePOST /special/send
ConsoleAppFactory (extends \SalesRender\Plugin\Core\Factories\ConsoleAppFactory):
- Registers
ChatSendQueueCommand(chatSendQueue:queue) - Registers
ChatSendQueueHandleCommand(chatSendQueue:handle) - Adds a cron task that runs
chatSendQueue:queueevery minute
Message Flow
SalesRender CRM Chat Plugin External Gateway
| | |
|--- POST /special/send (outgoing) ---->| |
| |--- ChatSendTask (queued) ----------->|
| |--- ChatSenderInterface::__invoke() -->|
| | |
|<-- MessageStatusSender::send() -------|<--- delivery status -----------------|
|<-- Chat::send() (incoming) -----------|<--- incoming message ----------------|
Getting Started: Creating a Chat Plugin
Step 1: Project Setup
Create a new project and add the dependency:
mkdir my-chat-plugin && cd my-chat-plugin composer init --name="myvendor/plugin-chat-mygateway" --type="project" composer require salesrender/plugin-core-chat
Create the directory structure:
my-chat-plugin/
bootstrap.php
console.php
composer.json
example.env
public/
.htaccess
index.php
icon.png
src/
Sender/
ChatSender.php
Forms/
SettingsForm.php
db/
runtime/
Step 2: Bootstrap Configuration
Create bootstrap.php in the project root. This file configures all plugin components:
<?php use SalesRender\Plugin\Components\Db\Components\Connector; use SalesRender\Plugin\Components\Form\Autocomplete\AutocompleteRegistry; use SalesRender\Plugin\Components\Info\Developer; use SalesRender\Plugin\Components\Info\Info; use SalesRender\Plugin\Components\Info\PluginType; use SalesRender\Plugin\Components\Settings\Settings; use SalesRender\Plugin\Components\Translations\Translator; use SalesRender\Plugin\Core\Actions\Upload\LocalUploadAction; use SalesRender\Plugin\Core\Actions\Upload\UploadersContainer; use SalesRender\Plugin\Core\Chat\SendMessageQueue\ChatSendQueueHandleCommand; use SalesRender\Plugin\Instance\Chat\Forms\SettingsForm; use SalesRender\Plugin\Instance\Chat\Sender\ChatSender; use Medoo\Medoo; use XAKEPEHOK\Path\Path; # 0. Configure environment variable in .env file, that placed into root of app # 1. Configure DB (for SQLite *.db file and parent directory should be writable) Connector::config(new Medoo([ 'database_type' => 'sqlite', 'database_file' => Path::root()->down('db/database.db') ])); # 2. Set plugin default language Translator::config('ru_RU'); # 3. Configure info about plugin Info::config( new PluginType(PluginType::CHAT), fn() => Translator::get('info', 'Example chat plugin'), fn() => Translator::get('info', 'This plugin created only for demo purposes'), [ "contactType" => $_ENV['LV_PLUGIN_CONTACT_TYPE'], "capabilities" => [ "subject" => (bool) (int) $_ENV['LV_PLUGIN_CAPABILITY_SUBJECT'], "typing" => false, "messages" => [ "formats" => empty($_ENV['LV_PLUGIN_MESSAGE_FORMATS']) ? ['text'] : explode(' ', $_ENV['LV_PLUGIN_MESSAGE_FORMATS']), "incoming" => (bool) (int) $_ENV['LV_PLUGIN_MESSAGE_INCOMING'], "outgoing" => (bool) (int) $_ENV['LV_PLUGIN_MESSAGE_OUTGOING'], "writeFirst" => (bool) (int) $_ENV['LV_PLUGIN_MESSAGE_WRITE_FIRST'], "statuses" => [ "sent", "delivered", "read", "error" ] ], "attachments" => empty($_ENV['LV_PLUGIN_ATTACHMENTS']) ? [] : explode(' ', $_ENV['LV_PLUGIN_ATTACHMENTS']), ] ], new Developer( 'Your Company', 'support@example.com', 'example.com', ) ); # Upload actions (for attachments support) $uploader = new LocalUploadAction(['*' => 10 * 1024 * 1024]); UploadersContainer::addDefaultUploader($uploader); UploadersContainer::addCustomUploader('image', $uploader); UploadersContainer::addCustomUploader('voice', $uploader); # 4. Configure settings form Settings::setForm(fn() => new SettingsForm()); # 5. Configure form autocompletes (or remove this block if not used) AutocompleteRegistry::config(function (string $name) { return null; }); # 6. Configure ChatSendQueueHandleCommand with your sender implementation ChatSendQueueHandleCommand::config(new ChatSender());
Key chat-specific configuration points:
PluginType::CHAT-- identifies this plugin as a Chat typecontactType-- the type of contact identifier the gateway uses (e.g.,"uri","phone","email")capabilities-- declares what the messaging gateway supports: message formats (text,markdown,html), incoming/outgoing messages, write-first capability, attachment types (file,image,voice), and message statusesChatSendQueueHandleCommand::config()-- registers yourChatSenderInterfaceimplementation
Step 3: Implement ChatSenderInterface
This is the core of your chat plugin. Create a class that implements ChatSenderInterface:
<?php namespace SalesRender\Plugin\Instance\Chat\Sender; use SalesRender\Plugin\Components\Settings\Settings; use SalesRender\Plugin\Core\Chat\Components\Chat\Chat; use SalesRender\Plugin\Core\Chat\Components\Chat\Message\Message; use SalesRender\Plugin\Core\Chat\Components\Chat\Message\MessageContent; use SalesRender\Plugin\Core\Chat\Components\MessageStatusSender\MessageStatusSender; use SalesRender\Plugin\Core\Chat\SendMessageQueue\ChatSenderInterface; use SalesRender\Plugin\Core\Chat\SendMessageQueue\ChatSendTask; class ChatSender implements ChatSenderInterface { public function __invoke(ChatSendTask $task) { $chat = $task->getChat(); $message = $chat->getMessage(); $setting = Settings::find()->getData(); // 1. Extract message content and attachments $content = $message->getContent(); // MessageContent|null $attachments = $message->getAttachments(); // MessageAttachment[] // 2. Send the message via your gateway API // Use $chat->getContact() for the recipient identifier // Use $chat->getSubject() for conversation subject (if supported) // Use $content->getText() and $content->getFormat() for message text // Use $attachments for files/images/voice $gatewayMessageId = $this->sendViaGateway($chat); // 3. Report message status back to SalesRender MessageStatusSender::send($message->getId(), MessageStatusSender::SENT); // Later, when you receive delivery confirmation from the gateway: // MessageStatusSender::send($message->getId(), MessageStatusSender::DELIVERED); // MessageStatusSender::send($message->getId(), MessageStatusSender::READ); // On error: // MessageStatusSender::send($message->getId(), MessageStatusSender::ERROR); } private function sendViaGateway(Chat $chat): string { // Your gateway-specific sending logic here return 'gateway-message-id'; } }
The __invoke() method receives a ChatSendTask that wraps a Chat object. If the method throws an exception, the task will be retried (up to 100 attempts with a 10-second interval). After all attempts are exhausted, the task is deleted.
Step 4: Create Web Entry Point
Create public/index.php:
<?php use SalesRender\Plugin\Core\Chat\Factories\WebAppFactory; require_once __DIR__ . '/../vendor/autoload.php'; $factory = new WebAppFactory(); $application = $factory->build(); $application->run();
Create public/.htaccess:
RewriteEngine On RewriteRule ^output - [L] RewriteRule ^uploaded - [L] RewriteCond %{REQUEST_FILENAME} -f [OR] RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php [L,QSA]
Step 5: Create Console Entry Point
Create console.php:
#!/usr/bin/env php <?php use SalesRender\Plugin\Core\Chat\Factories\ConsoleAppFactory; require __DIR__ . '/vendor/autoload.php'; $factory = new ConsoleAppFactory(); $application = $factory->build(); $application->run();
Step 6: Create Settings Form
Create src/Forms/SettingsForm.php:
<?php namespace SalesRender\Plugin\Instance\Chat\Forms; use SalesRender\Plugin\Components\Form\FieldDefinitions\FieldDefinition; use SalesRender\Plugin\Components\Form\FieldDefinitions\StringDefinition; use SalesRender\Plugin\Components\Form\FieldGroup; use SalesRender\Plugin\Components\Form\Form; use SalesRender\Plugin\Components\Form\FormData; use SalesRender\Plugin\Components\Translations\Translator; class SettingsForm extends Form { public function __construct() { $nonNull = function ($value, FieldDefinition $definition, FormData $data) { $errors = []; if (is_null($value)) { $errors[] = Translator::get('settings', 'Field can not be empty'); } return $errors; }; parent::__construct( Translator::get('settings', 'Settings'), Translator::get('settings', 'My Chat Plugin'), [ 'main' => new FieldGroup( Translator::get('settings', 'Main'), null, [ 'api_key' => new StringDefinition( Translator::get('settings', 'API Key'), Translator::get('settings', 'Your gateway API key'), $nonNull, ), ] ), ], Translator::get('settings', 'Save'), ); } }
Step 7: Create .env
Create example.env (copy to .env for local development):
LV_PLUGIN_DEBUG=1 LV_PLUGIN_PHP_BINARY=php LV_PLUGIN_SELF_URI=http://plugin-example/ LV_PLUGIN_COMPONENT_HANDSHAKE_SCHEME=http LV_PLUGIN_COMPONENT_HANDSHAKE_HOSTNAME=lv-app LV_PLUGIN_CONTACT_TYPE=uri LV_PLUGIN_CAPABILITY_SUBJECT=1 LV_PLUGIN_MESSAGE_FORMATS="html text markdown" LV_PLUGIN_MESSAGE_INCOMING=1 LV_PLUGIN_MESSAGE_OUTGOING=1 LV_PLUGIN_MESSAGE_WRITE_FIRST=1 LV_PLUGIN_ATTACHMENTS="file image voice"
Environment variables explained:
| Variable | Description |
|---|---|
LV_PLUGIN_CONTACT_TYPE |
Type of contact identifier: uri, phone, email, etc. |
LV_PLUGIN_CAPABILITY_SUBJECT |
1 if conversation subjects are supported |
LV_PLUGIN_MESSAGE_FORMATS |
Space-separated list: text, markdown, html |
LV_PLUGIN_MESSAGE_INCOMING |
1 if the plugin can receive incoming messages |
LV_PLUGIN_MESSAGE_OUTGOING |
1 if the plugin can send outgoing messages |
LV_PLUGIN_MESSAGE_WRITE_FIRST |
1 if the plugin can initiate conversations |
LV_PLUGIN_ATTACHMENTS |
Space-separated list: file, image, voice |
LV_PLUGIN_CHAT_SEND_QUEUE_LIMIT |
Max tasks per queue cycle (default: 100) |
Step 8: Initialize & Deploy
# Install dependencies composer install # Create database tables php console.php db:create # Start cron (handles message sending queue) php console.php cron
HTTP Routes
Routes added by \SalesRender\Plugin\Core\Chat\Factories\WebAppFactory:
| Method | Path | Description | Source |
|---|---|---|---|
POST |
/special/send |
Receives outgoing messages from SalesRender CRM. Parses the message into a Chat object, creates a ChatSendTask, and queues it for sending. Returns 202 Accepted. |
SenderAction |
Additionally, all base plugin-core routes are inherited:
| Method | Path | Description |
|---|---|---|
GET |
/info |
Plugin information |
PUT |
/registration |
Plugin registration |
GET |
/protected/forms/settings |
Settings form definition |
PUT |
/protected/data/settings |
Save settings |
GET |
/protected/data/settings |
Get settings data |
GET |
/protected/autocomplete/{name} |
Autocomplete handler |
POST |
/protected/upload |
File upload |
GET |
/robots.txt |
Robots.txt |
CLI Commands
Commands added by \SalesRender\Plugin\Core\Chat\Factories\ConsoleAppFactory:
| Command | Description |
|---|---|
chatSendQueue:queue |
Finds pending ChatSendTask models and spawns child processes to handle them. Automatically scheduled via cron (every minute). |
chatSendQueue:handle {id} |
Handles a single ChatSendTask by invoking the configured ChatSenderInterface. Called by the queue command. |
Inherited base commands:
| Command | Description |
|---|---|
db:create |
Create database tables |
db:clean |
Clean database tables |
specialRequest:queue |
Process special request queue |
specialRequest:handle |
Handle a special request |
cron |
Run all scheduled cron tasks |
lang:add |
Add a translation language |
lang:update |
Update translations |
directory:clean |
Clean temporary directories |
Key Classes & Interfaces
ChatSenderInterface
Namespace: SalesRender\Plugin\Core\Chat\SendMessageQueue\ChatSenderInterface
The primary interface that every chat plugin must implement. It defines a single invokable method:
interface ChatSenderInterface { /** * Handle sending a message via the gateway and manage message status tracking. * @param ChatSendTask $task * @return mixed */ public function __invoke(ChatSendTask $task); }
Chat
Namespace: SalesRender\Plugin\Core\Chat\Components\Chat\Chat
Represents a chat conversation with a message. Implements JsonSerializable.
| Method | Return Type | Description |
|---|---|---|
__construct(?string $id, string $contact, ?string $subject, Message $message) |
Create a new Chat instance | |
getId() |
?string |
Chat identifier (null for new chats) |
getContact() |
string |
Contact identifier (phone, email, URI, etc.) |
getSubject() |
?string |
Conversation subject |
getMessage() |
Message |
The message object |
send() |
void |
Send this chat as an incoming message to SalesRender CRM |
parseFromArray(array $chat) |
static |
Static factory: parse a Chat from an array |
Chat::send() sends incoming messages (from the external gateway) to SalesRender CRM via a SpecialRequest to the path CRM/plugin/chat/incoming.
Message
Namespace: SalesRender\Plugin\Core\Chat\Components\Chat\Message\Message
Represents a single message. A message must have either content or at least one attachment.
| Method | Return Type | Description |
|---|---|---|
__construct(?string $id, ?MessageContent $content, array $attachments = []) |
Create a new Message. Throws EmptyMessageException if both content and attachments are empty. |
|
getId() |
?string |
Message identifier |
getContent() |
?MessageContent |
Text content of the message |
getAttachments() |
MessageAttachment[] |
Array of attachments |
MessageContent
Namespace: SalesRender\Plugin\Core\Chat\Components\Chat\Message\MessageContent
Holds the text body of a message with its format.
| Method | Return Type | Description |
|---|---|---|
__construct(string $text, string $format) |
Create content. Throws EmptyMessageContentException if text is empty. |
|
getText() |
string |
The text body |
getFormat() |
string |
Format: MessageContent::PLAIN_TEXT, MessageContent::MARKDOWN, or MessageContent::HTML |
Format constants:
| Constant | Value |
|---|---|
MessageContent::PLAIN_TEXT |
"text" |
MessageContent::MARKDOWN |
"markdown" |
MessageContent::HTML |
"html" |
MessageAttachment
Namespace: SalesRender\Plugin\Core\Chat\Components\Chat\Message\MessageAttachment
Represents a file attachment. Extends EnumHelper for type validation.
| Method | Return Type | Description |
|---|---|---|
__construct(string $name, string $uri, string $type) |
Create an attachment. Type must be one of the valid values. | |
getName() |
string |
File name |
getUri() |
string |
File URI |
getType() |
string |
Attachment type |
Type constants:
| Constant | Value |
|---|---|
MessageAttachment::FILE |
"file" |
MessageAttachment::IMAGE |
"image" |
MessageAttachment::VOICE |
"voice" |
ChatSendTask
Namespace: SalesRender\Plugin\Core\Chat\SendMessageQueue\ChatSendTask
A queue task that wraps a Chat object for deferred sending. Extends Task from plugin-component-queue.
| Method | Return Type | Description |
|---|---|---|
__construct(Chat $chat) |
Create a task with 100 max attempts and 10-second retry interval | |
getChat() |
Chat |
Get the wrapped Chat object |
getAttempt() |
TaskAttempt |
Get the attempt tracker |
MessageStatusSender
Namespace: SalesRender\Plugin\Core\Chat\Components\MessageStatusSender\MessageStatusSender
Static utility for reporting message delivery statuses back to SalesRender CRM. Sends a PATCH request to CRM/plugin/chat/status.
MessageStatusSender::send(string $messageId, string $status): void
Status constants:
| Constant | Value | Description |
|---|---|---|
MessageStatusSender::SENT |
"sent" |
Message was sent to the gateway |
MessageStatusSender::DELIVERED |
"delivered" |
Message was delivered to the recipient |
MessageStatusSender::READ |
"read" |
Message was read by the recipient |
MessageStatusSender::ERROR |
"error" |
Message sending failed |
SenderAction
Namespace: SalesRender\Plugin\Core\Chat\Actions\SenderAction
HTTP action that handles POST /special/send. Extends SpecialRequestAction. Parses the incoming request body into a Chat object and creates a ChatSendTask in the queue. Returns:
202 Accepted-- message queued successfully405-- invalid or empty message data500-- error saving the queue task
Message Queue
The chat core uses a queue-based architecture for reliable message delivery:
ChatSendQueueCommand
Runs every minute via cron. Fetches pending ChatSendTask records from the database (respecting LV_PLUGIN_CHAT_SEND_QUEUE_LIMIT, default 100) and spawns up to 25 parallel child processes.
ChatSendQueueHandleCommand
Handles a single task by ID. Loads the ChatSendTask, sets the plugin reference context, and invokes the configured ChatSenderInterface. On success, the task is deleted. On failure, the attempt counter is incremented and the task is retried. After all attempts are exhausted (100 attempts), the task is deleted.
Configuration
Register your sender in bootstrap.php:
ChatSendQueueHandleCommand::config(new ChatSender());
Receiving Incoming Messages
If your plugin also receives messages from the external gateway (e.g., via webhook or API polling), you need to construct a Chat object and call Chat::send():
Via webhook -- create a custom action implementing ActionInterface and register it in the WebAppFactory:
// In your incoming message webhook handler: $chat = new Chat( null, // id (null for new incoming messages) $contactIdentifier, // the sender's contact identifier $subject, // conversation subject or null new Message( null, // message id (null for new messages) new MessageContent($text, MessageContent::PLAIN_TEXT), [] // attachments ) ); $chat->send();
Via API polling -- create a custom console command and add it to the cron schedule.
Exceptions
| Exception | Description |
|---|---|
EmptyMessageException |
Thrown when creating a Message with neither content nor attachments |
EmptyMessageContentException |
Thrown when creating MessageContent with empty text |
Example Plugin
See the reference implementation: plugin-chat-example
plugin-chat-example/
bootstrap.php -- Plugin configuration & ChatSender registration
console.php -- Console entry point (ConsoleAppFactory)
public/
index.php -- Web entry point (WebAppFactory)
.htaccess -- Apache rewrite rules
icon.png -- Plugin icon
src/
Sender/
ChatSender.php -- ChatSenderInterface implementation
Forms/
SettingsForm.php -- Settings form definition
db/ -- SQLite database directory
runtime/ -- Runtime files directory
example.env -- Environment variables template
Dependencies
| Package | Version | Purpose |
|---|---|---|
salesrender/plugin-core |
^0.4.1 | Base plugin framework |
salesrender/plugin-component-queue |
^0.3.0 | Queue system for task processing |
xakepehok/enum-helper |
^0.1.1 | Enum validation for attachment types and statuses |
See Also
- salesrender/plugin-core -- Base plugin framework
- plugin-chat-example -- Example chat plugin implementation