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

Teams Connector Laravel Package

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.

Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • Microsoft is retiring Office 365 connectors. Incoming webhooks created through the Teams "Connectors" UI are being deprecated in favor of Power Automate Workflows. If channel owners can no longer create a webhook, the URL may still work but new setup requires a Workflow HTTP trigger. Budget for this; the payload contract is close but not identical, and it affects the package's long-term fit.
  • Connector deprecation timing. A package last released in Feb 2023 predates most of this. Verify against your tenant rather than assuming the README is current.
  • Success is HTTP 200 with body 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.
  • Webhook URLs are bearer credentials. Anyone with the URL can post to the channel. Keep them in the secret store, rotate on leak, and scrub them from logs — Laravel's HTTP client can log full request URLs.
  • Rate limits and size. Roughly 4 messages/second per webhook and ~28 KB per payload. Batch digests instead of looping one message per row, especially in scheduled commands.
  • 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.
  • Guard against an empty webhook. If the env var is unset, you'll either throw inside the connector or attempt a POST to a blank URL. Add a blank() check and no-op (or throw in non-production only) in your wrapper.
  • Markdown is limited. MessageCard 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.
  • Laravel version compatibility. Last release was early 2023, so composer constraints likely stop before Laravel 11/12. Check illuminate/support in composer.json; if it's too tight, fork it (it's tiny) rather than relying on --ignore-platform-reqs in production.
  • Local development. Point local/staging at a dedicated #dev-notifications channel or set an enabled flag in config. Nothing is worse than a php artisan migrate:fresh seed announcing itself in #general.
  • Extension points. The useful ones are: published config (channel map, enabled flag, timeout), the message/payload object (custom card builders), and container binding (swap in your own transport for tests). If the package doesn't expose a timeout, wrap the call in your own HTTP client rather than waiting on the default.
  • Read the source before adopting. At ~22 stars, this is a small package with a narrow surface area. Spending ten minutes in src/ and the service provider is cheaper than debugging a production incident caused by an assumption about method names, retries, or response parsing.
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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route