skrepr/teams-connector
Skrepr Teams Connector is a Laravel/PHP package for sending messages to Microsoft Teams through incoming webhooks. It provides a simple API and notification channel for building and dispatching Teams cards, letting you configure webhook URLs and compose payloads without boilerplate.
Install and publish the config
composer require skrepr/teams-connector
php artisan vendor:publish --tag=teams-connector-config
If the tag isn't documented in your version, publish by provider: --provider="Skrepr\TeamsConnector\TeamsConnectorServiceProvider". The package is small — read the service provider and config/teams-connector.php first; they tell you whether the connector is bound as a singleton, whether a facade alias exists, and whether the package expects one webhook URL or a map of named channels.
Create the webhook in Teams
Channel → … → Connectors → Incoming Webhook → name/icon → copy the URL. Store it in .env, never in the repo:
TEAMS_WEBHOOK_URL=https://outlook.office.com/webhook/xxxxxxxx/IncomingWebhook/yyyyyyyy
// config/teams-connector.php
return [
'webhook_url' => env('TEAMS_WEBHOOK_URL'),
];
First use case: a deploy/CI notification. Don't start with a customer-facing feature; start with a low-risk operational message so you can verify payload shape and channel routing:
use Skrepr\TeamsConnector\TeamsConnector;
app(TeamsConnector::class)->send('✅ Deploy to **production** finished — `'.substr($sha, 0, 7).'`');
The exact class/method names vary slightly by release (send() vs sendMessage(), facade vs container binding). The shape doesn't: resolve the connector, hand it either a string or a payload array/object, let it POST to the configured webhook. Confirm the real signature in src/ before standardizing on it.
Wrap the package behind your own notifier. This is the single highest-leverage move. Your app should depend on DeployNotifier/AlertNotifier, not on Skrepr\TeamsConnector. When the vendor package goes stale or Teams changes the webhook model, you change one class.
class TeamsNotifier
{
public function __construct(private TeamsConnector $teams) {}
public function deployFinished(string $env, string $sha): void
{
$this->teams->send("✅ Deploy to **{$env}** finished — `{$sha}`");
}
public function deployFailed(string $env, string $sha, string $url): void
{
$this->teams->send([
'@type' => 'MessageCard',
'@context' => 'https://schema.org/extensions',
'summary' => "Deploy failed on {$env}",
'themeColor' => 'D13438',
'title' => "Deploy failed on {$env}",
'sections' => [[
'facts' => [
['name' => 'Commit', 'value' => $sha],
['name' => 'Branch', 'value' => $env],
],
]],
'potentialAction' => [[
'@type' => 'OpenUri',
'name' => 'Open pipeline',
'targets' => [['os' => 'default', 'uri' => $url]],
]],
]);
}
}
Always send from a queued job in request paths. The connector does a synchronous HTTP call; a slow or down Teams endpoint must not slow down a checkout or an API response.
class SendTeamsMessage implements ShouldQueue
{
use Queueable, InteractsWithQueue, SerializesModels;
public int $tries = 3;
public array $backoff = [10, 60, 300];
public function __construct(public string|array $payload) {}
public function handle(TeamsNotifier $teams): void
{
// resolve the connector inside handle so a bad config doesn't
// blow up at dispatch time
}
}
Dispatch after commit (DB::afterCommit) for order/payment events so you never announce something that rolled back.
Add a Laravel Notification channel. The package gives you transport, not integration with Notifiable. A ~15-line channel is usually all you need:
class TeamsChannel
{
public function __construct(private TeamsConnector $teams) {}
public function send(object $notifiable, Notification $notification): void
{
$this->teams->send($notification->toTeams($notifiable));
}
}
class InvoicePaid extends Notification
{
public function via($notifiable): array { return [TeamsChannel::class]; }
public function toTeams($notifiable): string { return "Invoice {$this->invoice->id} paid"; }
}
Support multiple channels. Ops wants #alerts, sales wants #deals. Model it explicitly:
'webhooks' => [
'default' => env('TEAMS_WEBHOOK_URL'),
'alerts' => env('TEAMS_ALERTS_WEBHOOK_URL'),
],
then resolve a connector per channel in a small factory. Don't let #deals receive stack traces because someone forgot to override a config value.
Testing. Put the webhook host behind Http::fake() and assert the payload:
Http::fake(['outlook.office.com/*' => Http::response('1', 200)]);
app(TeamsNotifier::class)->deployFinished('production', 'abc1234');
Http::assertSent(fn ($request) => str_contains($request['text'], 'abc1234'));
If the package uses Guzzle directly rather than Laravel's HTTP client, Http::fake() won't intercept — use a Guzzle MockHandler instead. Check composer.json before writing the test.
1. Teams returns the literal string 1 on success. A package that only checks the status code can silently swallow "webhook disabled" responses. If you own the wrapper, treat non-200 or non-1 bodies as failures and log them.config:cache freezes env resolution. Changing TEAMS_WEBHOOK_URL requires php artisan config:cache again on deploy. This is the most common "it works locally but not in prod" cause.blank() check and no-op (or throw in non-production only) in your wrapper.text supports a small subset — bold, links, inline code, line breaks. HTML is not rendered. Escape user-supplied strings or your card will look broken.illuminate/support in composer.json; if it's too tight, fork it (it's tiny) rather than relying on --ignore-platform-reqs in production.#dev-notifications channel or set an enabled flag in config. Nothing is worse than a php artisan migrate:fresh seed announcing itself in #general.src/ and the service provider is cheaper than debugging a production incident caused by an assumption about method names, retries, or response parsing.How can I help you explore Laravel packages today?