laravel-notification-channels/gitter
Laravel Gitter Notification Channel lets you send notifications from your Laravel app to Gitter chat rooms using Laravel's built-in notification system. Add the channel to a notification, configure your Gitter webhook or token, and deliver updates to your team without custom integration code.
Know what you're adopting first. This package is a thin, single-purpose notification channel: it wraps the Gitter REST API (POST /v1/rooms/{roomId}/chatMessages) behind Laravel's Notification system. It has ~10 stars and no dependents. The latest release, v2.2.0, is a "Laravel 8" compatibility bump — i.e. the package's illuminate/support constraint tops out at Laravel 8. That release line predates Laravel 9/10/11/12 entirely, so treat it as "vendored code you happen to install," not a maintained dependency. The code is only a few dozen lines and will run on modern Laravel if you get it installed, but Composer will refuse to resolve it on Laravel 9+. Also confirm Gitter itself is still acceptable in your org: Gitter has been in maintenance mode since the Element acquisition and has a long-running plan to migrate users to Matrix.
Install
composer require laravel-notification-channels/gitter
On Laravel 9+ expect this to fail against the illuminate/support constraint. Run composer why-not laravel-notification-channels/gitter to confirm. Your options, in order of preference:
composer.json, and pull it in via repositories (vcs or path).--ignore-platform-reqs as a last resort — it silences the resolver, not the incompatibility, and will bite you at the next composer update.Get credentials
https://developer.gitter.im/apps (scope: flow/chat write is enough).curl -H "Authorization: Bearer $TOKEN" https://api.gitter.im/v1/rooms — the id field is the opaque hex string the channel needs, not the uri/name.Configure in config/services.php:
'gitter' => [
'token' => env('GITTER_TOKEN'),
],
Add the room ID here too ('room_id' => env('GITTER_ROOM_ID')) if you'll use on-demand routes — you'll want it in one place.
First use case — a deploy/alert notification, sent on-demand (no Notifiable model required):
use Illuminate\Notifications\Notification;
use NotificationChannels\Gitter\GitterChannel;
use NotificationChannels\Gitter\GitterMessage;
class DeployFailed extends Notification
{
public function __construct(public string $reason) {}
public function via(object $notifiable): array
{
return [GitterChannel::class];
}
public function toGitter(object $notifiable): GitterMessage
{
return GitterMessage::create()
->text("**Deploy failed**\n> {$this->reason}");
}
}
Notification::route('gitter', config('services.gitter.room_id'))
->notify(new DeployFailed('migration timed out'));
Where to look first: the package's src/GitterChannel.php and src/GitterMessage.php. They are the entire API surface. Reading them (5 minutes) beats relying on the README, which hasn't changed since 2020 — and it's also the fastest way to decide whether to just copy them into app/Notifications/Channels/.
Attach the channel to a Notifiable model when the destination is stable:
class Team extends Model
{
use Notifiable;
public function routeNotificationForGitter(): string
{
return $this->gitter_room_id;
}
}
Then $team->notify(new DeployFailed(...)) resolves the room automatically. The channel reads this via routeNotificationFor('gitter'); if it returns null/empty, the channel returns silently (see Gotchas).
Fan out to multiple rooms by looping and using on-demand routes — cleanest when rooms are per-environment:
foreach (['staging' => $stagingRoom, 'production' => $prodRoom] as $env => $room) {
Notification::route('gitter', $room)
->notify(new DeployFailed($reason, $env));
}
Use it as one leg of a multi-channel alert. The channel composes naturally with mail/Slack:
public function via($notifiable): array
{
return $this->severity === 'critical'
? [GitterChannel::class, 'mail', 'slack']
: ['mail'];
}
Keep messages terse and Markdown-safe. Gitter renders Markdown, so **bold**, backticks, and > quotes work; but any user-supplied text (exception messages, branch names) must be escaped or wrapped in code fences to avoid broken formatting and accidental mentions:
return GitterMessage::create()
->text("Deploy failed on `".e($this->branch)."`\n```\n".$this->reason."\n```");
Trigger from events, not controllers. A listener is the natural integration point:
class NotifyGitterOfFailedJob
{
public function handle(JobFailed $event): void
{
Notification::route('gitter', config('services.gitter.room_id'))
->notify(new DeployFailed($event->exception->getMessage()));
}
}
Queue the notification, not the HTTP call, so alerts don't block the request/job that failed:
class DeployFailed extends Notification implements ShouldQueue
{
use Queueable;
}
Note the package's GitterChannel does a synchronous HTTP call inside send(); ShouldQueue on the notification is what moves it off the request path.
Testing. Notification::fake() asserts the notification was dispatched, which is usually all you need:
Notification::fake();
$team->notify(new DeployFailed('boom'));
Notification::assertSentTo($team, DeployFailed::class);
To assert the payload, call toGitter() directly and inspect the message object — faster and more stable than mocking HTTP:
$message = (new DeployFailed('boom'))->toGitter($team);
$this->assertStringContainsString('boom', $message->text);
For an end-to-end check: the channel predates Laravel's HTTP client, so it likely talks to Guzzle directly (Http::fake() won't intercept it). If you write or fork the channel, switching it to Http::withToken(...) means your existing Http::fake() tests cover it for free — a good reason to prefer the DIY version.
^8.0-era. On Laravel 9+ Composer simply won't install it. Decide up front: fork, vendor-copy the two classes, or reimplement. Don't ship a --ignore-platform-reqs install to production and call it done.routeNotificationForGitter() returns null or the on-demand route key is wrong (e.g. 'gitter ' with a space, or 'Gitter'), the channel simply returns. Nothing throws, nothing logs. This is the #1 "why isn't it sending?" cause — verify the route key matches exactly gitter.org/channel or the display name will 404. Use the hex id from GET /v1/rooms.php artisan config:cache in production will bake in a stale GITTER_TOKEN if you changed .env without recaching. Also never commit the token; it's a full-account credential.Forking into your monorepo (repositories + vcs in composer.json) with a widened illuminate/support constraint.
Writing the channel yourself — it is genuinely small, and this is the recommended path:
class GitterChannel
{
public function send($notifiable, Notification $notification): void
{
if (! $room = $notifiable->routeNotificationFor('gitter')) {
return;
}
Http::withToken(config('services.gitter.token'))
->post("https://api.gitter.im/v1/rooms/{$room}/chatMessages", [
'text' => $notification->toGitter($notifiable)->text,
])->throw();
}
}
This version uses Http::fake() in tests and ->throw() for observable failures — both improvements over the package, on top of having no version constraint to fight.
Str::limit($e->getMessage(), 1500)) so the important first lines survive.curl the send endpoint with the token — if that fails, it's credentials/room, not Laravel; (2) confirm the config key is services.gitter.token as the provider expects; (3) Log::info() inside your toGitter() to prove it's reached; (4) check queue workers are running if ShouldQueue.toGitter()-returning notification base class so you can swap the transport (Gitter → Slack → Matrix) later without touching call sites. Keep the room IDs in config, not in code, for the same reason.How can I help you explore Laravel packages today?