nexylan/slack
A maintained PHP package for sending messages to Slack via incoming webhooks, with a fluent API for rich messages and attachments. Laravel-friendly and ideal for notifications, alerts, and simple integrations.
Install it straight from Composer — the package is framework-agnostic but trivially consumable inside Laravel:
composer require nexylan/slack
The only hard requirement is a Slack incoming webhook URL (create one at api.slack.com/apps → Incoming Webhooks). Everything happens through Nexy\Slack\Client:
use Nexy\Slack\Client;
$client = new Client('https://hooks.slack.com/services/T000/B000/XXXX');
$client->send('Deploy finished'); // first use case: fire a one-liner
Where to look first
Nexy\Slack\Client — the constructor signature new Client(string $endpoint, array $attributes = [], ?GuzzleHttp\ClientInterface $http = null). That third argument is what makes it testable; the second argument is where you set default channel, username, icon, link_names, unfurl_links, unfurl_media, allow_markdown, markdown_in_attachments.Nexy\Slack\Message — created via $client->createMessage(), exposes setText(), setChannel(), setUsername(), setIcon(), attach(), send().Nexy\Slack\Attachment / AttachmentField — for rich, structured notifications.First real use case: a Laravel controller or console command that pings a channel.
// app/Http/Controllers/DeployController.php
app(\Nexy\Slack\Client::class)->send('*Deploy* to :rocket: `production` finished.');
Do not instantiate new Client(...) inline in controllers. Register it once (see below) so config, channel defaults, and the Guzzle client are centralized.
Singleton binding with config. Keep the webhook out of source and expose your defaults:
// config/services.php
'slack' => [
'webhook' => env('SLACK_WEBHOOK_URL'),
'channel' => env('SLACK_CHANNEL', '#general'),
'username' => env('SLACK_USERNAME', config('app.name')),
],
// app/Providers/AppServiceProvider.php
use Nexy\Slack\Client;
public function register(): void
{
$this->app->singleton(Client::class, fn () => new Client(
config('services.slack.webhook'),
[
'channel' => config('services.slack.channel'),
'username' => config('services.slack.username'),
'link_names' => true,
'allow_markdown' => true,
'markdown_in_attachments' => ['text', 'pretext', 'title', 'fields'],
],
));
}
Message object for per-send overrides. Anything set on the Message wins over constructor defaults — useful when one job needs a different channel:
$message = $client->createMessage()
->setChannel('#alerts')
->setUsername('Deploy Bot')
->setIcon(':rocket:')
->setText('Build #' . $build->id . ' failed');
$client->sendMessage($message);
Attachments for structured alerts. Slack payloads with attachments render much better than free text, and fields are the idiomatic shape for CI/CD, monitoring, and error reports:
$attachment = $client->createAttachment()
->setColor($status === 'failed' ? 'danger' : 'good')
->setFallback("Build {$build->id} {$status}") // required for push notifications
->setTitle("Build {$build->id} {$status}")
->setTitleLink(route('builds.show', $build))
->addField('Branch', $build->branch, true)
->addField('Duration', $build->duration, true)
->addField('Commit', $build->commit, false);
$client->sendMessage(
$client->createMessage()
->setText('') // text can be empty when attachments exist
->attach($attachment)
);
Queue it. Sends are synchronous Guzzle calls, so wrap in a job. This also gives you retries/backoff, which is the right place to handle Slack outages:
class SendSlackAlert implements ShouldQueue
{
public int $tries = 3;
public int $backoff = 5;
public function __construct(public string $text) {}
public function handle(Client $client): void
{
$client->send($this->text);
}
}
Testing. Inject a Guzzle client with a MockHandler — no HTTP is performed and you can assert on the payload:
use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
$mock = new MockHandler([new Response(200, [], 'ok')]);
$client = new Client('https://hooks.slack.com/services/fake', [], new Guzzle(['handler' => HandlerStack::create($mock)]));
$this->app->instance(Client::class, $client);
You can also drop Http::fake() entirely — Slack webhooks aren't driven by the Laravel HTTP client here.
Integrations. Wire it into Laravel’s exception handler (report()), event listeners (Illuminate\Notifications is a heavier alternative if you need per-user routing), or scheduler hooks. For anything that might be user-facing rather than ops-facing, prefer Laravel's built-in slack notification channel — this package shines for broadcast to a channel alerts.
#channel-safe. If SLACK_CHANNEL is empty, you’ll get a payload Slack silently accepts but routes to the app’s default channel. Validate the env var in a config check.allow_markdown and markdown_in_attachments are separate. allow_markdown => true does not enable mrkdwn inside attachments. You must list text, pretext, title, fields explicitly, otherwise *bold* renders literally.fallback is required in practice. Attachments without a fallback won’t trigger desktop/mobile notifications, which defeats the point of alerting.good, warning, danger, or a hex string like #439FE0. Arbitrary names are ignored by Slack.Redis::throttle() or backoff).fallback, invalid field shape) or a 404 (revoked webhook) will throw. Catch GuzzleHttp\Exception\* at the job boundary and treat Slack failures as non-blocking.to() / from(). Old blog posts (and the upstream maknz/slack README) show $client->to('#x')->from('Bot')->send(...). In this fork you get createMessage()->setChannel()->setUsername()->setText() and then sendMessage(). If a snippet doesn't work, this is almost always why.icon vs icon_url. setIcon(':ghost:') sends an emoji; setIconUrl('https://...') sends an image. Setting both produces unpredictable results — pick one.text + attachments is fine, but both empty is not. Slack responds with invalid_payload; add a guard if you build messages dynamically.nexylan/slack in composer.json and keep Guzzle aligned with the version your composer.lock resolved, or you’ll hit type errors in the injected ClientInterface.Message — the Client calls $message->toArray() before POSTing, so overriding that method lets you inject blocks without forking.How can I help you explore Laravel packages today?