Search by

agencelex / notifications-microsoft-teams

agencelex

A Microsoft Teams notification channel provider for the Laravel-style notification system (agencelex/notifications). Enables sending real-time alerts, structured data, and rich adaptive cards directly to Microsoft Teams channels from any notifiable object.

Package info

github.com/agencelex/typo3-notifications-microsoft-teams

Type:typo3-cms-extension

pkg:composer/agencelex/notifications-microsoft-teams

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-10-05 16:34 UTC

This package is auto-updated.

Last update: 2026-10-16 02:13:52 UTC


README

Microsoft Teams channel provider for lex_notifications. Send rich Adaptive Cards to any Teams channel from any TYPO3 extension, scheduler task, middleware, or domain service — using the same Laravel-inspired notification API you already know.

Table of Contents

Requirements

Requirement Version
PHP ^8.2
TYPO3 ^13.4 || ^14.3
agencelex/notifications ^1.3

Installation

composer require agencelex/notifications-microsoft-teams

Activate the extension in the TYPO3 Extension Manager or via composer (Composer-mode installations activate it automatically).

Setting Up a Teams Webhook

This extension sends messages via the Teams Workflows webhook (the modern replacement for the deprecated Office 365 Connectors).

  1. Open the Teams channel you want to post to.
  2. Click … → Workflows.
  3. Search for "Post to a channel when a webhook request is received" and select it.
  4. Follow the wizard — give it a name and click Next.
  5. Copy the generated webhook URL (it looks like https://prod.webhook.office.com/webhookb2/…).

Official guide: Create an Incoming Webhook — Microsoft Learn

Quick Start

use Lex\Notifications\Notification;
use Lex\Notifications\MicrosoftTeams\Channel\TeamsChannel;
use Lex\Notifications\MicrosoftTeams\Domain\Model\Ability\CanSendToTeams;
use Lex\Notifications\MicrosoftTeams\Message\AdaptiveCard;
use Lex\Notifications\MicrosoftTeams\Message\Action\OpenUrl;
use Lex\Notifications\MicrosoftTeams\Message\Element\TextBlock;
use Lex\Notifications\MicrosoftTeams\Message\TeamsMessage;

class HelloTeamsNotification extends Notification
{
    use CanSendToTeams;

    public function via(object $notifiable): array
    {
        return ['teams'];
    }

    public function toTeams(object $notifiable): TeamsMessage
    {
        return TeamsMessage::create()
            ->card(
                AdaptiveCard::make()
                    ->body([
                        TextBlock::make('Hello from TYPO3!')->weight('Bolder')->size('Large'),
                        TextBlock::make('Your notification system is working.')->wrap(true),
                    ])
                    ->actions([
                        OpenUrl::make('Visit site', 'https://example.com'),
                    ])
            );
    }
}

Send it from anywhere:

// 1. Inject NotificationDispatcherInterface via constructor or get it from GeneralUtility::makeInstance()
// 2. Use one of the multiple ways to send the notification
$this->notificationDispatcher->send($frontendUser, new HelloTeamsNotification());
// Even easier: use the on-demand notification feature
$this->notificationDispatcher->route('teams', $webhookUrl)->notify(new HelloTeamsNotification());

Core Concepts

This extension provides two independent traits. Each addresses a different side of the notification contract.

CanSendToTeams — on the Notification class

Lex\Notifications\MicrosoftTeams\Domain\Model\Ability\CanSendToTeams

Add this trait to any class that extends Lex\Notifications\Notification to declare that it can be sent to Teams. The trait enforces a single contract: the class must implement toTeams(object $notifiable): TeamsMessage.

use Lex\Notifications\Notification;
use Lex\Notifications\MicrosoftTeams\Domain\Model\Ability\CanSendToTeams;
use Lex\Notifications\MicrosoftTeams\Message\TeamsMessage;

class MyNotification extends Notification
{
    use CanSendToTeams;

    // Enforced by the trait
    public function toTeams(object $notifiable): TeamsMessage
    {
        return TeamsMessage::create()->card(AdaptiveCard::make()->body([...]));
    }
}

TeamsChannel checks for the presence of toTeams() at runtime and throws an \InvalidArgumentException (code 1789350405) if the notification was dispatched to the teams channel without implementing it.

HasRouteNotificationForTeams — on the notifiable model

Lex\Notifications\MicrosoftTeams\Domain\Model\Ability\HasRouteNotificationForTeams

Add this trait to the notifiable model (e.g. a FrontendUser, a BackendUser, or any domain entity using the Notifiable trait) to declare that it can provide a Teams webhook URL. The trait enforces that the class implements routeNotificationForTeams(): string.

use Lex\Notifications\Notification;
use Lex\Notifications\Domain\Model\Ability\Notifiable;
use Lex\Notifications\MicrosoftTeams\Domain\Model\Ability\HasRouteNotificationForTeams;

class MyFrontendUser extends AbstractEntity
{
    use Notifiable;
    use HasRouteNotificationForTeams;

    public function routeNotificationForTeams(?Notification $notification = null): ?string
    {
        // Implement the logic to get the webhook URL.
        // You can also get it from the notification object if it exposes it.
        return $settings['teams_webhook_url'] ?? null;
    }
}

Building Adaptive Cards

Adaptive Cards are JSON documents that Teams renders natively. Use the fluent PHP builders below — no JSON authoring required.

Interactive designer: Adaptive Cards Designer Full schema reference: Adaptive Cards Schema Explorer Teams-specific guidance: Adaptive Cards in Teams — Microsoft Learn

AdaptiveCard

The root container for all card content.

AdaptiveCard::make()
    ->version('1.6')                         // optional, default 1.6
    ->body([...elements...])                 // set all body elements at once
    ->add(TextBlock::make('More text'))      // append individual elements
    ->actions([...actions...])               // card-level action buttons
    ->addAction(OpenUrl::make('...', '…'))   // append individual actions
    ->minHeight('200px')                     // optional minimum card height
    ->verticalContentAlignment('Center')     // Top | Center | Bottom
    ->backgroundImage('https://…/bg.png')   // optional background image URL
    ->rtl(true)                              // right-to-left text
    ->speak('Screen reader text');           // accessibility narration

TextBlock

Displays plain or formatted text. wrap(true) is enabled by default.

TextBlock::make('Hello, world!')
    ->size('Large')                 // Default | Small | Medium | Large | ExtraLarge
    ->weight('Bolder')              // Default | Lighter | Bolder
    ->color('Accent')               // Default | Dark | Light | Accent | Good | Warning | Attention
    ->horizontalAlignment('Center') // Left | Center | Right
    ->fontType('Monospace')         // Default | Monospace
    ->isSubtle(true)                // reduced emphasis
    ->wrap(false)                   // disable wrapping
    ->maxLines(3)                   // truncate after N lines
    ->style('heading')              // default | heading | columnHeader
    ->spacing('Medium')             // None | Small | Default | Medium | Large | ExtraLarge | Padding
    ->separator(true);              // draw a line above this element

FactSet & Fact

Renders a two-column key/value table — ideal for structured metadata.

FactSet::make([
    Fact::make('Customer', 'Jane Doe'),
    Fact::make('Order #',  '98765'),
    Fact::make('Total',    '€149.00'),
    Fact::make('Status',   'Paid'),
])->spacing('Medium');

Image & ImageSet

// Single image
Image::make('https://example.com/logo.png')
    ->altText('Company logo')
    ->size('Medium')                // Auto | Stretch | Small | Medium | Large
    ->style('Person')               // Default | Person (renders as a circle avatar)
    ->horizontalAlignment('Center')
    ->backgroundColor('#f0f0f0')    // CSS colour for transparent PNGs
    ->width('80px')                 // explicit pixel width
    ->selectAction(OpenUrl::make('Visit', 'https://example.com'));

// Collection of images
ImageSet::make([
    Image::make('https://example.com/a.png'),
    Image::make('https://example.com/b.png'),
    Image::make('https://example.com/c.png'),
])->size('Small');

Container

Groups elements with shared styling or a background.

Container::make([
    TextBlock::make('Section Title')->weight('Bolder'),
    TextBlock::make('Body text goes here.')->wrap(true),
    FactSet::make([Fact::make('Key', 'Value')]),
])
->style('emphasis')                 // default | emphasis | good | attention | warning | accent
->bleed(true)                       // extend to cover surrounding padding
->minHeight('100px')
->verticalContentAlignment('Top')   // Top | Center | Bottom
->selectAction(OpenUrl::make('Open', 'https://example.com'));

ColumnSet & Column

Divides content into side-by-side columns.

ColumnSet::make([
    Column::make([
        Image::make('https://example.com/avatar.png')
            ->style('Person')
            ->size('Small'),
    ])->width('auto'),

    Column::make([
        TextBlock::make('Jane Doe')->weight('Bolder'),
        TextBlock::make('Engineering Lead')->isSubtle(true)->spacing('None'),
    ])->width('stretch'),
])->spacing('Medium');

Column width accepts:

  • "auto" — shrinks to fit content
  • "stretch" — fills remaining space
  • "50px" — explicit pixel width
  • "1", "2", … — proportional weight

RichTextBlock

Inline-styled text in a single paragraph. Mix weights, colours, and sizes without separate TextBlock elements.

RichTextBlock::make([
    ['type' => 'TextRun', 'text' => 'Deploy status: '],
    ['type' => 'TextRun', 'text' => 'SUCCESS', 'color' => 'Good', 'weight' => 'Bolder'],
])
->addInline(['type' => 'TextRun', 'text' => ' — no issues found.', 'isSubtle' => true]);

ActionSet

Embeds action buttons at a specific position in the body (rather than always at the bottom).

ActionSet::make([
    OpenUrl::make('View in browser', 'https://example.com'),
    Submit::make('Acknowledge', ['action' => 'ack']),
]);

Actions

All actions accept common properties: ->id(), ->style(), ->iconUrl(), ->tooltip(), ->isEnabled(), ->mode().

Action\OpenUrl — open a URL

use Lex\Notifications\MicrosoftTeams\Message\Action\OpenUrl;

OpenUrl::make('View Order', 'https://example.com/orders/123')
    ->style('positive')   // default | positive | destructive
    ->iconUrl('https://example.com/icons/view.png')
    ->tooltip('Opens the order detail page');

Action\Submit — submit form data

use Lex\Notifications\MicrosoftTeams\Message\Action\Submit;

Submit::make('Approve', ['action' => 'approve', 'orderId' => 123])
    ->style('positive')
    ->associatedInputs('Auto'); // Auto | None

Action\ShowCard — reveal an inline card

use Lex\Notifications\MicrosoftTeams\Message\Action\ShowCard;

ShowCard::make('Show Details',
    AdaptiveCard::make()->body([
        TextBlock::make('Here are the full details…')->wrap(true),
    ])
);

Action\ToggleVisibility — show/hide elements

use Lex\Notifications\MicrosoftTeams\Message\Action\ToggleVisibility;

// Toggle by element id
ToggleVisibility::make('Toggle Details', ['detailsContainer']);

// Force a specific state
ToggleVisibility::make('Show Details', [
    ['elementId' => 'detailsContainer', 'isVisible' => true],
    ['elementId' => 'summaryContainer',  'isVisible' => false],
]);

Real-World Examples

Order Confirmation

Notifies the fulfilment team when a new order is placed.

use Lex\Notifications\Notification;
use Lex\Notifications\MicrosoftTeams\Channel\TeamsChannel;
use Lex\Notifications\MicrosoftTeams\Domain\Model\Ability\CanSendToTeams;
use Lex\Notifications\MicrosoftTeams\Message\Action\OpenUrl;
use Lex\Notifications\MicrosoftTeams\Message\AdaptiveCard;
use Lex\Notifications\MicrosoftTeams\Message\Element\Fact;
use Lex\Notifications\MicrosoftTeams\Message\Element\FactSet;
use Lex\Notifications\MicrosoftTeams\Message\Element\TextBlock;
use Lex\Notifications\MicrosoftTeams\Message\TeamsMessage;

class OrderPlacedNotification extends Notification
{
    use CanSendToTeams;

    public function __construct(private readonly Order $order) {}

    public function via(object $notifiable): array
    {
        return ['teams'];
    }

    public function toTeams(object $notifiable): TeamsMessage
    {
        return TeamsMessage::create()
            ->card(
                AdaptiveCard::make()
                    ->body([
                        TextBlock::make('New Order Received')
                            ->size('Large')
                            ->weight('Bolder')
                            ->color('Accent'),
                        TextBlock::make('Order #' . $this->order->getNumber())
                            ->isSubtle(true)
                            ->spacing('None'),
                        FactSet::make([
                            Fact::make('Customer', $this->order->getCustomerName()),
                            Fact::make('Items',    (string) $this->order->getItemCount()),
                            Fact::make('Total',    $this->order->getFormattedTotal()),
                            Fact::make('Payment',  $this->order->getPaymentMethod()),
                            Fact::make('Shipping', $this->order->getShippingMethod()),
                        ])->spacing('Medium'),
                    ])
                    ->actions([
                        OpenUrl::make('View Order', $this->order->getBackendUrl())
                            ->style('positive'),
                        OpenUrl::make('View Customer', $this->order->getCustomerUrl()),
                    ])
            );
    }
}

Keep it simple by using the on-demand feature:

$this->notificationDispatcher->route('teams', $this->extensionConfiguration['notifications']['channels']['teams']['fulfilment'] ?? null)
    ->notify(new OrderPlacedNotification($order));

Server Alert

Sends a critical alert with severity colour coding when a monitor threshold is exceeded.

use Lex\Notifications\Notification;
use Lex\Notifications\MicrosoftTeams\Channel\TeamsChannel;
use Lex\Notifications\MicrosoftTeams\Domain\Model\Ability\CanSendToTeams;
use Lex\Notifications\MicrosoftTeams\Message\Action\OpenUrl;
use Lex\Notifications\MicrosoftTeams\Message\AdaptiveCard;
use Lex\Notifications\MicrosoftTeams\Message\Element\Fact;
use Lex\Notifications\MicrosoftTeams\Message\Element\FactSet;
use Lex\Notifications\MicrosoftTeams\Message\Element\TextBlock;
use Lex\Notifications\MicrosoftTeams\Message\TeamsMessage;

class ServerAlertNotification extends Notification
{
    use CanSendToTeams;

    public function __construct(
        private readonly string $serverName,
        private readonly string $metric,
        private readonly string $value,
        private readonly string $threshold,
        private readonly string $dashboardUrl,
    ) {}

    public function via(object $notifiable): array
    {
        return ['teams'];
    }

    public function toTeams(object $notifiable): TeamsMessage
    {
        return TeamsMessage::create()
            ->card(
                AdaptiveCard::make()
                    ->body([
                        TextBlock::make('CRITICAL — ' . $this->serverName)
                            ->size('Large')
                            ->weight('Bolder')
                            ->color('Attention'),
                        TextBlock::make('Threshold exceeded. Immediate attention required.')
                            ->wrap(true)
                            ->isSubtle(true),
                        FactSet::make([
                            Fact::make('Metric',    $this->metric),
                            Fact::make('Value',     $this->value),
                            Fact::make('Threshold', $this->threshold),
                            Fact::make('Time',      (new \DateTimeImmutable())->format('Y-m-d H:i:s T')),
                        ])->spacing('Medium'),
                    ])
                    ->actions([
                        OpenUrl::make('Open Dashboard', $this->dashboardUrl)
                            ->style('destructive'),
                    ])
            );
    }
}
// Dispatch
$this->notificationDispatcher->route('teams', $criticalAlertWebhookUrl)
    ->notifyNow(new ServerAlertNotification(
        serverName:   'prod-web-01',
        metric:       'CPU Usage',
        value:        '98%',
        threshold:    '85%',
        dashboardUrl: 'https://grafana.internal/d/servers',
    ));

New User Registration with Avatar

Posts a profile card with a person-style avatar when a new frontend user registers.

use Lex\Notifications\Notification;
use Lex\Notifications\MicrosoftTeams\Domain\Model\Ability\CanSendToTeams;
use Lex\Notifications\MicrosoftTeams\Message\Action\OpenUrl;
use Lex\Notifications\MicrosoftTeams\Message\AdaptiveCard;
use Lex\Notifications\MicrosoftTeams\Message\Element\Column;
use Lex\Notifications\MicrosoftTeams\Message\Element\ColumnSet;
use Lex\Notifications\MicrosoftTeams\Message\Element\Fact;
use Lex\Notifications\MicrosoftTeams\Message\Element\FactSet;
use Lex\Notifications\MicrosoftTeams\Message\Element\Image;
use Lex\Notifications\MicrosoftTeams\Message\Element\TextBlock;
use Lex\Notifications\MicrosoftTeams\Message\TeamsMessage;

class UserRegisteredNotification extends Notification
{
    use CanSendToTeams;

    public function __construct(private readonly FrontendUser $user) {}

    public function via(object $notifiable): array
    {
        return ['teams'];
    }

    public function toTeams(object $notifiable): TeamsMessage
    {
        $avatarUrl = sprintf(
            'https://ui-avatars.com/api/?name=%s&size=128&background=0D3880&color=fff',
            urlencode($this->user->getName()),
        );

        return TeamsMessage::create()
            ->card(
                AdaptiveCard::make()
                    ->body([
                        TextBlock::make('New User Registered')
                            ->size('Large')
                            ->weight('Bolder'),
                        ColumnSet::make([
                            Column::make([
                                Image::make($avatarUrl)
                                    ->style('Person')
                                    ->size('Small'),
                            ])->width('auto'),
                            Column::make([
                                TextBlock::make($this->user->getName())
                                    ->weight('Bolder'),
                                TextBlock::make($this->user->getEmail())
                                    ->isSubtle(true)
                                    ->spacing('None'),
                            ])->width('stretch'),
                        ])->spacing('Medium'),
                        FactSet::make([
                            Fact::make('Username',   $this->user->getUsername()),
                            Fact::make('Registered', (new \DateTimeImmutable())->format('d/m/Y H:i')),
                            Fact::make('User group', $this->user->getUsergroup()->first()?->getTitle() ?? '—'),
                        ]),
                    ])
                    ->actions([
                        OpenUrl::make('Edit user', $this->user->getBackendEditUrl()),
                    ])
            );
    }
}

Content Approval Request

Sent to the editorial team when an editor submits a page for review. Uses ToggleVisibility to reveal the full editorial note on demand.

use Lex\Notifications\Notification;
use Lex\Notifications\MicrosoftTeams\Domain\Model\Ability\CanSendToTeams;
use Lex\Notifications\MicrosoftTeams\Message\Action\OpenUrl;
use Lex\Notifications\MicrosoftTeams\Message\Action\ToggleVisibility;
use Lex\Notifications\MicrosoftTeams\Message\AdaptiveCard;
use Lex\Notifications\MicrosoftTeams\Message\Element\Container;
use Lex\Notifications\MicrosoftTeams\Message\Element\Fact;
use Lex\Notifications\MicrosoftTeams\Message\Element\FactSet;
use Lex\Notifications\MicrosoftTeams\Message\Element\TextBlock;
use Lex\Notifications\MicrosoftTeams\Message\TeamsMessage;

class ContentApprovalRequestNotification extends Notification
{
    use CanSendToTeams;

    public function __construct(
        private readonly PageRecord $page,
        private readonly BackendUser $submitter,
        private readonly string $note,
    ) {}

    public function via(object $notifiable): array
    {
        return ['teams'];
    }

    public function toTeams(object $notifiable): TeamsMessage
    {
        return TeamsMessage::create()
            ->card(
                AdaptiveCard::make()
                    ->body([
                        TextBlock::make('Content Approval Required')
                            ->size('Large')
                            ->weight('Bolder'),
                        FactSet::make([
                            Fact::make('Page',         $this->page->getTitle()),
                            Fact::make('Submitted by', $this->submitter->getRealName()),
                            Fact::make('URL',          $this->page->getSlug()),
                        ])->spacing('Medium'),
                        Container::make([
                            TextBlock::make('Editorial Note')->weight('Bolder'),
                            TextBlock::make($this->note)->wrap(true)->isSubtle(true),
                        ])
                        ->id('editorialNote')
                        ->isVisible(false)
                        ->style('emphasis')
                        ->spacing('Medium'),
                    ])
                    ->actions([
                        OpenUrl::make('Preview page', $this->page->getFrontendPreviewUrl()),
                        OpenUrl::make('Open in backend', $this->page->getBackendUrl()),
                        ToggleVisibility::make('Show editorial note', ['editorialNote']),
                    ])
            );
    }
}

Multi-Channel Notification (Teams + Mail)

Both traits can be composed freely. Include teams alongside any other channel in via().

use Lex\Notifications\Notification;
use Lex\Notifications\NotificationChannel;
use Lex\Notifications\MicrosoftTeams\Domain\Model\Ability\CanSendToTeams;
use Lex\Notifications\MicrosoftTeams\Message\AdaptiveCard;
use Lex\Notifications\MicrosoftTeams\Message\Element\TextBlock;
use Lex\Notifications\MicrosoftTeams\Message\TeamsMessage;
use TYPO3\CMS\Core\Mail\MailMessage;

class DeploymentFinishedNotification extends Notification
{
    use CanSendToTeams;

    public function __construct(
        private readonly string $environment,
        private readonly string $version,
        private readonly bool $success,
    ) {}

    public function via(object $notifiable): array
    {
        return ['teams', NotificationChannel::CHANNEL_MAIL];
    }

    public function toTeams(object $notifiable): TeamsMessage
    {
        $status = $this->success ? 'SUCCESS' : 'FAILED';
        $color  = $this->success ? 'Good'    : 'Attention';

        return TeamsMessage::create()
            ->card(
                AdaptiveCard::make()
                    ->body([
                        TextBlock::make("Deploy {$status} — {$this->environment}")
                            ->size('Large')
                            ->weight('Bolder')
                            ->color($color),
                        TextBlock::make("Version {$this->version} deployed to {$this->environment}.")
                            ->wrap(true),
                    ])
            );
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage())
            ->subject("Deploy {$this->environment}: " . ($this->success ? 'SUCCESS' : 'FAILED'))
            ->text("Version {$this->version} was deployed to {$this->environment}.");
    }
}

Dispatch

$this->notificationDispatcher->routes([
    'teams' => $devWebhookUrl,
    NotificationChannel::CHANNEL_MAIL => 'dev@example.com'
])
    ->notifyNow(new ServerAlertNotification(
        ...
    ));

References

Resource Link
Set up a Teams Incoming Webhook Microsoft Learn — Incoming Webhooks
Adaptive Card schema explorer adaptivecards.io/explorer
Adaptive Cards interactive designer adaptivecards.io/designer
Adaptive Cards in Microsoft Teams Microsoft Learn — Cards reference
Teams Workflows (Power Automate) Microsoft Learn — Power Automate & Teams
Message card object reference Microsoft Learn — Message card reference
lex_notifications (core) GitHub — agencelex/typo3-notifications