DataValue and Variant
Variant is the value container. DataValue wraps it with status and timestamps. Both are read-only; the conversion rules between PHP and OPC UA types matter.
DataValue is what read() returns. It carries:
value(Variant) — the actual value, typedstatusCode(int) — quality / error code,0=GoodsourceTimestamp(?DateTimeImmutable) — when the device sampledserverTimestamp(?DateTimeImmutable) — when the server replied
A Variant carries:
type(BuiltinType) — which OPC UA primitive this isvalue(mixed) — the PHP value (scalar or array)dimensions(?array) — for multidimensional arrays only
Reading values
DataValue::getValue() unwraps the Variant to its native PHP value, and DataValue::getType() returns the OPC UA BuiltinType of that value — symmetric accessors for the two halves of every read:
$dv = $client->read('ns=2;s=Devices/PLC/Speed');
$dv->getValue(); // float — unwrapped scalar
$dv->getType(); // BuiltinType::Double — the OPC UA data type
$dv->type; // BuiltinType::Double — same as getType(), public readonly property
$dv->statusCode; // int
$dv->sourceTimestamp; // ?DateTimeImmutable
The type property and the matching getType() accessor (both added in v4.4.0) are the canonical way to inspect the OPC UA data type of a read result. They return null only when the DataValue was constructed without a Variant (e.g. a bad() fault).
getValue() also handles two convenience cases:
- Decoded ExtensionObjects — when a value is an
ExtensionObjectwith a registered codec,getValue()returns the decoded body rather than the wrapper. Pass the wrapper itself via$dv->value->valueif you need the rawExtensionObject. - Arrays — array
Variants return PHP arrays directly.
Typed accessors (v4.5.0)
When you need a guaranteed scalar instead of mixed, Variant offers
two coercing accessors:
$variant->asInt(); // int — accepts int, float, bool, numeric string
$variant->asString(); // string — accepts string, int, float, Stringable
Both throw EncodingException when the underlying value cannot be
coerced (e.g. asInt() on a non-numeric string or an object). They are
what the file-transfer module uses internally to read method output
arguments.
Bad-status reads
A DataValue with BadNodeIdUnknown or BadAttributeIdInvalid will
still have a value field — usually a Variant of Null. Trust the
status code, not the value:
use PhpOpcua\Client\Types\StatusCode;
if (! StatusCode::isGood($dv->statusCode)) {
throw new RuntimeException(
'Read failed: ' . StatusCode::getName($dv->statusCode)
);
}
$value = $dv->getValue();
Overflow and limit bits
Besides success or failure, the status code carries InfoBits — flags about the value itself. Two of them matter on subscription data:
use PhpOpcua\Client\Types\DataValueLimit;
$dv->isOverflow(); // bool — the queue discarded a value in favour of this one
$dv->limit(); // DataValueLimit — None, Low, High or Constant
$dv->hasInfoBits(); // bool — whether the server sent DataValue InfoBits at all
The same checks work on a raw code as StatusCode::isOverflow(),
StatusCode::limit() and StatusCode::hasDataValueInfoBits(). The bits
only mean something under the DataValue InfoType, and both forms check it
for you: masking & 0x80 on its own reads the same bit out of codes where
it means something else. An overflowed Good value is still Good —
StatusCode::isGood() looks at severity only.
The overflow bit is only set when the monitored item has a queueSize
greater than 1 — and the library's default is 1. With a single-slot queue
the server overwrites the value on every sample: that is the intended
sampling behaviour, not an overflow, so the bit is never set and
isOverflow() stays false even when intermediate values were skipped.
To detect lost values, give the item a queue large enough for the samples
expected between two publishes and watch isOverflow(): with
discardOldest (the default) the bit is set on the first value delivered
after the gap.
PHP ↔ OPC UA type mapping
The library maps between PHP types and BuiltinType cases as follows:
OPC UA BuiltinType |
PHP type on read | Accepted on write |
|---|---|---|
Boolean |
bool |
bool, int (0/1) |
SByte / Byte |
int |
int in -128..127 / 0..255 |
Int16 / UInt16 |
int |
int in range |
Int32 / UInt32 |
int |
int |
Int64 / UInt64 |
int |
int |
Float / Double |
float |
float, int |
String |
string |
string |
DateTime |
DateTimeImmutable |
DateTimeImmutable, ISO string |
Guid |
string (canonical hex) |
string in canonical form |
ByteString |
string (raw bytes) |
string (raw bytes) |
XmlElement |
string (raw XML) |
string |
NodeId |
NodeId |
NodeId, string shorthand |
StatusCode |
int |
int |
QualifiedName |
QualifiedName |
QualifiedName |
LocalizedText |
LocalizedText |
LocalizedText |
ExtensionObject |
ExtensionObject (raw or decoded) |
ExtensionObject |
DataValue |
DataValue |
DataValue |
Variant |
Variant |
Variant |
Float is IEEE-754 single precision (32 bits). PHP's float is
internally double precision; round-trips through Float lose
precision below ~7 decimal digits.
Int64 / UInt64 use PHP's native int type, which is 64-bit on
LP64 systems (every supported platform). Values outside the signed
range will overflow into negative on the wire — be deliberate with
UInt64 values above 2^63 - 1.
Building Variants explicitly
When the wire type matters and auto-detect is off, build a Variant:
use PhpOpcua\Client\Types\Variant;
use PhpOpcua\Client\Types\BuiltinType;
$v = new Variant(
type: BuiltinType::Int32,
value: 42,
);
$client->write('ns=2;s=Tag', $v);
// For arrays:
$arr = new Variant(
type: BuiltinType::Double,
value: [1.0, 2.0, 3.0],
);
// For 2D arrays:
$mat = new Variant(
type: BuiltinType::Double,
value: [1.0, 2.0, 3.0, 4.0],
dimensions: [2, 2],
);
dimensions is required for arrays with valueRank > 1. A 1-D array
leaves it null and the count is derived from the array size.
Building DataValues
DataValue ships factory methods for the common write/test cases:
use PhpOpcua\Client\Types\DataValue;
DataValue::ofBool(true);
DataValue::ofInt(42);
DataValue::ofDouble(3.14);
DataValue::ofString('hello');
DataValue::ofVariant(new Variant(BuiltinType::Int32, [1, 2, 3]));
// With explicit status:
DataValue::ofVariant($v, statusCode: 0x80000000);
// All factories accept optional timestamps:
DataValue::ofDouble(42.0, sourceTimestamp: new DateTimeImmutable());
These are primarily useful in tests (see Testing ·
Handlers) but compose anywhere a DataValue
is needed.
Timestamps
OPC UA encodes timestamps as 100-nanosecond ticks since 1601-01-01 UTC
(Windows FILETIME format). PHP's DateTimeImmutable resolves to
microseconds; the conversion truncates to the nearest microsecond on
read and zero-pads on write. The library transparently handles both
directions.
DateTimeImmutable is the only representation exposed; the raw 64-bit
tick value is not surfaced.
Equality
Variant and DataValue are value objects. PHP's == does structural
equality:
$a = DataValue::ofInt(42);
$b = DataValue::ofInt(42);
$a == $b; // true
$a === $b; // false (different objects)
The statusCode and timestamps participate in equality. To compare
only the underlying value, compare $a->getValue() === $b->getValue().
Common pitfalls
getValue()on a Null Variant. Returnsnull. Always check$dv->type !== BuiltinType::Nullbefore treating the value as present.- Float comparisons. OPC UA
Float(single precision) → PHPfloat(double precision) round-trips lose precision. Compare with a tolerance, never===. - DateTime in non-UTC contexts. The library returns
DateTimeImmutablein UTC. Reformat withsetTimezone()for display, do not pass non-UTC instances back towrite(). - String identifiers in NodeId. A
Variant<NodeId>whose identifier is"path/with/slash"round-trips correctly through the API — but printing it to logs loses the namespace prefix unless you cast theNodeIdto string yourself.