xmon-org/notification-bundle
A notification bundle for PHP/Symfony apps that centralizes creating, sending, storing, and managing user notifications with pluggable channels and reusable templates.
Minimal steps
composer require xmon-org/notification-bundle:^1.5chat_id. Keep the token in .env, never in the committed config.ForceReply enabled to confirm the UI prompt appears in the client.Where to look first
text payload and a ForceReply flag.c722bb8, which is small and shows exactly which options are exposed.First use case
Transactional alerts to an ops chat (deploy finished, queue backed up, invoice failed). Plain text messages are enough here; you don't need templates or keyboards to get value on day one.
Plain text notifications
Treat the new text support as the default channel for short, human-readable alerts. Keep payloads under Telegram's 4096-character limit and prefer a single message per event over concatenating a digest — splitting/truncation is on you, not the transport.
ForceReply for lightweight two-way flows
ForceReply is the interesting part of this release: it opens the user's client with the reply box pre-focused and quoting your message. Use it for confirmations and short prompts:
yes <ticket-id>" — the reply carries reply_to_message, which gives you a reliable correlation key back to the original notification.Design the pattern as send → store message_id → match inbound reply on reply_to_message.message_id. ForceReply only renders the prompt; the round trip is your responsibility.
Routing and fan-out
Keep notification routing declarative (event → recipients/channels) rather than sprinkling send() calls through controllers. That way adding a second channel later doesn't touch business logic, and you can route the same event to a chat for humans and a webhook/log for machines.
Async delivery
Telegram calls are network I/O — dispatch notifications on a queue so a slow or rate-limited API never blocks a request. Pair this with a retry policy that respects retry_after from Telegram's 429 response instead of a fixed backoff.
Testing
Bind a fake/null transport in the test container and assert on the composed notification (recipient, text, ForceReply flag) rather than hitting the API. Reserve real API calls for a single manual smoke test per environment.
It's a bundle, not a Laravel package. This is the biggest friction point. There's no Laravel service provider, no facade, no config/notifications.php auto-merge. Expect to write a small bridge provider, resolve services from the container, and own the config loading yourself. Budget for that in your first estimate.
ForceReply does not mean "wait for a reply." The flag only affects the client UI. If you don't already have an update receiver (webhook endpoint or getUpdates poller), replies go nowhere. Confirm your inbound path exists before building an approval flow on top of this.
Rate limits are real. Roughly 30 messages/second globally and ~1 message/second per chat. Broadcast loops to many chats will hit 429s — batch, throttle, and respect retry_after.
Escaping and parse mode. If you pass parse_mode (MarkdownV2/HTML), unescaped user data will break the message or silently drop it. Escape interpolated values, or send as plain text and skip formatting until you need it.
chat_id resolution. Numeric IDs (including the -100... supergroup form) and @channelusername behave differently regarding permissions and bot membership. Prefer explicit numeric IDs in config; usernames break silently when a channel is renamed.
Secret handling. The bot token embeds the bot ID and is effectively a password. Rotate via @BotFather if it ever lands in logs — and scrub request/response logging so tokens and message bodies don't end up in your log aggregator.
Version pinning. Pin to ^1.5 and read the diff for c722bb8 before upgrading further; the Telegram transport is clearly under active development, so small releases may adjust the message-construction API.
Debugging checklist. No message delivered? In order: wrong chat_id, bot not a member/admin of the chat, token invalid or revoked, 400 from an unescaped entity, or 429 swallowed by a permissive catch block. Log the raw Telegram response body at debug level — the error descriptions are specific enough to pinpoint the cause.
How can I help you explore Laravel packages today?