Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Rocket Chat Notifier Laravel Package

symfony/rocket-chat-notifier

Symfony Notifier integration for Rocket.Chat. Configure a webhook DSN with access token and channel, then send ChatMessages with custom payloads and attachments to control bot name, avatar, channel, and message content.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you are installing. This is not a Laravel package. symfony/rocket-chat-notifier is a Symfony Notifier bridge: it contributes a transport (RocketChatTransport) and a DSN factory to symfony/notifier. There is no service provider, no config file, no facade, and zero Laravel auto-wiring. Day-to-day, you use it as a thin, well-tested HTTP client for Rocket.Chat Incoming Webhooks, wrapped in your own Laravel plumbing.

Install both packages (the bridge is useless alone):

composer require symfony/notifier symfony/rocket-chat-notifier

It pulls in symfony/http-client transitively; that is the client the transport expects, not Guzzle.

Get the credential from Rocket.Chat. Admin → Workspace → Integrations → New → Incoming WebHook. You get a URL like:

https://rocketchat.example.com/hooks/a847c392165c41f7bc5bbf273dd701f3/9343289d1c33464bb15ef132b5a7628d

The ACCESS_TOKEN is the two slash-separated segments, URL-encoded as one string (...f3%2F9343...). Getting this wrong is the #1 first-day failure — see Gotchas.

Configure via .env (single string, exactly like Symfony expects):

ROCKETCHAT_DSN="rocketchat://a847c392165c41f7bc5bbf273dd701f3%[email protected]?channel=alerts"

First use case: pipe a deployment or billing failure into #alerts. The fastest sanity check is php artisan tinker:

use Symfony\Component\Notifier\Chatter;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Transport;

$transport = Transport::fromDsn(config('services.rocketchat.dsn'));
$chatter = new Chatter([$transport]);

$chatter->send(new ChatMessage('Deploy #4821 to production failed: migration timeout'));

If the message lands, you are done with step one. If it 401s, the token encoding is wrong; if it 404s, the host or channel is wrong.

Where to look first, in this order:

  1. RocketChatTransport::send() — see exactly what it posts and what it throws.
  2. RocketChatOptions — the only customization surface (attachments + free-form payload).
  3. Rocket.Chat's Incoming Webhook Script editor for that integration — this determines whether your payload is even readable (see the script in Gotchas).
  4. RocketChatTransportFactory — only if you want extra DSN parameters.

Implementation Patterns

1. Bind one transport, inject ChatterInterface

Do not parse the DSN at call sites. Register a singleton in AppServiceProvider (or a small dedicated provider) and let the container do the work:

// config/services.php
'rocketchat' => [
    'dsn' => env('ROCKETCHAT_DSN'),
    'alerts' => env('ROCKETCHAT_DSN_ALERTS'),
    'deploys' => env('ROCKETCHAT_DSN_DEPLOYS'),
],
// app/Providers/RocketChatServiceProvider.php
use Symfony\Component\Notifier\Chatter;
use Symfony\Component\Notifier\ChatterInterface;
use Symfony\Component\Notifier\Transport;

public function register(): void
{
    $this->app->singleton('rocketchat.alerts', fn () =>
        new Chatter([Transport::fromDsn(config('services.rocketchat.alerts'))])
    );

    $this->app->bind(AlertChatter::class, fn ($app) => new AlertChatter(
        $app->make('rocketchat.alerts')
    ));
}

Wrap ChatterInterface in your own small class (AlertChatter, DeployChatter). This keeps ChatMessage construction in one place and gives you a seam to fake in tests without touching Symfony internals.

If you prefer to skip Transport::fromDsn() (which relies on factory auto-discovery), construct the transport directly — but read the constructor signature in vendor/symfony/rocket-chat-notifier/RocketChatTransport.php first, since it takes the client, token, default channel, and endpoint host as separate arguments rather than a DSN.

2. Rich alerts with attachments

Attachments are the reason to use this over a raw Http::post(). One colored block per affected service:

use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatOptions;

$options = new RocketChatOptions(
    [
        [
            'color' => '#e01e5a',
            'title' => 'api-gateway: 5xx rate 12%',
            'text' => 'Window 14:02–14:07 UTC · 431 failed requests',
            'fields' => [
                ['short' => true, 'title' => 'Env',   'value' => 'production'],
                ['short' => true, 'title' => 'Runbook', 'value' => '/wiki/runbooks/api-gateway'],
            ],
        ],
        [
            'color' => '#f2c744',
            'title' => 'queue-worker: backlog 8.4k',
            'text' => 'Threshold 5k exceeded for 9 minutes',
        ],
    ],
    [
        'alias' => 'Laravel Alerts',
        'emoji' => ':rotating_light:',
        'channel' => '#alerts-prod',   // overrides the DSN channel
    ]
);

$chatter->send(new ChatMessage('Production health check', $options));

The first constructor argument is always a list of attachment arrays, even for a single attachment. The single-array form is still accepted for backward compatibility, but write the list explicitly so nobody has to guess.

3. A Laravel Notification channel

Laravel's Notification system has no Rocket.Chat channel, so add one. This is the pattern that scales best, because it gives you via(), ShouldQueue, queue, and per-notifiable routing for free:

// app/Notifications/Channels/RocketChatChannel.php
public function send(object $notifiable, Notification $notification): void
{
    $message = $notification->toRocketChat($notifiable);
    $this->chatter->send($message);   // ChatMessage from toRocketChat()
}

