PHPackages                             conduit-ui/mattermost - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. conduit-ui/mattermost

ActiveLibrary[HTTP &amp; Networking](/categories/http)

conduit-ui/mattermost
=====================

The Laravel way to build Mattermost bots — Saloon client, WebSocket, bot framework, streaming replies, slash commands, Filament panel

v0.1.0(1mo ago)01[2 issues](https://github.com/conduit-ui/mattermost/issues)[1 PRs](https://github.com/conduit-ui/mattermost/pulls)MITPHPPHP ^8.4CI passing

Since Apr 11Pushed 1mo agoCompare

[ Source](https://github.com/conduit-ui/mattermost)[ Packagist](https://packagist.org/packages/conduit-ui/mattermost)[ RSS](/packages/conduit-ui-mattermost/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (14)Versions (6)Used By (0)

Mattermost for Laravel
======================

[](#mattermost-for-laravel)

[![CI](https://camo.githubusercontent.com/bfb580597a0155ae512b090d3ed64de06a946ff64db48424b2b4306248743c3f/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f636f6e647569742d75692f6d61747465726d6f73742f63692e796d6c3f6272616e63683d6d61696e266c6162656c3d4349266c6f676f3d676974687562)](https://github.com/conduit-ui/mattermost/actions/workflows/ci.yml)[![PHP](https://camo.githubusercontent.com/e9b4fc8e2edfe2ad83ab5e32546edefd37388dfa099b72dfe95f1a4820360f55/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d253545382e342d3737374242343f6c6f676f3d706870266c6f676f436f6c6f723d7768697465)](https://php.net)[![Laravel](https://camo.githubusercontent.com/9c1ddacbc07ba8f25245361f97e8b8ca14f8cfe6cc2e2b3b5f6536aa842407d6/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d25354531332d4646324432303f6c6f676f3d6c61726176656c266c6f676f436f6c6f723d7768697465)](https://laravel.com)[![Saloon](https://camo.githubusercontent.com/b905dd55b4bbedc89dd69434a70894349faabe062ca57c1e5c1d9e6d03497f00/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53616c6f6f6e2d253545342d303038303830)](https://docs.saloon.dev)[![Pest](https://camo.githubusercontent.com/b784d4fd15fcade3e7dff7d8f1592780e12762f7a7bf18012a8ec6214acdff8b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f746573746564253230776974682d506573742d3842354346363f6c6f676f3d706870)](https://pestphp.com)[![PHPStan](https://camo.githubusercontent.com/ff3c7f8c8667ce643f47e74532748f673482a5f95d7d4269f925f2eebbe5117e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230382d627269676874677265656e)](https://phpstan.org)[![License](https://camo.githubusercontent.com/5aefe2f1c0ee5d6f8b0e66e3e14332e9c1fed7f4abbc5251fa4e5fc1b71e00ae/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f636f6e647569742d75692f6d61747465726d6f7374)](LICENSE)

The Laravel way to build Mattermost bots.

> **Status:** Under active development. Not yet published on Packagist.

Contents
--------

[](#contents)

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Quick start](#quick-start)
- [REST API client](#rest-api-client)
- [Sending messages](#sending-messages)
- [Bot framework](#bot-framework)
- [Slash commands](#slash-commands)
- [Interactive messages](#interactive-messages)
- [Streaming replies](#streaming-replies)
- [Notifications](#notifications)
- [Filament panel](#filament-panel)
- [Artisan commands](#artisan-commands)
- [Testing](#testing)
- [Multi-server](#multi-server)
- [Local development](#local-development)
- [License](#license)

Features
--------

[](#features)

- Saloon-based REST client covering the full Mattermost API v4
- WebSocket client with auto-reconnect, typed events, and signal handling
- Event-driven bot framework — class-based handlers, middleware pipeline, queueable
- Permission guards — role, permission, channel-membership middleware
- Slash command routing
- Interactive messages — buttons, menus, action handlers
- Streaming replies — observer-driven create → edit → finalize flow
- Fluent message builder with attachments, threading, file uploads
- Laravel notification channel + broadcast driver
- Filament admin panel for status, message log, health
- Artisan commands — `mattermost:listen`, `mattermost:post`, `mattermost:health`
- First-class testing — `Mattermost::fake()`, fixture helpers, request assertions
- Multi-server support via named connections

Requirements
------------

[](#requirements)

- PHP 8.4+
- Laravel 13+

Installation
------------

[](#installation)

```
composer require conduit-ui/mattermost
php artisan vendor:publish --tag=mattermost-config
```

Set the minimum env in your `.env`:

```
MATTERMOST_URL=https://your-mattermost.example.com
MATTERMOST_BOT_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx
MATTERMOST_TEAM=your-team-name
MATTERMOST_BOT_USER_ID=zzzzzzzzzzzzzzzzzzzzzzzzzz
```

Configuration
-------------

[](#configuration)

`config/mattermost.php` defines connections, slash commands, bot middleware, and websocket settings. Most of it has sensible defaults — only `connections` typically needs touching.

```
return [
    'default' => env('MATTERMOST_CONNECTION', 'default'),

    'connections' => [
        'default' => [
            'url' => env('MATTERMOST_URL', 'http://localhost:8065'),
            'token' => env('MATTERMOST_BOT_TOKEN'),
            'bot_user_id' => env('MATTERMOST_BOT_USER_ID'),
            'team' => env('MATTERMOST_TEAM'),
        ],
    ],

    'bot' => [
        // Global middleware applied to every handler. Per-handler
        // middleware stacks on top via the #[Middleware(...)] attribute.
        'middleware' => [
            ConduitUI\Mattermost\Bot\Middleware\IgnoreBots::class,
            ConduitUI\Mattermost\Bot\Middleware\Dedup::class,
        ],
        'dedup_ttl' => 60,
        'rate_limit_seconds' => 30,
        'allowed_channels' => [],
    ],
];
```

Quick start
-----------

[](#quick-start)

```
use ConduitUI\Mattermost\Facades\Mattermost;

// One-liner post
Mattermost::posts()->createPost([
    'channel_id' => $channelId,
    'message' => 'Hello from Laravel!',
]);

// Or the fluent builder
use ConduitUI\Mattermost\Messages\Message;

Mattermost::posts()->createPost(
    Message::make('Deploy started')
        ->to($channelId)
        ->thread($rootPostId)
        ->toArray()
);
```

REST API client
---------------

[](#rest-api-client)

Every Mattermost API v4 endpoint is wrapped in a Saloon resource. Resources are auto-generated from the OpenAPI spec — every parameter and response is typed.

```
Mattermost::posts()->createPost([...]);
Mattermost::posts()->updatePost($postId, ['message' => 'edited']);
Mattermost::posts()->deletePost($postId);

Mattermost::channels()->getChannelByName($teamId, 'town-square');
Mattermost::channels()->getChannelMembers($channelId);

Mattermost::users()->getUserByUsername('alice');
Mattermost::users()->getUserStatus($userId);

Mattermost::reactions()->saveReaction([
    'user_id' => $botUserId,
    'post_id' => $postId,
    'emoji_name' => 'eyes',
]);

Mattermost::files()->uploadFile($channelId, $fileResource);
```

Available resources via the `Mattermost` facade: `posts()`, `channels()`, `users()`, `teams()`, `files()`, `reactions()`, `bots()`, `webhooks()`, `commands()`, `emoji()`, `status()`, `system()`.

Sending messages
----------------

[](#sending-messages)

The fluent `Message` builder produces a payload for `POST /api/v4/posts` without making any HTTP call — pass the result to `posts()->createPost()`.

```
use ConduitUI\Mattermost\Messages\Message;
use ConduitUI\Mattermost\Messages\Attachment;

$payload = Message::make('Build complete')
    ->to($channelId)
    ->thread($parentPostId)
    ->attachment(
        Attachment::make()
            ->color('#36a64f')
            ->title('main', 'https://github.com/...')
            ->text('All checks passed in 2m31s.')
            ->field('Branch', 'feat/foo', short: true)
            ->field('Commit', 'abc1234', short: true)
    )
    ->toArray();

Mattermost::posts()->createPost($payload);
```

Bot framework
-------------

[](#bot-framework)

Mattermost WebSocket events route through a global + per-handler middleware pipeline to handler classes resolved from the container.

```
use App\Mattermost\HandleNewPost;
use ConduitUI\Mattermost\Bot\Events\PostCreated;
use ConduitUI\Mattermost\Facades\Mattermost;

// Class-based handler
Mattermost::on(PostCreated::class, HandleNewPost::class);

// By Mattermost event name
Mattermost::on('posted', HandleNewPost::class);

// Wildcard — every event
Mattermost::on('*', LogAllEvents::class);
```

### Handler classes

[](#handler-classes)

```
use ConduitUI\Mattermost\Bot\Attributes\Middleware;
use ConduitUI\Mattermost\Bot\Events\PostCreated;
use ConduitUI\Mattermost\Bot\Handler;
use ConduitUI\Mattermost\Bot\Middleware\IgnoreBots;
use ConduitUI\Mattermost\Bot\Middleware\RateLimit;

#[Middleware(IgnoreBots::class, RateLimit::class)]
class HandleMentions extends Handler
{
    public function handle(PostCreated $event): void
    {
        $this->reply($event, 'on it');
        $this->react($event, 'eyes');
        $this->typing($event);
    }
}
```

`reply()`, `react()`, and `typing()` come from the base `Handler` class — they pick up the correct connection, bot user id, and threading rules automatically.

### Middleware

[](#middleware)

Built-in middleware ships under `ConduitUI\Mattermost\Bot\Middleware`:

MiddlewareWhat it does`IgnoreBots`Drops events authored by any configured bot user`Dedup`Cache-locks `channel:post` so duplicate event redeliveries are dropped`RateLimit`Per-channel cooldown between accepted posts. DMs exempt`ChannelFilter`Allowlist by channel id or name`AdminOnly`Requires the post author to be a system/team/channel adminCustom middleware implements the `Middleware` interface:

```
use ConduitUI\Mattermost\Bot\Events\Event;
use ConduitUI\Mattermost\Bot\Middleware\Middleware;

class StripPrefix implements Middleware
{
    public function handle(Event $event, \Closure $next): mixed
    {
        if ($event instanceof PostCreated) {
            $event->message = ltrim($event->message, '!');
        }

        return $next($event);
    }
}
```

### Guards

[](#guards)

Permission-based middleware under `ConduitUI\Mattermost\Bot\Middleware\Guards`:

```
use ConduitUI\Mattermost\Bot\Attributes\Middleware;
use ConduitUI\Mattermost\Bot\Middleware\Guards\ChannelMember;
use ConduitUI\Mattermost\Bot\Middleware\Guards\RequiresPermission;
use ConduitUI\Mattermost\Bot\Middleware\Guards\RequiresRole;

#[Middleware(RequiresRole::class)]
class DeployHandler extends Handler
{
    public static array $roles = ['system_admin'];

    public function handle(PostCreated $event): void { /* ... */ }
}
```

### Queueable handlers

[](#queueable-handlers)

Handlers implementing `ShouldQueue` are dispatched onto Laravel's queue rather than running inline:

```
class HandleHeavyJob extends Handler implements \Illuminate\Contracts\Queue\ShouldQueue
{
    public function handle(PostCreated $event): void { /* ... */ }
}
```

Configure the queue/connection per `config/mattermost.php`:

```
'bot' => [
    'queue' => 'mattermost',
    'queue_connection' => 'redis',
],
```

Slash commands
--------------

[](#slash-commands)

Register slash commands with the same route-style API as event handlers:

```
use App\Mattermost\DeployHandler;
use ConduitUI\Mattermost\Facades\Mattermost;
use ConduitUI\Mattermost\SlashCommands\SlashCommand;
use ConduitUI\Mattermost\SlashCommands\SlashCommandResponse;

Mattermost::slash('deploy', DeployHandler::class);

Mattermost::slash('status', fn (SlashCommand $cmd) =>
    SlashCommandResponse::make()->inChannel('All systems go')
);
```

Mattermost POSTs slash command webhooks to `/mattermost/slash/{command}`. The route is auto-registered when `enable_slash_commands` is true in config.

Inside a handler:

```
class DeployHandler
{
    public function __invoke(SlashCommand $cmd): SlashCommandResponse
    {
        return SlashCommandResponse::make()
            ->ephemeral("Deploying {$cmd->arg(0)}...");
    }
}
```

Interactive messages
--------------------

[](#interactive-messages)

Buttons and menus on Mattermost messages POST to `/mattermost/interactive` when clicked. Register handlers for action ids:

```
use ConduitUI\Mattermost\Facades\Mattermost;
use ConduitUI\Mattermost\Interactive\InteractiveAction;
use ConduitUI\Mattermost\Interactive\InteractiveActionResponse;

Mattermost::interactive('approve', fn (InteractiveAction $a) =>
    InteractiveActionResponse::make()->update('Approved!')
);
```

The button itself is built into your post's `attachments`:

```
Mattermost::posts()->createPost([
    'channel_id' => $channelId,
    'message' => 'Ready to deploy?',
    'props' => ['attachments' => [[
        'text' => 'Choose:',
        'actions' => [
            [
                'id' => 'deploy-approve',
                'name' => 'Approve',
                'type' => 'button',
                'integration' => [
                    'url' => url('/mattermost/interactive'),
                    'context' => ['action' => 'approve', 'env' => 'prod'],
                ],
            ],
        ],
    ]]],
]);
```

The `context` you set on the button is what the DTO surfaces as `$action->context()`, and `context.action` is what `actionId()` returns.

Streaming replies
-----------------

[](#streaming-replies)

For bots replying with LLM tokens, the streaming layer creates a placeholder, edits it as chunks arrive, and finalizes:

```
use ConduitUI\Mattermost\Streaming\MattermostStreamingReply;

$reply = MattermostStreamingReply::create($channelId, rootId: $event->postId);

foreach ($llm->stream($prompt) as $chunk) {
    $reply->append($chunk);
}

$reply->finalize();
```

Behind the scenes: `posts()->createPost()` for the placeholder, batched `updatePost()`calls debounced to respect rate limits, and a final patch with the full text.

Notifications
-------------

[](#notifications)

Use Mattermost as a Laravel notification channel:

```
use ConduitUI\Mattermost\Notifications\MattermostMessage;

class DeployFinished extends Notification
{
    public function via(mixed $notifiable): array
    {
        return ['mattermost'];
    }

    public function toMattermost(mixed $notifiable): MattermostMessage
    {
        return MattermostMessage::make("Deploy of `{$this->ref}` finished")
            ->color('#36a64f')
            ->field('Duration', '2m31s', short: true);
    }
}
```

Route via `routeNotificationFor('mattermost', ...)` on the notifiable, or pass `['channel' => 'channel-id', 'connection' => 'staging']` to target a specific server.

A broadcast driver is also registered — set `BROADCAST_CONNECTION=mattermost` and `broadcast()->channel('mattermost:channel-id')` posts to Mattermost.

Filament panel
--------------

[](#filament-panel)

Auto-discovered Filament pages give you a status dashboard, message log, and health check inside your existing Filament admin:

```
// In your PanelProvider
use ConduitUI\Mattermost\Filament\MattermostPlugin;

return $panel->plugin(MattermostPlugin::make());
```

Pages: **Dashboard** (connection + recent activity), **Health** (per-connection ping + latency), **Message Log** (recent posts).

Artisan commands
----------------

[](#artisan-commands)

```
# Start the bot — connects WebSocket, dispatches events through the router
php artisan mattermost:listen

# One-off post from CLI
php artisan mattermost:post town-square "Deploy underway"

# Connectivity / auth check
php artisan mattermost:health
```

`mattermost:listen` handles SIGTERM/SIGINT cleanly; running it under supervisord or systemd is the recommended path.

Testing
-------

[](#testing)

`Mattermost::fake()` swaps the underlying client for an in-memory recorder. No HTTP, no network — every API call is captured for assertions.

```
use ConduitUI\Mattermost\Facades\Mattermost;

beforeEach(fn () => Mattermost::fake());

it('posts an announcement when deploy finishes', function () {
    DeployFinished::dispatch($build);

    Mattermost::assertPosted(fn ($payload) =>
        $payload['channel_id'] === 'town-square'
        && str_contains($payload['message'], 'Deploy of')
    );
    Mattermost::assertPostCount(1);
});
```

Available assertions: `assertSent`, `assertNotSent`, `assertNothingSent`, `assertPosted`, `assertNotPosted`, `assertNothingPosted`, `assertPostCount`, `assertUpdated`, `assertPatched`, `assertDeleted`, `assertReacted`, `assertNotReacted`, `assertFileUploaded`, `preventStrayPosts`, `recorded`.

Fixture helpers under `MattermostFixtures` build realistic payloads for tests:

```
use ConduitUI\Mattermost\Testing\MattermostFixtures;

$post = MattermostFixtures::post(['message' => 'hello']);
$wsEvent = MattermostFixtures::websocketEvent('posted', ['post' => $post]);
$buttonClick = MattermostFixtures::fakeButtonClick('approve', context: ['env' => 'prod']);
```

Multi-server
------------

[](#multi-server)

Define multiple connections in `config/mattermost.php`:

```
'connections' => [
    'default' => [...],
    'staging' => [
        'url' => env('MATTERMOST_STAGING_URL'),
        'token' => env('MATTERMOST_STAGING_TOKEN'),
    ],
],
```

Pick the connection per call:

```
Mattermost::connection('staging')->posts()->createPost([...]);
```

Local development
-----------------

[](#local-development)

A `docker-compose.yml` ships a local Mattermost for development:

```
docker compose up -d         # start
docker compose down          # stop
```

After the first boot, create an admin via `mmctl` or the web UI at `localhost:8065`, generate a bot token, and drop it into your `.env`.

The integration test suite runs against the live container:

```
vendor/bin/pest --testsuite=Integration
```

License
-------

[](#license)

MIT

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance91

Actively maintained with recent releases

Popularity1

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

44d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/a6bb27de88a541a632427686306c8fc56366d72582f6a3316d20500efe7971f3?d=identicon)[conduit-ui](/maintainers/conduit-ui)

---

Top Contributors

[![jordanpartridge](https://avatars.githubusercontent.com/u/9040417?v=4)](https://github.com/jordanpartridge "jordanpartridge (20 commits)")

---

Tags

bot-frameworklaravelmattermostphpsaloonslash-commandswebsocketlaravelwebsocketsaloonbotchatMattermost

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/conduit-ui-mattermost/health.svg)

```
[![Health](https://phpackages.com/badges/conduit-ui-mattermost/health.svg)](https://phpackages.com/packages/conduit-ui-mattermost)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[laravel/horizon

Dashboard and code-driven configuration for Laravel queues.

4.1k91.3M277](/packages/laravel-horizon)[illuminate/database

The Illuminate Database package.

3.0k54.1M11.0k](/packages/illuminate-database)[laravel/ai

The official AI SDK for Laravel.

9782.1M153](/packages/laravel-ai)[moonshine/moonshine

Laravel administration panel

1.3k239.9k72](/packages/moonshine-moonshine)[mateusjunges/laravel-kafka

A kafka driver for laravel

7243.4M20](/packages/mateusjunges-laravel-kafka)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
