Notifier
Symfony Notifier for alerts from OPC UA events — Slack, email, SMS, Discord. Channel policy, severity routing, on-call rotation.
Symfony Notifier turns plant-floor events into messages across channels — Slack, mail, SMS, Discord, MS Teams, database. The OPC UA bundle's events make this a small bridge.
Install
composer require symfony/notifier
# Channel-specific transports
composer require symfony/slack-notifier
composer require symfony/twilio-notifier
composer require symfony/discord-notifier
Configure transports
framework:
notifier:
chatter_transports:
slack: '%env(SLACK_DSN)%'
discord: '%env(DISCORD_DSN)%'
texter_transports:
twilio: '%env(TWILIO_DSN)%'
channel_policy:
# mapping importance → channels
urgent: ['email', 'sms', 'chat/slack']
high: ['email', 'chat/slack']
medium: ['email']
low: ['email']
admin_recipients:
- { email: [email protected], phone: '+15551234567' }
.env:
SLACK_DSN=slack://TOKEN@default?channel=ops-alerts
DISCORD_DSN=discord://TOKEN@CHANNEL_ID
TWILIO_DSN=twilio://ACCOUNT_SID:AUTH_TOKEN@default?from=PHONE
A Notification class
namespace App\Notification;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\Notification\ChatNotificationInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Notification\SmsNotificationInterface;
use Symfony\Component\Notifier\Recipient\RecipientInterface;
final class PlcAlarmNotification extends Notification implements
ChatNotificationInterface,
SmsNotificationInterface
{
public function __construct(
public readonly string $source,
public readonly int $severity,
public readonly string $message,
) {
parent::__construct("[PLC alarm] $source");
$this->content("$message (severity $severity)");
$this->importance($this->mapImportance());
}
private function mapImportance(): string
{
return match (true) {
$this->severity >= 900 => Notification::IMPORTANCE_URGENT,
$this->severity >= 700 => Notification::IMPORTANCE_HIGH,
$this->severity >= 400 => Notification::IMPORTANCE_MEDIUM,
default => Notification::IMPORTANCE_LOW,
};
}
public function asChatMessage(RecipientInterface $r, ?string $transport = null): ?ChatMessage
{
return new ChatMessage(
sprintf("⚠️ PLC alarm — %s (severity %d): %s", $this->source, $this->severity, $this->message),
);
}
public function asSmsMessage(RecipientInterface $r, ?string $transport = null): ?SmsMessage
{
return new SmsMessage(
$r->getPhone(),
sprintf("PLC sev=%d %s: %s", $this->severity, $this->source, $this->message),
);
}
}
The event listener
namespace App\EventListener;
use App\Notification\PlcAlarmNotification;
use PhpOpcua\Client\Event\AlarmActivated;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\Recipient;
final class RouteAlarmToOps
{
public function __construct(private NotifierInterface $notifier) {}
#[AsEventListener]
public function __invoke(AlarmActivated $event): void
{
if ($event->severity < 400) {
return;
}
$this->notifier->send(
new PlcAlarmNotification(
source: $event->sourceName,
severity: $event->severity,
message: $event->message,
),
new Recipient('[email protected]', '+15551234567'),
);
}
}
Notifier::send() consults the channel_policy and routes
based on importance. AlarmActivated carries
$sourceName / $severity / $message directly — they are
non-null public fields.
Per-recipient routing
For routing different alerts to different teams:
$this->notifier->send($notif, ...$this->recipientsForSource($event->sourceName));
// ...
private function recipientsForSource(string $source): array
{
return match (true) {
str_starts_with($source, 'Line-A') => [
new Recipient('[email protected]', '+15551111111'),
],
str_starts_with($source, 'Line-B') => [
new Recipient('[email protected]', '+15552222222'),
],
default => [
new Recipient('[email protected]', '+15551234567'),
],
};
}
For dynamic team rosters, look up in a Team Doctrine entity.
On-call rotation
For rotating on-call:
use App\Repository\OncallRosterRepository;
#[AsEventListener]
public function __invoke(AlarmActivated $event): void
{
if ($event->severity < 800) return;
$oncall = $this->oncallRepo->current();
foreach ($oncall as $person) {
$this->notifier->send(
new PlcAlarmNotification($event->sourceName, $event->severity, $event->message),
new Recipient($person->getEmail(), $person->getPhone()),
);
}
}
OncallRosterRepository::current() is your domain — a query
like WHERE start_at <= NOW() AND end_at >= NOW().
Connection-failure alerts
namespace App\EventListener;
use App\Notification\PlcConnectionLostNotification;
use PhpOpcua\Client\Event\ConnectionFailed;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\Recipient;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
final class NotifyConnectionLost
{
public function __construct(
private NotifierInterface $notifier,
private CacheInterface $cache,
) {}
#[AsEventListener]
public function __invoke(ConnectionFailed $event): void
{
// Throttle — one per endpoint per 10 minutes
$key = 'conn-fail.' . sha1($event->endpointUrl);
$blocked = $this->cache->get($key, function (ItemInterface $i) {
$i->expiresAfter(600);
return false;
});
if ($blocked) return;
$this->cache->delete($key);
$this->cache->get($key, function (ItemInterface $i) {
$i->expiresAfter(600);
return true;
});
$this->notifier->send(
new PlcConnectionLostNotification(
endpoint: $event->endpointUrl,
reason: $event->exception::class,
message: $event->exception->getMessage(),
),
new Recipient('[email protected]', '+15551234567'),
);
}
}
Throttling is essential — flapping connections fire many events per minute.
All-clear notification
There is no dedicated "reconnected" event in opcua-client. After
a recovery attempt (ClientReconnecting fires when it starts),
the next ClientConnected is the all-clear signal. Use it to
clear a previously-set "lost" flag:
namespace App\EventListener;
use App\Notification\PlcReconnectedNotification;
use PhpOpcua\Client\Event\ClientConnected;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\Recipient;
use Symfony\Contracts\Cache\CacheInterface;
final class NotifyReconnected
{
public function __construct(
private NotifierInterface $notifier,
private CacheInterface $cache,
) {}
#[AsEventListener]
public function __invoke(ClientConnected $event): void
{
$key = 'conn-fail.' . sha1($event->endpointUrl);
$hasOpenAlert = $this->cache->getItem($key)->isHit();
if ($hasOpenAlert) {
$this->cache->delete($key);
$this->notifier->send(
new PlcReconnectedNotification($event->endpointUrl),
new Recipient('[email protected]'),
);
}
}
}
Only fires "all clear" if we previously fired "lost".
Database channel
For an audit trail of all notifications:
framework:
notifier:
chatter_transports:
slack: '%env(SLACK_DSN)%'
# ...
channel_policy:
urgent: ['email', 'sms', 'chat/slack', 'database']
You'd add a database:// transport via a custom factory — see
the Symfony Notifier docs.
Where to read next
You've finished Integrations. Next: Reference · OpcuaManager API.