beyondcode/slack-notification-channel
A Laravel notification channel for sending rich Slack messages, including Block Kit layouts and interactive components. Integrates with Laravel’s notification system to deliver customizable Slack alerts from your app.
1. Install and confirm the matrix first. The package is a single-purpose notification channel driver — no migrations, no publishable assets, no service to run.
composer require beyondcode/slack-notification-channel:^1.10
composer show beyondcode/slack-notification-channel # check requires: php, illuminate/*
Auto-discovery registers the service provider on Laravel 5.5+. Pin ^1.10; the 1.9.x → 1.10.0 upgrade is a version bump with no documented config or API change.
2. Read the channel source before anything else. It is tiny — open vendor/beyondcode/slack-notification-channel/src/SlackChannel.php and SlackMessage.php. Two things you need from it: the exact routing method it calls on the notifiable (conventionally routeNotificationForSlack()) and how it reads a default webhook URL from config. Two minutes here saves an hour of guessing.
3. Create the incoming webhook and store it as a secret.
// config/services.php
'slack' => [
'webhook_url' => env('SLACK_WEBHOOK_URL'),
'alerts_channel' => env('SLACK_ALERTS_CHANNEL', '#ops'),
],
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T000/B000/xxxx
Never commit the URL. If you use a secrets manager, resolve it in a config or a service provider, not inline at the call site.
4. Send your first message — a single ops alert, not a whole suite. Pick one event where a Slack message actually changes behavior (deploy failure, error-budget breach, billing exception). Write it as a notification class:
namespace App\Notifications;
use Illuminate\Notifications\Notification;
use BeyondCode\SlackNotificationChannel\SlackChannel;
use BeyondCode\SlackNotificationChannel\SlackMessage;
class DeploymentFailed extends Notification
{
public function __construct(public string $environment, public string $reason) {}
public function via($notifiable): array
{
return [SlackChannel::class];
}
public function toSlack($notifiable): SlackMessage
{
return (new SlackMessage)
->from('Deploy Bot', ':robot_face:')
->to(config('services.slack.alerts_channel'))
->content(":rotating_light: *Deploy failed* in `{$this->environment}`\n{$this->reason}");
}
}
5. Route it. The cleanest pattern is a small, dedicated notifiable rather than bolting routeNotificationForSlack() onto User:
namespace App\Support;
use Illuminate\Notifications\Notifiable;
class SlackChannelNotifiable
{
use Notifiable;
public function routeNotificationForSlack($notification = null): string
{
return config('services.slack.webhook_url');
}
}
(new SlackChannelNotifiable)->notify(new DeploymentFailed('production', 'migration timeout'));
6. Prove delivery in staging, not in analysis. Send one real message to each destination workspace. Verify formatting renders, then verify a queued send works (QUEUE_CONNECTION=database locally is enough).
Notification class per message type. Keep toSlack() dumb: build the payload from scalar constructor args, don't query the database inside it. This is what makes the class trivially testable and safe to queue.
Queue from day one. A webhook POST inside a request cycle is a latency and failure risk.
class DeploymentFailed extends Notification implements ShouldQueue
{
public $tries = 3;
public $backoff = [10, 60, 300];
}
When combined with mail, order matters only for readability — but be aware each channel succeeds or fails independently.
Centralize construction in a notifier/alert service, not scattered notify() calls. Otherwise you get fifteen ad-hoc payload shapes and no house style.
namespace App\Support;
class Alerts
{
public static function deploymentFailed(string $env, string $reason): void
{
if (! config('services.slack.enabled', true)) {
return;
}
(new SlackChannelNotifiable)->notify(new \App\Notifications\DeploymentFailed($env, $reason));
}
}
Then trigger it from a domain event listener (DeploymentFailed event → SendDeploymentFailedAlert listener), so business code never imports a notification class.
House style via a base message class. Subclass SlackMessage to encode formatting conventions once instead of copy-pasting emoji and field order:
namespace App\Support;
use BeyondCode\SlackNotificationChannel\SlackMessage;
class AppSlackMessage extends SlackMessage
{
public function critical(): static
{
return $this->from('Alerts', ':rotating_light:');
}
public function withContext(array $fields): static
{
// attach a fields-shaped attachment built from key => value pairs
return $this->attachment(function ($attachment) use ($fields) {
$attachment->fields($fields);
});
}
}
Test with Notification::fake() for routing, and assert the payload directly for formatting. The fake proves the right notification went to the right notifiable; it proves nothing about what Slack will render.
Notification::fake();
(new SlackChannelNotifiable)->notify(new DeploymentFailed('production', 'timeout'));
Notification::assertSentTo(
new SlackChannelNotifiable,
DeploymentFailed::class,
fn ($notification) => count($notification->via(new SlackChannelNotifiable)) === 1
);
For payload shape, instantiate the notification and inspect toSlack() output — or, if SlackMessage exposes the payload array, assert against it. A single env-gated integration test that posts to a #sandbox webhook covers the rest.
Use the 1.10.0 container seam for the things you actually need. Because the channel is now resolved from the container, you can decorate it for audit logging or per-workspace routing without forking:
// AppServiceProvider::register()
$this->app->extend(SlackChannel::class, function ($channel, $app) {
return new LoggingSlackChannel($channel, $app->make(SlackLogger::class));
});
// tests
$this->app->instance(SlackChannel::class, $fakeChannel);
Throttle noisy events at the source. Slack will 429 you if a flapping alert fires every ten seconds. Use ShouldBeUnique on the queued notification, or a RateLimiter/cache-key dedupe window around the trigger. This belongs in your code — the package doesn't do it.
Decide explicitly, once, why you're not on the first-party channel. Modern Laravel ships laravel/slack-notification-channel with Block Kit. This package is essentially the pre-Block-Kit builder. Valid reasons to be here: existing toSlack() code already standardized on this API, or you specifically want the container-resolution seam. If you need Blocks, this isn't the package — record that decision in the PR that adds the dependency so nobody re-litigates it at every upgrade.
Treat maintenance as a real risk, cheaply. 85 stars, 0 dependents, single maintainer, last release January 2024. None of that disqualifies it for internal alerting; all of it disqualifies it for anything customer-facing or paging-critical. The mitigation is trivial because the surface is tiny and MIT: fork or vendor it if a critical fix is needed. Note the last release date and assign one owner to re-check compatibility at every Laravel major upgrade.
1.10.0's container resolution has two behavioral edges. Bindings that were previously bypassed are now honored — usually what you want, occasionally surprising if you have a stale or accidental binding. And a subclass with a non-autowirable constructor argument now fails at send time, not instantiation, which means a green deploy and a silent drop. Both are covered by sending one real message per destination after the upgrade.
The webhook URL must not leak into logs. A failed HTTP request's exception message commonly includes the full request URI. Failed notification jobs and exception trackers will happily store it. Sanitize on the way out and rotate the webhook if it's ever committed or logged.
No retries or Retry-After handling is provided for you. Slack returns 429 with a Retry-After header on webhook rate limits. Your queue retry/backoff is the retry policy. Equally important: alert on failed sends — a dead queue worker or a rotated webhook produces completely silent dropped notifications otherwise. Add a failed() hook on the queued notification that logs loudly and, if you have a second path, escalates through it.
Formatting failures are silent from the application's perspective. Slack accepts the POST and drops malformed blocks or unknown attachment keys. There is no error to catch. This is why the "send one real message per workspace after changing payload code" step is not optional — automated tests will never catch it.
Slack mrkdwn ≠ Markdown. No headings, no tables, no nested lists. *bold* not **bold**; _italic_; <https://example.com|label> for links; <@U12345> and <#C12345> for mentions (IDs, not names). Escape &, <, > in user-supplied values or your alert will render as garbage.
Queued toSlack() runs in a worker. Don't touch auth(), request(), session(), or the current locale. Don't assume a passed Eloquent model is fresh — SerializesModels re-fetches it, and a deleted record throws ModelNotFoundException inside the job. Pass scalars into the constructor.
Webhooks can't upload files. Image-only attachments need a publicly reachable image_url. If you need file uploads, that's a Slack app with files.upload, not this channel.
Disable in local and CI. A config flag checked in the notifier (or via() returning [] when the flag is off) keeps your test suite and local dev from spamming #ops — and from failing when a developer has no webhook configured.
config:cache means env() only works inside config files. Reading env('SLACK_WEBHOOK_URL') directly in a notification or service class works locally and mysteriously returns null in production.
Multiple channels or workspaces means multiple webhooks, not one webhook with a channel override — an incoming webhook is bound to a single channel. Model this as multiple notifiables (one per destination) or pass the webhook URL through per-notification routing. There is no cross-workspace posting here; if you need that, you need OAuth and a Slack app.
Extension points, in order of cost. Subclass SlackMessage for formatting helpers → extend SlackChannel in the container for logging/audit/rewriting → fork the package. Anything beyond a container extension and you should be asking whether the first-party channel plus your own thin wrapper is the better long-term home.
How can I help you explore Laravel packages today?