quitoque/slack-bundle
Symfony bundle that simplifies sending Slack notifications and messages from PHP applications, with configurable integration for Slack clients, webhooks, and channel alerts so teams can add messaging to Symfony projects quickly.
Reality check before the first composer require. This is a Symfony bundle (Quitoque\SlackBundle\SlackBundle), not a Laravel package. There is no service provider, no facade, no config/slack.php to publish, and no php artisan vendor:publish. If your app is Laravel, you will not get automatic wiring — you either (a) use it only as a reference for how to call Slack, or (b) bind its underlying client yourself in a service provider. If your app is Symfony, it drops in and you can skip the rest of this paragraph.
Install and register.
composer require quitoque/slack-bundle
config/bundles.php — it may auto-register.new Quitoque\SlackBundle\SlackBundle(), to app/AppKernel.php.Look here first — not the README. The README on a 0-star bundle is usually stale, and the exact config keys will not match. Spend five minutes reading the source instead:
ls vendor/quitoque/slack-bundle/
cat vendor/quitoque/slack-bundle/DependencyInjection/Configuration.php # the real config tree
cat vendor/quitoque/slack-bundle/Resources/config/services.* # the real service IDs
cat vendor/quitoque/slack-bundle/composer.json # PHP/Symfony/client constraints
From Configuration.php you learn the actual keys (token, channel, username, icon, etc.). From services.xml|yaml you learn the service alias and, critically, which Slack client library it wraps — that underlying library is what actually talks to Slack, and its maintenance status matters more than the bundle's.
Then confirm the service exists:
bin/console debug:container slack --show-private
bin/console debug:container --tag=monolog.logger
Configure with a secret, never a literal.
# config/packages/slack.yaml
quitoque_slack:
token: '%env(SLACK_API_TOKEN)%'
# remaining keys: read Configuration.php, not the README
SLACK_API_TOKEN=xoxb-...
First use case: a console command that pings a channel. This is the lowest-risk entry point — no request path, immediate feedback on auth and channel routing, and trivially deletable if the bundle turns out to be unusable.
// src/Command/SlackPingCommand.php
#[AsCommand(name: 'app:slack:ping')]
class SlackPingCommand extends Command
{
public function __construct(private SlackClient $slack) // exact type = the wrapped client
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->slack->sendMessage('#ops', 'ping from ' . gethostname());
$output->writeln('sent');
return Command::SUCCESS;
}
}
If that prints sent and the message lands in Slack, the bundle is functional. Everything after that is integration hygiene.
Pattern 1 — inject the client, never the container. Whatever the bundle exposes, the day-to-day leverage is the same: inject the Slack service into the class that needs it and keep Slack call sites in one layer. If the bundle's service type is concrete and awkward, wrap it immediately:
interface SlackNotifier
{
public function notify(string $channel, string $text, array $blocks = []): void;
}
final class BundleSlackNotifier implements SlackNotifier
{
public function __construct(private SlackClient $client) {}
public function notify(string $channel, string $text, array $blocks = []): void
{
$this->client->sendMessage($channel, $text, $blocks);
}
}
Register the interface in services.yaml (Symfony) or in a SlackServiceProvider (Laravel). Every call site now depends on SlackNotifier, and swapping the bundle for the official SDK later is a one-file change. Given the package's abandonment profile, this abstraction is the whole point — do not skip it.
Pattern 2 — behind Laravel's notification channel. If you're on Laravel, the idiomatic integration is not to call the bundle directly from controllers; it's to implement toSlack() on a notification and let the queue and channel handle it. Where the bundle fits: back a custom notification channel with it.
final class BundleSlackChannel
{
public function __construct(private SlackNotifier $slack) {}
public function send(object $notifiable, Notification $notification): void
{
[$channel, $text] = $notification->toBundleSlack($notifiable);
$this->slack->notify($channel, $text);
}
}
In practice, Illuminate\Notifications\Channels\SlackWebhookChannel already does this with an incoming webhook and zero dependency risk — reach for the bundle only if you need the Web API (chat.postMessage with blocks) rather than a webhook.
Pattern 3 — queue everything. Never call Slack inside a request/response cycle. Slack latency (and 429 backoff) becomes your latency.
// Laravel
final class SendSlackAlert implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public array $backoff = [5, 30, 120];
public function __construct(
private string $channel,
private string $text,
private bool $critical = false,
) {}
public function handle(SlackNotifier $slack): void
{
$slack->notify($this->channel, $this->text);
}
}
# Symfony equivalent: dispatch on Messenger, handle with the notifier
Pattern 4 — one adapter method per Slack operation, not per payload. Post-message, deploy-announcement, incident-open, incident-resolve, and PR-review-request are the operations you actually have. Model those as methods, not as a generic send() you call with hand-built Block Kit arrays from controllers. It keeps Block Kit JSON in one file and makes snapshot tests meaningful.
public function deploySucceeded(string $service, string $sha, string $env): void
{
$this->notify('#deploys', "✅ {$service} deployed to {$env}", [[
'type' => 'section',
'fields' => [
['type' => 'mrkdwn', 'text' => "*Commit:*\n`{$sha}`"],
['type' => 'mrkdwn', 'text' => "*Env:*\n{$env}"],
],
]]);
}
Pattern 5 — fan out to multiple workspaces/channels via config, not code. Keep channel names in config (config/packages/slack.yaml / config/slack.php), not in class constants. You will want to point staging at #deploys-staging and production at #deploys.
# config/packages/slack.yaml
parameters:
slack.channels:
deploys: '%env(SLACK_CHANNEL_DEPLOYS)%'
alerts: '%env(SLACK_CHANNEL_ALERTS)%'
Pattern 6 — test against the interface, not Slack. Fake SlackNotifier in feature tests and assert on operation calls. For the adapter itself, snapshot the payload arrays. Never hit Slack in CI; a sandbox workspace at most, and only in a manually triggered job.
$this->mock(SlackNotifier::class)
->shouldReceive('notify')
->once()
->with('#alerts', Mockery::pattern('/CPU.*95%/'));
Pattern 7 — deploy/incident/ChatOps wiring. The three integration points that earn their keep:
deploySucceeded().Resources/config/); if not, write a small handler that calls SlackNotifier and register it in config/packages/monolog.yaml.App\Exceptions\Handler::report() (Laravel) with a rate limit or you will get one Slack message per failed request.The framework mismatch is the first gotcha and it doesn't go away. A Symfony bundle has no Laravel service provider. In Laravel, composer require gives you classes but no wiring; you must bind them yourself, and you must confirm the bundle doesn't hard-depend on symfony/framework-bundle services (event_dispatcher, cache.app, monolog) at construction time. Read the service definition before planning — if the constructor needs Symfony DI services, your service provider has to manufacture them, and that's a fork in disguise.
"0 stars, 0 dependents" is not cosmetic — treat it as owned code. Nothing in this repo will tell you when Slack deprecates a method or when PHP 8.x bumps a deprecation into an error. Pin the version, note it in a decision record, and set a reminder to re-check composer.json constraints each framework upgrade. If the bundle doesn't resolve on your PHP/Symfony target, don't fight it — the underlying client is almost certainly usable directly.
The underlying Slack client is the real dependency. The bundle is a DI wrapper; the client it pins is what will break. Check that client's maintenance status separately — you inherit two layers of drift, not one. If the bundle pins maknz/slack (the common old wrapper), you're on a library that predates Block Kit and OAuth v2.
Token type determines API. A bot token (xoxb-) uses chat.postMessage and needs chat:write + channel membership. An incoming webhook URL (https://hooks.slack.com/...) posts to exactly one channel and ignores channel params. Mixing these up produces a "channel not found" or channel_not_found/not_in_channel error that looks like an auth problem. Confirm which the bundle expects from its config tree.
Secrets land in the compiled container cache. Symfony compiles env vars into var/cache/{env}/. If you change SLACK_API_TOKEN and skip cache:clear, you'll authenticate with the old token and debug a ghost. In CI, always bin/console cache:clear after env changes. Never commit SLACK_API_TOKEN to .env variants that get bundled.
Rate limits are your problem, not the bundle's. Slack is tiered per-method, per-workspace, and returns 429 with Retry-After. A thin wrapper almost certainly sends synchronously and ignores both. Put retries and backoff at the queue layer (see $backoff above) and assert on response codes.
Errors are silently swallowed by default. The oldest failure mode: Slack returns {"ok": false, "error": "channel_not_found"} with HTTP 200. If the bundle's thin wrapper only checks for a 2xx, you log nothing and lose notifications quietly. Wrap the call and assert ok === true yourself, or at minimum log the full response body at warning during rollout. This is the single most valuable diagnostic you can add.
Block Kit payloads are untyped arrays — snapshot them. There's no schema, no IDE help, and a misplaced bracket yields a 400 invalid_blocks with no line number. Keep payload builders in one place, snapshot-test them, and paste failing payloads into Slack's Block Kit Builder to see the error.
Debugging an empty-feeling integration. In order: debug:container (is the service actually registered?), a one-line console command (does auth work?), the raw response body (did Slack accept it?), and Slack's app settings → Event Subscriptions / OAuth scopes (does the token have chat:write?). Ninety percent of "nothing happens" is one of those four.
Extension points worth knowing. The service definition is overridable — redeclare the service ID in your own services.yaml to swap the underlying client. The bundle's own client can be decorated. If it registers a Monolog handler, override its channel/formatter via monolog.yaml rather than subclassing. Check whether the service is public — in a modern Symfony app it likely isn't, and debug:container --show-private is how you confirm.
Don't build ingress on this. If you later need slash commands or interactivity, you need signature verification (X-Slack-Signature HMAC over the raw body), idempotency keys, and a 3-second response budget — the bundle almost certainly provides none of these. Handle ingress in your own controller and hand off the outbound part to the notifier.
How can I help you explore Laravel packages today?