php-opcua/opcua-client-nodeset — v4.5.0 skill
Pre-generated PHP types for 51 OPC Foundation companion specifications. A read-only library — every file in src/ is the deterministic output of composer generate against UA-Nodeset/. Plug it into opcua-client with one builder call and structured OPC UA values come back as typed PHP enums and readonly DTOs.
When to use this skill
Activate when any of these apply:
- The user mentions a companion specification by name (Robotics, MachineTool, Machinery, DI, BACnet, MTConnect, ISA-95, PackML, AutoID, PROFINET, MachineVision, LADS, …)
- They want typed enums back from a Read instead of raw integers (
OperationalModeEnumeration::MANUAL_HIGH_SPEEDvsint(2)) - They're reading a Structure DataType (custom OPC UA struct) and want a PHP DTO, not a raw
ExtensionObject - They reference well-known NodeIds from a spec (
MachineryNodeIds::MACHINE_IDENTIFICATION_TYPE) - They ask about regenerating from a NodeSet2.xml (their own vendor's, or after an upstream
UA-Nodesetupdate) - They use
ClientBuilder::loadGeneratedTypes(...)orGeneratedTypeRegistrar
Do NOT activate for: generic OPC UA tasks not touching companion specs — use the opcua-client skill instead.
The 60-second mental model
UA-Nodeset/Schema/*.NodeSet2.xml (OPC Foundation XML — vendored)
│
▼ composer generate
src/<Spec>/Enums/*.php ← PHP enums
src/<Spec>/DataTypes/*.php ← readonly DTOs
src/<Spec>/Codecs/*.php ← ExtensionObject codecs
src/<Spec>/<Spec>NodeIds.php ← well-known NodeId constants
src/<Spec>/<Spec>Registrar.php ← implements GeneratedTypeRegistrar
│
▼ composer require php-opcua/opcua-client-nodeset
Application code:
$builder->loadGeneratedTypes(new <Spec>Registrar())
│
▼
opcua-client walks dependencyRegistrars() transitively
→ registers every codec into ExtensionObjectRepository
→ merges enum mappings into the typed-read pipeline
│
▼
$client->read('ns=2;s=Mode')->getValue()
// → OperationalModeEnumeration::AUTOMATIC (typed!)
Three things to know:
- Read-only library. All 807 PHP files are generator output. Don't hand-edit them — they'll be overwritten next regeneration. Hand-edit the generator (
generate.php) if behavior needs to change. - Registrars walk dependencies themselves.
new RoboticsRegistrar()auto-loads DI + Machinery. You almost never need to list multiple registrars unless they're independent branches (e.g. Robotics + BACnet — both depend on DI but on nothing in common otherwise). - Typed reads need both: the codec (for Structure decode) AND the enum mapping (for Enumeration auto-cast). Both come from the same registrar. Don't register codecs manually if a registrar exists for the spec.
Quick start (90% of use cases fit this shape)
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Nodeset\Robotics\RoboticsRegistrar;
use PhpOpcua\Nodeset\Robotics\Enums\OperationalModeEnumeration;
use PhpOpcua\Nodeset\Robotics\RoboticsNodeIds;
$client = ClientBuilder::create()
->loadGeneratedTypes(new RoboticsRegistrar()) // pulls in DI + Machinery transitively
->connect('opc.tcp://plc.example:4840');
// Enum auto-cast (Int32 → typed enum case via getEnumMappings())
$mode = $client->read('ns=2;s=OperationalMode')->getValue();
assert($mode === OperationalModeEnumeration::MANUAL_HIGH_SPEED);
// Structure decode (ExtensionObject → typed DTO via registered codec)
$axis = $client->read('ns=2;s=Axis1')->getValue();
echo "Axis range: [{$axis->minValue}, {$axis->maxValue}] {$axis->unit->name}\n";
// Well-known NodeId from the spec
$systemType = $client->read(RoboticsNodeIds::MOTION_DEVICE_SYSTEM_TYPE);
When to load deeper references
| If the task involves… | Read |
|---|---|
| Picking the right registrar / understanding the dependency graph (DI vs Machinery vs Robotics) | references/REGISTRARS.md |
| Working with the generated enums (auto-cast, lookup, all-cases iteration) | references/ENUMS.md |
| Working with Structure DTOs and ExtensionObject codecs | references/DTOS.md |
Re-generating from XML (composer generate, what generate.php does, adding a new spec) |
references/REGENERATION.md |
| The 51 spec catalogue — what each spec is for, primary use cases | references/CATALOGUE.md |
| Debugging unexpected behaviour or common mistakes | references/PITFALLS.md |
| Complete worked examples (Laravel, Symfony, ManagedClient, multi-spec, vendor NodeSet2) | assets/recipes.md |
Core API surface (must-know)
Three types matter; nothing else needs to leak into application code.
<Spec>Registrar
Every companion spec has a registrar at PhpOpcua\Nodeset\<Spec>\<Spec>Registrar. They all share the same shape:
final class RoboticsRegistrar implements \PhpOpcua\Client\Repository\GeneratedTypeRegistrar
{
public function __construct(public bool $only = false) {}
public function registerCodecs(ExtensionObjectRepository $repository): void;
public function dependencyRegistrars(): array; // @return self[]
public function getEnumMappings(): array; // [nodeIdStr => enum-class]
}
$only = false(default):loadGeneratedTypes()walksdependencyRegistrars()transitively.$only = true: skips dependency resolution. Debug-only — production should always let the graph resolve.
<Spec>NodeIds constants
final class RoboticsNodeIds
{
public const string MOTION_DEVICE_SYSTEM_TYPE = 'ns=20;i=1004';
public const string MOTION_DEVICE_TYPE = 'ns=20;i=1002';
// …per spec — usually 5–50 well-known IDs
}
The ns=N here is the canonical namespace index per the OPC Foundation. At runtime, your server's actual namespace index may differ — Client::resolveNodeId() and the typed-read pipeline auto-remap. Use the constants for readability; the runtime resolution is automatic.
Generated enums and DTOs
Reachable but rarely written explicitly — the typed-read pipeline gives them to you naturally:
use PhpOpcua\Nodeset\Robotics\Enums\OperationalModeEnumeration;
use PhpOpcua\Nodeset\Robotics\DataTypes\AxisInformation;
OperationalModeEnumeration::from(2); // explicit int → case
foreach (OperationalModeEnumeration::cases() as $case) { /* … */ }
$dto = new AxisInformation(name: 'X', minValue: 0.0, maxValue: 100.0, unit: …);
v4.5.0 alignment
Lock-step with php-opcua/opcua-client v4.5.0:
- Uses
ClientBuilder::loadGeneratedTypes(GeneratedTypeRegistrar ...)— added in core v4.4 - Uses
GeneratedTypeRegistrarinterface from corePhpOpcua\Client\Repository\ - Every generated DTO implements
WireSerializableso it round-trips throughopcua-session-manager'sManagedClientand through the PSR-16 cache safely (JSON-gated byWireTypeRegistry) - Composer constraint:
"php-opcua/opcua-client": "^4.5.0"
Nothing in core v4.5.0 touches the Repository\* / Encoding\* surface this package consumes — its security hardening, typed PublishResult notifications and final wire DTOs are all outside it. The v4.5.0 release of this package does ship generated-code changes, but they fix pre-existing defects (registrar dependency references that did not resolve on case-sensitive filesystems, and 25 codecs that could not resolve their enums) — see the CHANGELOG.
Idiomatic patterns AI agents should follow
Pass registrars to
loadGeneratedTypes(), never callregisterCodecs()yourself. The builder handles topo-sort and dedup.Use the spec's
NodeIdsconstants instead of magic strings.RoboticsNodeIds::MOTION_DEVICE_SYSTEM_TYPEis more durable than'ns=20;i=1004'(the constant survives namespace remapping, the literal doesn't).Don't construct registrars with
only: trueunless explicitly debugging. The dependency graph exists for a reason.Don't try to import every spec at once. Each
<Spec>Registraryou add costs codec registration time at boot. Add only the specs you actually use.DTOs are
readonly class— public readonly properties, not getters.$axis->minValue, never$axis->getMinValue().Enum case names are TitleCase + underscore-separated for multi-word names. Generator output is deterministic —
MANUAL_HIGH_SPEED, notMANUALHIGHSPEEDormanualHighSpeed.Never hand-edit files under
src/— they're regenerated. Editgenerate.phpif generation logic needs to change.Use
RoboticsRegistraretc. by class-string, not instance, incomposer.jsonautoload hints if you're listing them somewhere for IDE help (they're already PSR-4 autoloaded).
Common pitfalls (read before generating code)
Don't write code that:
- Hard-codes
ns=Nnumeric prefixes from the constants — the runtime namespace index may differ. Always pass the constant (or useresolveNodeId('/Objects/MyMachine/…')). - Tries to
unserialize()a cached DTO — they're WireSerializable, decoded via the wire registry (gadget-chain-safe). - Calls
$client->getRepository()->register(new <Spec>Codec())manually when a<Spec>Registrarexists — duplicate registration is rejected with a clear exception, but you're doing extra work for nothing. - Imports a DTO class directly when the value comes from a
read()— the auto-decode gives you the right type automatically. - Edits files under
src/— they get overwritten on nextcomposer generate. - Mixes registrars from different
opcua-client-nodesetversions — pin the same minor version as youropcua-client. - Assumes a TYPE exists if the spec's
<UADataType>only has aDefinition(some specs define abstract Structure types — there's no codec for those, only their concrete subtypes get codecs).
Full catalog in references/PITFALLS.md.
Related packages in the php-opcua ecosystem
opcua-client— the OPC UA client. v4.4+ required.loadGeneratedTypes()andGeneratedTypeRegistrarlive there.opcua-session-manager— generated codecs work transparently throughManagedClient(the daemon registers them server-side; results decode through the wire registry).opcua-cli—opcua-cli generate:nodeset <file.NodeSet2.xml>runs the same generator against an arbitrary NodeSet2 (e.g. a vendor-specific XML you got from the manufacturer).laravel-opcua/symfony-opcua— framework integrations.Opcua::connect()->loadGeneratedTypes(...)works identically to directClientBuilderusage.