// app/Notifications/DeployFailed.php
public function via(object $notifiable): array
{
    return [RocketChatChannel::class];
}

public function toRocketChat(object $notifiable): ChatMessage
{
    return new ChatMessage(
        "Deploy {$this->deployment->id} failed",
        new RocketChatOptions([...], ['channel' => '#deploys'])
    );
}

Return the ChatMessage from toRocketChat(), not a DTO — the channel stays a one-liner and the options travel with the message.

4. Per-tenant and per-environment routing

Because channel in the payload overrides the DSN channel, one transport can serve many destinations. This is the cheapest multi-tenancy story:

$payload = [
    'alias' => config('app.name'),
    'channel' => "#tenant-{$tenant->slug}",
];

Caveat: the webhook must be allowed to post there (the incoming webhook's channel scope). If you need hard isolation, use one DSN per tenant and pick the transport instance — it is more env vars but far fewer support tickets.

5. Queue everything

The transport is synchronous and has no retry logic. Every send should be inside a job:

class SendAlertToRocketChat implements ShouldQueue
{
    public int $tries = 5;
    public array $backoff = [5, 15, 60, 180];

    public function handle(AlertChatter $chatter): void
    {
        $chatter->send($this->message);
    }
}

Dispatch with ->afterCommit() so you never announce a deploy that rolled back. Add Illuminate\Queue\Middleware\RateLimited::class with a Redis limiter set to Rocket.Chat's webhook throttle (default is roughly 30 messages/minute per webhook) instead of discovering it via silent drops.

6. Test with MockHttpClient, not HTTP fakes

The transport takes a Symfony\Contracts\HttpClient\HttpClientInterface; Http::fake() is irrelevant here. Inject MockHttpClient and assert on the decoded body:

$client = new MockHttpClient();
$transport = new RocketChatTransport($client, 'token', 'alerts', 'https://rc.test');

$transport->send(new ChatMessage('hi', new RocketChatOptions([], ['emoji' => ':wave:'])));

Then assert the posted JSON contains text, attachments, and your payload keys. Because you wrapped the transport in your own class, 95% of your tests never touch it at all.


Gotchas and Tips

The token contains a slash and must be escaped as %2F. From the README: rocketchat://<tokenA>%2F<tokenB>@host?channel=.... Unescaped, the DSN parser treats the second half as part of the host and you get confusing connection errors. In .env, wrap the whole value in double quotes so % survives shell/config parsing, and re-run php artisan config:cache after any change — cached config is the most common reason "the new channel isn't being used."

Only ChatMessage's subject goes out as text. The second constructor argument is the subject, and its content/message body is ignored. new ChatMessage('Title') — that's your message. The transport overrides the payload's text key with it, and overrides attachments with whatever you passed to RocketChatOptions. If you set text in the payload array, it is silently replaced.

You need a webhook script if you want attachments or custom keys to survive. Rocket.Chat's default Incoming WebHook only reads content.text; the rest of your payload is discarded. Paste this into the integration's Script editor:

class Script {
    process_incoming_request({ request }) {
        return { content: request.content };
    }
}

Without it, rich attachments vanish and you will spend an hour blaming the package. If the script throws, Rocket.Chat often still returns HTTP 200 — so a "successful" send with no message is a script bug, not a transport bug.

Errors arrive as Symfony HTTP client exceptions. Non-2xx responses surface as Symfony\Component\HttpClient\Exception\TransportExceptionInterface. Catch it, log the channel, and rethrow so the queue retries. Never let an alerting failure take down the request that triggered it — dispatch, don't send inline.

Debugging a payload you can't see. Two tricks: (a) bind a MockHttpClient in a local-only provider and dd() the request body; (b) enable symfony/http-client's logger in config/logging.php and read the outgoing JSON. Pointing the DSN at a throwaway host is only half useful, because the webhook path includes the token.

RocketChatOptions is per-message, not per-transport. There is no place to set a global bot alias. If every message should post as "Laravel Bot," build a factory method (RocketChatOptions::botDefaults()) or have your AlertChatter merge a default payload into whatever the caller passes.

Constructor backward compatibility is a trap for readers, not writers. new RocketChatOptions($attachment1, $payload) and new RocketChatOptions([$attachment1, $attachment2], $payload) both work. In review, always require the list form; the single-array form looks identical to a payload array at a glance.

Version pinning matters more than usual. Dependents are effectively zero and the bridge tracks Symfony's release train (Symfony 8 requires PHP ≥ 8.4). A patch release can bump the symfony/notifier constraint out from under you. Pin a minor version in composer.json, and don't let a composer update on an unrelated package silently bump it.

Message length and markdown. Rocket.Chat caps message length; long stack traces get truncated or rejected. Trim to the first N lines and link to your log viewer instead of dumping the trace. Rocket.Chat accepts markdown in text, so `inline code` and fenced blocks render — use them for command output.

Extension point, if you truly need one. RocketChatTransportFactory parses the DSN; if you want ?emoji=:robot: as a DSN param, that factory is where it would go. In practice, don't — pass options per message. Fewer moving parts, and your own code stays the thing that's tested.

Don't send secrets. Webhook integrations are frequently readable by the whole workspace, and the channel history persists. Reference a runbook URL or an internal ticket ID; never paste env values, tokens, or customer PII into an alert payload.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource