xou816 / slack-components
Slack交互消息的声明式库
Requires
- php: >=5.5
- guzzlehttp/guzzle: ^6.3
Requires (Dev)
- phpunit/phpunit: ^5.0
This package is not auto-updated.
Last update: 2024-09-27 16:33:43 UTC
README
Slack交互消息的声明式库。
特性
- 强大:管理智能、有状态的组件
- 简单灵活:可以根据需要选择是否使用组件
- 易于测试:对您的消息进行Slack交互的模拟
- 与PHP 5.5+兼容,仅依赖Guzzle
安装
通过Composer: composer require xou816/slack-components
。
入门指南
本指南假设您对Slack交互消息有一定的了解。
创建路由器
SlackRouter
负责处理传入的Slack交互以及派发您的消息、对话框等。
// $client is a GuzzleHttp\Client $router = new SlackRouter($client, $options);
有效选项
或者,您也可以构建一个启用默认中间件的路由器
// $client is still a GuzzleHttp\Client $router = SlackRouter::defaults($client, $options);
请求URL
一旦您设置了请求URL并在您的应用程序中设置了一个匹配的路由,响应来自Slack的请求为HTTP 200
,并在您的响应发送后调用一次hookAfterResponse
。
例如,在Symfony的terminate
事件之后添加以下片段,其中$router
是一个SlackRouter
实例,$request
是一个Symfony请求
$payload = json_decode($request->request->get('payload'), true); $router->hookAfterResponse($payload);
此钩子负责向Slack交互发送响应。
中间件
中间件是可以在处理之前更改传入的Slack操作负载的函数
$router->push(function($payload, $next) { $modified = modify($payload); return $next($modified); });
或者修改发送的响应
$router->push(function($payload, $next) { $response = $next($payload); return modify($response); });
默认中间件
$router->checkToken()
:验证负载令牌,您必须有一个路由器实例才能使用该中间件,并且它应该是堆栈中的第一个Middleware::parseCallbacks()
:用CallbackId
组件替换原始回调IDMiddleware::parseInteractions()
:用相应的SlackInteraction
对象替换交互Middleware::parseUser()
:用SlackUser
对象替换负载用户Middleware::wrapResponse()
:将普通对象包装在适当的SlackResponse
对象中(默认使用response_url
)。
其他中间件可以编写来处理授权、日志记录等。
发送消息
发送示例(静态)消息
$msg = new SlackPayload(SlackPayload::WEBHOOK, '#channel', 'Hello world!'); // uses the webhooks option $router->send($msg);
幸运的是,交互式消息通过智能组件让您无需自己构建SlackPayload
,这简化了操作。
交互
使用when
方法注册由特定回调ID触发的处理程序。回调ID通常附加到您发送的消息中的操作。
$router->when('callback', function($payload) { // if $payload matches a button press... });
组件
组件用于简化构建Slack消息的过程。例如,如果您打算发布一条带有点击计数器的消息,而不是发送这个(您当然可以!)
[ 'text' => $count, 'attachments' => [ [ 'callback_id' => 'mycallback', 'actions' => [ [ 'type' => 'button', 'style' => 'primary', 'value' => 'increment', 'name' => 'increment', 'text' => 'Increment' ] ] ] ] ]
您可能会写成这样
[ 'text' => $count, 'attachments' => [ [ 'callback_id' => CallbackId::wrap(['count' => 0]), 'actions' => [ Button::create('btn_name')->withLabel('Increment') ] ] ] ]
这有什么好处?除了您的IDE中的代码补全问题之外,使用组件将大大简化对操作的响应和管理状态的过程。实际上,在这个例子中,您需要跟踪点击次数。
管理状态
此库通过回调ID提供状态管理。由于它在每条消息和交互中都会发送,因此它似乎是解决我们的状态管理问题的合适候选者。
它可以用于存储“完整”状态,当它足够小的时候...
['count' => 0]
...或者它还可以帮助在数据库中跟踪该状态的更大表示。
['state_id' => 123]
CallbackId
组件是一个相当简单的组件,当 构建 后会产生一个字符串——这是一个键和某些数据的 base64 编码。
$callbackId ->withKey('mykey') ->withData(['count' => 0])
键 识别消息的来源(以及能够处理未来交互的内容),而 数据 可以用来存储状态。
如果您想以这种方式处理传入的回调 ID,则 必须 使用 parseCallbacks
中间件。
构建组件
组件是复杂的对象,我们需要向 Slack 发送纯对象(JSON)。因此,组件必须通过一个 构建步骤:首先构建完整的组件树,然后将其渲染为一个纯对象。
组件可能看起来与流行的前端框架中可以找到的内容相似,但有几个重要区别,其中之一是 组件树中的状态由所有组件共享。
根组件通常可以通过组件的 getContext
方法访问。
为什么状态很重要
给定一个组件树的先前渲染(例如 Slack 有时提供的 original_message
)和先前的状态(存储在我们的回调 ID 中),我们能够高效地重建树(即我们的消息)...但关于这一点稍后详述。
交互
组件提供了一种方便的方法在路由器中注册交互处理程序。例如,启用 parseInteractions
中间件后
$router->when('callback', $button->clicked(function(ButtonAction $action) { // ... }));
当按钮实例 $button
被点击时,此处理程序将为指定的回调触发。
反射
反射在处理程序中与提供给 clicked
的处理程序一样被使用。因此,您可以在这样的闭包中注入以下元素
- 交互对象,通过请求
SlackInteraction
的子类,例如ButtonAction
、DialogSubmission
- 负责交互的用户,通过请求
SlackUser
类型的参数 - 完整的 Slack 有效负载,通过请求单个参数或名为
payload
的参数 - 状态键,通过请求具有相同名称的参数
而不是闭包,您还可以提供可调用对象。
响应用户交互
在响应用户交互时,您可以返回以下之一
- 一条消息(构建):例如,使用
InteractiveMessage::patchState
,但它也可以是完全新的消息 - 使用
Dialog::open
或Dialog::doOpen
打开对话框(见对话框)的请求 - 使用
SlackUser::sendMessage
向触发动作的用户发送消息。
交互式消息
交互式消息提供了一个方便的方法来管理组件、交互、回调键和状态,所有这些都在一个地方。
class MyMessage extends InteractiveMessage { public function __construct(SlackRouter $router) { parent::__construct($router); $this->increment = new Button('increment'); $this->when($this->increment->clicked(function($count) { return $this->patchState(['count' => $count + 1]); })); } protected function buildMessage($count) { return [ 'text' => $count, 'attachments' => [ [ 'callback_id' => $this->callback([ 'count' => $count ]), 'actions' => [ $this->increment ->withLabel('Increment') ] ] ] ]; } }
注意事项
- 我们使用一个类来表示我们的消息
- 其内容由
buildMessage
描述 - 我们使用
when
监听我们对increment
按钮的交互 - 我们使用
callback
存储消息的状态 - 在交互时,我们会修补我们的消息,即再次使用更新后的状态渲染它。
您还可以使用 InteractiveMessage::create($router, $buildMesssageClosure)
构建所谓的匿名消息。
根据消息类自动为您选择回调键。这就是为什么您不需要向消息的 when
方法提供它。
构建和发送
假设您有一个消息类的实例
$built = $myMessage->build('#channel', ['count' => 0]); $myMessage->send($built); // or... $myMessage->buildAndSend('#channel', ['count' => 0]);
消息将通过您的 webhooks 发送到指定的频道。
send
方法接受任何 SlackPayload
对象。
修补消息
打补丁消息意味着使用补丁(新状态的一个子集)更新状态。《patchState》方法返回一个普通对象的消息。
《patchState》方法在使用《LazyComponent》时特别有用。后者仅在需要时渲染--如果我们有它的前一个渲染版本,并且它所依赖的状态相关部分没有变化,我们就保留前一个渲染。
如果你需要对特定组件执行昂贵的计算,这非常有用--将其包装在《LazyComponent》中!
可以使用简单的闭包构建此类组件,其中参数表示我们依赖的状态。
function($count) { return /**/; }
这是一个有效的组件,它仅在状态中的《count》发生变化时渲染。下面是一个完整的示例。
class MyMessage extends InteractiveMessage { public function __construct(SlackRouter $router) { parent::__construct($router); $this->increment = new Button('increment'); $this->when($this->increment->clicked(function($count, $reverse) { $patch = ['count' => $count + ($reverse ? -1 : 1)]; if (abs($patch['count']) === 10) { $patch['reverse'] = !$reverse; } return $this->patchState($patch); })); } protected function defaultState() { return ['reverse' => false, 'count' => 0]; } protected function buildMessage($count, $reverse) { return [ 'text' => $count, 'attachments' => [ [ 'callback_id' => $this->callback([ 'count' => $count, 'reverse' => $reverse ]), 'actions' => function($reverse) { return [ $this->increment ->withLabel($reverse ? 'Decrement' : 'Increment') ]; } ] ] ]; } }
在那个例子中,消息的《actions》部分仅在十次点击中计算一次--直到标签需要更改!你不信?在这个按钮中显示日期!
计算属性
计算属性是直接依赖于状态且你希望尽量避免频繁计算的性质。
例如,如果你在数据库中存储一个非常复杂的状态,你可能会通过在消息状态中保存其数据库ID来访问它。
然而,如果你在某个时刻从数据库中获取了这个状态,你希望避免在消息内部再次获取它。
创建计算属性
// assuming $state = ['id' => ..., ...] $this->fullState = function($id) { return $this->database->fetch($id); };
访问它
$fullState = $this->fullState;
分配现有的计算
$this->fullState = $theFullState;
故障排除
重要提示。你的消息实例必须存在,以便在路由器中注册交互处理程序。
常见组件
在消息中:按钮《Button》,选择《Select》。你可以使用《clicked》和《selected》分别将这些组件附加(反射就绪)处理程序。
在对话框中:《TextInput》,《Textarea》,《Select》。
对话框
对话框不是常见组件,因为它们不是直接附加到消息上的。然而,就像按钮或选择一样,它们提供了一个轻松响应用户交互的方法。
$myDialog = Dialog::create('Test dialog') ->withElements([ function($default) { return TextInput::create('name') ->withValue($default) ->withLabel('Please enter your name below'); }, Select::create('select') ->withOption('opt1', 'Option 1') ->withOption('opt2', 'Option 2') ]);
对话框本身没有状态,当你第一次创建它时,它不像按钮那样附加到消息主体上。然而,当一个交互导致对话框打开时,那个交互携带一个回调ID,因此有一个状态。这个状态被传达给对话框,并且组件可以查询它(这就是上述《LazyComponent》/闭包所做的那样)。
class MyMessageWithDialog extends InteractiveMessage { private $dialog; private $button; public function __construct(SlackRouter $router) { global $myDialog; parent::__construct($router); $this->dialog = $myDialog; $this->button = new Button('btn'); $this->when($this->button->clicked($this->dialog->doOpen())); $this->when($this->dialog->submitted(function(DialogSubmission $sub, $greet) { return $greet.', '.$sub->name; })); } protected function buildMessage($greet) { return [ 'text' => 'Dialog demo', 'attachments' => [ [ 'callback_id' => $this->callback([ 'greet' => $greet, 'default' => 'Robert' // this part is communcated to the dialog ]), 'actions' => [ $this->button ->withLabel('Open dialog') ] ] ] ]; } }
为了打开对话框,必须先发生一个交互。然后你可以调用以下方法之一
- 《doOpen》,它返回一个适当的闭包
- 或《open》,你必须《必须》给出完整的交互《$payload》。
一旦打开,对话框就可以像按钮或选择一样使用,你可以使用《submitted》来响应用户提交。