guanguans/notify
Send notifications through dozens of channels—DingTalk, WeChat Work, Feishu, Bark, Slack, Telegram, and more—via one unified, framework-agnostic PHP API. Clean, extensible SDK with Laravel support for fast, consistent integration.
Install and verify the runtime first. The package targets modern PHP (8.1+) and pulls in a PSR-18 HTTP client (Guzzle by default). Before writing any notification code, confirm your app's PHP version and that a PSR-18 client + PSR-17 factories are resolvable in the container — most "it silently does nothing" reports trace back to a missing/duplicate HTTP client binding, not to the notifier itself.
composer require guanguans/notify
Pick one channel and one credential set. Don't wire up five channels on day one. Choose the one your team already uses (DingTalk, WeCom, Telegram Bot, Bark, Slack, etc.), generate the token/webhook, and put it in .env — never in code.
First send (webhook-style channel):
use Guanguans\Notify\Factory;
$notifier = Factory::dingTalk([
'token' => env('DINGTALK_TOKEN'),
'secret' => env('DINGTALK_SECRET'), // only if your bot uses signed webhooks
]);
$response = $notifier->send([
'msgtype' => 'text',
'text' => ['content' => 'Deploy finished: '.config('app.env')],
]);
Where to look first, in this order:
src/Factory.php — the authoritative list of channel names and the config keys each one accepts. This is your map; the README's channel table is derived from it.src/{Channel}/ for the channel you picked — look at the Notifier class for the endpoint, required config, and signing logic, and at src/Messages/{Channel}/ for the exact payload shape the API expects.src/Exceptions/ — know what gets thrown before you write your first try/catch.Typical first real use case: a deploy/failure alert from a queued job or an Artisan command. That's the lowest-risk integration — no user-facing path, easy to roll back, and it immediately tells you whether credentials, signing, and network egress are all correct.
Ad-hoc sends from commands and jobs. The Factory::channel($config)->send($payload) shape is the workhorse. Keep the config array in config/services.php (or a dedicated config/notify.php) rather than inline, so each channel has one construction site:
// config/notify.php
'channels' => [
'ops' => [
'driver' => 'weWork',
'token' => env('WEWORK_WEBHOOK'),
],
],
Then wrap construction in a small service so you can swap the channel without touching call sites.
Use message objects for anything non-trivial. Raw arrays are a passthrough to the remote API, so a typo in a nested key surfaces as an opaque 400 from the provider. The bundled message classes (src/Messages/{Channel}/…) give you named constructors and per-channel validation, and they're far easier to unit test:
$message = new TextMessage('Build #1234 failed');
$notifier->send($message);
Rule of thumb: arrays for one-off scripts, message objects for anything that lives in app/.
Laravel notification channel for user-facing notifications. If the package's Laravel channel is installed, implement toNotify() on the notification class and return the configured notifier/message. This is the pattern to prefer when the notification is about a user ($notifiable), because you inherit ShouldQueue, via() routing, and per-user channel preferences for free:
public function via($notifiable): array
{
return [NotifyChannel::class];
}
public function toNotify($notifiable)
{
return Factory::telegram([
'token' => config('notify.channels.telegram.token'),
'chat_id' => $notifiable->telegram_chat_id,
]);
}
public function toNotifyMessage($notifiable)
{
return new TextMessage("Order #{$this->order->id} shipped");
}
Keep the notifier construction and the message construction separate — that's what makes a notification reusable across channels later.
Fan-out with a fallback. For operational alerts, don't hard-code one provider. Build a tiny composite that iterates channels and swallows per-channel failures after logging, so a broken DingTalk webhook doesn't take down the alerting path:
foreach (config('notify.alert_channels') as $name => $config) {
try {
Factory::{$config['driver']}($config)->send($message);
} catch (Throwable $e) {
report($e); // try the next channel
}
}
Test with a fake HTTP client, not by hitting the provider. Bind a fake PSR-18 client in the container (or use Http::fake() if you're on Illuminate and the package resolves through it) and assert on the outgoing request body. This is the only way to cheaply cover the per-channel payload shape, and it keeps CI from burning rate limits.
Centralize credentials and recipient IDs. Webhook tokens and chat IDs are the two things that change between staging and production. Put both in config; never branch on app()->environment() inside a notifier call.
Notifier and check that it still matches the provider's current endpoint and auth scheme — and smoke-test it against a real endpoint, because CI can't.send(['text' => ...]) will happily dispatch a malformed body and you'll debug a provider-side error message. If a channel is business-critical, use its message class or write a thin DTO of your own.timestamp + HMAC sign, WeCom and Lark have their own schemes, some Bark setups need a device key in the path. If your notifier returns 200 with an error body, suspect the signature before the payload.composer.json (e.g. ^2.0) and treat upgrades as code changes, not routine. Given the release cadence, composer update on a Friday is how alerting quietly breaks.try/catch(Throwable) for non-critical paths. Exceptions from a notification should almost never fail the business operation that triggered it. For critical paths, the inverse: let it throw and retry via the queue.token/secret/webhook keys from any logging or exception reporting you add.Notifier, override the endpoint/payload method, and register it via your own factory or container binding — then upstream the fix if it's generally useful.via()/toNotify() don't line up, fall back to calling the notifier directly from a queued listener and skip the channel abstraction.send() is a blocking network call unless you queue it.How can I help you explore Laravel packages today?