opcua-session-manager · v4.3.x
Docs · Daemon

Auto-publish

The daemon drives the OPC UA publish loop for you when you wire an event dispatcher. Application code listens to PSR-14 events instead of calling publish() in a loop.

publish() is the OPC UA service call that retrieves queued notifications from a subscription. By spec, the client drives the loop: send publish(), receive the response, send another. The session manager can run that loop on your behalf — the AutoPublisher — so application code listens for PSR-14 events instead of polling.

Auto-publish is opt-in and embedded-only. Enable it by constructing the daemon with both an EventDispatcherInterface and autoPublish: true.

What it does

For every session that has at least one active subscription, the AutoPublisher schedules a periodic call to publish() and dispatches the resulting DataChangeReceived / EventNotificationReceived events through the configured PSR-14 dispatcher.

The publish cadence comes from the session's smallest publishingInterval — typically 250-1000 ms. Sessions without subscriptions are idle and do not trigger publish calls.

When to use it

  • Long-lived background workers that need live data without hand-coding the publish loop in every consumer.
  • Event-driven architectures that already speak PSR-14 — wire the daemon's dispatcher into your existing event bus and let listeners react.
  • Multi-tenant subscriptions managed by the daemon (via Auto-connect) where no application code exists to drive the loop manually.

If your application explicitly calls publish() in a tight loop (typical of single-purpose CLI workers), do not turn on auto-publish — you would be driving the loop twice.

Wiring

Auto-publish requires the embedded path — the bin script does not expose it. Construct the daemon with both pieces:

php examples/auto-publish-daemon.php
use PhpOpcua\SessionManager\Daemon\SessionManagerDaemon;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use PhpOpcua\Client\Event\DataChangeReceived;

$dispatcher = new EventDispatcher();

$dispatcher->addListener(DataChangeReceived::class, function (DataChangeReceived $e) {
    // Your reaction here. Correlate via $e->clientHandle (the
    // value you passed when creating the monitored item) — there
    // is no monitoredItemId on the event.
    persist($e->clientHandle, $e->dataValue);
});

$daemon = new SessionManagerDaemon(
    socketPath: '/var/run/opcua/sessions.sock',
    timeout: 1800,
    cleanupInterval: 60,
    clientEventDispatcher: $dispatcher,
    autoPublish: true,
);

$daemon->autoConnect([
    /* see auto-connect.md */
]);

$daemon->run();

clientEventDispatcher is the PSR-14 dispatcher the daemon injects into every Client it constructs. autoPublish: true enables the AutoPublisher. Without both, the dispatcher is wired but the publish loop is your responsibility.

How sessions enter the publish loop

AutoPublisher::startSession(sessionId) is called by CommandHandler whenever a createSubscription reply is processed for that session. From that moment on, the autoplisher schedules publish() calls until stopSession(sessionId) runs — triggered by deleteSubscription (when the last subscription is removed) or by session shutdown.

The state model:

text autoplisher lifecycle
Session opened

   │ first createSubscription

AutoPublisher::startSession   ← scheduler now polls publish() every $publishingInterval

   │ deleteSubscription (last one)

AutoPublisher::stopSession    ← scheduler stops; session remains open

   │ session close

gone

The autoplisher does not introduce sessions of its own. It only schedules calls against sessions other code has opened.

Events you can listen for

Every PSR-14 event from opcua-client is dispatchable through auto-publish. The most common are:

Event Fires when
DataChangeReceived A data-change notification was delivered
EventNotificationReceived An event notification was delivered
AlarmActivated An alarm transitioned to Active (auto-deduced from payload)
AlarmAcknowledged An alarm was acknowledged
PublishResponseReceived Every publish response, including keep-alives
SubscriptionKeepAlive The server sent an empty publish (no notifications)

For the full catalogue, see opcua-client — events reference.

What the events carry

Each event payload reaches the listener as a fully-decoded PHP object — same shape your application would see calling publish() directly. The auto-publish layer does not add or remove fields.

There is one caveat: the dispatcher runs inside the daemon process. Your listener runs in the daemon's address space, not in the application's. This means:

  • Listeners cannot access request-scoped state.
  • Side effects (database writes, queue publishes, HTTP calls) happen from the daemon process, with whatever credentials and network access it has.
  • Listener exceptions propagate inside the daemon — wrap your listener body in try/catch to keep the publish loop healthy.

If you want application-side reactions, your listener typically publishes to a queue (Redis, Beanstalk, SQS) and the application consumes that queue. Direct synchronous calls from the listener to application infrastructure are possible but couple the two processes tightly.

Cost

Every active subscription schedules one publish() per publishing interval per session. At 250 ms publishing intervals on 10 subscriptions: ~40 publish round-trips per second across the daemon's OPC UA fan-out. The library buffers internally, so the listener side does not feel it directly — but the OPC UA server needs the headroom.

If notifications are dense (alarm storms, high-frequency tag changes), the AutoPublisher keeps calling publish() back-to-back as long as the server reports moreNotifications: true. There is no artificial back-pressure beyond ReactPHP's natural event-loop fairness.

When auto-publish is the wrong tool

  • Workers that explicitly run publish(). Two loops, double cost, possible double-dispatch.
  • Application that needs notifications in its own process. The daemon's dispatcher does not bridge across processes — you need a queue or some other IPC for that.
  • Per-subscription pace control. All sessions share the same autoplisher cadence model (per-subscription publishingInterval). Custom backoff per session requires hand-driving the loop.

See Recipes · Auto-publish pattern for an end-to-end worker.