{"id":50709541,"url":"https://github.com/skaisser/howl","last_synced_at":"2026-06-09T14:30:21.811Z","repository":{"id":357286324,"uuid":"1235627194","full_name":"skaisser/howl","owner":"skaisser","description":"Multi-driver Laravel notifier with Discord-first rich embeds, mentions, and bot-parseable metadata.","archived":false,"fork":false,"pushed_at":"2026-05-12T06:52:54.000Z","size":267,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-12T07:04:44.765Z","etag":null,"topics":["discord","embeds","events","laravel","logging","notifications","observability","webhook"],"latest_commit_sha":null,"homepage":"https://packagist.org/packages/skaisser/howl","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skaisser.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-11T13:58:43.000Z","updated_at":"2026-05-12T05:38:45.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/skaisser/howl","commit_stats":null,"previous_names":["skaisser/howl"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/skaisser/howl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skaisser%2Fhowl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skaisser%2Fhowl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skaisser%2Fhowl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skaisser%2Fhowl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skaisser","download_url":"https://codeload.github.com/skaisser/howl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skaisser%2Fhowl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34112225,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["discord","embeds","events","laravel","logging","notifications","observability","webhook"],"created_at":"2026-06-09T14:30:20.857Z","updated_at":"2026-06-09T14:30:21.803Z","avatar_url":"https://github.com/skaisser.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"docs/public/logo-howl.svg\" alt=\"Howl — Multi-driver Laravel notifier\" width=\"420\"\u003e\n\n**Multi-driver Laravel notifier — Discord, Slack, and Telegram with rich embeds.**\n\n*When something goes wrong, your app should howl into the night.*\n\n\u003cp\u003e\n  \u003ca href=\"https://packagist.org/packages/skaisser/howl\"\u003e\u003cimg src=\"https://img.shields.io/packagist/v/skaisser/howl.svg?style=for-the-badge\u0026label=Packagist\u0026color=ED4245\" alt=\"Latest Version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://packagist.org/packages/skaisser/howl\"\u003e\u003cimg src=\"https://img.shields.io/packagist/dt/skaisser/howl.svg?style=for-the-badge\u0026label=Downloads\u0026color=57F287\" alt=\"Total Downloads\"\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/packagist/l/skaisser/howl.svg?style=for-the-badge\u0026label=License\u0026color=4169E1\" alt=\"License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp\u003e\n  \u003ca href=\"https://github.com/skaisser/howl/actions/workflows/test.yml\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/skaisser/howl/test.yml?style=for-the-badge\u0026label=Tests\u0026logo=github\" alt=\"Tests\"\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Tests-487%20passing-success?style=for-the-badge\u0026logo=pestphp\" alt=\"487 Tests Passing\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Coverage-100%25-brightgreen?style=for-the-badge\u0026logo=codecov\" alt=\"100% Coverage\"\u003e\n\u003c/p\u003e\n\n\u003cp\u003e\n  \u003cimg src=\"https://img.shields.io/badge/PHP-8.3%20%7C%208.4-777BB4?style=for-the-badge\u0026logo=php\u0026logoColor=white\" alt=\"PHP 8.3 | 8.4\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Laravel-12.x%20%7C%2013.x-FF2D20?style=for-the-badge\u0026logo=laravel\u0026logoColor=white\" alt=\"Laravel 12 | 13\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Pest-3.x%20%7C%204.x-5d3eef?style=for-the-badge\u0026logo=pestphp\u0026logoColor=white\" alt=\"Pest 3 | 4\"\u003e\n\u003c/p\u003e\n\n\u003cp\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Discord-5865F2?style=for-the-badge\u0026logo=discord\u0026logoColor=white\" alt=\"Discord\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Slack-4A154B?style=for-the-badge\u0026logo=slack\u0026logoColor=white\" alt=\"Slack\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Telegram-26A5E4?style=for-the-badge\u0026logo=telegram\u0026logoColor=white\" alt=\"Telegram\"\u003e\n\u003c/p\u003e\n\n**[📖 Full documentation at howl.skaisser.dev →](https://howl.skaisser.dev)**\n\n\u003c/div\u003e\n\n---\n\n## ✨ Why Howl?\n\nA 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.\n\n- 🎯 **One fluent API for all three drivers** — switch per-call without touching business logic\n- 🎨 **Rich, native formatting per platform** — Discord embeds, Slack Block Kit, Telegram HTML — with mentions, fields, code blocks, buttons, attachments\n- 🛰️ **Channel failover \u0026 fan-out** — automatic backup channel dispatch on failure\n- 📦 **Seven built-in event templates** — exceptions, deployments, audits, cron heartbeats, job failures, manual ops, generic info\n- 🧪 **HowlFake test helper** — assert notifications without real HTTP calls; per-driver assertions\n- ⚡ **Queue-aware** with exponential backoff and opt-in Redis rate limiting\n- ✅ **100% line coverage** across 487 tests, enforced by `pest --coverage --min=100`\n- 📚 **Versioned docs** at `howl.skaisser.dev` plus machine-readable `llms.txt` for AI agents\n\n---\n\n## 🛠️ Compatibility\n\n| PHP | Laravel | Pest | PHPUnit | Testbench | Status |\n|:---:|:-------:|:----:|:-------:|:---------:|:------:|\n| 8.3 | 12.x | 3.x | 11.x | 10.x | ✅ |\n| 8.3 | 13.x | 4.x | 12.x | 11.x | ✅ |\n| 8.4 | 12.x | 3.x | 11.x | 10.x | ✅ |\n| 8.4 | 13.x | 4.x | 12.x | 11.x | ✅ |\n\nComposer 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.\n\n---\n\n## 📦 Installation\n\n```bash\ncomposer require skaisser/howl\nphp artisan vendor:publish --tag=howl-config\n```\n\nAdd your driver credentials to `.env`:\n\n```env\nHOWL_DRIVER=discord                            # discord | slack | telegram\nHOWL_DEFAULT_CHANNEL=errors                    # primary channel name\n\n# Discord\nHOWL_DISCORD_DEFAULT=https://discord.com/api/webhooks/...\n\n# Slack (optional — only if you use the slack driver)\nHOWL_SLACK_BOT_TOKEN=xoxb-...\nHOWL_SLACK_DEFAULT_CHANNEL=C0XXXXXXX\n\n# Telegram (optional — only if you use the telegram driver)\nHOWL_TELEGRAM_BOT_TOKEN=123456:ABC-DEF...\nHOWL_TELEGRAM_CHAT_ID=-1001234567890\n```\n\n---\n\n## 🚀 Quick Start\n\n```php\nuse Skaisser\\Howl\\Facades\\Howl;\nuse Skaisser\\Howl\\Events\\GenericExceptionEvent;\n\n// Direct severity verbs — use config('howl.driver') by default\nHowl::error(new GenericExceptionEvent($exception));\nHowl::info('Scheduled job completed');\nHowl::audit($auditEvent);\n\n// Channel routing — per-call override beats event default beats config\nHowl::on('audits')-\u003eaudit($event);\n\n// Per-call driver override — same payload, different destination\nHowl::driver('slack')-\u003einfo('Deploy succeeded');\nHowl::driver('telegram')-\u003eerror('Database connection lost');\n\n// Chainable: pick driver + channel + severity in one go\nHowl::driver('slack')-\u003echannel('deployments')-\u003esuccess('v1.2.0 shipped');\n```\n\n### 📨 Built-in event templates\n\n```php\nuse Skaisser\\Howl\\Events\\{\n    GenericExceptionEvent,\n    DeploymentEvent,\n    AuditEvent,\n    CronHeartbeatEvent,\n    JobRetryExhaustedEvent,\n    ManualOperationEvent,\n    GenericInfoEvent,\n};\n\nHowl::error(new GenericExceptionEvent($e));\nHowl::deployment(new DeploymentEvent(version: 'v1.2.0', env: 'production', commit: 'abc1234'));\nHowl::audit(new AuditEvent(actor: $user-\u003eemail, action: 'role.changed', target: $role));\n```\n\n---\n\n## 🛰️ Channel Failover \u0026 Fan-Out\n\nConfigure a backup channel and pick the mode:\n\n```php\n// config/howl.php\n'channel' =\u003e 'errors',\n'channel_backup' =\u003e 'errors-backup',\n'channel_mode' =\u003e 'failover',   // try primary; on failure, backup once\n// or\n'channel_mode' =\u003e 'fan_out',    // dispatch to BOTH channels in parallel\n```\n\n- **`failover`** (default): primary first, backup only on failure. `true` on first success, `false` if both fail.\n- **`fan_out`**: dispatch to primary AND backup sequentially. `true` if at least one succeeds. **Doubles rate-limit consumption** — size your `RateLimiter::for()` quota accordingly.\n\n---\n\n## 🧪 Testing with HowlFake\n\n```php\nuse Skaisser\\Howl\\Facades\\Howl;\n\n$fake = Howl::fake();\n\nHowl::error('Something broke');\nHowl::driver('slack')-\u003einfo('Deploy started');\n\n// Global assertions\n$fake-\u003eassertSent(fn ($p) =\u003e $p-\u003eseverity === 'error');\n$fake-\u003eassertNothingSent(); // negation form\nexpect($fake-\u003esent())-\u003etoHaveCount(2); // count via the sent() accessor\n\n// Per-driver assertions (v1.0+)\n$fake-\u003eassertSentVia('discord', fn ($p) =\u003e $p-\u003eseverity === 'error');\n$fake-\u003eassertSentVia('slack', fn ($p) =\u003e $p-\u003eseverity === 'info');\n$fake-\u003eassertSentViaNothing('telegram');\n```\n\nNo real HTTP calls. No mocks of HTTP clients. Drop-in replacement.\n\n---\n\n## ⚡ Queue Mode + Rate Limiting\n\n```env\nHOWL_QUEUE=true\nHOWL_QUEUE_CONNECTION=redis\nHOWL_QUEUE_NAME=default\nHOWL_RATE_LIMITER_KEY=howl-discord   # opt-in Redis rate limiter\n```\n\n```php\n// AppServiceProvider::boot()\nuse Illuminate\\Cache\\RateLimiter;\nuse Illuminate\\Cache\\RateLimiting\\Limit;\n\nRateLimiter::for('howl-discord', fn () =\u003e Limit::perMinute(28));\n```\n\n`SendHowlJob` ships with 3 retries + exponential backoff. Queue-failure events always force sync to avoid recursive loops.\n\n---\n\n## 📖 Documentation\n\nThe full docs site at **[howl.skaisser.dev](https://howl.skaisser.dev)** covers everything in depth:\n\n- 🚦 [Installation \u0026 Quick Start](https://howl.skaisser.dev/v1.0.0/guide/installation)\n- 🤖 Drivers: [Discord](https://howl.skaisser.dev/v1.0.0/drivers/discord) · [Slack](https://howl.skaisser.dev/v1.0.0/drivers/slack) · [Telegram](https://howl.skaisser.dev/v1.0.0/drivers/telegram)\n- 🎛️ [Configuration reference](https://howl.skaisser.dev/v1.0.0/configuration/reference)\n- 🧪 [HowlFake testing guide](https://howl.skaisser.dev/v1.0.0/testing/howl-fake)\n- 📜 [API reference](https://howl.skaisser.dev/v1.0.0/reference/api)\n- ⬆️ [Upgrade guide v0.x → v1.0](https://howl.skaisser.dev/v1.0.0/upgrade)\n- 📝 [Release notes](https://howl.skaisser.dev/v1.0.0/releases)\n\n**For AI agents:** [llms.txt](https://howl.skaisser.dev/llms.txt) (index) · [llms-full.txt](https://howl.skaisser.dev/llms-full.txt) (inline)\n\n---\n\n## 🤝 Contributing\n\nIssues and pull requests welcome at [github.com/skaisser/howl](https://github.com/skaisser/howl).\n\nBefore opening a PR, run the full suite locally:\n\n```bash\ncomposer install\nvendor/bin/pest --parallel\nvendor/bin/pest --coverage --min=100   # enforces 100% line coverage\nvendor/bin/pint                         # code style\n```\n\n---\n\n## 📜 License\n\nMIT — see [LICENSE](LICENSE). Copyright © Shirleyson Kaisser.\n// worktree test commit Wed May 13 03:12:54 -03 2026\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskaisser%2Fhowl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskaisser%2Fhowl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskaisser%2Fhowl/lists"}