Differences from the direct client
Same interface, different costs, slightly different lifecycle. Five things change between the direct Client and ManagedClient — knowing them up front avoids surprises in production.
ManagedClient implements OpcUaClientInterface and looks
identical to the direct Client at the call site. The differences
are below the call site — in cost, in lifecycle, in what is and
isn't observable. Five differences worth internalising before
production.
1 — Every call pays IPC
Client operations are local to the PHP process. ManagedClient
operations cross a Unix socket (or TCP loopback) for every method —
the daemon executes the actual OPC UA call.
| Cost | Direct Client |
ManagedClient |
|---|---|---|
read() single value |
~1-5 ms (OPC UA round-trip) | ~1-5 ms + ~0.5-2 ms IPC |
connect() first time |
~1 s (OPC UA handshake) | ~1 s (handshake on daemon) + IPC |
connect() reused |
n/a — every connect is fresh | ~0.5-2 ms (no handshake) |
disconnect() |
~50 ms (CloseSession) | ~50 ms + IPC (daemon performs CloseSession) |
The IPC cost is dominated by the OPC UA round-trip for any call
that talks to the server. It is not dominated for local-only
operations like wasSessionReused() or getConnectionState() —
those are an IPC round-trip on ManagedClient, free on direct
Client.
The asymmetry of connect() is the whole point: pay the handshake
once on the daemon, every subsequent client connect() reuses.
2 — Lifecycle semantics
Same method names, different meaning. Internalise the table:
| Method | Direct Client |
ManagedClient |
|---|---|---|
connect($url) |
Opens TCP + secure channel + session, every time | Reuses daemon session when keys match; otherwise opens a fresh one |
disconnect() |
Closes session, channel, TCP socket | Sends close IPC; daemon tears the session down on the server too |
reconnect() |
Rebuilds channel + session | Sends query method=reconnect — daemon rebuilds the channel for the current session |
isConnected() |
In-process boolean | IPC round-trip to query the daemon |
getConnectionState() |
In-process enum | IPC round-trip to query the daemon |
The biggest pitfall: disconnect() does close the OPC UA
session. Callers that "tidy up" by disconnecting at the end of a
request defeat reuse — the next request from a fresh
ManagedClient pays the handshake again because the daemon's
session-store entry is gone. To benefit from reuse, keep the
ManagedClient instance alive across requests (singleton bind in
Laravel, long-running worker) and let the inactivity timeout
(--timeout, default 600 s) reclaim idle sessions instead.
3 — Events are dispatched in the daemon, not in your process
getEventDispatcher() is part of OpcUaClientInterface. On
ManagedClient it returns the client-side dispatcher (a
NullEventDispatcher by default), not the daemon's. Setting a
dispatcher on the managed client has no effect on the events
the daemon dispatches.
| Want to listen for events in | Set the dispatcher on |
|---|---|
| The application process | Wire your own — but the daemon won't fire into it |
| The daemon process | Pass clientEventDispatcher to the daemon constructor; embed the daemon |
The auto-publish feature is the canonical bridge — the daemon dispatches events in its own process, and a queue / message bus carries them to application listeners. See Daemon · Auto-publish and Recipes · Auto-publish pattern.
4 — Configuration is frozen at connect()
On the direct Client, the configuration is owned by the
ClientBuilder and frozen once connect() returns — same as
ManagedClient. The difference: on the direct client there is no
way to "re-configure" without building a new client. On
ManagedClient, setting a different value and calling connect()
again opens a different daemon-side session (the keys do not
match) — your getSessionId() changes, wasSessionReused() is
false.
This is the "fragmentation" trap warned in
Session reuse. Build one canonical client
per (endpoint, config) pair, share it, do not vary the setters
per call site.
5 — Some surfaces are inherently different
A handful of OpcUaClientInterface methods cannot fully cross the
IPC boundary. They work — they just have caveats.
getExtensionObjectRepository()
Returns the client-side ExtensionObject repository. Codecs
registered on it apply to client-side decoding of responses —
typically the path where the daemon returns raw bytes that the
client decodes. For codecs that need to run on the daemon side
(decoding ExtensionObjects on Read responses that the daemon
already auto-decodes), register them on the daemon-side client
through a custom param deserializer or a third-party module. See
Extensibility · Third-party modules.
getCache() and the cache methods
setCache() configures a client-side cache. The daemon uses
its own cache (the --cache-driver configured at startup); the
client-side cache layered on top is useful for caching responses
the application receives, separate from what the daemon has
cached.
In practice, do not bother setting a client-side cache on
ManagedClient unless you have a measured need. The daemon's
cache is shared across all clients; the client-side cache is
process-local and adds little.
getLogger()
Returns the client-side logger. Logs from inside the OPC UA stack
land in the daemon's logger, not this one. The client-side
logger captures ManagedClient's own diagnostics — IPC retries,
serialisation traces — and nothing more.
hasMethod(), hasModule(), getRegisteredMethods(), getLoadedModules()
These v4.2.0 introspection methods reach the daemon via the
describe IPC command on first call and cache the response for
the client's lifetime. The cached response is invalidated when
the IPC connection closes (disconnect()).
Two consequences:
- The first call pays an IPC round-trip; subsequent calls are free.
- If the daemon's module set changes mid-session (extremely rare — the daemon would have to be restarted with new modules), the client sees the old set until it reconnects.
When to keep using the direct client
For services where every operation is well below request latency and there is no value to amortise across requests — long-running CLI scripts, batch importers, integration tests against a local test server — the direct client is simpler. No daemon to start, no IPC layer, no session reuse semantics to think about.
The session manager is the right answer for request-driven applications. Direct client for everything else.