gnello/php-mattermost-driver
A PHP client for the Mattermost API that makes it easy to send messages, manage channels and users, handle webhooks, and automate team chat workflows from Laravel or any PHP application.
The driver is framework-agnostic — there is no service provider, facade, or config file. In Laravel you wire it yourself once, as a singleton, and then inject it.
composer require gnello/php-mattermost-driver guzzlehttp/guzzle
Add credentials to config/services.php (note: the v3 base URL includes /api/v3 and no trailing slash):
'mattermost' => [
'base_url' => env('MATTERMOST_URL', 'https://chat.example.com') . '/api/v3',
'token' => env('MATTERMOST_TOKEN'),
'timeout' => env('MATTERMOST_TIMEOUT', 10),
],
Bind it in AppServiceProvider::register():
use Gnello\Mattermost\Driver;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
$this->app->singleton(Driver::class, function () {
$driver = new Driver(
new Client(['timeout' => config('services.mattermost.timeout')]),
new Response(),
['base_url' => config('services.mattermost.base_url')]
);
if ($token = config('services.mattermost.token')) {
$driver->setToken($token);
}
return $driver;
});
Where to look first: the Driver class getters (getUserModel(), getTeamModel(), getChannelModel(), getPostModel(), getFileModel(), getSystemModel()) and the matching model classes — those are the API surface. The vendor README is thin, so treat the model classes as the documentation.
First real use case: a deployment or exception notifier. Resolve the team and channel by name, then post:
$channel = $driver->getChannelModel()->getChannelByName($teamId, 'town-square');
$driver->getPostModel()->createPost([
'channel_id' => $channel->getId(),
'message' => ":rocket: Deploy `{$sha}` to production finished.",
]);
If you only ever need to send messages, stop here and use a Mattermost incoming webhook instead — a single Http::post() with no token, no session expiry, no stale SDK.
Wrap the driver, don't scatter it. The Driver is a low-level transport; your app should talk to one small service so you can swap it or fake it:
class MattermostNotifier
{
public function __construct(private Driver $driver) {}
public function toChannel(string $channelId, string $message, array $attachments = []): void
{
try {
$this->driver->getPostModel()->createPost([
'channel_id' => $channelId,
'message' => $message,
'props' => $attachments ? ['attachments' => $attachments] : [],
]);
} catch (RequestException $e) {
report($e); // never let chat notifications break the request
}
}
}
Send from a queued job. Mattermost round-trips are 100–800 ms and can 429. A ShouldQueue job with $tries = 3 and backoff() keeps your HTTP response fast and gives you retries for free.
Cache IDs, not objects. Team IDs, channel IDs and user IDs are stable; resolve them once and Cache::remember("mm:channel:{$teamId}:{$name}", 3600, ...). A notification path that does three API calls to send one message will fall over during an incident — exactly when you need it.
Inbound slash commands / outgoing webhooks fit naturally as a Laravel route. Mattermost posts application/x-www-form-urlencoded with a token; verify it against config with hash_equals, dispatch a job, and return a small JSON payload (response_type, text). Keep the handler under the 3-second response budget by queueing the real work.
File uploads go through the file model with a multipart body (channel_id + files), then you attach the returned file IDs to a post. Uploads are the one place where this driver saves meaningful work over raw Guzzle.
Testing: never hit a real server in the test suite. Bind a Driver backed by Guzzle's MockHandler in a test service provider, or bind a MattermostNotifier fake and assert on expects(). If you go the MockHandler route, keep a couple of real captured JSON fixtures — the v3 response shapes are their own trap.
Multiple workspaces: register named singletons (mattermost.ops, mattermost.support) rather than a single binding; the Driver holds mutable state (the token), so one instance cannot safely serve two servers.
curl https://your-server/api/v4/system/ping and check your server version. On a current server, expect 404s and plan on wrapping v4 REST calls yourself or forking the client.php and guzzlehttp/guzzle constraints are from the PHP 5.6/7.0 era. You may need --ignore-platform-reqs, an inline alias, or a fork. Pin the exact version and add a comment in composer.json explaining why.login), typically valid ~30 days and invalidated by logout or password change. Personal access tokens are a v4 feature, so with v3 you either persist and refresh the token from a bot account or accept intermittent 401s. Store the token in config/services.php sourced from env, never in the repo.GuzzleHttp\Exception\ClientException / ServerException. The body is JSON with id, message, status_code — log message and id, not the whole stack trace, or your logs fill with HTML from the 404-on-wrong-base-URL case./api/v3, a trailing slash, or http vs https are the top three causes of "it just doesn't work". Log the effective URL once at boot.Response instance in the constructor is a prototype. Passing new Response() is boilerplate; do not inspect it afterwards expecting the last call's data. Use the returned model objects.getUser(), getChannel() etc. return package model objects with getters (getId(), getUsername()); the underlying payload is an array. Don't try to persist them directly — map to your own DTOs.Retry-After; add a Guzzle middleware for retry/backoff rather than sprinkling try/catch around every call.props differ from what current Mattermost renders. Verify formatting against your server, and prefer plain Markdown for anything important.HandlerStack and Middleware::log() when constructing the Client. That is also your best debugging tool: dump request/response bodies in local env only.How can I help you explore Laravel packages today?