# OPC UA PHP Client ? Full Documentation
> Pure PHP OPC UA client library. Communicates directly over TCP using the OPC UA binary protocol. No external C/C++ dependencies ? only ext-openssl required. All types use public readonly properties.
Package: php-opcua/opcua-client
Repository: https://github.com/php-opcua/opcua-client
Packagist: https://packagist.org/packages/php-opcua/opcua-client
License: MIT
PHP: >= 8.2
Dependencies: ext-openssl, psr/log ^3.0, psr/simple-cache ^3.0, psr/event-dispatcher ^1.0 (all interface-only, zero runtime code)
---
## 1. Introduction
`php-opcua/opcua-client` is an OPC UA client written entirely in PHP. It speaks the OPC UA binary protocol over TCP, handles secure channels, sessions, and crypto ? all without external C/C++ extensions.
### Installation
```
composer require php-opcua/opcua-client
```
### Features
- Binary Protocol ? full OPC UA binary encoding/decoding over TCP
- Human-readable NodeId strings ? all methods accept `'i=2259'` or `'ns=2;s=MyNode'` in addition to NodeId objects; invalid strings throw InvalidNodeIdException
- Browse ? navigate the server address space, recursive browsing with automatic continuation
- Path Resolution ? resolve paths like /Objects/MyPLC/Temperature to NodeIds
- Read/Write ? single and multi operations with all OPC UA data types, automatic write type detection (read-before-write) with PSR-16 caching and type mismatch validation, configurable via setAutoDetectWriteType()
- Server BuildInfo ? getServerBuildInfo() returns BuildInfo DTO (productName, manufacturerName, softwareVersion, buildNumber, buildDate) in a single readMulti(). Individual methods: getServerProductName(), getServerManufacturerName(), getServerSoftwareVersion(), getServerBuildNumber(), getServerBuildDate()
- Node Management ? addNodes(), deleteNodes(), addReferences(), deleteReferences() for dynamic address space modification. addNodes() supports all 8 node classes with automatic ExtensionObject attribute encoding, returns AddNodesResult[]. Other methods return int[] status codes.
- Method Call ? invoke OPC UA methods, returns CallResult DTO
- Subscriptions ? data change and event monitoring, returns typed DTOs
- History Read ? raw, processed, and at-time historical queries
- Security ? 10 policies: 6 RSA (None through Aes256Sha256RsaPss) + 4 ECC (EccNistP256, EccNistP384, EccBrainpoolP256r1, EccBrainpoolP384r1), 3 modes
- Authentication ? anonymous, username/password, X.509 certificate
- PSR-3 Logging ? optional structured logging via any PSR-3 logger; NullLogger by default
- PSR-16 Cache ? browse/browseAll/resolveNodeId/getEndpoints/discoverDataTypes results cached by default (InMemoryCache, 300s TTL). Any PSR-16 driver works (FileCache, Laravel, Redis). Per-call bypass with useCache: false. discoverDataTypes replays cached type definitions without server round-trips. Metadata read cache opt-in via setReadMetadataCache(true): caches non-Value attributes (DisplayName, BrowseName, DataType, NodeClass, etc.). Value never cached. read($nodeId, $attr, refresh: true) to bypass.
- PSR-14 Events ? 47 granular events at lifecycle points (connection, session, subscription, data change, alarms, read/write, browse, cache, retry, trust store). NullEventDispatcher by default (zero overhead). Alarm events auto-deduced from notification fields. All events carry a $client reference.
- Server Trust Store ? persistent server certificate validation via FileTrustStore. Three policies (Fingerprint, FingerprintAndExpiry, Full). TOFU auto-accept. setTrustPolicy(null) disables (default). 5 trust-specific events. CLI trust/trust:list/trust:remove commands.
- Auto-Retry ? automatic reconnect on failure
- Fluent Builder API ? readMulti(), writeMulti(), createMonitoredItems(), and translateBrowsePaths() support a chainable builder when called without arguments
- Auto-Batching ? transparent batching for readMulti/writeMulti
- ExtensionObject Codecs ? per-client instance-level codec registry
- Automatic DataType Discovery ? discoverDataTypes() auto-detects custom structures from server metadata (OPC UA 1.04+)
- Typed everywhere ? all responses return public readonly DTOs, not arrays
- MockClient ? in-memory test double implementing OpcUaClientInterface; register handlers, assert calls, no TCP connection
- DataValue factory methods ? ofInt32(), ofDouble(), ofString(), ofBoolean(), of($value, BuiltinType), bad(StatusCode), etc.
- 1300+ tests (1040+ unit, 250+ integration), 99%+ code coverage on PHP 8.2/8.3/8.4/8.5
### Property access style
All Type classes and result DTOs use public readonly properties:
- $ref->nodeId instead of $ref->getNodeId()
- $dv->statusCode instead of $dv->getStatusCode()
- $result->subscriptionId instead of $result['subscriptionId']
Old getter methods are deprecated but still work.
### Architecture
```
ClientBuilder (entry point, config traits in ClientBuilder/, addModule/replaceModule)
+-- connect() ? boots Kernel ? registers Modules ? returns Client
Client (proxy to modules, implements OpcUaClientInterface, hasMethod, hasModule)
+-- __call() for custom module methods
Kernel/ClientKernel (shared infrastructure for all modules)
+-- Transport/TcpTransport (TCP socket communication)
+-- Protocol/SessionService (session management, kernel-level)
+-- Encoding/BinaryEncoder (binary serialization)
+-- Encoding/BinaryDecoder (binary deserialization)
+-- Security/SecureChannel (message-level security)
+-- Security/MessageSecurity (crypto operations)
+-- Security/CertificateManager (certificate handling)
Kernel/ModuleRegistry (module lifecycle, topological dependency sort, method registry)
Module/* (8 built-in service modules, each self-contained)
+-- Module/ReadWrite/ (read, write, call + CallResult)
+-- Module/Browse/ (browse, getEndpoints + BrowseResultSet)
+-- Module/Subscription/ (subscriptions, monitoring + SubscriptionResult, etc.)
+-- Module/History/ (history read)
+-- Module/NodeManagement/ (add/delete nodes + AddNodesResult)
+-- Module/TranslateBrowsePath/ (path resolution + BrowsePathResult)
+-- Module/ServerInfo/ (server build info + BuildInfo)
+-- Module/TypeDiscovery/ (auto type discovery)
Types/* (shared types: NodeId, DataValue, Variant, etc.)
Repository/* (per-client codec registry)
Exception/* (error hierarchy)
```
### Module System
The Client uses a modular architecture. Each OPC UA service set is a self-contained `ServiceModule` with its own protocol services, DTOs, and method implementations.
**Boot flow:** ClientBuilder::connect() creates ClientKernel, creates ModuleRegistry, registers all modules (8 built-in + custom), resolves dependencies with topological sort, boots all modules in order, returns Client with method handlers populated.
**Extending:** `ClientBuilder::addModule(new MyModule())` adds a custom module. `ClientBuilder::replaceModule(ReadWriteModule::class, new MyReadWrite())` swaps a built-in. Custom module methods are accessible via `__call()`. Built-in methods are concrete typed one-liners on Client that delegate to handlers.
**Introspection:** `$client->hasMethod('read')` and `$client->hasModule(ReadWriteModule::class)` for runtime checks.
**Dependencies:** Modules declare dependencies via `requires()`. The registry resolves the graph and throws `MissingModuleDependencyException` for unsatisfied deps. `ModuleConflictException` is thrown if two modules register the same method name.
### Quick Start
```php
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\Types\NodeId;
$client = ClientBuilder::create()
->connect('opc.tcp://localhost:4840');
// String format ? all methods accept NodeId|string
$dv = $client->read('i=2259');
echo $dv->getValue(); // unwrapped value
echo $dv->statusCode; // 0 (Good)
echo $dv->sourceTimestamp; // DateTimeImmutable
// NodeId objects still work
$dv = $client->read(NodeId::numeric(0, 2259));
$refs = $client->browse('i=85');
foreach ($refs as $ref) {
echo "{$ref->displayName} ({$ref->nodeId})\n";
}
$client->disconnect();
```
---
## 2. Connection & Configuration
### Basic Connection
```php
$client = ClientBuilder::create()
->connect('opc.tcp://localhost:4840');
// ... do stuff ...
$client->disconnect();
```
### Timeout
```php
$builder = ClientBuilder::create();
$builder->setTimeout(10.0); // 10 seconds (default: 5)
$client = $builder->connect('opc.tcp://localhost:4840');
```
### Connection State
States: Disconnected, Connected, Broken
```php
$client->getConnectionState(); // ConnectionState enum
$client->isConnected(); // bool
$client->reconnect(); // re-establishes using last URL
```
### Auto-Retry
```php
$builder = ClientBuilder::create();
$builder->setAutoRetry(3); // retry up to 3 times on connection failure
```
### Security Configuration
```php
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\Security\SecurityPolicy;
use PhpOpcua\Client\Security\SecurityMode;
$client = ClientBuilder::create()
->setSecurityPolicy(SecurityPolicy::Basic256Sha256)
->setSecurityMode(SecurityMode::SignAndEncrypt)
->setClientCertificate('/certs/client.pem', '/certs/client.key', '/certs/ca.pem')
->connect('opc.tcp://localhost:4840');
```
RSA Policies: None, Basic128Rsa15, Basic256, Basic256Sha256, Aes128Sha256RsaOaep, Aes256Sha256RsaPss
ECC Policies: EccNistP256, EccNistP384, EccBrainpoolP256r1, EccBrainpoolP384r1
Modes: None (1), Sign (2), SignAndEncrypt (3)
If no certificate provided, one gets auto-generated in memory (RSA 2048 for RSA policies, ECC matching curve for ECC policies).
ECC uses ECDH key agreement (no RSA encryption), HKDF key derivation, and EccEncryptedSecret for password authentication.
### Authentication
```php
$builder = ClientBuilder::create();
$builder->setUserCredentials('user', 'password'); // username/password
$builder->setUserCertificate('/certs/user.pem', '/certs/user.key'); // X.509
// or nothing ? anonymous by default
```
### Logging
The builder accepts any PSR-3 logger. Without one, a NullLogger is used (zero overhead).
```php
use PhpOpcua\Client\ClientBuilder;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('opcua');
$logger->pushHandler(new StreamHandler('php://stderr', Logger::DEBUG));
$client = ClientBuilder::create(logger: $logger)
->connect('opc.tcp://localhost:4840');
// or
$builder = ClientBuilder::create();
$builder->setLogger($logger);
```
Laravel integration:
```php
$client = ClientBuilder::create(logger: app('log'))
->connect('opc.tcp://localhost:4840');
```
### Events (PSR-14)
```php
use Psr\EventDispatcher\EventDispatcherInterface;
$builder = ClientBuilder::create();
$builder->setEventDispatcher($yourDispatcher);
// or in Laravel:
$builder->setEventDispatcher(app(EventDispatcherInterface::class));
```
NullEventDispatcher by default (zero overhead). 47 events covering connection, session, subscription, data change, alarms, read/write, write type detection, browse, cache, retry, trust store. See section 14 for the full list.
Log levels: DEBUG (handshake, secure channel, session), INFO (connect/disconnect, batch splits), WARNING (retries, server limits), ERROR (connection failures).
### Endpoint Discovery
```php
$endpoints = $client->getEndpoints('opc.tcp://localhost:4840');
foreach ($endpoints as $ep) {
echo "{$ep->endpointUrl} ? {$ep->securityPolicyUri} (mode: {$ep->securityMode})\n";
foreach ($ep->userIdentityTokens as $token) {
echo " Auth: {$token->policyId} (type={$token->tokenType})\n";
}
}
```
---
## 3. Browsing the Address Space
### Basic Browse
```php
$refs = $client->browse('i=85'); // Objects folder
foreach ($refs as $ref) {
echo "{$ref->displayName} (ns={$ref->nodeId->namespaceIndex};i={$ref->nodeId->identifier})\n";
}
```
### Browse All (automatic continuation)
```php
$refs = $client->browseAll('i=85');
```
### Browse with Continuation (manual)
```php
$result = $client->browseWithContinuation('i=85');
$allRefs = $result->references;
while ($result->continuationPoint !== null) {
$result = $client->browseNext($result->continuationPoint);
array_push($allRefs, ...$result->references);
}
```
### ReferenceDescription Properties
```php
$ref->referenceTypeId // NodeId
$ref->isForward // bool
$ref->nodeId // NodeId
$ref->browseName // QualifiedName
$ref->displayName // LocalizedText
$ref->nodeClass // NodeClass enum
$ref->typeDefinition // ?NodeId
```
### Recursive Browse
```php
$tree = $client->browseRecursive('i=85', maxDepth: 3);
foreach ($tree as $node) {
echo "{$node->reference->displayName}\n";
foreach ($node->getChildren() as $child) {
echo " {$child->reference->displayName}\n";
}
}
```
### Path Resolution
```php
$nodeId = $client->resolveNodeId('/Objects/Server/ServerStatus/State');
$value = $client->read($nodeId);
```
### translateBrowsePaths (Advanced)
```php
// Fluent builder
$results = $client->translateBrowsePaths()
->from('i=85')->path('Server', 'ServerStatus')
->execute();
$nodeId = $results[0]->targets[0]->targetId; // NodeId
// Or with array (still works)
$results = $client->translateBrowsePaths([
[
'startingNodeId' => NodeId::numeric(0, 85),
'relativePath' => [
['targetName' => new QualifiedName(0, 'Server')],
],
],
]);
```
### Caching
Browse, browseAll, and resolveNodeId results are cached by default (InMemoryCache, 300s TTL).
```php
use PhpOpcua\Client\Cache\InMemoryCache;
use PhpOpcua\Client\Cache\FileCache;
// Default: InMemoryCache with 300s TTL (active out of the box)
// File-based cache (on the builder, before connect)
$builder = ClientBuilder::create();
$builder->setCache(new FileCache('/tmp/opcua-cache', defaultTtl: 600));
// Laravel
$builder->setCache(app('cache')->store('redis'));
// Disable
$builder->setCache(null);
// Bypass cache for a single call
$refs = $client->browse('i=85', useCache: false);
$refs = $client->browseAll('i=85', useCache: false);
$nodeId = $client->resolveNodeId('/Objects/Server', useCache: false);
// Invalidation
$client->invalidateCache(NodeId::numeric(0, 85));
$client->flushCache();
```
---
## 4. Reading & Writing Values
### Server BuildInfo
```php
// All at once ? single readMulti() call, returns BuildInfo DTO
$info = $client->getServerBuildInfo();
echo $info->productName; // ?string (ns=0;i=2262)
echo $info->manufacturerName; // ?string (ns=0;i=2263)
echo $info->softwareVersion; // ?string (ns=0;i=2264)
echo $info->buildNumber; // ?string (ns=0;i=2265)
echo $info->buildDate; // ?DateTimeImmutable (ns=0;i=2266)
// Individual fields
$client->getServerProductName(); // ?string
$client->getServerManufacturerName(); // ?string
$client->getServerSoftwareVersion(); // ?string
$client->getServerBuildNumber(); // ?string
$client->getServerBuildDate(); // ?DateTimeImmutable
```
### Read Single Value
```php
$dv = $client->read('i=2259'); // or NodeId::numeric(0, 2259)
echo $dv->getValue(); // unwrapped value (via Variant)
echo $dv->statusCode; // int
echo $dv->sourceTimestamp; // ?DateTimeImmutable
echo $dv->serverTimestamp; // ?DateTimeImmutable
```
### Read Multiple Values
```php
// Fluent builder
$results = $client->readMulti()
->node('i=2259')->value()
->node('i=2267')->value()
->execute();
// Or with array (still works)
$results = $client->readMulti([
['nodeId' => 'i=2259'],
['nodeId' => 'i=2267'],
]);
```
### Write
```php
use PhpOpcua\Client\Types\BuiltinType;
$statusCode = $client->write('ns=2;i=1234', 42, BuiltinType::Int32);
```
### Auto-Batching
After connect(), the client reads server limits (MaxNodesPerRead/Write). When readMulti/writeMulti exceeds the limit, requests are split automatically.
```php
$builder = ClientBuilder::create();
$builder->setBatchSize(50); // manual override
$builder->setBatchSize(0); // disable batching
```
### Status Codes
```php
StatusCode::isGood($code); // true if 0x0XXXXXXX
StatusCode::isBad($code); // true if 0x8XXXXXXX
StatusCode::getName($code); // e.g. "BadNodeIdUnknown"
```
### Node Management
For servers that support dynamic address space modification:
```php
use PhpOpcua\Client\Types\NodeClass;
use PhpOpcua\Client\Types\QualifiedName;
use PhpOpcua\Client\Types\NodeId;
// Add nodes (supports all 8 node classes)
$results = $client->addNodes([
[
'parentNodeId' => 'i=85',
'referenceTypeId' => 'i=35',
'requestedNewNodeId' => 'ns=2;s=MyVar',
'browseName' => new QualifiedName(2, 'MyVar'),
'nodeClass' => NodeClass::Variable,
'typeDefinition' => 'i=63',
'dataType' => NodeId::numeric(0, 6), // Int32
],
]);
// $results[0]->statusCode, $results[0]->addedNodeId
// Delete nodes
$statusCodes = $client->deleteNodes([
['nodeId' => 'ns=2;s=MyVar', 'deleteTargetReferences' => true],
]);
// Add/delete references
$statusCodes = $client->addReferences([
['sourceNodeId' => 'ns=2;s=A', 'referenceTypeId' => 'i=35', 'isForward' => true, 'targetNodeId' => 'ns=2;s=B', 'targetNodeClass' => NodeClass::Variable],
]);
$statusCodes = $client->deleteReferences([
['sourceNodeId' => 'ns=2;s=A', 'referenceTypeId' => 'i=35', 'isForward' => true, 'targetNodeId' => 'ns=2;s=B', 'deleteBidirectional' => true],
]);
```
AddNodesResult: public readonly statusCode (int), addedNodeId (NodeId).
---
## 5. Method Calling
```php
use PhpOpcua\Client\Types\Variant;
$result = $client->call(
'i=2253', // or NodeId::numeric(0, 2253)
'i=11492',
[new Variant(BuiltinType::UInt32, 1)]
);
$result->statusCode; // int
$result->inputArgumentResults; // int[]
$result->outputArguments; // Variant[]
echo $result->outputArguments[0]->value;
```
---
## 6. Subscriptions & Monitoring
### Create Subscription
```php
$sub = $client->createSubscription(publishingInterval: 1000.0);
$subscriptionId = $sub->subscriptionId;
```
### Data Change Monitoring
```php
// Fluent builder
$results = $client->createMonitoredItems($subscriptionId)
->add('i=2258')->samplingInterval(500.0)->queueSize(10)
->add('ns=2;i=1001')
->execute();
// Or with array (still works)
$results = $client->createMonitoredItems($subscriptionId, [
['nodeId' => 'i=2258', 'samplingInterval' => 500.0, 'queueSize' => 10],
['nodeId' => 'ns=2;i=1001'],
]);
```
### Event Monitoring
```php
$result = $client->createEventMonitoredItem(
$subscriptionId,
NodeId::numeric(0, 2253),
['EventId', 'EventType', 'SourceName', 'Time', 'Message', 'Severity'],
);
```
### Receiving Notifications
```php
$response = $client->publish();
echo $response->subscriptionId;
echo $response->sequenceNumber;
echo $response->moreNotifications;
foreach ($response->notifications as $notif) {
if ($notif['type'] === 'DataChange') {
echo $notif['dataValue']->getValue();
} elseif ($notif['type'] === 'Event') {
foreach ($notif['eventFields'] as $field) {
echo $field->value;
}
}
}
```
### Transfer & Recovery
Transfer subscriptions to a new session and re-request unacknowledged notifications. Primarily used by the session manager package.
```php
// Transfer subscriptions from a previous session
$results = $client->transferSubscriptions([1, 2, 3], sendInitialValues: true);
foreach ($results as $result) {
// $result->statusCode, $result->availableSequenceNumbers
}
// Re-request an unacknowledged notification
$notifications = $client->republish(subscriptionId: 1, retransmitSequenceNumber: 42);
```
TransferResult DTO: statusCode (int), availableSequenceNumbers (int[])
### Cleanup
```php
$client->deleteMonitoredItems($subscriptionId, [$monitoredItemId]);
$client->deleteSubscription($subscriptionId);
```
---
## 7. History Read
### Raw History
```php
$values = $client->historyReadRaw(
'ns=2;i=1001',
startTime: new \DateTimeImmutable('-1 hour'),
endTime: new \DateTimeImmutable(),
numValuesPerNode: 100,
);
foreach ($values as $dv) {
echo "[{$dv->sourceTimestamp->format('H:i:s')}] {$dv->getValue()}\n";
}
```
### Processed History
```php
$values = $client->historyReadProcessed(
'ns=2;i=1001',
$startTime, $endTime,
processingInterval: 3600000.0,
aggregateType: 'i=2342', // Average
);
```
Aggregates: Average (2342), Interpolative (2341), Minimum (2346), Maximum (2347), Count (2352), Total (2344)
### History At Time
```php
$values = $client->historyReadAtTime('ns=2;i=1001', $timestamps);
```
---
## 8. Types Reference
### NodeId
Properties: namespaceIndex (int), identifier (int|string), type (string)
Methods: isNumeric(), isString(), isGuid(), isOpaque(), getEncodingByte(), parse(), toString(), __toString()
All client methods accept `NodeId|string`. Strings like `'i=2259'`, `'ns=2;i=1001'`, `'ns=2;s=MyNode'` are parsed automatically. Invalid strings throw `InvalidNodeIdException`.
### Variant
Properties: type (BuiltinType), value (mixed), dimensions (?array)
Methods: isMultiDimensional()
### DataValue
Properties: statusCode (int), sourceTimestamp (?DateTimeImmutable), serverTimestamp (?DateTimeImmutable)
Methods: getValue() (unwraps Variant), getVariant()
Factory methods: ofInt32(int), ofDouble(float), ofString(string), ofBoolean(bool), ofFloat(float), ofUInt32(int), ofInt16(int), ofUInt16(int), ofInt64(int), ofUInt64(int), ofDateTime(DateTimeImmutable), of(mixed, BuiltinType), bad(int statusCode)
### QualifiedName
Properties: namespaceIndex (int), name (string)
Methods: __toString()
### LocalizedText
Properties: locale (?string), text (?string)
Methods: __toString()
### ReferenceDescription
Properties: referenceTypeId (NodeId), isForward (bool), nodeId (NodeId), browseName (QualifiedName), displayName (LocalizedText), nodeClass (NodeClass), typeDefinition (?NodeId)
### EndpointDescription
Properties: endpointUrl (string), serverCertificate (?string), securityMode (int), securityPolicyUri (string), userIdentityTokens (array), transportProfileUri (string), securityLevel (int)
### UserTokenPolicy
Properties: policyId (?string), tokenType (int), issuedTokenType (?string), issuerEndpointUrl (?string), securityPolicyUri (?string)
### BrowseNode
Properties: reference (ReferenceDescription)
Methods: getChildren(), hasChildren(), addChild()
### BuiltinType (Enum)
Boolean(1), SByte(2), Byte(3), Int16(4), UInt16(5), Int32(6), UInt32(7), Int64(8), UInt64(9), Float(10), Double(11), String(12), DateTime(13), Guid(14), ByteString(15), XmlElement(16), NodeId(17), ExpandedNodeId(18), StatusCode(19), QualifiedName(20), LocalizedText(21), ExtensionObject(22), DataValue(23), Variant(24), DiagnosticInfo(25)
### NodeClass (Enum)
Unspecified(0), Object(1), Variable(2), Method(4), ObjectType(8), VariableType(16), ReferenceType(32), DataType(64), View(128)
### BrowseDirection (Enum)
Forward(0), Inverse(1), Both(2)
### ConnectionState (Enum)
Disconnected, Connected, Broken
### StatusCode constants
Good (0x00000000), BadUnexpectedError (0x80010000), BadTimeout (0x800A0000), BadNodeIdUnknown (0x80340000), BadNotWritable (0x803B0000), BadNotReadable (0x803E0000), BadTypeMismatch (0x80740000), BadUserAccessDenied (0x801F0000), BadMethodInvalid (0x80750000), BadNoData (0x80B10000)
### AttributeId constants
NodeId(1), NodeClass(2), BrowseName(3), DisplayName(4), Description(5), Value(13), DataType(14), AccessLevel(17)
### Result DTOs (all public readonly)
Note: Module-specific DTOs have moved from Types\ to their module namespace in v5.0.0. Shared types remain in Types\.
Module\Browse\BrowseResultSet: references (ReferenceDescription[]), continuationPoint (?string)
Module\TranslateBrowsePath\BrowsePathResult: statusCode (int), targets (BrowsePathTarget[])
Types\BrowsePathTarget: targetId (NodeId), remainingPathIndex (int)
Module\ReadWrite\CallResult: statusCode (int), inputArgumentResults (int[]), outputArguments (Variant[])
Module\Subscription\SubscriptionResult: subscriptionId (int), revisedPublishingInterval (float), revisedLifetimeCount (int), revisedMaxKeepAliveCount (int)
Module\Subscription\MonitoredItemResult: statusCode (int), monitoredItemId (int), revisedSamplingInterval (float), revisedQueueSize (int)
Module\Subscription\PublishResult: subscriptionId (int), sequenceNumber (int), moreNotifications (bool), notifications (array), availableSequenceNumbers (int[])
Module\Subscription\TransferResult: statusCode (int), availableSequenceNumbers (int[])
Types\ExtensionObject: typeId (NodeId), encoding (int), body (?string), value (mixed); methods: isDecoded(), isRaw(). DataValue::getValue() auto-extracts decoded value when codec is registered.
Module\ServerInfo\BuildInfo: productName (?string), manufacturerName (?string), softwareVersion (?string), buildNumber (?string), buildDate (?DateTimeImmutable)
Module\NodeManagement\AddNodesResult: statusCode (int), addedNodeId (NodeId)
---
## 9. Error Handling
Exception hierarchy:
```
RuntimeException
??? OpcUaException
??? ConfigurationException
??? ConnectionException
??? EncodingException
??? InvalidNodeIdException
??? ProtocolException
? ??? HandshakeException ($errorCode)
? ??? MessageTypeException ($expected, $actual)
??? SecurityException
? ??? CertificateParseException
? ??? OpenSslException
? ??? SignatureVerificationException
? ??? UnsupportedCurveException ($curveName)
??? ServiceException (carries getStatusCode())
??? UntrustedCertificateException ($fingerprint, $certDer)
??? WriteTypeDetectionException
??? WriteTypeMismatchException ($nodeId, $expectedType, $givenType)
```
Status codes vs exceptions:
- Exceptions: connection failures, protocol errors, security failures
- Status codes in results: per-item results from read/write/call (check $dv->statusCode)
---
## 10. Security
RSA Policies: None, Basic128Rsa15, Basic256, Basic256Sha256, Aes128Sha256RsaOaep, Aes256Sha256RsaPss
ECC Policies: EccNistP256 (P-256, AES-128, HMAC-SHA256), EccNistP384 (P-384, AES-256, HMAC-SHA384), EccBrainpoolP256r1 (BP-256, AES-128, HMAC-SHA256), EccBrainpoolP384r1 (BP-384, AES-256, HMAC-SHA384)
Connection flow with RSA security:
1. Discovery ? connect without security, get server certificate
2. Asymmetric Phase ? OPN request encrypted with server's RSA public key, nonce exchange
3. Symmetric Phase ? all messages use derived keys (AES-CBC + HMAC), keys from P_SHA
Connection flow with ECC security:
1. Discovery ? connect without security, get server ECC certificate
2. Asymmetric Phase ? OPN request signed only (ECDSA, no encryption), nonce = ephemeral ECDH pubkey (64/96 bytes X+Y)
3. Symmetric Phase ? keys derived via HKDF from ECDH shared secret, messages use AES-CBC + HMAC
4. Password auth uses EccEncryptedSecret: client requests ECDHKey via AdditionalHeader, encrypts password with ECDH+AES
---
## 11. Architecture
```
src/
??? ClientBuilder.php # Builder / entry point (config traits, addModule, replaceModule)
??? ClientBuilderInterface.php # Builder interface
??? Client.php # Connected client (proxy to modules, hasMethod, hasModule, getRegisteredMethods, getLoadedModules, __call)
??? OpcUaClientInterface.php # Public API contract (all built-in methods + hasMethod + hasModule + getRegisteredMethods + getLoadedModules)
??? ClientBuilder/*.php # Builder traits (configuration: cache, events, timeout, trust, modules, etc.)
??? Kernel/ClientKernel.php # Shared infrastructure for modules
??? Kernel/ClientKernelInterface.php # Kernel contract for modules
??? Kernel/ModuleRegistry.php # Module lifecycle, dependency sort, method registry
??? Module/ServiceModule.php # Abstract base class (register, boot, reset, requires)
??? Module/ReadWrite/ # ReadWriteModule + ReadService, WriteService, CallService, CallResult
??? Module/Browse/ # BrowseModule + BrowseService, GetEndpointsService, BrowseResultSet
??? Module/Subscription/ # SubscriptionModule + SubscriptionService, MonitoredItemService, PublishService, SubscriptionResult, MonitoredItemResult, PublishResult, TransferResult
??? Module/History/ # HistoryModule + HistoryReadService
??? Module/NodeManagement/ # NodeManagementModule + NodeManagementService, AddNodesResult
??? Module/TranslateBrowsePath/ # TranslateBrowsePathModule + TranslateBrowsePathService, BrowsePathResult
??? Module/ServerInfo/ # ServerInfoModule + BuildInfo
??? Module/TypeDiscovery/ # TypeDiscoveryModule
??? Transport/TcpTransport.php # TCP socket I/O
??? Encoding/BinaryEncoder.php # Serialization
??? Encoding/BinaryDecoder.php # Deserialization (accepts optional ExtensionObjectRepository)
??? Encoding/ExtensionObjectCodec.php # Codec interface
??? Protocol/AbstractProtocolService.php # Shared encode/decode base class
??? Protocol/ServiceTypeId.php # Named constants for all OPC UA service NodeIds
??? Protocol/SessionService.php # Session management (kernel-level)
??? Security/*.php # SecureChannel, MessageSecurity, CertificateManager, enums
??? Types/*.php # Shared types (NodeId, DataValue, Variant, etc. ? module-specific DTOs live in Module/*)
??? Builder/*.php # Fluent builders for multi-operations
??? Event/NullEventDispatcher.php # No-op PSR-14 dispatcher
??? Event/*.php # 38 event classes
??? Cache/InMemoryCache.php # PSR-16 in-memory cache
??? Cache/FileCache.php # PSR-16 file-based cache
??? Repository/ExtensionObjectRepository.php # Per-client codec registry
??? Wire/WireSerializable.php # Interface for JSON-IPC-safe value-objects (jsonSerialize + fromWireArray + wireTypeId)
??? Wire/WireTypeRegistry.php # Registry / encoder / decoder, security gate for __t discriminators (rejects unregistered ids at decode)
??? Wire/CoreWireTypes.php # Registers the cross-cutting core types on a WireTypeRegistry
??? Testing/MockClient.php # In-memory test double (no TCP, hasMethod, hasModule)
??? Exception/*.php # Exception classes (includes ModuleConflictException, MissingModuleDependencyException)
```
Binary encoding: little-endian, length-prefixed strings, NodeId compact encoding, DateTime as 100ns intervals since 1601-01-01 UTC.
### Wire serialization for cross-process IPC (v4.2.0)
`PhpOpcua\Client\Wire\WireTypeRegistry` + `WireSerializable` interface implement a JSON-based, gadget-chain-free serialization layer used by `opcua-session-manager` and future transports.
- Every core / module DTO implements `WireSerializable` with `jsonSerialize()` (pure associative array, no `__t`), `fromWireArray(array)` (inverse), and `wireTypeId()` (stable short id). The registry wraps each emitted value with `{"__t": "<id>", ...}` at encode time and rejects unknown `__t` ids at decode time.
- `CoreWireTypes::register()` installs NodeId, QualifiedName, LocalizedText, DataValue, Variant (with base64 ByteString), ExtensionObject (with base64 body), BrowseNode, ReferenceDescription, EndpointDescription, UserTokenPolicy plus enums BuiltinType, NodeClass, BrowseDirection, ConnectionState.
- Each `ServiceModule::registerWireTypes(WireTypeRegistry)` override contributes its module-specific DTOs (SubscriptionResult, TransferResult, MonitoredItemResult, MonitoredItemModifyResult, PublishResult, SetTriggeringResult, CallResult, BrowsePathResult, BrowsePathTarget, BrowseResultSet, AddNodesResult, BuildInfo).
- `ModuleRegistry::buildWireTypeRegistry()` composes core + module contributions ? the single method a remote peer uses to mirror the allowlist.
- Both backed enums (`::from($scalar)`) and pure unit enums (case-name scan) are supported.
- `DateTimeImmutable` is a built-in special case (`{"__t": "DateTime", "v": "<ISO 8601 with microseconds>"}`).
- Security: no `unserialize()` anywhere on the wire path ? only explicitly registered classes can be instantiated, gadget-chain surface is zero by construction.
---
## 12. ExtensionObject Codecs
```php
class MyPointCodec implements ExtensionObjectCodec
{
public function decode(BinaryDecoder $decoder): array
{
return ['x' => $decoder->readDouble(), 'y' => $decoder->readDouble(), 'z' => $decoder->readDouble()];
}
public function encode(BinaryEncoder $encoder, mixed $value): void
{
$encoder->writeDouble($value['x']);
$encoder->writeDouble($value['y']);
$encoder->writeDouble($value['z']);
}
}
```
Register per-client (not static):
```php
$repo = new ExtensionObjectRepository();
$repo->register(NodeId::numeric(2, 5001), MyPointCodec::class);
$client = ClientBuilder::create($repo)
->connect('opc.tcp://localhost:4840');
```
Or on the builder before connecting:
```php
$builder = ClientBuilder::create();
$builder->getExtensionObjectRepository()->register(NodeId::numeric(2, 5001), MyPointCodec::class);
$client = $builder->connect('opc.tcp://localhost:4840');
```
Repository API: register(), has(), get(), unregister(), clear()
### Automatic DataType Discovery
Instead of writing codecs manually, call discoverDataTypes() after connecting:
```php
$client = ClientBuilder::create()
->connect('opc.tcp://localhost:4840');
$client->discoverDataTypes();
$point = $client->read($pointNodeId)->getValue();
// ['x' => 1.5, 'y' => 2.5, 'z' => 3.5] ? decoded automatically
```
Filter by namespace: `$client->discoverDataTypes(namespaceIndex: 2)`
Manually registered codecs are never overwritten by auto-discovery.
Requires OPC UA 1.04+ servers that expose DataTypeDefinition attributes.
Limitations: binary encoding only (not XML), no built-in codecs shipped. Each Client is isolated.
---
## 13. MockClient (Testing)
`PhpOpcua\Client\Testing\MockClient` implements `OpcUaClientInterface` without any TCP connection. Use it to test code that depends on an OPC UA client without needing a real server.
### Creating a MockClient
```php
use PhpOpcua\Client\Testing\MockClient;
$client = MockClient::create();
```
### Registering Handlers
```php
use PhpOpcua\Client\Types\DataValue;
use PhpOpcua\Client\Types\NodeId;
// Read handler ? receives the NodeId, returns a DataValue
$client->onRead(function (NodeId $nodeId) {
return DataValue::ofDouble(23.5);
});
// Write handler ? receives NodeId, value, type; returns status code
$client->onWrite(function (NodeId $nodeId, mixed $value, BuiltinType $type) {
return StatusCode::Good;
});
// Browse handler ? receives NodeId, returns ReferenceDescription[]
$client->onBrowse(function (NodeId $nodeId) {
return [];
});
// Call handler ? receives objectId, methodId, arguments; returns CallResult
$client->onCall(function (NodeId $objectId, NodeId $methodId, array $args) {
return new CallResult(statusCode: 0, inputArgumentResults: [], outputArguments: []);
});
// Path resolution handler
$client->onResolveNodeId(function (string $path) {
return NodeId::numeric(2, 1001);
});
```
### Default Behaviors
Unregistered operations return sensible defaults: empty arrays for browse, status Good for writes, empty DataValue for reads.
### Call Tracking
```php
$client->read('ns=2;s=Temperature');
$client->read('ns=2;s=Pressure');
$client->write('ns=2;i=1001', 42, BuiltinType::Int32);
$client->getCalls(); // all recorded calls
$client->getCallsFor('read'); // only read calls
$client->callCount('read'); // 2
$client->callCount('write'); // 1
$client->resetCalls(); // clear all recorded calls
```
### Works with Fluent Builders
```php
$client = MockClient::create();
$client->onRead(fn (NodeId $n) => DataValue::ofInt32(42));
$results = $client->readMulti()
->node('i=2259')->value()
->node('ns=2;i=1001')->value()
->execute();
```
---
## 14. Events (PSR-14)
The client dispatches 47 granular PSR-14 events. Inject any EventDispatcherInterface via $builder->setEventDispatcher($dispatcher) on the ClientBuilder. NullEventDispatcher is used by default (zero overhead ? event objects lazily instantiated via closures).
### Configuration
```php
$builder = ClientBuilder::create();
$builder->setEventDispatcher($yourDispatcher);
$builder->getEventDispatcher(); // returns current dispatcher
$client = $builder->connect('opc.tcp://localhost:4840');
```
### Event Categories
Connection (6): ClientConnecting, ClientConnected, ConnectionFailed, ClientReconnecting, ClientDisconnecting, ClientDisconnected
Session (3): SessionCreated, SessionActivated, SessionClosed
Secure Channel (2): SecureChannelOpened, SecureChannelClosed
Subscription (9): SubscriptionCreated, SubscriptionDeleted, SubscriptionTransferred, MonitoredItemCreated, MonitoredItemDeleted, DataChangeReceived, EventNotificationReceived, PublishResponseReceived, SubscriptionKeepAlive
Alarms ? generic (1): AlarmEventReceived (subscriptionId, clientHandle, eventFields, severity, sourceName, message, eventType, time)
Alarms ? specific (8): AlarmActivated, AlarmDeactivated, AlarmAcknowledged, AlarmConfirmed, AlarmShelved, AlarmSeverityChanged, LimitAlarmExceeded, OffNormalAlarmTriggered
Read/Write/Browse (4): NodeValueRead, NodeValueWritten, NodeValueWriteFailed, NodeBrowsed
Type Discovery (1): DataTypesDiscovered
Cache (2): CacheHit, CacheMiss
Retry (2): RetryAttempt, RetryExhausted
All events are readonly classes in PhpOpcua\Client\Event\. All carry a $client property (OpcUaClientInterface).
### Alarm Deduction
Alarm-specific events are deduced from event notification fields:
- Default 6 fields (EventId, EventType, SourceName, Time, Message, Severity) trigger AlarmEventReceived and AlarmSeverityChanged
- ActiveState field ? AlarmActivated / AlarmDeactivated
- AckedState field ? AlarmAcknowledged
- ConfirmedState field ? AlarmConfirmed
- ShelvingState field ? AlarmShelved
- EventType matching known LimitAlarm NodeIds ? LimitAlarmExceeded
- EventType matching known OffNormalAlarm NodeIds ? OffNormalAlarmTriggered
### Key Classes
- PhpOpcua\Client\Event\NullEventDispatcher ? no-op PSR-14 dispatcher (default)
- PhpOpcua\Client\ClientBuilder\ManagesEventDispatcherTrait ? builder trait with setEventDispatcher(), getEventDispatcher()
- PhpOpcua\Client\Client\ManagesEventDispatchTrait ? client trait with runtime dispatch() helper
---
## Main Classes
- PhpOpcua\Client\ClientBuilder ? builder / entry point, implements ClientBuilderInterface, static create() factory, addModule(), replaceModule()
- PhpOpcua\Client\ClientBuilderInterface ? builder interface (configuration methods + connect + addModule + replaceModule)
- PhpOpcua\Client\Client ? connected client (proxy to modules), implements OpcUaClientInterface, hasMethod(), hasModule(), getRegisteredMethods(), getLoadedModules(), __call() for custom module methods
- PhpOpcua\Client\OpcUaClientInterface ? public API contract (all built-in service methods + hasMethod(string): bool + hasModule(string): bool + getRegisteredMethods(): string[] + getLoadedModules(): class-string[])
- PhpOpcua\Client\Kernel\ClientKernel ? shared infrastructure for modules (executeWithRetry, ensureConnected, send, receive, createDecoder, dispatch, logContext)
- PhpOpcua\Client\Kernel\ClientKernelInterface ? kernel contract that modules depend on
- PhpOpcua\Client\Kernel\ModuleRegistry ? module lifecycle, topological dependency sort, method conflict detection
- PhpOpcua\Client\Module\ServiceModule ? abstract base class for modules (register, boot, reset, requires)
- PhpOpcua\Client\Types\NodeId ? public readonly: namespaceIndex, identifier, type
- PhpOpcua\Client\Types\Variant ? public readonly: type, value, dimensions
- PhpOpcua\Client\Types\DataValue ? public readonly: statusCode, sourceTimestamp, serverTimestamp; method: getValue()
- PhpOpcua\Client\Types\ReferenceDescription ? public readonly: referenceTypeId, isForward, nodeId, browseName, displayName, nodeClass, typeDefinition
- PhpOpcua\Client\Module\Subscription\SubscriptionResult ? public readonly: subscriptionId, revisedPublishingInterval, revisedLifetimeCount, revisedMaxKeepAliveCount
- PhpOpcua\Client\Module\Subscription\MonitoredItemResult ? public readonly: statusCode, monitoredItemId, revisedSamplingInterval, revisedQueueSize
- PhpOpcua\Client\Module\ReadWrite\CallResult ? public readonly: statusCode, inputArgumentResults, outputArguments
- PhpOpcua\Client\Module\Subscription\PublishResult ? public readonly: subscriptionId, sequenceNumber, moreNotifications, notifications
- PhpOpcua\Client\Module\Subscription\TransferResult ? public readonly: statusCode, availableSequenceNumbers
- PhpOpcua\Client\Module\Browse\BrowseResultSet ? public readonly: references, continuationPoint
- PhpOpcua\Client\Module\TranslateBrowsePath\BrowsePathResult ? public readonly: statusCode, targets
- PhpOpcua\Client\Types\BrowsePathTarget ? public readonly: targetId, remainingPathIndex
- PhpOpcua\Client\Module\NodeManagement\AddNodesResult ? public readonly: statusCode, addedNodeId (NodeId)
- PhpOpcua\Client\Module\ServerInfo\BuildInfo ? public readonly: productName, manufacturerName, softwareVersion, buildNumber, buildDate
- PhpOpcua\Client\Event\NullEventDispatcher ? no-op PSR-14 dispatcher (default, zero overhead)
- PhpOpcua\Client\Exception\ModuleConflictException ? thrown when two modules register the same method name
- PhpOpcua\Client\Exception\MissingModuleDependencyException ? thrown when a module's required dependency is not registered
- PhpOpcua\Client\Exception\WriteTypeDetectionException ? thrown when write type cannot be auto-detected
- PhpOpcua\Client\Exception\WriteTypeMismatchException ? thrown when explicit write type mismatches detected type (public readonly: nodeId, expectedType, givenType)
- PhpOpcua\Client\Event\WriteTypeDetecting ? dispatched before write type detection starts (public readonly: client, nodeId)
- PhpOpcua\Client\Event\WriteTypeDetected ? dispatched after write type detection (public readonly: client, nodeId, detectedType, fromCache)
- PhpOpcua\Client\Event\* ? 42 readonly event classes with $client property (ClientConnecting, ClientConnected, ConnectionFailed, SessionCreated, SessionActivated, SubscriptionCreated, DataChangeReceived, EventNotificationReceived, AlarmEventReceived, AlarmActivated, AlarmDeactivated, NodeValueRead, NodeValueWritten, NodeBrowsed, WriteTypeDetecting, WriteTypeDetected, CacheHit, CacheMiss, RetryAttempt, etc.)
- PhpOpcua\Client\Testing\MockClient ? in-memory test double (static create(), onRead/onWrite/onBrowse/onCall/onResolveNodeId, getCalls/getCallsFor/callCount/resetCalls, hasMethod, hasModule)
- PhpOpcua\Client\Repository\ExtensionObjectRepository ? per-client codec registry (instance-level)
- PhpOpcua\Client\Wire\WireSerializable ? interface (jsonSerialize, fromWireArray, wireTypeId) implemented by every core / module DTO
- PhpOpcua\Client\Wire\WireTypeRegistry ? security gate + encoder/decoder; rejects unregistered __t discriminators at decode time, uniform for BackedEnum + pure UnitEnum + DateTimeImmutable
- PhpOpcua\Client\Wire\CoreWireTypes ? installs cross-cutting core types (NodeId, QualifiedName, LocalizedText, DataValue, Variant, ExtensionObject, BrowseNode, ReferenceDescription, EndpointDescription, UserTokenPolicy + 4 enums) on a WireTypeRegistry
- PhpOpcua\Client\Encoding\DynamicCodec ? auto-generated codec from StructureDefinition
- PhpOpcua\Client\Encoding\DataTypeMapping ? maps DataType NodeIds to BuiltinTypes
- PhpOpcua\Client\Encoding\StructureDefinitionParser ? parses DataTypeDefinition attributes
- PhpOpcua\Client\Types\StructureField ? public readonly: name, dataType, valueRank, builtinType, isOptional
- PhpOpcua\Client\Types\StructureDefinition ? public readonly: defaultEncodingId, baseDataType, structureType, fields
- PhpOpcua\Client\Types\ExtensionObject ? typed DTO for OPC UA ExtensionObject (public readonly: typeId, encoding, body, value; methods: isDecoded(), isRaw()). DataValue::getValue() auto-extracts decoded value.
- PhpOpcua\Client\Protocol\AbstractProtocolService ? shared base class for protocol services (encodeRequestAuto, writeRequestHeader, readResponseMetadata, wrapInMessage)
- PhpOpcua\Client\Protocol\ServiceTypeId ? final class with named constants for all OPC UA service NodeIds (35 constants)
## Related Packages
- php-opcua/opcua-client-nodeset: pre-generated PHP types from 51 OPC Foundation companion specifications ? 807 files, 338 enums, 191 DTOs, 191 codecs, registrars with automatic dependency resolution
- php-opcua/opcua-session-manager: session persistence across PHP requests
- php-opcua/laravel-opcua: Laravel integration (service provider, facade, config)
- php-opcua/uanetstandard-test-suite: Docker-based OPC UA test servers (UA-.NETStandard)
## Alternatives
PHP: techdock/opcua (PHP 8.4+, heavier deps, v0.2), techdock/opcua-webapi-client (HTTP gateway), QuickOPC (commercial, Windows COM)
Cross-language: node-opcua (TS), opcua-asyncio (Python), UA-.NETStandard (C#), gopcua (Go), open62541 (C)
## Links
- Repository: https://github.com/php-opcua/opcua-client
- Documentation: https://github.com/php-opcua/opcua-client/tree/master/doc
- Issues: https://github.com/php-opcua/opcua-client/issues
- Packagist: https://packagist.org/packages/php-opcua/opcua-client
- Changelog: https://github.com/php-opcua/opcua-client/blob/master/CHANGELOG.md
|