Extension objects
An ExtensionObject is OPC UA's answer to "structured value" — anything more complex than a primitive arrives wrapped in one. Decode them with a codec, or keep them raw and route the bytes to your own decoder.
When an OPC UA server publishes a value that is more structured than a
primitive — a 3-D vector, a vendor-specific status record, an OPC UA
Argument description — the wire format wraps it in an
ExtensionObject: a tagged binary blob whose typeId tells the
client which decoder to use.
The library exposes ExtensionObjects through the Types\ExtensionObject
class. It is read-only and has two flavours:
- Raw — the bytes are still encoded, because no codec was
registered for this
typeId.bodyholds the bytes,valueis null. - Decoded — a codec ran and produced a structured value.
bodyis null,valueholds the decoded payload.
The shape
| Property | Type | Meaning |
|---|---|---|
typeId |
NodeId |
DataType NodeId of the structure |
encoding |
int |
0 = none, 1 = binary body, 2 = XML body |
body |
?string |
Encoded bytes (if raw) or null (if decoded) |
value |
mixed |
Decoded value (if codec ran) or null (if raw) |
Two helper methods:
$ext = $dv->value->value; // assuming the Variant is an ExtensionObject
if ($ext->isDecoded()) {
$payload = $ext->value; // structured PHP value from the codec
} elseif ($ext->isRaw()) {
$bytes = $ext->body; // forward to your own decoder
}
DataValue auto-extraction
DataValue::getValue() does one extra step for ExtensionObjects: if
the wrapper is decoded, it returns the decoded value directly. You
rarely interact with ExtensionObject directly:
$dv = $client->read('ns=2;s=Devices/PLC/Vector3D');
// If a codec for the Vector3D type is registered:
$vector = $dv->getValue(); // ['x' => 1.0, 'y' => 2.0, 'z' => 3.0]
// If no codec is registered:
$ext = $dv->getValue(); // ExtensionObject (raw)
echo $ext->typeId;
echo bin2hex($ext->body);
Registering a codec
Codecs are per-client, not static. Build one that implements
Encoding\ExtensionObjectCodec and register it via the repository:
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\Encoding\BinaryDecoder;
use PhpOpcua\Client\Encoding\BinaryEncoder;
use PhpOpcua\Client\Encoding\ExtensionObjectCodec;
use PhpOpcua\Client\Types\NodeId;
class Vector3DCodec 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']);
}
}
$builder = ClientBuilder::create();
$builder->getExtensionObjectRepository()
->register(NodeId::numeric(2, 5001), Vector3DCodec::class);
$client = $builder->connect('opc.tcp://plc.local:4840');
Detail in Extensibility · Extension object codecs.
Automatic codec generation
For OPC UA 1.04+ servers that publish DataTypeDefinition attributes,
discoverDataTypes() synthesises codecs from those definitions:
$discovered = $client->discoverDataTypes(namespaceIndex: 2);
echo "Discovered {$discovered} dynamic structure types.\n";
// Any read that returns an ExtensionObject of a discovered type is now
// decoded automatically. No manual codec registration required.
$value = $client->read('ns=2;s=Sensors/Pump1/Status')->getValue();
See Extensibility · Type discovery.
Encoding for writes
Writing an ExtensionObject means either:
-
Decoded path — pass the structured PHP value to a write call that knows the type, with auto-detect on. The library encodes via the registered codec.
-
Raw path — build an
ExtensionObjectmanually with the pre-encoded bytes:
use PhpOpcua\Client\Types\ExtensionObject;
use PhpOpcua\Client\Types\NodeId;
$ext = new ExtensionObject(
typeId: NodeId::numeric(2, 5001),
encoding: 1, // binary body
body: $rawBytes, // produced by your own encoder
value: null,
);
$client->write('ns=2;s=Devices/PLC/Vector3D', $ext);
Raw writes are useful when the type is exotic and you have an existing
binary serializer (a .proto, a Argument builder, …) you'd rather
keep using.
Limitations
- Binary encoding only. XML-encoded ExtensionObjects (encoding
2) are decoded as raw bytes; the library does not ship an XML schema decoder. - No built-in codecs. The library ships zero pre-registered codecs.
The well-known
ArgumentandEnumValueTypestructures, used by the spec itself, must be registered manually or discovered. - Repository is instance-level. Each
Clienthas its ownExtensionObjectRepository. There is no global codec table — by design, so two clients targeting different servers cannot bleed codecs into each other.
The repository surface:
$repo = $client->getExtensionObjectRepository();
$repo->register(NodeId::numeric(2, 5001), Vector3DCodec::class);
$repo->unregister(NodeId::numeric(2, 5001));
$repo->has(NodeId::numeric(2, 5001)); // bool
$repo->get(NodeId::numeric(2, 5001)); // ?ExtensionObjectCodec
$repo->clear(); // remove all