Subscriptions
Subscriptions are the long-poll mechanism for OPC UA — once created, the server pushes notifications until the session ends. This page covers the subscription itself; monitored items, the things being watched, live on their own page.
A subscription is a server-side construct that delivers
notifications to the client at a configured cadence. It carries zero
or more monitored items (variables to sample, events to filter)
and runs an internal publish cycle: every publishingInterval, the
server packages whatever changed and queues a PublishResponse.
The client retrieves those responses by calling publish(). There is
no socket-level "push" — the publish loop is request/response under
the covers — but the model is otherwise the long-poll you'd expect.
Creating a subscription
$sub = $client->createSubscription(publishingInterval: 500.0);
echo "Subscription #{$sub->subscriptionId}, "
. "revised interval: {$sub->revisedPublishingInterval}ms\n";
$publishingInterval
Target sampling rate in milliseconds. The server may revise this — the
actual rate is in $sub->revisedPublishingInterval.
$lifetimeCount
Number of publish-interval ticks the server will keep the subscription alive without a publish request. Default of 2400 × 500 ms = 20 min.
$maxKeepAliveCount
After this many empty ticks, the server emits an empty PublishResponse
to keep the connection warm. Default of 10 × 500 ms = 5 s heartbeat.
$maxNotificationsPerPublish
Cap on notifications per publish reply. 0 = unbounded.
$publishingEnabled
Whether the server should start publishing immediately. Disable to
create a "paused" subscription you enable later with
setPublishingMode().
$priority
Server-side priority hint when multiple subscriptions compete.
SubscriptionResult:
| Field | Meaning |
|---|---|
subscriptionId |
Server-assigned id — use it for every later call |
revisedPublishingInterval |
Actual interval the server agreed to |
revisedLifetimeCount |
Actual lifetime count |
revisedMaxKeepAliveCount |
Actual keep-alive count |
The publish loop
After creating a subscription and adding monitored items, drive the publish loop yourself:
$running = true;
while ($running) {
$publish = $client->publish(acknowledgements: $pendingAcks);
$pendingAcks = [];
foreach ($publish->notifications as $notification) {
// DataChangeReceived events have already been dispatched at this point,
// but you also get the typed notification objects here.
handleNotification($notification);
}
if ($publish->sequenceNumber !== 0) {
$pendingAcks[] = [
'subscriptionId' => $publish->subscriptionId,
'sequenceNumber' => $publish->sequenceNumber,
];
}
if ($publish->moreNotifications === false) {
// Server has nothing else queued — back off slightly.
usleep(10_000);
}
}
The publish() call returns a PublishResult carrying:
subscriptionId— the subscription this response belongs tosequenceNumber— to acknowledge in a laterpublish()availableSequenceNumbers— outstanding (un-acked) sequence numbers the server still holdspublishTime— when the server sent the notification messageacknowledgementResults— the status code of each acknowledgement you passed to thispublish(), in order:Good, or e.g.BadSequenceNumberUnknownfor a sequence number the server no longer holds andBadSubscriptionIdInvalidfor an unknown subscriptionmoreNotifications—trueif more data is queued server-side; callpublish()again immediatelynotifications— an array ofDataChangeNotification(clientHandle,dataValue) andEventNotification(clientHandle,eventFields) value objects; distinguish them withinstanceof
The client also dispatches granular events (DataChangeReceived,
EventNotificationReceived, alarm-typed events). Consumers who prefer
event-driven code can register a PSR-14 listener and ignore the
notification array entirely. See Observability ·
Events.
Acknowledgements
OPC UA requires the client to acknowledge notification sequence numbers
so the server can retire them from its retransmission queue.
Acknowledgements are piggy-backed on the next publish() call:
$acks = [
['subscriptionId' => 12, 'sequenceNumber' => 42],
['subscriptionId' => 12, 'sequenceNumber' => 43],
];
$client->publish(acknowledgements: $acks);
If you fall behind, the server queues until availableSequenceNumbers
reaches its maxNotificationsPerPublish cap or the subscription
expires. To recover lost notifications after a brief outage, see the
republish section below.
Republish
Ask the server for a specific sequence number it still has buffered —
one it sent but you have not acknowledged yet. Useful when the client
missed a publish reply (process restart, brief network blip) but the
subscription itself is still alive. availableSequenceNumbers on the
last PublishResult (or on a TransferResult) lists what can still be
retransmitted:
use PhpOpcua\Client\Module\Subscription\DataChangeNotification;
try {
$missed = $client->republish($subId, retransmitSequenceNumber: 41);
foreach ($missed['notifications'] as $notification) {
if ($notification instanceof DataChangeNotification) {
process($notification->clientHandle, $notification->dataValue);
}
}
} catch (ServiceException $e) {
// BadMessageNotAvailable — the server already discarded it.
}
The result is an array with sequenceNumber, publishTime and
notifications, which holds the same DataChangeNotification /
EventNotification objects publish() returns.
republish() dispatches the same DataChangeReceived,
EventNotificationReceived and alarm events as publish(), with
republished set to true. A retransmitted message can repeat
notifications publish() already delivered — any sequence number you
had not acknowledged yet — so a listener that must not process a value
twice checks the flag:
use PhpOpcua\Client\Event\DataChangeReceived;
$listeners->listen(DataChangeReceived::class, function (DataChangeReceived $e) {
if ($e->republished && $store->has($e->subscriptionId, $e->sequenceNumber)) {
return;
}
$store->save($e->subscriptionId, $e->sequenceNumber, $e->clientHandle, $e->dataValue);
});
PublishResponseReceived and SubscriptionKeepAlive describe a
publish round-trip, so only publish() dispatches them.
Deleting
Delete the subscription cleanly when you are done. The server frees all its server-side resources (monitored items, queues, sequence numbers). Leaving subscriptions to expire works but burns server memory until the lifetime count runs out.
Transferring across sessions
When the channel dies and you reconnect(), the new session has no
subscriptions of its own. If the server preserved the subscription
resources (some servers do, some do not), transferSubscriptions()
re-binds them to the new session:
$client->reconnect();
$results = $client->transferSubscriptions(
subscriptionIds: [$oldSubId],
sendInitialValues: true,
);
if ($results[0]->statusCode !== 0) {
// Server lost the subscription too — recreate from scratch.
}
sendInitialValues: true asks the server to re-send the most recent
value of every monitored item, so the client's local cache is fresh.
See Recipes · Recovering from disconnection for the full re-subscription pattern.
What to read next
- Operations · Monitored items — what to put inside a subscription.
- Recipes · Subscribing to data changes — end-to-end pattern, event-driven.