Task-oriented recipes for AI coding assistants. Feed this file to your AI (Claude, Cursor, Copilot, GPT, etc.) so it knows how to use the php-opcua ecosystem correctly.
Add this file to your AI assistant's context:
- Claude Code: copy to your project's
CLAUDE.mdor reference via--add-file - Cursor: add to
.cursor/rules/or.cursorrules - GitHub Copilot: add to
.github/copilot-instructions.md - Other tools: paste into system prompt or project context
| Package | Install | Purpose |
|---|---|---|
php-opcua/opcua-client |
composer require php-opcua/opcua-client |
Core OPC UA client — required |
php-opcua/opcua-session-manager |
composer require php-opcua/opcua-session-manager |
Session persistence daemon — optional, for multi-request apps |
php-opcua/laravel-opcua |
composer require php-opcua/laravel-opcua |
Laravel integration — optional, provides Facade + config |
php-opcua/opcua-cli |
composer require php-opcua/opcua-cli |
CLI tool — optional, for terminal operations |
php-opcua/opcua-client-nodeset |
composer require php-opcua/opcua-client-nodeset |
Pre-built OPC UA companion types — optional |
Requirements: PHP >= 8.2, ext-openssl. No other extensions needed.
The user wants to connect to a PLC, SCADA system, sensor, or any OPC UA-compliant device.
use PhpOpcua\Client\ClientBuilder;
// Minimal — no security
$client = ClientBuilder::create()
->connect('opc.tcp://192.168.1.100:4840');
// ... do operations ...
$client->disconnect();- Always call
disconnect()when done (or use try/finally) - The endpoint format is always
opc.tcp://host:port - Default OPC UA port is 4840
ClientBuilder::create()is the only entry point — never instantiateClientdirectly- All configuration must happen BEFORE
connect()— the client is immutable after connection
The user needs encrypted connections, username/password, or certificate-based authentication.
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\Security\SecurityPolicy;
use PhpOpcua\Client\Security\SecurityMode;
// Username/password with encryption
$client = ClientBuilder::create()
->setSecurityPolicy(SecurityPolicy::Basic256Sha256)
->setSecurityMode(SecurityMode::SignAndEncrypt)
->setUserCredentials('operator', 'secret')
->connect('opc.tcp://192.168.1.100:4840');
// With explicit client certificate
$client = ClientBuilder::create()
->setSecurityPolicy(SecurityPolicy::Basic256Sha256)
->setSecurityMode(SecurityMode::SignAndEncrypt)
->setClientCertificate('/certs/client.pem', '/certs/client.key', '/certs/ca.pem')
->setUserCredentials('operator', 'secret')
->connect('opc.tcp://192.168.1.100:4840');
// X.509 certificate authentication (no password)
$client = ClientBuilder::create()
->setSecurityPolicy(SecurityPolicy::Basic256Sha256)
->setSecurityMode(SecurityMode::SignAndEncrypt)
->setClientCertificate('/certs/client.pem', '/certs/client.key')
->setUserCertificate('/certs/user.pem', '/certs/user.key')
->connect('opc.tcp://192.168.1.100:4840');
// ECC security — NIST (auto-generates ECC certificate if none provided)
$client = ClientBuilder::create()
->setSecurityPolicy(SecurityPolicy::EccNistP256)
->setSecurityMode(SecurityMode::SignAndEncrypt)
->setUserCredentials('admin', 'admin123')
->connect('opc.tcp://192.168.1.100:4848');
// ECC security — Brainpool (European alternative to NIST)
$client = ClientBuilder::create()
->setSecurityPolicy(SecurityPolicy::EccBrainpoolP256r1)
->setSecurityMode(SecurityMode::SignAndEncrypt)
->connect('opc.tcp://192.168.1.100:4849');- If
setClientCertificate()is omitted but security policy/mode are set, a self-signed cert is auto-generated in memory (RSA for RSA policies, ECC for ECC policies — good for testing, not production) - RSA policies:
None,Basic128Rsa15,Basic256,Basic256Sha256,Aes128Sha256RsaOaep,Aes256Sha256RsaPss - ECC policies:
EccNistP256,EccNistP384,EccBrainpoolP256r1,EccBrainpoolP384r1 - Available modes:
None,Sign,SignAndEncrypt - For production use
Basic256Sha256,Aes256Sha256RsaPss, or any ECC policy. Choose NIST for interoperability, Brainpool for EU regulatory compliance (BSI TR-03116) - Auth methods are: anonymous (default), username/password (
setUserCredentials), X.509 certificate (setUserCertificate) - ECC policies use ECDH key agreement instead of RSA encryption; password authentication uses EccEncryptedSecret protocol automatically
The user wants to read process variables — temperatures, pressures, motor speeds, counters, status values, etc.
use PhpOpcua\Client\Types\NodeId;
// Read a single value — string NodeId format
$dv = $client->read('i=2259'); // ServerState
$dv = $client->read('ns=2;i=1001'); // Namespace 2, numeric ID
$dv = $client->read('ns=2;s=Temperature'); // Namespace 2, string ID
// Access the result
echo $dv->getValue(); // The actual value (unwrapped from Variant)
echo $dv->statusCode; // 0 = Good
echo $dv->sourceTimestamp; // DateTimeImmutable or null
// Read multiple values — fluent builder (preferred)
$results = $client->readMulti()
->node('i=2259')->value()
->node('ns=2;i=1001')->displayName()
->node('ns=2;s=Temperature')->value()
->execute();
foreach ($results as $dv) {
echo $dv->getValue() . "\n";
}
// Read multiple values — array syntax
$results = $client->readMulti([
['nodeId' => 'i=2259'],
['nodeId' => 'ns=2;i=1001'],
]);- All methods accept string NodeIds (
'i=2259','ns=2;s=MyNode') ORNodeIdobjects — use strings for simplicity getValue()unwraps the Variant and returns the PHP-native value- Always check
$dv->statusCode—0means Good, non-zero means the read failed or the value is uncertain - Use
StatusCode::isGood($dv->statusCode)for proper status checking - Common well-known nodes:
i=2259(ServerState),i=2258(CurrentTime),i=2256(ServerStatus),i=85(Objects folder) - Use
getServerBuildInfo()to read all server build metadata in one call (see Server BuildInfo skill below)
The user wants to identify the OPC UA server — product name, manufacturer, version, build number, or build date.
// 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 (one server read each)
$client->getServerProductName(); // ?string
$client->getServerManufacturerName(); // ?string
$client->getServerSoftwareVersion(); // ?string
$client->getServerBuildNumber(); // ?string
$client->getServerBuildDate(); // ?DateTimeImmutable- These nodes are mandatory on every OPC UA server — they always exist
getServerBuildInfo()is more efficient than calling individual methods — it reads all 5 nodes in a singlereadMulti()request- Returns
nullfor any field the server does not populate BuildInfois areadonlyDTO with 5 public properties
The user wants to dynamically create or delete nodes and references in the server's address space — e.g., creating temporary variables, folders, or configuring custom structures at runtime.
use PhpOpcua\Client\Types\NodeClass;
use PhpOpcua\Client\Types\QualifiedName;
use PhpOpcua\Client\Types\NodeId;
use PhpOpcua\Client\Types\StatusCode;
// Add a Variable node
$results = $client->addNodes([
[
'parentNodeId' => 'i=85', // Objects folder
'referenceTypeId' => 'i=35', // Organizes
'requestedNewNodeId' => 'ns=2;s=MyVariable',
'browseName' => new QualifiedName(2, 'MyVariable'),
'nodeClass' => NodeClass::Variable,
'typeDefinition' => 'i=63', // BaseDataVariableType
'dataType' => NodeId::numeric(0, 6), // Int32
'accessLevel' => 3, // CurrentRead | CurrentWrite
],
]);
if (StatusCode::isGood($results[0]->statusCode)) {
echo "Created: {$results[0]->addedNodeId}\n";
}
// Delete nodes
$statusCodes = $client->deleteNodes([
['nodeId' => 'ns=2;s=MyVariable', 'deleteTargetReferences' => true],
]);
// Add a reference between existing nodes
$statusCodes = $client->addReferences([
[
'sourceNodeId' => 'ns=2;s=FolderA',
'referenceTypeId' => NodeId::numeric(0, 35), // Organizes
'isForward' => true,
'targetNodeId' => 'ns=2;s=NodeB',
'targetNodeClass' => NodeClass::Variable,
],
]);
// Delete a reference
$statusCodes = $client->deleteReferences([
[
'sourceNodeId' => 'ns=2;s=FolderA',
'referenceTypeId' => NodeId::numeric(0, 35),
'isForward' => true,
'targetNodeId' => 'ns=2;s=NodeB',
'deleteBidirectional' => true,
],
]);NodeManagementModuleis inClientBuilder::defaultModules()— no opt-in needed. The builder does not probe the server at connect time.- Not all servers implement node management. Servers that do not (e.g. UA-.NETStandard) reply with a top-level
ServiceFaultcarryingBadServiceUnsupported (0x800B0000). The client surfaces this asException\ServiceUnsupportedException(subclass ofServiceException) on the first call toaddNodes()/deleteNodes()/addReferences()/deleteReferences(). CatchServiceUnsupportedExceptionif you want a capability-specific fallback; existingServiceExceptionhandlers still match. - All 8 node classes are supported: Object, Variable, Method, ObjectType, VariableType, ReferenceType, DataType, View.
- Class-specific attributes (e.g.,
dataType,accessLevelfor Variable;executablefor Method) are encoded automatically. addNodes()returnsAddNodesResult[]— each result hasstatusCodeandaddedNodeId.deleteNodes(),addReferences(),deleteReferences()returnint[]status codes.- If
displayNameis omitted, the browse name is used automatically. - Always clean up created nodes in tests/temporary scenarios.
- String NodeIds work everywhere:
'i=85','ns=2;s=MyNode'.
The user wants to write setpoints, commands, or any value to a PLC or OPC UA server.
use PhpOpcua\Client\Types\BuiltinType;
use PhpOpcua\Client\Types\StatusCode;
// Auto-detect type (recommended) — reads the node's DataType first, caches it
$status = $client->write('ns=2;i=1001', 42);
// Explicit type — when you know the type and want to skip the extra read
$status = $client->write('ns=2;i=1001', 42, BuiltinType::Int32);
// Check result
if (StatusCode::isGood($status)) {
echo "Write successful";
}
// Write multiple values — fluent builder
$results = $client->writeMulti()
->node('ns=2;i=1001')->int32(42)
->node('ns=2;i=1002')->double(3.14)
->node('ns=2;s=Label')->string('active')
->execute();
// Write multiple values — auto-detect types
$results = $client->writeMulti()
->node('ns=2;i=1001')->value(42)
->node('ns=2;i=1002')->value(3.14)
->execute();- Auto-detect (
write($nodeId, $value)without type) is the default and recommended approach — it reads the node's DataType, caches it, and writes with the correct type - If you know the type, pass it explicitly to avoid the extra read:
write($nodeId, $value, BuiltinType::Int32) - Common BuiltinType values:
Boolean,Int16,Int32,Int64,UInt16,UInt32,UInt64,Float,Double,String,DateTime,ByteString - The return value is a status code integer — use
StatusCode::isGood()to check - Write failures typically return
StatusCode::BadNotWritableorStatusCode::BadTypeMismatch
The user wants to discover what nodes, variables, objects, or methods are available on a server.
use PhpOpcua\Client\Types\NodeClass;
// Browse a folder
$refs = $client->browse('i=85'); // Objects folder
foreach ($refs as $ref) {
echo "{$ref->displayName} ({$ref->nodeId}) [{$ref->nodeClass->name}]\n";
}
// Browse with node class filter — only variables
$refs = $client->browse('i=85', nodeClasses: [NodeClass::Variable]);
// Browse with node class filter — only objects and variables
$refs = $client->browse('i=85', nodeClasses: [NodeClass::Object, NodeClass::Variable]);
// Browse all (automatic continuation point handling)
$allRefs = $client->browseAll('i=85');
// Recursive browse — returns a tree
$tree = $client->browseRecursive('i=85', maxDepth: 3);
foreach ($tree as $node) {
echo $node->reference->displayName . "\n";
foreach ($node->children as $child) {
echo " " . $child->reference->displayName . "\n";
}
}
// Resolve a human-readable path to a NodeId
$nodeId = $client->resolveNodeId('/Objects/MyPLC/Temperature');
$value = $client->read($nodeId);i=85is the Objects folder — the standard starting point for browsingi=84is the Root folder (parent of Objects, Types, Views)browse()returnsReferenceDescription[]with properties:nodeId,displayName,browseName,nodeClass,isForward,referenceTypeId,typeDefinitionbrowseRecursive()returnsBrowseNode[]— each hasreferenceandchildren- Browse results are cached by default — use
useCache: falseto bypass resolveNodeId()translates paths like/Objects/Server/ServerStatusto NodeId objects
The user wants to invoke OPC UA methods — trigger operations, run diagnostics, execute PLC commands.
use PhpOpcua\Client\Types\Variant;
use PhpOpcua\Client\Types\BuiltinType;
use PhpOpcua\Client\Types\StatusCode;
$result = $client->call(
'i=2253', // Parent object NodeId (where the method lives)
'i=11492', // Method NodeId
[ // Input arguments as Variant array
new Variant(BuiltinType::UInt32, 1),
],
);
if (StatusCode::isGood($result->statusCode)) {
echo $result->outputArguments[0]->value; // Access output arguments
}
// Method with multiple arguments
$result = $client->call(
'ns=2;i=100',
'ns=2;i=200',
[
new Variant(BuiltinType::Double, 3.0),
new Variant(BuiltinType::Double, 4.0),
],
);- The first argument is the parent object NodeId, the second is the method NodeId
- Input arguments must be wrapped in
Variantobjects with explicit types $resultis aCallResultDTO withstatusCode,inputArgumentResults, andoutputArguments- Output arguments are
Variantobjects — access values via->value
The user wants to be notified when sensor values change, monitor variables in real time, or watch for events.
use PhpOpcua\Client\Types\NodeId;
// 1. Create a subscription
$sub = $client->createSubscription(publishingInterval: 500.0); // 500ms
// 2. Add monitored items
$monitored = $client->createMonitoredItems($sub->subscriptionId, [
['nodeId' => 'ns=2;i=1001', 'clientHandle' => 1],
['nodeId' => 'ns=2;i=1002', 'clientHandle' => 2],
]);
// Or use the fluent builder
$monitored = $client->createMonitoredItems($sub->subscriptionId)
->item('ns=2;i=1001', clientHandle: 1)
->item('ns=2;i=1002', clientHandle: 2)
->execute();
// 3. Poll for notifications
$response = $client->publish();
foreach ($response->notifications as $notif) {
echo "Handle {$notif->clientHandle}: {$notif->dataValue->getValue()}\n";
}
// 4. Clean up
$client->deleteSubscription($sub->subscriptionId);- Subscriptions require a polling loop — call
publish()repeatedly to receive notifications - In standard PHP (request/response), the subscription dies with the process. Use
opcua-session-managerfor persistent subscriptions across requests createSubscription()returns aSubscriptionResultwithsubscriptionIdcreateMonitoredItems()returnsMonitoredItemResult[]withmonitoredItemId- Always
deleteSubscription()before disconnecting to clean up server resources publishingIntervalis in milliseconds
The user wants to pull past values — trend analysis, logs, aggregated statistics from OPC UA historians.
// Raw historical values
$values = $client->historyReadRaw(
'ns=2;i=1001',
startTime: new \DateTimeImmutable('-1 hour'),
endTime: new \DateTimeImmutable(),
);
foreach ($values as $dv) {
echo "[{$dv->sourceTimestamp->format('H:i:s')}] {$dv->getValue()}\n";
}
// Aggregated (processed) — e.g., average over 1-minute intervals
$values = $client->historyReadProcessed(
'ns=2;i=1001',
startTime: new \DateTimeImmutable('-1 hour'),
endTime: new \DateTimeImmutable(),
processingInterval: 60000.0, // 60 seconds in ms
aggregateType: NodeId::numeric(0, 2341), // Average
);
// Values at specific timestamps
$values = $client->historyReadAtTime('ns=2;i=1001', [
new \DateTimeImmutable('-30 minutes'),
new \DateTimeImmutable('-15 minutes'),
new \DateTimeImmutable('now'),
]);- Not all OPC UA servers support history — the server must have a historian configured
- Common aggregate type NodeIds:
2341(Average),2342(Interpolative),2346(Minimum),2347(Maximum),2352(Count) processingIntervalis in milliseconds- Returns
DataValue[]— same format as regular reads
The user connects to a server for the first time and needs to handle certificate trust, or wants TOFU (Trust On First Use).
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\TrustStore\FileTrustStore;
use PhpOpcua\Client\TrustStore\TrustPolicy;
use PhpOpcua\Client\Exception\UntrustedCertificateException;
// Auto-accept on first use (TOFU) — good for development
$client = ClientBuilder::create()
->setTrustStore(new FileTrustStore()) // ~/.opcua/trusted/
->autoAccept(true)
->connect('opc.tcp://192.168.1.100:4840');
// Strict trust — reject unknown certificates
$client = ClientBuilder::create()
->setTrustStore(new FileTrustStore('/var/opcua/trust'))
->setTrustPolicy(TrustPolicy::Fingerprint)
->connect('opc.tcp://192.168.1.100:4840');
// throws UntrustedCertificateException if cert not in store
// Handle untrusted certificate interactively
try {
$client = ClientBuilder::create()
->setTrustStore(new FileTrustStore())
->setTrustPolicy(TrustPolicy::Fingerprint)
->connect('opc.tcp://192.168.1.100:4840');
} catch (UntrustedCertificateException $e) {
echo "Fingerprint: " . $e->getFingerprint() . "\n";
// Trust it programmatically:
$client->trustCertificate($e->getCertificate());
}
// Disable trust validation entirely
$client = ClientBuilder::create()
->setTrustPolicy(null)
->connect('opc.tcp://192.168.1.100:4840');- Trust policies:
Fingerprint(match SHA-256 only),FingerprintAndExpiry(+ expiry check),Full(full CA chain validation) setTrustPolicy(null)disables validation — this is the default (no trust store)- TOFU (
autoAccept(true)) is vulnerable on first connection — use for dev only autoAccept(true, force: true)also accepts changed certificates — very insecure
The user has a web application (Laravel, Symfony, plain PHP) and wants to avoid the 50-200ms OPC UA handshake on every HTTP request.
composer require php-opcua/opcua-session-manager# 1. Start the daemon (separate terminal or Supervisor/systemd)
php vendor/bin/opcua-session-manageruse PhpOpcua\SessionManager\Client\ManagedClient;
// 2. Use ManagedClient instead of ClientBuilder — same API
$client = new ManagedClient();
$client->connect('opc.tcp://192.168.1.100:4840');
$value = $client->read('i=2259');
echo $value->getValue();
// Do NOT disconnect if you want the session to persist!
// The daemon keeps it alive.
// In the next request — the session is automatically reused (~5ms instead of ~155ms)
$client = new ManagedClient();
$client->connect('opc.tcp://192.168.1.100:4840');
$client->wasSessionReused(); // trueManagedClientimplements the sameOpcUaClientInterfaceas the directClient— it's a drop-in replacement- The daemon creates a Unix socket at
/tmp/opcua-session-manager.sockby default - If the daemon is not running,
ManagedClientwill throw aDaemonException— there is no automatic fallback to direct connections (uselaravel-opcuafor that) - Sessions expire after
--timeoutseconds of inactivity (default: 600) - Call
disconnect()only when you want to explicitly close the session - For production use Supervisor or systemd to keep the daemon running
- Use
--auth-tokenorOPCUA_AUTH_TOKENenv var for IPC security
The user has a Laravel application and wants OPC UA with Facade, .env config, named connections, and automatic session manager integration.
composer require php-opcua/laravel-opcua
php artisan vendor:publish --tag=opcua-config# .env
OPCUA_ENDPOINT=opc.tcp://192.168.1.100:4840use PhpOpcua\LaravelOpcua\Facades\Opcua;
// Connect and read
$client = Opcua::connect();
$value = $client->read('i=2259');
echo $value->getValue();
$client->disconnect();
// Named connections (defined in config/opcua.php)
$plc1 = Opcua::connect('plc-line-1');
$plc2 = Opcua::connect('plc-line-2');
Opcua::disconnectAll();
// Ad-hoc connection
$client = Opcua::connectTo('opc.tcp://10.0.0.50:4840', [
'username' => 'operator',
'password' => 'secret',
]);
// Session manager integration — automatic
// If the daemon is running, connections persist across requests.
// If not, direct connections are used. Zero code changes.
php artisan opcua:session # start daemon- The
OpcuaFacade proxies toOpcuaManager— it manages multiple named connections - Config follows Laravel conventions:
config/opcua.phpwith.envvariables - The package auto-detects the session manager daemon — if its socket exists,
ManagedClientis used; otherwise, directClient - Named connections work like
config/database.php— define multiple servers inconnectionsarray - Logger and cache are automatically injected from Laravel's service container
The user wants to unit test code that uses OPC UA without a physical PLC or server.
use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use PhpOpcua\Client\Types\StatusCode;
// Create a mock client
$mock = MockClient::create();
// Register handlers
$mock->onRead(function ($nodeId) {
return match ((string) $nodeId) {
'ns=2;s=Temperature' => DataValue::ofDouble(23.5),
'i=2259' => DataValue::ofInt32(0),
default => DataValue::bad(StatusCode::BadNodeIdUnknown),
};
});
$mock->onWrite(function ($nodeId, $value) {
return StatusCode::Good;
});
$mock->onBrowse(function ($nodeId) {
return []; // empty reference list
});
// Use the same API as a real client
$value = $mock->read('ns=2;s=Temperature');
echo $value->getValue(); // 23.5
// Assert calls
echo $mock->callCount('read'); // 1
$mock->resetCalls();DataValue::ofBoolean(true);
DataValue::ofInt32(42);
DataValue::ofUInt32(100);
DataValue::ofDouble(3.14);
DataValue::ofFloat(2.5);
DataValue::ofString('hello');
DataValue::of($value, BuiltinType::Int32);
DataValue::bad(StatusCode::BadNodeIdUnknown); // bad statusMockClientimplementsOpcUaClientInterface— it can be injected anywhere a real client is expected- Handlers:
onRead(),onWrite(),onBrowse(),onCall(),onResolveNodeId(),onGetEndpoints() - Call tracking:
getCalls(),getCallsFor($method),callCount($method),resetCalls() - Fluent builders (
readMulti(),writeMulti()) work with MockClient
The user reads structured values from a server and gets raw bytes or arrays instead of typed data.
// Auto-discover all custom types from the server
$client->discoverDataTypes();
// Now structured reads return decoded values
$point = $client->read('ns=2;s=MyPoint')->getValue();
// ['x' => 1.5, 'y' => 2.5, 'z' => 3.5]
// Or use pre-built companion types (e.g., Robotics, DI, Machinery)
// composer require php-opcua/opcua-client-nodeset
use PhpOpcua\Nodeset\Robotics\RoboticsRegistrar;
$client = ClientBuilder::create()
->loadGeneratedTypes(new RoboticsRegistrar())
->connect('opc.tcp://192.168.1.100:4840');- Call
discoverDataTypes()AFTER connecting — it queries the server's type system - For custom codecs, implement
ExtensionObjectCodecand register viaExtensionObjectRepository - Each client has its own isolated codec registry — no global state
opcua-client-nodesetprovides pre-generated types for 51 OPC Foundation companion specs
The user wants to debug OPC UA communication or optimize performance with caching.
use PhpOpcua\Client\ClientBuilder;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Add PSR-3 logging
$logger = new Logger('opcua');
$logger->pushHandler(new StreamHandler('php://stderr', Logger::DEBUG));
$client = ClientBuilder::create()
->setLogger($logger)
->connect('opc.tcp://localhost:4840');
// Logs: handshake, secure channel, reads, retries, errors...
// Cache configuration
use PhpOpcua\Client\Cache\FileCache;
$client = ClientBuilder::create()
->setCache(new FileCache('/tmp/opcua-cache', 600)) // 600s TTL
->connect('opc.tcp://localhost:4840');
// Per-call cache control
$refs = $client->browse('i=85', useCache: false); // skip cache
$client->invalidateCache('i=85'); // clear one node
$client->flushCache(); // clear all- Any PSR-3 logger works (Monolog, Laravel's logger, etc.)
- Without a logger, a
NullLoggeris used (zero overhead) - Browse, resolve, endpoint, and discovery results are cached by default (
InMemoryCache, 300s TTL) - Any PSR-16 cache driver works (
InMemoryCache,FileCache, Laravel Cache, Redis) - Read values are NEVER cached — only metadata and browse results
setReadMetadataCache(true)enables caching of node metadata (DataType, DisplayName) — not values- Cached values go through
Cache\WireCacheCodec— JSON-only, gated byWire\WireTypeRegistry(nounserialize()anywhere on the cache path). Corrupted or unknown payloads raiseException\CacheCorruptedExceptionand are treated as cache misses. Swap the codec withClientBuilder::setCacheCodec(?CacheCodecInterface)if you need a custom on-disk format. Pre-v4.3.0 cache entries are discarded on first access — flush persistent caches on upgrade.
The user wants to react to OPC UA lifecycle events — log connections, track reads/writes, handle alarms.
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\Event\DataChangeReceived;
use PhpOpcua\Client\Event\AlarmActivated;
use PhpOpcua\Client\Event\Connected;
$client = ClientBuilder::create()
->setEventDispatcher($yourPsr14Dispatcher)
->connect('opc.tcp://localhost:4840');
// In your listener:
class HandleDataChange {
public function __invoke(DataChangeReceived $event): void {
echo "Value changed on sub {$event->subscriptionId}: "
. $event->dataValue->getValue() . "\n";
}
}
// Laravel integration:
// In EventServiceProvider:
protected $listen = [
\PhpOpcua\Client\Event\AfterRead::class => [LogOpcuaReads::class],
\PhpOpcua\Client\Event\AlarmActivated::class => [HandleAlarm::class],
];- 47 event types covering: connection, session, subscription, data change, alarms, read/write, browse, cache, retry, trust store
- Zero overhead when no dispatcher is set (
NullEventDispatcher) - All events are readonly DTOs with a
$clientproperty - Any PSR-14
EventDispatcherInterfaceworks
The user needs to tune connection behavior — slow networks, unreliable connections, large read/write operations.
$client = ClientBuilder::create()
->setTimeout(10.0) // 10 seconds network timeout
->setAutoRetry(3) // retry up to 3 times on failure
->setBatchSize(100) // max 100 nodes per read/write batch
->setDefaultBrowseMaxDepth(20) // browseRecursive depth limit
->connect('opc.tcp://192.168.1.100:4840');setTimeout()is in seconds (float)setAutoRetry(n)automatically reconnects and retries onConnectionExceptionsetBatchSize(n)splits largereadMulti/writeMultioperations transparently- The client auto-discovers server limits (
MaxNodesPerRead,MaxNodesPerWrite) and respects them - All configuration must happen before
connect()— immutable after connection
The user wants to discover what security configurations a server supports before connecting.
$client = ClientBuilder::create()
->connect('opc.tcp://192.168.1.100:4840');
$endpoints = $client->getEndpoints('opc.tcp://192.168.1.100:4840');
foreach ($endpoints as $ep) {
echo "{$ep->securityPolicyUri}\n";
echo " Mode: {$ep->securityMode->name}\n";
echo " Auth: " . implode(', ', array_map(
fn($t) => $t->tokenType->name,
$ep->userIdentityTokens
)) . "\n";
}The user wants to add a new OPC UA service set to the client that is not provided by the 8 built-in modules -- e.g., QueryFirst/QueryNext, custom vendor-specific services, or domain-specific operations.
use PhpOpcua\Client\Module\ServiceModule;
use PhpOpcua\Client\Kernel\ClientKernelInterface;
use PhpOpcua\Client\Module\ReadWrite\ReadWriteModule;
class MyQueryModule extends ServiceModule
{
private ?MyQueryService $queryService = null;
public function requires(): array
{
return [ReadWriteModule::class]; // declare dependencies
}
public function register(): void
{
$this->client->registerMethod('queryFirst', $this->queryFirst(...));
$this->client->registerMethod('queryNext', $this->queryNext(...));
}
public function boot(): void
{
$this->queryService = new MyQueryService($this->kernel->getSessionService());
}
public function reset(): void
{
$this->queryService = null;
}
public function queryFirst(array $filter): array
{
return $this->kernel->executeWithRetry(function () use ($filter) {
$this->kernel->ensureConnected();
$request = $this->queryService->encodeQueryFirstRequest($filter);
$this->kernel->send($request);
$response = $this->kernel->receive();
return $this->queryService->decodeQueryFirstResponse($response);
});
}
public function queryNext(string $continuationPoint): array
{
// similar pattern
}
}Register it on the builder:
use PhpOpcua\Client\ClientBuilder;
$client = ClientBuilder::create()
->addModule(new MyQueryModule())
->connect('opc.tcp://localhost:4840');
// Custom methods accessible via __call()
$results = $client->queryFirst(['nodeClass' => NodeClass::Variable]);- Extend
ServiceModuleand implementregister(),boot(),reset() - Use
$this->client->registerMethod()inregister()to inject methods onto the Client - Use
$this->kernelto access shared infrastructure (send, receive, retry, etc.) - Declare dependencies via
requires()-- the registry resolves them in order - Custom module methods are accessible via
__call()(not typed on the interface) - If two modules register the same method name,
ModuleConflictExceptionis thrown -- usereplaceModule()instead - Module DTOs and protocol services should live alongside the module class for co-location
The user wants to swap a built-in module with a custom implementation -- e.g., adding custom retry logic to reads, logging all writes to a database, or implementing a custom browse strategy.
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\Module\ReadWrite\ReadWriteModule;
use PhpOpcua\Client\Module\ServiceModule;
class MyCustomReadWriteModule extends ReadWriteModule
{
public function register(): void
{
parent::register(); // keep all built-in methods
// Override just the read method
$this->client->registerMethod('read', $this->customRead(...));
}
public function customRead(string|NodeId $nodeId, int $attributeId = 13, bool $refresh = false): DataValue
{
// Custom logic before the read
Log::info("Reading {$nodeId}");
// Call the kernel to do the actual read
$result = parent::read($nodeId, $attributeId, $refresh);
// Custom logic after the read
MyAuditLog::record('read', $nodeId, $result->statusCode);
return $result;
}
}
$client = ClientBuilder::create()
->replaceModule(ReadWriteModule::class, new MyCustomReadWriteModule())
->connect('opc.tcp://localhost:4840');
// All modules that call $this->client->read() automatically use the replacement
$value = $client->read('i=2259'); // uses MyCustomReadWriteModulereplaceModule()swaps a built-in module class with your custom implementation- Your replacement must provide the same methods -- other modules may depend on them
- Extend the original module class when you only want to override specific behavior
- All other modules that call
$this->client->read()automatically use the replacement -- no additional wiring needed - You cannot use
addModule()if your module has the same method names as an existing one -- usereplaceModule()instead hasModule(ReadWriteModule::class)returnsfalseafter replacement;hasModule(MyCustomReadWriteModule::class)returnstrue- Check module availability with
hasMethod()andhasModule()for defensive code
// WRONG — Client is immutable after connect()
$client = ClientBuilder::create()->connect('opc.tcp://...');
$client->setTimeout(10.0); // This method doesn't exist on Client
// CORRECT
$client = ClientBuilder::create()
->setTimeout(10.0)
->connect('opc.tcp://...');// WRONG — leaks connections
$client = ClientBuilder::create()->connect('opc.tcp://...');
$value = $client->read('i=2259');
// script ends without disconnect
// CORRECT
$client = ClientBuilder::create()->connect('opc.tcp://...');
try {
$value = $client->read('i=2259');
} finally {
$client->disconnect();
}// WRONG — old array access style
$sub = $client->createSubscription(500.0);
echo $sub['subscriptionId'];
// CORRECT — public readonly properties
$sub = $client->createSubscription(publishingInterval: 500.0);
echo $sub->subscriptionId;// WRONG — assuming success
$dv = $client->read('ns=99;i=99999');
echo $dv->getValue(); // could be null if node doesn't exist
// CORRECT — check status
$dv = $client->read('ns=99;i=99999');
if (StatusCode::isGood($dv->statusCode)) {
echo $dv->getValue();
} else {
echo "Read failed: {$dv->statusCode}";
}// WRONG in a web context — session dies with the request
$client = ClientBuilder::create()->connect('opc.tcp://...');
$sub = $client->createSubscription(500.0);
// next request: subscription is gone
// CORRECT — use session manager for persistent subscriptions
$client = new ManagedClient();
$client->connect('opc.tcp://...');| Format | Example | Meaning |
|---|---|---|
i=<number> |
i=2259 |
Namespace 0, numeric identifier |
ns=<n>;i=<number> |
ns=2;i=1001 |
Namespace n, numeric identifier |
s=<string> |
s=Temperature |
Namespace 0, string identifier |
ns=<n>;s=<string> |
ns=2;s=Temperature |
Namespace n, string identifier |
g=<guid> |
g=12345678-1234-1234-1234-123456789012 |
GUID identifier |
ns=<n>;g=<guid> |
ns=2;g=... |
Namespace n, GUID identifier |
All methods accepting NodeId also accept these string formats.
| Type | PHP Type | Common Use |
|---|---|---|
Boolean |
bool |
Digital I/O, flags |
Int16 |
int |
Small signed integers |
Int32 |
int |
Standard integers, counters |
Int64 |
int |
Large counters |
UInt16 |
int |
Unsigned small integers |
UInt32 |
int |
Status codes, handles |
UInt64 |
int |
Large unsigned values |
Float |
float |
Single-precision measurements |
Double |
float |
High-precision measurements |
String |
string |
Labels, names, descriptions |
DateTime |
DateTimeImmutable |
Timestamps |
ByteString |
string |
Binary data, certificates |
Byte |
int |
Single byte values |
SByte |
int |
Signed single byte |
All exceptions live in PhpOpcua\Client\Exception and extend OpcUaException (which extends \RuntimeException).
| Exception | Parent | When |
|---|---|---|
ConfigurationException |
OpcUaException |
Invalid configuration on ClientBuilder |
ConnectionException |
OpcUaException |
Cannot connect, timeout, network error, disconnect required |
EncodingException |
OpcUaException |
Binary codec encode/decode failure |
InvalidNodeIdException |
OpcUaException |
Malformed NodeId string |
CacheCorruptedException |
OpcUaException |
Cache payload cannot be decoded (treated as cache miss internally; pre-v4.3.0 entries fall here) |
ModuleConflictException |
OpcUaException |
Two modules register the same method (use replaceModule() to swap intentionally) |
MissingModuleDependencyException |
OpcUaException |
A module's requires() lists an unregistered class |
ProtocolException |
OpcUaException |
Generic OPC UA protocol violation |
HandshakeException |
ProtocolException |
Server returned ERR during HEL/ACK (carries $errorCode) |
MessageTypeException |
ProtocolException |
Unexpected message type (carries $expected / $actual) |
SecurityException |
OpcUaException |
Generic crypto failure |
CertificateParseException |
SecurityException |
Missing fields in parsed certificate |
OpenSslException |
SecurityException |
An openssl_* call returned false |
SignatureVerificationException |
SecurityException |
OPN or MSG signature did not verify |
UnsupportedCurveException |
SecurityException |
ECC curve not supported (carries $curveName) |
UntrustedCertificateException |
SecurityException |
Server certificate not in trust store (carries $fingerprint, $certDer) |
ServiceException |
OpcUaException |
Server rejected the request — carries getStatusCode() |
ServiceUnsupportedException |
ServiceException |
Server returned BadServiceUnsupported (0x800B0000) — typical when calling addNodes/deleteNodes/addReferences/deleteReferences on a server that does not implement NodeManagement (e.g. UA-.NETStandard) |
WriteTypeDetectionException |
OpcUaException |
Write type auto-detection failed |
WriteTypeMismatchException |
OpcUaException |
Detected type does not match a value passed to write() (carries $nodeId, $expectedType, $givenType) |
Catching
OpcUaExceptionmatches every exception above. CatchingServiceExceptionmatchesServiceUnsupportedException. CatchingProtocolExceptionmatches bothHandshakeExceptionandMessageTypeException. CatchingSecurityExceptionmatches the 5 security subclasses.
The session-manager daemon's
DaemonExceptionlives in thephp-opcua/opcua-session-managerpackage, not here.