opcua-cli · v4.3.x
Docs · Code generation

Consuming generated code

Load the generated registrar on a ClientBuilder; opcua-client picks up codecs and enum mappings. After that, reads return typed enums and DTOs automatically.

The output of generate:nodeset is PHP that integrates with opcua-client through a single entry point: ClientBuilder::loadGeneratedTypes(). One call wires every generated codec and enum mapping onto the client.

The integration

php examples/consume.php
use PhpOpcua\Client\ClientBuilder;
use App\OpcUa\MyVendor\MyVendorRegistrar;
use App\OpcUa\MyVendor\MyVendorNodeIds;
use App\OpcUa\MyVendor\Enums\OperatingStateEnum;
use App\OpcUa\MyVendor\Types\PumpStatusType;

$client = ClientBuilder::create()
    ->loadGeneratedTypes(new MyVendorRegistrar())
    ->connect('opc.tcp://plc.local:4840');

// Auto-cast on enum reads
$state = $client->read(MyVendorNodeIds::PumpState)->getValue();
// $state is OperatingStateEnum::RUNNING, not int 2

// Auto-decode on structured reads
$status = $client->read(MyVendorNodeIds::PumpStatus)->getValue();
// $status is PumpStatusType — typed property access available
echo $status->FlowRate_m3h;
echo $status->State->name;

$client->disconnect();

That's the full surface — one builder call, then typed PHP for the rest of the session.

What loadGeneratedTypes() does

  1. 01

    Walks `dependencyRegistrars()`

    recursively. Loads each dependency's codecs + enum mappings first.

  2. 02

    Calls `registerCodecs($repository)`

    on every registrar in the cascade. The codecs land in the client's per-instance ExtensionObjectRepository, keyed by DataType NodeId.

  3. 03

    Merges `getEnumMappings()`

    into the client's enum registry — a DataType NodeId → BackedEnum class map.

  4. 04

    Returns the builder

    for chaining.

After connect() runs, the client's read path consults these registries:

  • A Variant<ExtensionObject> response whose typeId matches a registered codec is decoded into the typed DTO.
  • A Variant<Int32> response on a node whose DataType is in the enum registry is wrapped in the matching BackedEnum::from().

Without the load, the same reads return raw ExtensionObject and int.

Loading multiple registrars

Stack them — the loader handles dedup:

php multiple registrars
$client = ClientBuilder::create()
    ->loadGeneratedTypes(new App\OpcUa\Vendor1Registrar())
    ->loadGeneratedTypes(new App\OpcUa\Vendor2Registrar())
    ->loadGeneratedTypes(new \PhpOpcua\Nodeset\Robotics\RoboticsRegistrar())
    ->connect('opc.tcp://multi-spec.plant.local:4840');

Common dependencies (DI, IA, Machinery) come along transitively from each. The registrar registry is idempotent — loading the same codec twice ends up registering the same codec instance.

For details: opcua-client-nodeset — Loading registrars.

Without auto-cast — the no-load fallback

If you skip loadGeneratedTypes() entirely, the generated code is still useful as constants:

php no-load — constants only
$client = ClientBuilder::create()
    ->connect('opc.tcp://plc.local:4840');

// NodeId constants — type-safe NodeIds, no auto-cast
$dv = $client->read(MyVendorNodeIds::PumpState);
$rawValue = $dv->getValue();  // int (raw value the server sent)

// You can still use the enum class directly:
$state = OperatingStateEnum::from($rawValue);
if ($state === OperatingStateEnum::RUNNING) { }

Useful when the registrar wiring isn't possible (e.g. third- party code controls the ClientBuilder and you only get to use constants).

With opcua-session-manager

opcua-session-manager's ManagedClient does not expose a loadGeneratedTypes() method directly — that entry point lives on opcua-client's ClientBuilder, which the managed client wraps but does not surface. To get typed decoding through the daemon today, configure the registrar on the daemon side (in the worker process that constructs the underlying ClientBuilder) rather than via a per-ManagedClient call.

See the session-manager docs for the current pattern; if your application needs in-process typed decoding, drop down to opcua-client directly and call loadGeneratedTypes() there.

Where to put the generated code

Repository layout option Pros Cons
src/Generated/<Spec>/ — checked into your application repo Versioned with the app Diffs explode on regeneration
Separate package — composer require yourorg/opcua-<vendor> Clean separation, reusable across apps Extra repo to maintain
Generated in CI, not committed No diffs to review CI must have access to the XML; gen is a build step
Pre-generated published package (for OPC Foundation specs) Zero generation cost Limited to specs the publisher covers

For vendor-specific code, the second option (a private package) scales best when multiple applications share the vendor.

Updating after a regeneration

When the source NodeSet2.xml changes — vendor publishes a new revision, the server's dump:nodeset differs after a firmware update — regenerate and re-deploy:

  1. 01

    Regenerate

    in the same --output dir. The generator overwrites existing files.

  2. 02

    Diff `src/Generated/<Spec>/`

    — review what changed (renamed enums, new fields, deprecated NodeIds).

  3. 03

    Adapt your application code

    to any breaking changes.

  4. 04

    Re-deploy

    with the new code.

The breakage surface for breaking spec changes is your application code referencing the renamed / removed names. The underlying loadGeneratedTypes() mechanism is stable.

PSR-4 autoload

Whatever --namespace=… you used at generation, point your project's composer.json PSR-4 entry at the matching directory:

text composer.json
{
    "autoload": {
        "psr-4": {
            "App\\OpcUa\\MyVendor\\": "src/Generated/MyVendor/"
        }
    }
}

Run composer dump-autoload after generating into a new directory — Composer's classmap optimisation makes the loaded class set discoverable without re-scanning on every request.

When the auto-cast doesn't trigger

A loaded registrar should produce typed values out of every matching read. When it doesn't:

Symptom Cause
Read returns raw int instead of enum getEnumMappings() doesn't include this DataType — server's namespace index differs from the XML's
Read returns ExtensionObject instead of DTO Codec not registered for this DataType — same root cause
EnumClass::from(N) throws ValueError Server sent a value outside the enum's case range — the library catches this and returns raw int

Diagnose with --debug to see the wire payload. The opcua-client-nodeset NodeId-constants page covers the namespace-index mismatch case.