Skip to content

Commit 6aa37ac

Browse files
committed
Updated from develop
2 parents 75440d8 + a1e0288 commit 6aa37ac

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+11565
-64
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/vendor/
2+
/.idea/
3+
/coverage/
4+
/.phpunit.result.cache
5+
/.php_cs.cache

.php_cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
$finder = PhpCsFixer\Finder::create()->in(__DIR__ . '/src');
4+
5+
$config = new PhpCsFixer\Config();
6+
return $config->setRules([
7+
'@PSR12' => true,
8+
'array_syntax' => ['syntax' => 'short'],
9+
'single_import_per_statement' => false,
10+
'global_namespace_import' => [
11+
'import_constants' => true,
12+
'import_functions' => true,
13+
'import_classes' => true,
14+
],
15+
'no_unused_imports' => true,
16+
'fully_qualified_strict_types' => true,
17+
'operator_linebreak' => ['position' => 'beginning'],
18+
])
19+
->setFinder($finder)
20+
;

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Jeremy Lindblom
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 238 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,88 +2,262 @@
22

33
A small, PHP framework for building Slack Apps. Takes inspiration from Slack's Bolt frameworks.
44

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>
654

755
```php
856
<?php
957

1058
declare(strict_types=1);
1159

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.
1764

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']);
2298
})
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!");
28104
})
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.');
31108
})
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());
38112
})
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'));
49117
})
118+
// Handles when the Hello World app "home" is accessed.
50119
->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}!");
55122
})
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.
56132
->run();
57133
```
58134

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+
59191
## Handling Requests with the `Context` Object
60192

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+
61196
```
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
89230
```
231+
232+
## High Level Design
233+
234+
![UML diagram of the framework](https://yuml.me/68717414.png)
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

Comments
 (0)