|
2 | 2 |
|
3 | 3 | A small, PHP framework for building Slack Apps. Takes inspiration from Slack's Bolt frameworks. |
4 | 4 |
|
5 | | -## Example |
| 5 | +This library is only useful if you understand the basics of building Slack applications. Here is some documentation to |
| 6 | +help you get started there if you are new to Slack app development: |
| 7 | + |
| 8 | +- [Getting Started Guide](https://api.slack.com/start) |
| 9 | +- [Interactivity Overview](https://api.slack.com/interactivity) |
| 10 | +- [Events Overview](https://api.slack.com/apis/connections/events-api) |
| 11 | +- [Messaging Overview](https://api.slack.com/messaging/managing) |
| 12 | +- [Block Kit Overview](https://api.slack.com/block-kit) |
| 13 | + |
| 14 | +## Under Development |
| 15 | + |
| 16 | +:warning: This is under heavy development. _Breaking changes may occur on any commit._ :warning: |
| 17 | + |
| 18 | +## Developing Apps |
| 19 | + |
| 20 | +When configuring the app in Slack, you should point all configurable URLs to the root URL of your deployed app code. |
| 21 | + |
| 22 | +When developing the app code, you declare one or more `Listener`s using the `App`'s routing methods, that correspond to |
| 23 | +the different types of app interaction. `Listener`s can be declared as closures, or as objects and class names of type |
| 24 | +`SlackPhp\Framework\Listener`. A `Listener` receives a `Context` object, which contains the payload data provided by |
| 25 | +Slack to the app and all the actions you can take to interact with or communicate back to Slack. |
| 26 | + |
| 27 | +## Quick Example |
| 28 | + |
| 29 | +This small app responds to the `/cool` slash command. |
| 30 | + |
| 31 | +```php |
| 32 | +<?php |
| 33 | + |
| 34 | +use SlackPhp\Framework\App; |
| 35 | +use SlackPhp\Framework\Context; |
| 36 | + |
| 37 | +// Note: Expects SLACK_SIGNING_KEY to be set in environment. |
| 38 | + |
| 39 | +App::new() |
| 40 | + ->command('cool', function (Context $ctx) { |
| 41 | + $ctx->ack(':thumbsup: That is so cool!'); |
| 42 | + }) |
| 43 | + ->run(); |
| 44 | +``` |
| 45 | + |
| 46 | +## Example Application |
| 47 | + |
| 48 | +The "Hello World" app says hello to you, by utilizing every type of app interactions, including: slash commands, block |
| 49 | +actions, block suggestions (i.e., options for menus), shortcuts (both global and message level), modals, events, and |
| 50 | +the app home page. |
| 51 | + |
| 52 | +<details> |
| 53 | +<summary>"Hello World" app code</summary> |
6 | 54 |
|
7 | 55 | ```php |
8 | 56 | <?php |
9 | 57 |
|
10 | 58 | declare(strict_types=1); |
11 | 59 |
|
12 | | -use Jeremeamia\Slack\Apps\App; |
13 | | -use Jeremeamia\Slack\Apps\Context; |
14 | | -use Jeremeamia\Slack\BlockKit\Partials\OptionList; |
15 | | -use Jeremeamia\Slack\BlockKit\Surfaces\Message; |
16 | | -use Jeremeamia\Slack\BlockKit\Surfaces\Modal; |
| 60 | +use SlackPhp\Framework\{App, Context, Route}; |
| 61 | +use Jeremeamia\Slack\BlockKit\Surfaces\{Message, Modal}; |
| 62 | + |
| 63 | +// Note: Expects SLACK_SIGNING_KEY and SLACK_BOT_TOKEN to be set in environment. |
17 | 64 |
|
18 | | -// Bootstrap Slack App |
19 | | -$app = App::new() |
20 | | - ->command('slack-test', function (Context $ctx) { |
21 | | - $ctx->respond(':thumbsup: *Success*'); |
| 65 | +// Helper for creating a modal with the "hello-form" for choosing a greeting. |
| 66 | +$createModal = function (): Modal { |
| 67 | + return Modal::new() |
| 68 | + ->title('Choose a Greeting') |
| 69 | + ->submit('Submit') |
| 70 | + ->callbackId('hello-form') |
| 71 | + ->notifyOnClose(true) |
| 72 | + ->tap(function (Modal $modal) { |
| 73 | + $modal->newInput('greeting-block') |
| 74 | + ->label('Which Greeting?') |
| 75 | + ->newSelectMenu('greeting') |
| 76 | + ->forExternalOptions() |
| 77 | + ->placeholder('Choose a greeting...'); |
| 78 | + }); |
| 79 | +}; |
| 80 | + |
| 81 | +App::new() |
| 82 | + // Handles the `/hello` slash command. |
| 83 | + ->command('hello', function (Context $ctx) { |
| 84 | + $ctx->ack(Message::new()->tap(function (Message $msg) { |
| 85 | + $msg->newSection() |
| 86 | + ->mrkdwnText(':wave: Hello world!') |
| 87 | + ->newButtonAccessory('open-form') |
| 88 | + ->text('Choose a Greeting'); |
| 89 | + })); |
| 90 | + }) |
| 91 | + // Handles the "open-form" button click. |
| 92 | + ->blockAction('open-form', function (Context $ctx) use ($createModal) { |
| 93 | + $ctx->modals()->open($createModal()); |
| 94 | + }) |
| 95 | + // Handles when the "greeting" select menu needs its options. |
| 96 | + ->blockSuggestion('greeting', function (Context $ctx) { |
| 97 | + $ctx->options(['Hello', 'Howdy', 'Good Morning', 'Hey']); |
22 | 98 | }) |
23 | | - ->shortcut('shortcut_test', function (Context $ctx) { |
24 | | - $modal = Modal::new() |
25 | | - ->title('Hello') |
26 | | - ->text(':wave: Hello from a *Global Shortcut*.'); |
27 | | - $ctx->modal($modal); |
| 99 | + // Handles when the "hello-form" modal is submitted. |
| 100 | + ->viewSubmission('hello-form', function (Context $ctx) { |
| 101 | + $state = $ctx->payload()->getState(); |
| 102 | + $greeting = $state->get('greeting-block.greeting.selected_option.value'); |
| 103 | + $ctx->view()->update(":wave: {$greeting} world!"); |
28 | 104 | }) |
29 | | - ->messageShortcut('message_action_test', function (Context $ctx) { |
30 | | - $ctx->respond(':wave: Hello from a *Message Action*.'); |
| 105 | + // Handles when the "hello-form" modal is closed without submitting. |
| 106 | + ->viewClosed('hello-form', function (Context $ctx) { |
| 107 | + $ctx->logger()->notice('User closed hello-form modal early.'); |
31 | 108 | }) |
32 | | - ->blockSuggestion('custom_options', function (Context $ctx) { |
33 | | - $ctx->options([ |
34 | | - 'Option 1' => 'foo', |
35 | | - 'Option 2' => 'bar', |
36 | | - 'Option 3' => 'baz', |
37 | | - ]); |
| 109 | + // Handles when the "hello-global" global shortcut is triggered from the lightning menu. |
| 110 | + ->globalShortcut('hello-global', function (Context $ctx) use ($createModal) { |
| 111 | + $ctx->modals()->open($createModal()); |
38 | 112 | }) |
39 | | - ->blockAction('test-button', function (Context $ctx) { |
40 | | - $action = $ctx->payload()->asBlockActions()->getActions()[0]; |
41 | | - $msg = $ctx->blocks()->newMessage(); |
42 | | - $msg->newTwoColumnTable() |
43 | | - ->caption('*Action*') |
44 | | - ->row('`type`', $action->getType()) |
45 | | - ->row('`block_id`', $action->getBlockId()) |
46 | | - ->row('`action_id`', $action->getActionId()) |
47 | | - ->row('`value`', $action->getValue()); |
48 | | - $ctx->respond($msg); |
| 113 | + // Handles when the "hello-message" message shortcut is triggered from a message context menu. |
| 114 | + ->messageShortcut('hello-message', function (Context $ctx) { |
| 115 | + $user = $ctx->fmt()->user($ctx->payload()->get('message.user')); |
| 116 | + $ctx->say(":wave: Hello {$user}!", null, $ctx->payload()->get('message.ts')); |
49 | 117 | }) |
| 118 | + // Handles when the Hello World app "home" is accessed. |
50 | 119 | ->event('app_home_opened', function (Context $ctx) { |
51 | | - $event = $ctx->payload()->asEventCallback()->getEvent(); |
52 | | - $user = $ctx->fmt()->user($event->get('user')); |
53 | | - $home = $ctx->blocks()->appHome()->text(":wave: Hello, {$user}! This is your *App Home*."); |
54 | | - $ctx->home($home); |
| 120 | + $user = $ctx->fmt()->user($ctx->payload()->get('event.user')); |
| 121 | + $ctx->home(":wave: Hello {$user}!"); |
55 | 122 | }) |
| 123 | + // Handles when any public message contains the word "hello". |
| 124 | + ->event('message', Route::filter( |
| 125 | + ['event.channel_type' => 'channel', 'event.text' => 'regex:/^.*hello.*$/i'], |
| 126 | + function (Context $ctx) { |
| 127 | + $user = $ctx->fmt()->user($ctx->payload()->get('event.user')); |
| 128 | + $ctx->say(":wave: Hello {$user}!"); |
| 129 | + }) |
| 130 | + ) |
| 131 | + // Run that app to process the incoming Slack request. |
56 | 132 | ->run(); |
57 | 133 | ``` |
58 | 134 |
|
| 135 | +</details> |
| 136 | + |
| 137 | +### Object-Oriented Version |
| 138 | + |
| 139 | +You can alternatively create your App and Listeners as a set of classes. I recommend this approach if you have more than |
| 140 | +a few listeners or if your listeners are complicated. Here is an example of how the "Hello World" app would look when |
| 141 | +developed in this way. |
| 142 | + |
| 143 | +<details> |
| 144 | +<summary>"Hello World" app code</summary> |
| 145 | + |
| 146 | +`App.php` |
| 147 | +```php |
| 148 | +<?php |
| 149 | + |
| 150 | +declare(strict_types=1); |
| 151 | + |
| 152 | +namespace MyApp; |
| 153 | + |
| 154 | +use SlackPhp\Framework\{BaseApp, Route, Router}; |
| 155 | +use MyApp\Listeners; |
| 156 | + |
| 157 | +class MyCoolApp extends BaseApp |
| 158 | +{ |
| 159 | + protected function prepareRouter(Router $router): void |
| 160 | + { |
| 161 | + $router->command('hello', Listeners\HelloCommand::class) |
| 162 | + ->blockAction('open-form', Listeners\OpenFormButtonClick::class) |
| 163 | + ->blockSuggestion('greeting', Listeners\GreetingOptions::class) |
| 164 | + ->viewSubmission('hello-form', Listeners\FormSubmission::class) |
| 165 | + ->viewClosed('hello-form', Listeners\FormClosed::class) |
| 166 | + ->globalShortcut('hello-global', Listeners\HelloGlobalShortcut::class) |
| 167 | + ->messageShortcut('hello-message', Listeners\HelloMessageShortcut::class) |
| 168 | + ->event('app_home_opened', Listeners\AppHome::class) |
| 169 | + ->event('message', Route::filter( |
| 170 | + ['event.channel_type' => 'channel', 'event.text' => 'regex:/^.*hello.*$/i'], |
| 171 | + Listeners\HelloMessage::class |
| 172 | + )); |
| 173 | + } |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +`index.php` |
| 178 | + |
| 179 | +```php |
| 180 | +<?php |
| 181 | + |
| 182 | +use MyApp\MyCoolApp; |
| 183 | + |
| 184 | +// Note: Expects SLACK_SIGNING_KEY and SLACK_BOT_TOKEN to be set in environment. |
| 185 | +$app = new MyCoolApp(); |
| 186 | +$app->run(); |
| 187 | +``` |
| 188 | + |
| 189 | +</details> |
| 190 | + |
59 | 191 | ## Handling Requests with the `Context` Object |
60 | 192 |
|
| 193 | +The `Context` object is the main point of interaction between your app and Slack. Here are all the things you can do |
| 194 | +with the `Context`: |
| 195 | + |
61 | 196 | ``` |
62 | | -$context |
63 | | -
|
64 | | - // To respond (ack) to incoming Slack request: |
65 | | - ->ack(Message|string|null) // Responds to request with 200 (opt. and message) and defers |
66 | | - ->done(Message|string|null) // Responds to request with 200 (opt. and message) and does not defer |
67 | | - ->options(OptionList|array|null) // Responds to request with an options list |
68 | | - ->view() |
69 | | - ->clear() // Responds to modal submission by clearing modal stack |
70 | | - ->close() // Responds to modal submission by clearing current modal |
71 | | - ->errors(array) // Responds to modal submission by providing form errors |
72 | | - ->push(Modal) // Responds to modal submission by pushing new modal to stack |
73 | | - ->update(Modal) // Responds to modal submission by updating current modal |
74 | | -
|
75 | | - // To call Slack APIs (to send messages, open/update modals, etc.) after the ack: |
76 | | - ->respond(Message|string|array) // Responds to message. Uses payload.response_url |
77 | | - ->say(Message|string|array) // Responds in channel. Uses API and payload.channel.id |
78 | | - ->modal(Modal) // Opens modal. Uses API and payload.trigger_id |
79 | | - ->home(AppHome) // Modifies App Home for user. Uses API and payload.user.id |
80 | | - ->api()->{$method}(...$args) // Use Slack API client for arbitrary API operations |
81 | | -
|
82 | | - // Extra helpers |
83 | | - ->payload() // Returns the payload of the incoming request from Slack |
84 | | - ->blocks() // Returns an object that provides ability to create BlockKit objects |
85 | | - ->fmt() // Returns the block kit formatter |
86 | | - ->logger() // Returns an instance of a PSR-3 logger |
87 | | - ->get(string) // Returns a value from the context |
88 | | - ->set(string, mixed) // Sets a value in the context |
| 197 | +// To respond (ack) to incoming Slack request: |
| 198 | +$ctx->ack(Message|array|string|null) // Responds to request with 200 (and optional message) |
| 199 | +$ctx->options(OptionList|array|null) // Responds to request with an options list |
| 200 | +$ctx->view(): View |
| 201 | + ->clear() // Responds to modal submission by clearing modal stack |
| 202 | + ->close() // Responds to modal submission by clearing current modal |
| 203 | + ->errors(array) // Responds to modal submission by providing form errors |
| 204 | + ->push(Modal|array|string) // Responds to modal submission by pushing new modal to stack |
| 205 | + ->update(Modal|array|string) // Responds to modal submission by updating current modal |
| 206 | +
|
| 207 | +// To call Slack APIs (to send messages, open/update modals, etc.) after the ack: |
| 208 | +$ctx->respond(Message|array|string) // Responds to message. Uses payload.response_url |
| 209 | +$ctx->say(Message|array|string) // Responds in channel. Uses API and payload.channel.id |
| 210 | +$ctx->modals(): Modals |
| 211 | + ->open(Modal|array|string) // Opens a modal. Uses API and payload.trigger_id |
| 212 | + ->push(Modal|array|string) // Pushes a new modal. Uses API and payload.trigger_id |
| 213 | + ->update(Modal|array|string) // Updates a modal. Uses API and payload.view.id |
| 214 | +$ctx->home(AppHome|array|string) // Modifies App Home for user. Uses API and payload.user.id |
| 215 | +$ctx->api(string $api, array $params) // Use Slack API client for arbitrary API operations |
| 216 | +
|
| 217 | +// Access payload or other contextual data: |
| 218 | +$ctx->payload(): Payload // Returns the payload of the incoming request from Slack |
| 219 | +$ctx->getAppId(): ?string // Gets the app ID, if it's known |
| 220 | +$ctx->get(string): mixed // Gets a value from the context |
| 221 | +$ctx->set(string, mixed) // Sets a value in the context |
| 222 | +$ctx->isAcknowledged(): bool // Returns true if ack has been sent |
| 223 | +$ctx->isDeferred(): bool // Returns true if additional processing will happen after the ack |
| 224 | +
|
| 225 | +// Access additional helpers: |
| 226 | +$ctx->blocks(): Blocks // Returns a helper for creating Block Kit surfaces |
| 227 | +$ctx->fmt(): Formatter // Returns the "mrkdwn" formatting helper for Block Kit text |
| 228 | +$ctx->logger(): LoggerInterface // Returns an instance of the configured PSR-3 logger |
| 229 | +$ctx->container(): ContainerInterface // Returns an instance of the configured PSR-11 container |
89 | 230 | ``` |
| 231 | + |
| 232 | +## High Level Design |
| 233 | + |
| 234 | + |
| 235 | + |
| 236 | +<details> |
| 237 | +<summary>YUML Source</summary> |
| 238 | +<pre> |
| 239 | +[AppServer]<>-runs>[App] |
| 240 | +[AppServer]creates->[Context] |
| 241 | +[App]<>->[AppConfig] |
| 242 | +[App]<>->[Router] |
| 243 | +[Router]-^[Listener] |
| 244 | +[Router]<>1-*>[Listener] |
| 245 | +[Listener]handles->[Context] |
| 246 | +[Context]<>->[Payload] |
| 247 | +[Context]<>->[AppConfig] |
| 248 | +[Context]<>->[_Clients_;RespondClient;ApiClient] |
| 249 | +[Context]<>->[_Helpers_;BlockKit;Modals;View] |
| 250 | +[Context]<>->[_Metadata_] |
| 251 | +[AppConfig]<>->[Logger] |
| 252 | +[AppConfig]<>->[Container] |
| 253 | +[AppConfig]<>->[_Credentials_] |
| 254 | +</pre> |
| 255 | +</details> |
| 256 | + |
| 257 | +## Standards |
| 258 | + |
| 259 | +- PSR-1, PSR-12: Coding Style |
| 260 | +- PSR-3: Logger Interface |
| 261 | +- PSR-4: Autoloading |
| 262 | +- PSR-7, PSR-15, PSR-17: HTTP |
| 263 | +- PSR-11: Container Interface |
0 commit comments