wangju/ding-notice
A Laravel package for sending notifications to DingTalk group robots. Configure webhooks and secrets, then push text, markdown, link, action card, or feed card messages via a simple fluent API. Supports signed requests, @mentions, and alerts for deployments, monitoring, and team updates.
Install and publish config
composer require wangju/ding-notice:^1.0.10
php artisan vendor:publish --tag=ding-notice
Pin to ^1.0.10 rather than floating. Release 1.0.10 is the first tag with Guzzle 7 support — earlier 1.0.x tags constrain Guzzle 6, which no modern Laravel app ships with. If you install wangju/ding-notice without a constraint on a Laravel 8+ project you may get an older tag and a Composer conflict (or a downgraded Guzzle that breaks other packages). Check composer show guzzlehttp/guzzle after install: you want 7.x and you want the package's own constraint to permit it.
The config file lands in config/ding-notice.php. It expects at minimum a webhook URL and, if the robot is set to "signed" access, a secret. Put both in .env and reference them from config — never commit a real webhook token.
DING_NOTICE_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx
DING_NOTICE_SECRET=SECxxxxxxxx
First use case: fire a text message from Tinker
use Wangju\DingNotice\Facades\DingNotice;
DingNotice::text('Deploy finished on prod')->send();
If that returns without throwing, your webhook and secret are correct. If it silently does nothing, the problem is almost always an HTTP 200 with a non-zero errcode in the JSON body (see Gotchas).
Where to look first
text, markdown, link, actionCard, feedCard) and how @-mentions are expressed — the fluent API has drifted between versions, so verify against the tag you installed, not random tutorials.config/app.php (or auto-discovered).The day-one use case is operational notification: CI/CD results, queue failures, exception alerts to a group robot. That is 90% of real usage; the richer message types are mostly for dashboards you'll build later.
Message-type builders. DingTalk robots support five message shapes: text, markdown, link, actionCard, and feedCard. A typical pattern:
DingNotice::markdown('**Build #123** passed', 'CI/CD', 'https://ci.example.com/123')->send();
Keep the content short. Markdown in DingTalk supports a subset (headings, bold, links, images, lists) — it is not full CommonMark, and tables render inconsistently.
@-mentions for text messages. @-ing people requires their DingTalk mobile number, not a username. Wrap this in a small helper so you don't scatter atMobiles arrays across the codebase:
DingNotice::text('Disk at 92% on web-01')
->at(['13800000000'])
->atAll(false)
->send();
For alerts you often want atAll only for P1s, and a mobile list for the on-call rotation.
Config-driven multi-robot routing. Most teams have separate robots for #alerts, #deploys, and #business-metrics. Model this as named robots in config and use a helper to pick one:
// config/ding-notice.php
'robots' => [
'alerts' => ['webhook' => env('DING_ALERTS_WEBHOOK'), 'secret' => env('DING_ALERTS_SECRET')],
'deploys' => ['webhook' => env('DING_DEPLOYS_WEBHOOK'), 'secret' => env('DING_DEPLOYS_SECRET')],
],
Then wrap the facade in a Notifier service so call sites read Notifier::alerts()->markdown(...) rather than knowing about webhooks.
Queue the sending. Network calls to DingTalk should never run inline in a request or a tight loop. Wrap in a queued job:
class SendDingMessage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public string $robot, public string $type, public array $args) {}
public function handle(): void
{
// resolve robot config, call facade
}
}
This protects you from DingTalk outages and lets you retry on 5xx.
Integrate with Laravel's exception handler. A common pattern is a reportable that also pings DingTalk for ProductionExceptions:
// App\Exceptions\Handler::register()
$this->reportable(function (Throwable $e) {
if (app()->environment('production')) {
SendDingMessage::dispatch('alerts', 'text', ['⚠️ '.$e->getMessage()]);
}
});
Rate-limit this (see Gotchas) — one alert per exception class per minute is sane.
Health/status pages. Use markdown with an image URL to embed a Grafana or CI screenshot in the group chat. This is where markdown earns its keep over text.
Testing seam. Because the facade is a Laravel service, you can DingNotice::fake() if the package supports it, or (more reliably for a 2020 package) swap the underlying client via the container in tests. Assert on dispatched jobs instead — that's more robust than asserting on HTTP payloads. With Guzzle 7 you also have the option of injecting a MockHandler handler stack if you bind your own client (below).
HTTP 200 does not mean success. DingTalk returns {"errcode":0,"errmsg":"ok"} on success, but non-zero errcode values (e.g. 310000 for a bad signature, 300001 for an invalid token) still come back with HTTP 200. A naive ->send() will look fine while nothing appears in the group. Wrap calls and inspect the body; log errcode/errmsg on every send in staging.
Guzzle 7 support landed in 1.0.10 — and only there. This is the meaningful change in this release and it's what makes the package viable on Laravel 8+. Practical consequences:
^1.0.10 so you don't silently land on a Guzzle 6 tag. If your lockfile still resolves guzzlehttp/guzzle:^6, you installed an older tag and the assessment below about compatibility applies again.GuzzleHttp\Client::send signatures are largely stable, but RequestException handling, HandlerStack middleware signatures, and the \Psr\Http\Message usage all changed subtly). If the package's internals were written against Guzzle 6, expect edge-case breakage in exception types rather than in the happy path — test the failure paths (bad token, timeout, TLS error), not just a successful send.composer update and re-test your alerting paths.Secret signing is fiddly. The algorithm is: timestamp . "\n" . secret → HMAC-SHA256 → base64 → urlencode, appended as ×tamp=...&sign=.... Two classic failures:
urlencode, so + and = break the signature (310000).If signing is enabled on the robot but no secret is configured, every request will 310000. Verify the secret in config matches the robot's secret exactly.
Rate limits are real and per-robot. DingTalk caps robot messages at roughly 20 per minute per robot. A naive exception handler under load will blow through this, and DingTalk will start returning rate-limit errors that are easy to miss because of the HTTP-200 issue above. Use RateLimiter or Redis::throttle keyed by robot name, and batch/coalesce alerts.
Content size limits. Text and markdown bodies are capped (text around 20KB). Feed a stack trace into a text message and it gets truncated or rejected. Truncate stack traces to the first ~15 lines or push the full trace to your log/error tracker and send only a link.
@-mentions need to be group members. Putting a number in atMobiles for someone not in the group silently does nothing. Also note: for markdown messages, @-mentions only render correctly if the message type supports them — verify whether this package forwards atMobiles/isAtAll for each builder, since some only wire it into text.
Config cache. If you add a robot to config/ding-notice.php and it doesn't take effect, you forgot php artisan config:clear. Same for .env changes.
Package age and framework compatibility. The package is still essentially a 2020 codebase; 1.0.10 is a dependency-compatibility patch, not a rewrite. What changed: Guzzle 7 is now allowed, which removes the single hardest blocker for Laravel 8–11 installs. What did not change:
$signature, and the service-provider API are stable enough that it generally still works, but verify on your target major — no framework constraint bump is implied by "guzzle 7 support".If you hit a wall, the package is thin — one client class and one facade. Swapping to a maintained alternative (e.g. laravel-notification-channels/dingtalk or your own 30-line Guzzle 7 client) is often cheaper than fighting a stale dependency.
HTTPS only. DingTalk webhook URLs are HTTPS. If you're behind a corporate proxy that MITMs TLS, Guzzle will fail with a certificate error. Set verify or your CA bundle via the package's HTTP client config if it exposes one, otherwise bind your own Guzzle client.
Timeouts and retries. Set a short connect timeout (~3s) and a modest total timeout (~10s). Don't retry 4xx-class errors; only retry on timeouts and 5xx. Retry storms against a rate-limited endpoint just multiply the rate-limit errors.
Extension point: the HTTP client. Since the package is small, the useful extension point is the Guzzle client / underlying sender. Now that Guzzle 7 is supported, bind your own instance in a service provider with a HandlerStack carrying your proxy config, timeouts, and a logging middleware (\GuzzleHttp\Middleware::log), rather than forking. This is where you'd add structured logging of every errcode, which is the single highest-value addition for day-to-day reliability — and it's also your MockHandler seam for tests.
Don't send from migrations or seeders. It's a common early mistake and produces noisy, non-idempotent side effects in CI. Keep sending at the edges: jobs, listeners, exception handler, scheduled tasks.
How can I help you explore Laravel packages today?