Events
Overview
The client dispatches granular PSR-14 events at every key lifecycle point. Inject any EventDispatcherInterface implementation — Laravel's dispatcher, Symfony's event dispatcher, or your own — and react to connections, sessions, subscriptions, data changes, alarms, reads, writes, browses, cache operations, and retries.
A NullEventDispatcher is used by default, ensuring zero overhead when no dispatcher is configured. Event objects are lazily instantiated via closures, so no allocation happens unless a real dispatcher is listening.
Configuration
use PhpOpcua\Client\ClientBuilder;
use Psr\EventDispatcher\EventDispatcherInterface;
$client = ClientBuilder::create()
->setEventDispatcher($yourDispatcher)
->connect('opc.tcp://localhost:4840');
// Get the current dispatcher (on the builder before connecting)
$builder = ClientBuilder::create();
$dispatcher = $builder->getEventDispatcher();
Laravel
$builder = ClientBuilder::create();
$builder->setEventDispatcher(app(EventDispatcherInterface::class));
$client = $builder->connect('opc.tcp://localhost:4840');
Or in a service provider:
$this->app->afterResolving(ClientBuilder::class, function (ClientBuilder $builder) {
$builder->setEventDispatcher($this->app->make(EventDispatcherInterface::class));
});
Then listen with standard Laravel listeners:
// EventServiceProvider
protected $listen = [
\PhpOpcua\Client\Event\DataChangeReceived::class => [
\App\Listeners\HandleOpcUaDataChange::class,
],
\PhpOpcua\Client\Event\AlarmActivated::class => [
\App\Listeners\HandleOpcUaAlarm::class,
],
];
Event Reference
Every event is a readonly class in PhpOpcua\Client\Event\. All events carry a $client property referencing the OpcUaClientInterface that emitted them.
Connection Events
| Event |
Properties |
When |
ClientConnecting |
$endpointUrl |
Before connect() starts |
ClientConnected |
$endpointUrl |
After successful connection |
ConnectionFailed |
$endpointUrl, $exception |
When connection attempt fails |
ClientReconnecting |
$endpointUrl |
Before reconnect() starts |
ClientDisconnecting |
$endpointUrl |
Before disconnect() starts |
ClientDisconnected |
|
After full disconnect |
Session Events
| Event |
Properties |
When |
SessionCreated |
$endpointUrl, $authenticationToken |
After CreateSession succeeds |
SessionActivated |
$endpointUrl |
After ActivateSession succeeds |
SessionClosed |
|
Before session close request |
Secure Channel Events
| Event |
Properties |
When |
SecureChannelOpened |
$channelId, $securityPolicy, $securityMode |
After secure channel is opened |
SecureChannelClosed |
$channelId |
Before secure channel close |
Subscription Events
| Event |
Properties |
When |
SubscriptionCreated |
$subscriptionId, $revisedPublishingInterval, $revisedLifetimeCount, $revisedMaxKeepAliveCount |
After createSubscription() |
SubscriptionDeleted |
$subscriptionId, $statusCode |
After deleteSubscription() |
SubscriptionTransferred |
$subscriptionId, $statusCode |
After transferSubscriptions() (per item) |
MonitoredItemCreated |
$subscriptionId, $monitoredItemId, $nodeId, $statusCode |
After createMonitoredItems() / createEventMonitoredItem() (per item) |
MonitoredItemDeleted |
$subscriptionId, $monitoredItemId, $statusCode |
After deleteMonitoredItems() (per item) |
MonitoredItemModified |
$subscriptionId, $monitoredItemId, $statusCode |
After modifyMonitoredItems() (per item) |
TriggeringConfigured |
$subscriptionId, $triggeringItemId, $addResults, $removeResults |
After setTriggering() |
Publish Events
| Event |
Properties |
When |
PublishResponseReceived |
$subscriptionId, $sequenceNumber, $notificationCount, $moreNotifications |
After every publish() call |
SubscriptionKeepAlive |
$subscriptionId, $sequenceNumber |
When publish() returns no notifications |
DataChangeReceived |
$subscriptionId, $sequenceNumber, $clientHandle, $dataValue |
Per data change notification |
EventNotificationReceived |
$subscriptionId, $sequenceNumber, $clientHandle, $eventFields |
Per event notification |
Alarm Events (Generic)
| Event |
Properties |
When |
AlarmEventReceived |
$subscriptionId, $clientHandle, $eventFields, $severity, $sourceName, $message, $eventType, $time |
For every event notification with alarm-relevant data |
Alarm Events (Specific)
These are automatically deduced from event notification fields. They require the corresponding fields to be included in createEventMonitoredItem()'s $selectFields.
| Event |
Properties |
Deduced from |
AlarmActivated |
$subscriptionId, $clientHandle, $sourceName, $severity, $message |
ActiveState = true / "Active" |
AlarmDeactivated |
$subscriptionId, $clientHandle, $sourceName, $message |
ActiveState = false / "Inactive" |
AlarmAcknowledged |
$subscriptionId, $clientHandle, $sourceName |
AckedState text contains "acknowledged" |
AlarmConfirmed |
$subscriptionId, $clientHandle, $sourceName |
ConfirmedState text contains "confirmed" |
AlarmShelved |
$subscriptionId, $clientHandle, $sourceName |
ShelvingState text contains "shelved" |
AlarmSeverityChanged |
$subscriptionId, $clientHandle, $sourceName, $severity |
Severity field present in notification |
LimitAlarmExceeded |
$subscriptionId, $clientHandle, $sourceName, $limitState, $severity |
EventType is a known LimitAlarm type |
OffNormalAlarmTriggered |
$subscriptionId, $clientHandle, $sourceName, $severity |
EventType is OffNormalAlarm/DiscreteAlarm |
Read / Write / Browse Events
| Event |
Properties |
When |
NodeValueRead |
$nodeId, $attributeId, $dataValue |
After read() |
NodeValueWritten |
$nodeId, $value, $type, $statusCode |
After successful write() |
NodeValueWriteFailed |
$nodeId, $statusCode |
After write() with non-Good status |
NodeBrowsed |
$nodeId, $direction, $resultCount |
After browse() |
Write Type Detection Events
| Event |
Properties |
When |
WriteTypeDetecting |
$nodeId |
Before type detection starts (read or cache lookup) |
WriteTypeDetected |
$nodeId, $detectedType, $fromCache |
After type is successfully determined |
Cache Events
| Event |
Properties |
When |
CacheHit |
$key |
When a cached result is found |
CacheMiss |
$key |
When a cached result is not found |
Retry Events
| Event |
Properties |
When |
RetryAttempt |
$attempt, $maxRetries, $exception |
Before each automatic retry |
RetryExhausted |
$attempts, $exception |
When all retries are exhausted |
Type Discovery Events
| Event |
Properties |
When |
DataTypesDiscovered |
$namespaceIndex, $count |
After discoverDataTypes() completes |
Trust Store Events
| Event |
Properties |
When |
ServerCertificateTrusted |
$fingerprint, $subject |
Server cert passes trust store validation |
ServerCertificateRejected |
$fingerprint, $reason, $subject |
Server cert rejected by trust store |
ServerCertificateAutoAccepted |
$fingerprint, $subject |
Server cert auto-accepted via TOFU |
ServerCertificateManuallyTrusted |
$fingerprint, $subject |
Cert added via trustCertificate() |
ServerCertificateRemoved |
$fingerprint |
Cert removed via untrustCertificate() |
Practical Examples
Log all data changes to a database
class DataChangeListener
{
public function __invoke(DataChangeReceived $event): void
{
DB::table('opcua_values')->insert([
'subscription_id' => $event->subscriptionId,
'client_handle' => $event->clientHandle,
'value' => $event->dataValue->getValue(),
'status_code' => $event->dataValue->statusCode,
'source_timestamp' => $event->dataValue->sourceTimestamp,
'recorded_at' => now(),
]);
}
}
Send Slack alerts on alarm activation
class AlarmAlertListener
{
public function __invoke(AlarmActivated $event): void
{
Notification::route('slack', config('opcua.slack_webhook'))
->notify(new AlarmNotification(
source: $event->sourceName,
severity: $event->severity,
message: $event->message,
));
}
}
Monitor connection health
class ConnectionHealthListener
{
public function handleConnected(ClientConnected $event): void
{
Cache::put("opcua:{$event->endpointUrl}:status", 'connected');
Metrics::gauge('opcua.connections.active', 1);
}
public function handleFailed(ConnectionFailed $event): void
{
Cache::put("opcua:{$event->endpointUrl}:status", 'failed');
Log::error('OPC UA connection failed', [
'endpoint' => $event->endpointUrl,
'error' => $event->exception->getMessage(),
]);
}
public function handleRetry(RetryAttempt $event): void
{
Metrics::increment('opcua.retries', tags: [
'attempt' => $event->attempt,
]);
}
}
Track subscription lifecycle for session manager
class SubscriptionTracker
{
public function handleCreated(SubscriptionCreated $event): void
{
Redis::hSet('opcua:subscriptions', $event->subscriptionId, json_encode([
'interval' => $event->revisedPublishingInterval,
'created_at' => now()->toIso8601String(),
]));
}
public function handleDeleted(SubscriptionDeleted $event): void
{
Redis::hDel('opcua:subscriptions', $event->subscriptionId);
}
}
Alarm event monitoring with extended fields
To receive specific alarm events (AlarmActivated, AlarmDeactivated, etc.), include the relevant state fields when creating the event monitored item:
$result = $client->createEventMonitoredItem(
$sub->subscriptionId,
$alarmNodeId,
[
'EventId', 'EventType', 'SourceName', 'Time', 'Message', 'Severity',
'ActiveState', // enables AlarmActivated / AlarmDeactivated
'AckedState', // enables AlarmAcknowledged
'ConfirmedState', // enables AlarmConfirmed
],
);
The default 6 fields (EventId, EventType, SourceName, Time, Message, Severity) always trigger AlarmEventReceived and AlarmSeverityChanged. Adding state fields beyond position 6 enables the corresponding specific events.
- NullEventDispatcher (default):
dispatch() does an instanceof check and returns immediately. No event object is allocated.
- Lazy closures: all dispatch calls use
fn() => new Event(...). The closure is only invoked when a real dispatcher is set.
- Zero overhead when unused: the entire event system adds no measurable cost to operations when no dispatcher is configured.