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.

Performance

  • 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.