Opcua client
Connect a PHP application to an OPC UA server (industrial automation protocol) using php-opcua/opcua-client v4.5.0 — read, write, browse, call methods, subscribe to data changes, query history, manage trust, and extend with custom modules. Use this skill whenever a task involves OPC UA, opc.tcp://, opc.https://, PLC / SCADA / sensor / historian / DCS integration, Part 6 / Part 4 OPC UA service sets, or the php-opcua ecosystem.From its SKILL.md
npx -y skills add php-opcua/ai-skills --skill opcua-clientAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
16.8 KB, ~4.1k tokens by cl100k_base, as published. Nobody here has run it
php-opcua/opcua-client — v4.5.0 skill
A pure-PHP OPC UA client. Speaks the binary protocol over TCP (and opc.https:// via the optional opcua-client-ext-transport-https extension). Pluggable transport, security, modular service architecture.
When to use this skill
Activate when any of these apply:
- The task mentions OPC UA, PLC, SCADA, historian, DCS, sensor data, industrial automation, IIoT, building automation, robotics, machine tools, or MTConnect bridges
- An endpoint URL starts with
opc.tcp://,opc.https://, oropc.wss:// - A NodeId is mentioned in the form
i=2259,ns=2;s=Temperature,ns=3;g=<guid>, orns=4;b=<base64> - Service set names appear:
Read,Write,Browse,TranslateBrowsePathsToNodeIds,Call,CreateSubscription,CreateMonitoredItems,Publish,HistoryRead,HistoryUpdate,AddNodes,DeleteNodes,AddReferences,DeleteReferences,GetEndpoints, etc. - The user is using or extending any
php-opcua/*package
Do NOT activate for: generic PHP work, web frameworks, databases, or anything unrelated to industrial protocols.
The 60-second mental model
ClientBuilder (config) ─► connect() ─► Client (proxy)
│
▼
ClientKernelInterface
│
┌───────┼───────┐
▼ ▼
ServiceModules ClientTransportInterface
(10 built-in) (TcpTransport default)
ReadWrite, Browse,
Subscription, History,
NodeManagement, Aggregate,
TranslateBrowsePath,
ServerInfo, TypeDiscovery,
FileTransfer
Three things to know:
- One entry point:
ClientBuilder::create()->connect($endpointUrl)returns aClient(which isOpcUaClientInterface). Application code calls service methods on the client. - Service methods accept
NodeId|string:'i=2259','ns=2;s=Temp', orNodeId::numeric(2, 1001)all work. Prefer strings in user-facing code — they are more readable. - Result DTOs are
public readonly: access$result->subscriptionId(NOT$result->getSubscriptionId()or$result['subscriptionId']). Old getter methods are@deprecatedbut still function for back-compat.
Quick start (90% of use cases fit this shape)
use PhpOpcua\Client\ClientBuilder;
$client = ClientBuilder::create()
->connect('opc.tcp://localhost:4840');
// Single read — auto-detect attribute (Value), unwrap value
$temperature = $client->read('ns=2;s=Sensors/Temp')->getValue();
// Multi-read — fluent builder
$results = $client->readMulti()
->node('i=2259')->value() // Server.ServerStatus.State (Int32)
->node('ns=2;s=Sensors/Temp')->value()
->node('ns=2;s=Sensors/Temp')->displayName()
->execute();
// Write — auto-detect type via read-before-write
$client->write('ns=2;s=Setpoint', 42.5);
// Write — explicit type (faster, no round-trip)
use PhpOpcua\Client\Types\BuiltinType;
$client->write('ns=2;s=Setpoint', 42.5, BuiltinType::Double);
// Browse
foreach ($client->browse('i=85') as $ref) {
echo "{$ref->displayName} ({$ref->nodeId})\n";
}
// Subscribe to a data change
use PhpOpcua\Client\Module\Subscription\DataChangeNotification;
$sub = $client->createSubscription(publishingInterval: 500.0);
$client->createMonitoredItems($sub->subscriptionId)
->add('ns=2;s=Sensors/Temp')->samplingInterval(500.0)
->execute();
// PublishResult::$notifications is array<int, DataChangeNotification|EventNotification> — guard by type.
foreach ($client->publish()->notifications as $notif) {
if ($notif instanceof DataChangeNotification) {
echo $notif->dataValue->getValue() . "\n"; // DataChangeNotification: ->clientHandle, ->dataValue
}
// EventNotification carries ->clientHandle and ->eventFields (Variant[]) instead — no ->dataValue.
}
$client->disconnect();
When to load deeper references
| If the task involves... | Read |
|---|---|
| Designing the connection (security policy, auth, certificates, transport, retry, timeouts, trust store) | references/CONNECTION.md |
| Picking the right service method (read, write, browse, call, subscribe, history, node management, aggregates, file transfer) | references/OPERATIONS.md |
| Understanding why this code is structured the way it is (builder, kernel, modules, traits, registry) | references/ARCHITECTURE.md |
Writing or replacing a ServiceModule (custom service set, override built-in behaviour) | references/MODULES.md |
Working with NodeId, Variant, DataValue, ExtensionObject, custom OPC UA types | references/TYPES.md |
| Hooking PSR-14 listeners for observability / audit / metrics | references/EVENTS.md |
Writing tests with MockClient | references/TESTING.md |
| Debugging an unfamiliar error / behaving unexpectedly | references/PITFALLS.md |
| Looking for a complete working example for a specific task | assets/recipes.md |
Core API surface (must-know)
OpcUaClientInterface (src/OpcUaClientInterface.php) is the public contract. Everything Client exposes lives here or is reachable via $client->__call() for custom modules.
| Group | Methods |
|---|---|
| Lifecycle | connect() (on builder), disconnect(), reconnect(), isConnected(), getConnectionState(): ConnectionState |
| Read | read(NodeId|string $id, int $attributeId = AttributeId::Value, bool $refresh = false): DataValue (AttributeId is a class of int constants; AttributeId::Value = 13), readMulti() (builder) |
| Write | write(NodeId|string $id, mixed $value, ?BuiltinType $type = null): int, writeMulti(?array $writeItems = null): array|WriteMultiBuilder (array shape or fluent builder) |
| Browse | browse(NodeId|string $id, BrowseDirection $dir = Forward, ...): ReferenceDescription[], browseAll() (auto-continuation), browseRecursive(NodeId|string $id, BrowseDirection $dir = Forward, ?int $maxDepth = null, ...): BrowseNode[], getEndpoints(), resolveNodeId(string $path, NodeId|string|null $startingNodeId = null, bool $useCache = true): NodeId (throws ServiceException if the path cannot be resolved) |
| Translate browse paths | translateBrowsePaths(?array $browsePaths = null): array|BrowsePathsBuilder (returns BrowsePathResult[], or a BrowsePathsBuilder when called with no args); for a single slash-separated path like "Objects/MyFolder/MyNode" use resolveNodeId(string $path, NodeId|string|null $startingNodeId = null, bool $useCache = true): NodeId |
| Call methods | call(NodeId|string $objectId, NodeId|string $methodId, array $inputArguments = []): CallResult ($inputArguments is Variant[]) |
| Subscriptions | createSubscription(...): SubscriptionResult, createMonitoredItems() (builder), publish(): PublishResult, modifyMonitoredItems(), deleteMonitoredItems(), deleteSubscription(int $subscriptionId): int, transferSubscriptions(), republish() |
| History — Read | historyReadRaw(NodeId|string, DateTimeImmutable $start, DateTimeImmutable $end, ...): DataValue[], historyReadProcessed(), historyReadAtTime() |
| History — Update | historyInsertData(), historyReplaceData(), historyUpdateData(), historyDeleteRawModified(), historyDeleteAtTime(), historyInsertEvent(), historyReplaceEvent(), historyUpdateEvent(), historyDeleteEvent() |
| Aggregate | aggregate(DataValue[], $start, $end, $intervalMs, AggregateFunction, ?AggregateOptions), historyAggregate(NodeId|string, ...) |
| Node management | addNodes(), deleteNodes(), addReferences(), deleteReferences() — return AddNodesResult[] / int[] |
| File transfer | FileTransferModule (OPC UA Part 5): openFile(NodeId|string, OpenFileMode|int): int, readFile(), writeFile(), closeFile(), getFilePosition(), setFilePosition(); FileDirectoryType helpers createDirectory(), createFileInDirectory(): CreateFileResult, deleteFileSystemObject(), moveOrCopyFileSystemObject(): NodeId |
| Server info | getServerBuildInfo(): BuildInfo, getServerProductName(), getServerSoftwareVersion(), getServerBuildNumber(), getServerBuildDate() |
| Type discovery | discoverDataTypes(?int $namespaceIndex = null, bool $useCache = true): int — discovers server-defined structured types, registers dynamic codecs, and returns the count discovered (caching entries via the DiscoveredType cache) |
| Introspection | hasMethod(string): bool, hasModule(class-string): bool, getRegisteredMethods(): string[], getLoadedModules(): class-string[] |
Every multi-operation method (readMulti, writeMulti, createMonitoredItems, translateBrowsePaths) accepts both an array shape and a fluent builder (chainable) form. The builder is friendlier for code generation; both work identically.
What v4.5.0 added on top of v4.4
A security-hardening release, interoperability-tested against UA-.NETStandard in None / Sign / SignAndEncrypt, RSA (Basic256Sha256) and ECC (NIST P-256/P-384, Brainpool P256r1/P384r1).
- Security hardening —
CreateSessionResponse.serverSignatureis now verified (Part 4 §5.6.2 proof of possession), the ECDH ephemeral-key signature is verified for ECC profiles, incoming secure-channel headers (channelId/tokenId/strictly-increasing sequence number, anti-replay) are validated inSecureChannel::processMessage, and trust-store decisions compare the stored DER via SHA-256 instead of relying on SHA-1 alone. Failures throwServiceException/SecurityException/UntrustedCertificateException. - ApplicationUri binding — on secure connections the server certificate's SAN
ApplicationUrimust match the endpoint'sApplicationDescription. Configurable viaClientBuilder::verifyApplicationUri(bool $enabled = true)(defaulttrue).EndpointDescriptiongains a nullableapplicationUriproperty. - PHPStan level 9 on
src/— run viacomposer phpstan, no baseline and no@phpstan-ignorecomments (treatPhpDocTypesAsCertain: false). - Value objects replace internal associative arrays —
Module\NodeManagement\AddNodeItem,Module\TranslateBrowsePath\BrowsePath/RelativePathElement,Module\TypeDiscovery\DiscoveredType(wire-serializable cached discovery entries), plus the newfinal readonlynotification objectsModule\Subscription\DataChangeNotification/EventNotification. The publicaddNodes()/translateBrowsePaths()still accept arrays and convert internally. - BREAKING —
PublishResult::$notificationsnow holds objects —array<int, DataChangeNotification|EventNotification>instead of['type' => …, 'clientHandle' => …]arrays. Discriminate withinstanceof, access via properties ($n->clientHandle,$n->dataValue,$n->eventFields). - BREAKING — the wire DTOs are now
final—NodeId,QualifiedName,LocalizedText,DataValue,Variant,ExtensionObject,EndpointDescription,ReferenceDescription,BrowseNode,StructureDefinition,StructureField,UserTokenPolicy, and module result DTOs (PublishResult,CallResult,BrowseResultSet, …).ClientBuilder::__constructisfinalto makeClientBuilder::create()(new static) safe. Variant::asInt()/Variant::asString()— typed accessors with validated coercion; throwEncodingExceptionwhen the value cannot be coerced.addNodes()items:valuemust be a?Variant— already required at encode time; the PHPDoc shape is now corrected.
See references/ARCHITECTURE.md and references/CONNECTION.md for details.
Idiomatic patterns AI agents should follow
-
Use string NodeIds in application code.
$client->read('ns=2;s=Temp')reads better thanNodeId::string(2, 'Temp'). ReserveNodeIdobjects for places that take/return them explicitly. -
Prefer the fluent multi-builders for >1 operation. They're more readable, support per-node attribute selection (
->value()/->displayName()), and are easier to extend. Don't generate the array form unless the user explicitly asks. -
Access result properties as
public readonly, not via getters.$sub->subscriptionId,$dv->statusCode,$dv->sourceTimestamp. Use$dv->getValue()ONLY for unwrapping the underlying variant value (it auto-decodes registered ExtensionObjects). -
Don't pass
BuiltinTypewhen the user doesn't supply one — the client auto-detects via read-before-write (cached with PSR-16). When they DO supply it, useBuiltinType::Int32etc. directly. -
Always
disconnect()infinallyunless usingopcua-session-manager(which keeps sessions alive across requests). -
Logs go through PSR-3:
$builder->setLogger($psr3Logger). Don'terror_log()orechodebug info. -
Events go through PSR-14:
$builder->setEventDispatcher($psr14Dispatcher). 56 event classes available; defaultNullEventDispatcher(the dispatcher itself, not an event) for zero overhead. Seereferences/EVENTS.md. -
Tests use
MockClient, NOT real TCP.use PhpOpcua\Client\Testing\MockClient;. Seereferences/TESTING.md. -
Never
unserialize()data from cache or IPC — the wire serialization pipeline (Wire\WireTypeRegistry) uses JSON gated by a__tallowlist. UseWireCacheCodec(default) for PSR-16 cache values. -
Custom service modules extend
ServiceModule, register their methods viaregister(), return DTOs implementingWireSerializable. Seereferences/MODULES.md.
Common pitfalls (read before generating code)
Don't write code that:
- Uses
$result->getNodeId()style getters when a public readonly property exists — use property access unless the user explicitly wants the deprecated getter. - Creates a new
ClientBuilderperread()call — one client per session. - Builds NodeIds with concatenation:
'ns=' . $ns . ';i=' . $i— useNodeId::numeric($ns, $i)or just pass a literal string template. - Calls
disconnect()from__destruct()of a wrapping class — the client already handles cleanup. - Uses
array_filteron returned DTOs without preserving keys —BrowseResultSet::$referencesis an array, mutations may break ordering. - Writes blocking loops calling
publish()in tight loop without a sleep — use the subscription'spublishingIntervalinstead. - Ignores
$dataValue->statusCode— a "Good" read can still hold non-zero status (e.g.Uncertain*,GoodLocalOverride) that affects business logic.
Full catalog in references/PITFALLS.md.
Related packages in the php-opcua ecosystem
opcua-session-manager— ReactPHP daemon that keeps OPC UA sessions alive across PHP requests via local IPC. Drop-inManagedClientforOpcUaClientInterface.opcua-client-nodeset— Pre-generated PHP types from 51 OPC Foundation companion specifications (Robotics, MachineTool, BACnet, DI, MTConnect, etc.). One->loadGeneratedTypes(new RoboticsRegistrar())call and every read on a structured node returns a typed PHP object.laravel-opcua/symfony-opcua— Framework integrations (facade / autowiring / config / Artisan / console).opcua-cli— Terminal companion (browse, read, write, watch, trust, dump:nodeset, generate:nodeset).opcua-client-ext-reverse-connect— Listener for OPC UA Reverse Connect (Part 6 §7.1.2.3).opcua-client-ext-transport-https—opc.https://wire transport (Part 6 §7.4).uanetstandard-test-suite/extra-test-suite— Docker-based test servers.
When the user is on Laravel/Symfony, prefer the framework integration over plain ClientBuilder. When they need cross-request session persistence, mention opcua-session-manager.
What ships with it: 9 files
107.4 KB alongside SKILL.md
assets/
- recipes.md9.9 KB
references/
- ARCHITECTURE.md16.5 KB
- CONNECTION.md12.9 KB
- EVENTS.md10.1 KB
- MODULES.md9.4 KB
- OPERATIONS.md17.6 KB
- PITFALLS.md13.2 KB
- TESTING.md8.5 KB
- TYPES.md9.3 KB