skaisser / howl
Multi-driver Laravel notifier with Discord-first rich embeds, mentions, and bot-parseable metadata.
Requires
- php: ^8.3
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpunit/phpunit: ^11.5|^12.5
This package is auto-updated.
Last update: 2026-05-12 09:26:25 UTC
README
Multi-driver Laravel notifier โ Discord, Slack, and Telegram with rich embeds.
When something goes wrong, your app should howl into the night.
๐ Full documentation at howl.skaisser.dev โ
โจ Why Howl?
A single driver-agnostic API for Discord, Slack, and Telegram. Drop it into any Laravel 12 or 13 app and start delivering rich, structured notifications in minutes.
- ๐ฏ One fluent API for all three drivers โ switch per-call without touching business logic
- ๐จ Rich, native formatting per platform โ Discord embeds, Slack Block Kit, Telegram HTML โ with mentions, fields, code blocks, buttons, attachments
- ๐ฐ๏ธ Channel failover & fan-out โ automatic backup channel dispatch on failure
- ๐ฆ Seven built-in event templates โ exceptions, deployments, audits, cron heartbeats, job failures, manual ops, generic info
- ๐งช HowlFake test helper โ assert notifications without real HTTP calls; per-driver assertions
- โก Queue-aware with exponential backoff and opt-in Redis rate limiting
- โ
100% line coverage across 487 tests, enforced by
pest --coverage --min=100 - ๐ Versioned docs at
howl.skaisser.devplus machine-readablellms.txtfor AI agents
๐ ๏ธ Compatibility
| PHP | Laravel | Pest | PHPUnit | Testbench | Status |
|---|---|---|---|---|---|
| 8.3 | 12.x | 3.x | 11.x | 10.x | โ |
| 8.3 | 13.x | 4.x | 12.x | 11.x | โ |
| 8.4 | 12.x | 3.x | 11.x | 10.x | โ |
| 8.4 | 13.x | 4.x | 12.x | 11.x | โ |
Composer constraints support all four combinations. CI validates the latest combo (PHP 8.4 ร Laravel 13) on every push and PR targeting main; the other rows are validated locally before each release.
๐ฆ Installation
composer require skaisser/howl php artisan vendor:publish --tag=howl-config
Add your driver credentials to .env:
HOWL_DRIVER=discord # discord | slack | telegram HOWL_DEFAULT_CHANNEL=errors # primary channel name # Discord HOWL_DISCORD_DEFAULT=https://discord.com/api/webhooks/... # Slack (optional โ only if you use the slack driver) HOWL_SLACK_BOT_TOKEN=xoxb-... HOWL_SLACK_DEFAULT_CHANNEL=C0XXXXXXX # Telegram (optional โ only if you use the telegram driver) HOWL_TELEGRAM_BOT_TOKEN=123456:ABC-DEF... HOWL_TELEGRAM_CHAT_ID=-1001234567890
๐ Quick Start
use Skaisser\Howl\Facades\Howl; use Skaisser\Howl\Events\GenericExceptionEvent; // Direct severity verbs โ use config('howl.driver') by default Howl::error(new GenericExceptionEvent($exception)); Howl::info('Scheduled job completed'); Howl::audit($auditEvent); // Channel routing โ per-call override beats event default beats config Howl::on('audits')->audit($event); // Per-call driver override โ same payload, different destination Howl::driver('slack')->info('Deploy succeeded'); Howl::driver('telegram')->error('Database connection lost'); // Chainable: pick driver + channel + severity in one go Howl::driver('slack')->channel('deployments')->success('v1.2.0 shipped');
๐จ Built-in event templates
use Skaisser\Howl\Events\{ GenericExceptionEvent, DeploymentEvent, AuditEvent, CronHeartbeatEvent, JobRetryExhaustedEvent, ManualOperationEvent, GenericInfoEvent, }; Howl::error(new GenericExceptionEvent($e)); Howl::deployment(new DeploymentEvent(version: 'v1.2.0', env: 'production', commit: 'abc1234')); Howl::audit(new AuditEvent(actor: $user->email, action: 'role.changed', target: $role));
๐ฐ๏ธ Channel Failover & Fan-Out
Configure a backup channel and pick the mode:
// config/howl.php 'channel' => 'errors', 'channel_backup' => 'errors-backup', 'channel_mode' => 'failover', // try primary; on failure, backup once // or 'channel_mode' => 'fan_out', // dispatch to BOTH channels in parallel
failover(default): primary first, backup only on failure.trueon first success,falseif both fail.fan_out: dispatch to primary AND backup sequentially.trueif at least one succeeds. Doubles rate-limit consumption โ size yourRateLimiter::for()quota accordingly.
๐งช Testing with HowlFake
use Skaisser\Howl\Facades\Howl; $fake = Howl::fake(); Howl::error('Something broke'); Howl::driver('slack')->info('Deploy started'); // Global assertions $fake->assertSent(fn ($p) => $p->severity === 'error'); $fake->assertNothingSent(); // negation form expect($fake->sent())->toHaveCount(2); // count via the sent() accessor // Per-driver assertions (v1.0+) $fake->assertSentVia('discord', fn ($p) => $p->severity === 'error'); $fake->assertSentVia('slack', fn ($p) => $p->severity === 'info'); $fake->assertSentViaNothing('telegram');
No real HTTP calls. No mocks of HTTP clients. Drop-in replacement.
โก Queue Mode + Rate Limiting
HOWL_QUEUE=true HOWL_QUEUE_CONNECTION=redis HOWL_QUEUE_NAME=default HOWL_RATE_LIMITER_KEY=howl-discord # opt-in Redis rate limiter
// AppServiceProvider::boot() use Illuminate\Cache\RateLimiter; use Illuminate\Cache\RateLimiting\Limit; RateLimiter::for('howl-discord', fn () => Limit::perMinute(28));
SendHowlJob ships with 3 retries + exponential backoff. Queue-failure events always force sync to avoid recursive loops.
๐ Documentation
The full docs site at howl.skaisser.dev covers everything in depth:
- ๐ฆ Installation & Quick Start
- ๐ค Drivers: Discord ยท Slack ยท Telegram
- ๐๏ธ Configuration reference
- ๐งช HowlFake testing guide
- ๐ API reference
- โฌ๏ธ Upgrade guide v0.x โ v1.0
- ๐ Release notes
For AI agents: llms.txt (index) ยท llms-full.txt (inline)
๐ค Contributing
Issues and pull requests welcome at github.com/skaisser/howl.
Before opening a PR, run the full suite locally:
composer install vendor/bin/pest --parallel vendor/bin/pest --coverage --min=100 # enforces 100% line coverage vendor/bin/pint # code style
๐ License
MIT โ see LICENSE. Copyright ยฉ Shirleyson Kaisser.