Opening and closing
The connection lifecycle has three states and three transitions. Get the state machine right and most "the client just hung" bugs disappear.
The client's connection state is exposed as a three-case enum,
ConnectionState:
┌──────────────────────┐
│ Disconnected │ ← initial state
└──────────────────────┘
│ ▲
connect()│ │ disconnect()
▼ │
┌──────────────────────┐
│ Connected │
└──────────────────────┘
│ ▲
I/O error, │ │ reconnect()
server reset, │ │ succeeds
stale token │ │
▼ │
┌──────────────────────┐
│ Broken │
└──────────────────────┘
Three states, three transitions:
| From | Transition | To |
|---|---|---|
Disconnected |
connect() |
Connected |
Connected |
I/O error | Broken |
Broken |
reconnect() |
Connected |
| any | disconnect() |
Disconnected |
Read the state with $client->getConnectionState(). The boolean
shortcut $client->isConnected() returns true only when the state is
Connected.
Opening
There is no separate open() call. ClientBuilder::connect() returns
a Client already in ConnectionState::Connected. Under the hood it
performs:
- TCP open + HEL/ACK handshake
- GetEndpoints discovery, if needed
OpenSecureChannel(OPN)CreateSession+ActivateSession
If any step fails, no Client is returned — connect() throws and the
caller never sees a half-initialised object.
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\Exception\ConnectionException;
use PhpOpcua\Client\Exception\SecurityException;
try {
$client = ClientBuilder::create()
->setTimeout(10.0)
->connect('opc.tcp://plc.local:4840');
} catch (ConnectionException $e) {
// TCP failure, DNS error, HEL/ACK timeout, server refused, etc.
throw $e;
} catch (SecurityException $e) {
// Certificate validation, key load, OpenSSL primitive failure
throw $e;
}
Configure the builder before calling connect() — the builder is
the only configuration surface. Once you have a Client, the connection
parameters (security, timeout, batching, …) are frozen until you build
a new one.
Closing
Always disconnect() when you are done. The call sends CloseSession
and CloseSecureChannel to the server and frees the TCP socket. Both
messages can fail silently — disconnect() does not raise on the
return trip, so it is safe in finally blocks.
$client = ClientBuilder::create()->connect('opc.tcp://plc.local:4840');
try {
$client->read('i=2261');
// … real work …
} finally {
$client->disconnect();
}
Warning
A Client that has been disconnect()ed cannot be reused for normal
operations — every service call will raise ConnectionException("Not connected: call connect() first"). To revive it, call reconnect().
The builder is a one-shot factory by design; if you need a second
connection, build a second client.
Reconnecting
When a connection drops, the client transitions to
ConnectionState::Broken on the next service call. Call reconnect()
to rebuild the channel and session against the same endpoint, with the
same configuration:
reconnect() does not restore subscriptions on its own — the
server has discarded them along with the old session. If your
application maintains live subscriptions, see Recipes · Recovering
from disconnection for the
re-subscription pattern.
Detecting a broken connection
A connection only flips to Broken after the client tries to use it.
There is no background heartbeat in this library. The detection points
are:
- A blocking read or write on the socket fails or times out.
- The server returns a session-invalid status (
BadSessionIdInvalid,BadSessionNotActivated). - The secure channel is rejected (
BadSecureChannelClosed).
When that happens, the client dispatches ClientDisconnected, sets
the state to Broken, and raises the underlying exception to the
caller. The next call into the client will
raise ConnectionException("Connection broken: call reconnect()") —
the state remains stuck on Broken until reconnect() succeeds or
disconnect() is called.
Auto-retry
For transient failures inside a single call, the client supports an opt-in retry loop. Configure it on the builder:
$client = ClientBuilder::create()
->setAutoRetry(maxRetries: 3)
->connect('opc.tcp://plc.local:4840');
When maxRetries > 0, every service method routes through
executeWithRetry. On a recoverable error it calls reconnect() and
re-issues the request, up to maxRetries total tries. See Connection
· Timeouts and retry for the classification
of recoverable vs fatal failures.
Lifecycle events
The connection lifecycle is fully observable via PSR-14 events. Wire a
dispatcher with setEventDispatcher() and listen for:
| Event | When |
|---|---|
ClientConnecting |
connect() started |
ClientConnected |
Session activated; client is usable |
ConnectionFailed |
connect() raised — payload carries the exception |
ClientDisconnecting |
disconnect() started |
ClientDisconnected |
Disconnected (clean or broken — distinguish via prior ConnectionFailed / ClientDisconnecting) |
ClientReconnecting |
reconnect() started |
SecureChannelOpened / SecureChannelClosed |
OPN / CLO frames |
SessionCreated / SessionActivated / SessionClosed |
CreateSession / ActivateSession / CloseSession |