Slack Notifications
Introduction
The Slack Notification plugin provides a powerful way to send notifications to Slack channels using Slack's modern Block Kit API. Block Kit allows you to create rich, interactive messages with buttons, images, sections, and more.
This plugin integrates seamlessly with CakePHP's notification system, allowing you to send Slack notifications alongside other channels like email, SMS, and database storage.
Prerequisites
Installation
The Slack notification channel is included as part of the notification ecosystem. Ensure you have the main Notification plugin installed and configured.
Installation via Composer
composer require skie/notification-slackLoad Plugin
In src/Application.php:
public function bootstrap(): void
{
parent::bootstrap();
$this->addPlugin('Cake/Notification');
$this->addPlugin('Cake/SlackNotification');
}Slack App Setup
Before sending Slack notifications, you must create a Slack App:
- Go to https://api.slack.com/apps and create a new app
- Navigate to "OAuth & Permissions"
- Add the following Bot Token Scopes:
chat:write- Send messages as the appchat:write.public- Send messages to channels without joiningchat:write.customize- Customize message username and icon
- Install the app to your workspace
- Copy the "Bot User OAuth Token"
Configuration
Configure your Slack credentials in config/app.php or use environment variables:
return [
// ...
'Slack' => [
'bot_token' => env('SLACK_BOT_TOKEN', 'xoxb-your-token'),
'default_channel' => env('SLACK_DEFAULT_CHANNEL', '#general'),
],
];Or in your .env file:
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
SLACK_DEFAULT_CHANNEL=#notificationsFormatting Slack Notifications
Block Kit Messages
To send Slack notifications, define a toSlack() method on your notification class that returns a BlockKitMessage instance:
<?php
namespace App\Notification;
use Cake\Datasource\EntityInterface;
use Cake\Notification\AnonymousNotifiable;
use Cake\Notification\Notification;
use Cake\SlackNotification\BlockKit\BlockKitMessage;
use Cake\SlackNotification\BlockKit\Block\SectionBlock;
class InvoicePaid extends Notification
{
protected int $invoiceId;
protected float $amount;
public function __construct(int $invoiceId, float $amount)
{
$this->invoiceId = $invoiceId;
$this->amount = $amount;
}
public function via(EntityInterface|AnonymousNotifiable $notifiable): array
{
return ['slack', 'database'];
}
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Invoice Paid')
->headerBlock('Invoice Paid')
->sectionBlock(function (SectionBlock $block) {
$block->text('An invoice has been paid.');
$block->field("*Invoice ID:*\n{$this->invoiceId}")->markdown();
$block->field("*Amount:*\n\${$this->amount}")->markdown();
})
->dividerBlock()
->sectionBlock(function (SectionBlock $block) {
$block->text('Thank you for your business!');
});
}
}Available Blocks
The Block Kit API provides several block types that you can use to build your messages:
- Header Block - Large text header
- Section Block - Text with optional fields and accessories
- Context Block - Contextual information with small text and images
- Divider Block - Visual separator
- Image Block - Standalone image
- Actions Block - Interactive elements like buttons
Building Rich Messages
Header Blocks
Header blocks display large, prominent text at the top of your message:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('New Order')
->headerBlock('New Order Received')
->sectionBlock(function (SectionBlock $block) {
$block->text("Order #{$this->orderId} has been placed.");
});
}Section Blocks
Section blocks are the most versatile and commonly used block type:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Order Details')
->sectionBlock(function (SectionBlock $block) {
// Main text
$block->text('Your order has been confirmed!');
// Add fields (displayed in columns)
$block->field("*Order ID:*\n{$this->orderId}")->markdown();
$block->field("*Total:*\n\${$this->total}")->markdown();
$block->field("*Items:*\n{$this->itemCount}")->markdown();
$block->field("*Status:*\nProcessing")->markdown();
});
}Context Blocks
Context blocks display small, supplementary information:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Server Alert')
->headerBlock('Server Alert')
->contextBlock(function (ContextBlock $block) {
$block->text('Server: web-01');
$block->text('Time: ' . date('Y-m-d H:i:s'));
$block->text('Severity: High');
});
}Divider Blocks
Divider blocks create visual separation between sections:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Report')
->headerBlock('Daily Report')
->sectionBlock(function (SectionBlock $block) {
$block->text('Morning statistics');
})
->dividerBlock()
->sectionBlock(function (SectionBlock $block) {
$block->text('Afternoon statistics');
});
}Image Blocks
Image blocks display images in your messages:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('New Product')
->headerBlock('New Product Launch')
->imageBlock('https://example.com/product.jpg', 'Product Image')
->sectionBlock(function (SectionBlock $block) {
$block->text('Check out our new product!');
});
}You can also add images as accessories to section blocks:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Profile Update')
->sectionBlock(function (SectionBlock $block) {
$block->text("*{$this->userName}* updated their profile");
$block->image($this->avatarUrl, 'Avatar');
});
}Actions Blocks
Actions blocks contain interactive elements like buttons:
use Cake\SlackNotification\BlockKit\Block\ActionsBlock;
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Approval Required')
->headerBlock('Approval Required')
->sectionBlock(function (SectionBlock $block) {
$block->text("Request #{$this->requestId} needs your approval");
})
->actionsBlock(function (ActionsBlock $block) {
$block->button('Approve')->primary()->value($this->requestId);
$block->button('Deny')->danger()->value($this->requestId);
});
}Interactive Elements
Buttons
Buttons allow users to take actions directly from Slack:
use Cake\SlackNotification\BlockKit\Block\ActionsBlock;
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Order Confirmation')
->headerBlock('Confirm Your Order')
->sectionBlock(function (SectionBlock $block) {
$block->text("Please confirm order #{$this->orderId}");
})
->actionsBlock(function (ActionsBlock $block) {
// Primary button (green)
$block->button('Confirm Order')
->primary()
->value($this->orderId)
->id('confirm_order');
// Danger button (red)
$block->button('Cancel Order')
->danger()
->value($this->orderId)
->id('cancel_order');
// Default button (grey)
$block->button('View Details')
->value($this->orderId)
->url("https://example.com/orders/{$this->orderId}");
});
}Button styles:
primary()- Green button for primary actionsdanger()- Red button for destructive actions- Default (no style) - Grey button for secondary actions
Confirmation Dialogs
Add confirmation dialogs to buttons to prevent accidental actions:
use Cake\SlackNotification\BlockKit\Block\ActionsBlock;
use Cake\SlackNotification\BlockKit\Composite\ConfirmObject;
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Delete Account')
->actionsBlock(function (ActionsBlock $block) {
$block->button('Delete Account')
->danger()
->value($this->userId)
->confirm(
'Are you sure you want to delete this account?',
function (ConfirmObject $dialog) {
$dialog->title('Delete Account');
$dialog->text('This action cannot be undone.');
$dialog->confirm('Yes, Delete');
$dialog->deny('Cancel');
}
);
});
}Select Menus
Select menus allow users to choose from a list of options:
use Cake\SlackNotification\BlockKit\Block\ActionsBlock;
use Cake\SlackNotification\BlockKit\Element\Select\SelectOption;
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Choose Priority')
->actionsBlock(function (ActionsBlock $block) {
$block->staticSelect('priority', 'Select Priority')
->option('Low', 'low')
->option('Medium', 'medium')
->option('High', 'high')
->option('Critical', 'critical');
});
}Message Formatting
Text Formatting
Slack supports rich text formatting in text blocks:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Formatted Text')
->sectionBlock(function (SectionBlock $block) {
$text = "*Bold Text*\n";
$text .= "_Italic Text_\n";
$text .= "~Strikethrough~\n";
$text .= "`Code`\n";
$text .= "```Code Block```\n";
$text .= ">Quoted Text\n";
$text .= "<https://example.com|Link Text>";
$block->text($text)->markdown();
});
}Markdown Support
To enable markdown formatting, call markdown() on text objects:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Invoice Details')
->sectionBlock(function (SectionBlock $block) {
$block->text('*Invoice #' . $this->invoiceId . '* has been paid.')
->markdown();
$block->field("*Amount:*\n\${$this->amount}")->markdown();
$block->field("*Date:*\n" . date('Y-m-d'))->markdown();
});
}Emojis
Slack supports emoji codes in messages:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->text('Celebration!')
->headerBlock(':tada: Congratulations!')
->sectionBlock(function (SectionBlock $block) {
$block->text(':white_check_mark: Task completed successfully!');
})
->icon(':rocket:'); // Set custom emoji icon for the message
}Routing Slack Notifications
Using Behavior Methods
Define a routeNotificationForSlack() method on your entity to specify which channel to send to:
<?php
namespace App\Model\Entity;
use Cake\Notification\Notification;
use Cake\ORM\Entity;
class User extends Entity
{
/**
* Route notifications for the Slack channel
*
* @param \Cake\Notification\Notification $notification
* @return string|null
*/
public function routeNotificationForSlack(Notification $notification): ?string
{
// Send to user's preferred channel
return $this->slack_channel ?? '#general';
}
}You can also specify the channel directly in the notification:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
return (new BlockKitMessage())
->to('#important-alerts')
->text('Critical Alert')
->headerBlock('Critical Alert')
->sectionBlock(function (SectionBlock $block) {
$block->text('System is down!');
});
}External Workspaces
To send notifications to external Slack workspaces, use SlackRoute:
use Cake\SlackNotification\BlockKit\SlackRoute;
public function routeNotificationForSlack(Notification $notification): SlackRoute|string
{
return SlackRoute::make(
$this->slack_channel,
$this->slack_token
);
}This is useful when your users have their own Slack workspaces and you've obtained OAuth tokens for them.
Testing and Debugging
Block Kit Builder
Use the dd() method during development to preview your message in Slack's Block Kit Builder:
public function toSlack(EntityInterface|AnonymousNotifiable $notifiable): BlockKitMessage
{
$message = (new BlockKitMessage())
->text('Test Message')
->headerBlock('Test')
->sectionBlock(function (SectionBlock $block) {
$block->text('Testing the message');
});
// This will output a URL to the Block Kit Builder
$message->dd();
return $message;
}To see the raw JSON payload instead:
$message->dd(true); // Dump raw JSONTesting Notifications
When testing Slack notifications, you can use the NotificationTrait to capture notifications instead of sending them. After adding the trait to your test case, you can assert that Slack notifications were sent and inspect their content:
<?php
namespace App\Test\TestCase;
use App\Notification\InvoicePaid;
use Cake\SlackNotification\BlockKit\BlockKitMessage;
use Cake\Notification\TestSuite\NotificationTrait;
use Cake\TestSuite\TestCase;
class SlackNotificationTest extends TestCase
{
use NotificationTrait;
protected array $fixtures = ['app.Users', 'app.Invoices'];
public function testSlackNotificationIsSent(): void
{
$usersTable = $this->getTableLocator()->get('Users');
$user = $usersTable->get(1);
$usersTable->notify($user, new InvoicePaid(123, 99.99));
$this->assertNotificationSentTo($user, InvoicePaid::class);
$this->assertNotificationSentToChannel('slack', InvoicePaid::class);
}
public function testSlackMessageFormat(): void
{
$usersTable = $this->getTableLocator()->get('Users');
$user = $usersTable->get(1);
$usersTable->notify($user, new InvoicePaid(123, 99.99));
$notifications = $this->getNotificationsByClass(InvoicePaid::class);
$notification = $notifications[0]['notification'];
$slackMessage = $notification->toSlack($user);
$this->assertInstanceOf(BlockKitMessage::class, $slackMessage);
$payload = $slackMessage->toArray();
$this->assertArrayHasKey('blocks', $payload);
$this->assertArrayHasKey('text', $payload);
}
public function testSlackMessageContent(): void
{
$usersTable = $this->getTableLocator()->get('Users');
$user = $usersTable->get(1);
$usersTable->notify($user, new InvoicePaid(123, 99.99));
$notifications = $this->getNotificationsByClass(InvoicePaid::class);
$notification = $notifications[0]['notification'];
$slackMessage = $notification->toSlack($user);
$payload = $slackMessage->toArray();
$this->assertNotEmpty($payload['blocks']);
$this->assertStringContainsString('Invoice', $payload['text']);
$this->assertStringContainsString('123', $payload['text']);
}
public function testSlackBlockStructure(): void
{
$usersTable = $this->getTableLocator()->get('Users');
$user = $usersTable->get(1);
$usersTable->notify($user, new InvoicePaid(123, 99.99));
$notifications = $this->getNotificationsByClass(InvoicePaid::class);
$notification = $notifications[0]['notification'];
$slackMessage = $notification->toSlack($user);
$payload = $slackMessage->toArray();
$this->assertIsArray($payload['blocks']);
$this->assertNotEmpty($payload['blocks']);
$firstBlock = $payload['blocks'][0];
$this->assertArrayHasKey('type', $firstBlock);
}
}