Reading & Writing Values

Reading a Value

use PhpOpcua\Client\Types\NodeId;
use PhpOpcua\Client\Types\StatusCode;

// Using string format
$dataValue = $client->read('i=2259'); // ServerStatus_State

// Or with a NodeId object
$dataValue = $client->read(NodeId::numeric(0, 2259));

if (StatusCode::isGood($dataValue->statusCode)) {
    echo "Value: " . $dataValue->getValue() . "\n";
}

Events: Every read() dispatches a NodeValueRead event. See Events.

Metadata Cache

Attributes like DisplayName, BrowseName, DataType, and NodeClass are static — they don't change at runtime. Enable metadata caching to avoid redundant server reads:

// Enable on the builder before connecting
$client = ClientBuilder::create()
    ->setReadMetadataCache(true)
    ->connect('opc.tcp://localhost:4840');

// First call: reads from server, caches the result
$name = $client->read('ns=2;i=1001', AttributeId::DisplayName);

// Second call: served from cache (no server round-trip)
$name = $client->read('ns=2;i=1001', AttributeId::DisplayName);

// Force a refresh from the server
$name = $client->read('ns=2;i=1001', AttributeId::DisplayName, refresh: true);

Rules:

  • Disabled by default — opt-in via setReadMetadataCache(true).
  • Value (attribute 13) is never cached — always reads from the server, regardless of the setting.
  • refresh: true bypasses the cache and re-reads from the server, then updates the cache.
  • Uses the same PSR-16 cache backend as browse and write type detection.
  • invalidateCache($nodeId) clears all cached metadata for that node.

Reading a Specific Attribute

By default, read() targets the Value attribute (id 13). You can read any attribute:

use PhpOpcua\Client\Types\AttributeId;

$displayName = $client->read(NodeId::numeric(0, 2259), AttributeId::DisplayName);
$dataType = $client->read(NodeId::numeric(0, 2259), AttributeId::DataType);

Common attributes:

Constant Value Description
AttributeId::NodeId 1 The node's NodeId
AttributeId::NodeClass 2 Node class
AttributeId::BrowseName 3 Browse name
AttributeId::DisplayName 4 Display name
AttributeId::Description 5 Description
AttributeId::Value 13 The value (default)
AttributeId::DataType 14 Data type NodeId
AttributeId::AccessLevel 17 Access level bitmask

Reading Multiple Values

// Fluent builder
$results = $client->readMulti()
    ->node('i=2259')->value()
    ->node('i=2267')->value()
    ->node('ns=2;s=Temperature')->value()
    ->execute();

foreach ($results as $dataValue) {
    if (StatusCode::isGood($dataValue->statusCode)) {
        echo $dataValue->getValue() . "\n";
    }
}

// Or with array (still works)
$results = $client->readMulti([
    ['nodeId' => 'i=2259'],
    ['nodeId' => 'i=2267'],
    ['nodeId' => 'ns=2;s=Temperature', 'attributeId' => AttributeId::Value],
]);

Tip: The builder's ->node() adds a node, then you pick the attribute (->value(), ->displayName(), etc.). Call ->execute() to send the request.

DataValue Properties

$dataValue->getValue();           // mixed -- unwrapped value (extracts from Variant)
$dataValue->variant;              // ?Variant -- typed variant
$dataValue->statusCode;           // int -- OPC UA status code
$dataValue->sourceTimestamp;      // ?DateTimeImmutable
$dataValue->serverTimestamp;      // ?DateTimeImmutable

Writing a Value

use PhpOpcua\Client\Types\BuiltinType;

$statusCode = $client->write(
    'ns=2;i=1234',  // or NodeId::numeric(2, 1234)
    42,
    BuiltinType::Int32
);

if (StatusCode::isGood($statusCode)) {
    echo "Write successful\n";
} else {
    echo "Write failed: " . StatusCode::getName($statusCode) . "\n";
}
// Events: dispatches NodeValueWritten on success, NodeValueWriteFailed otherwise

Auto-Detect Write Type

By default, the client automatically detects the node's type before writing. You can omit the BuiltinType parameter:

// Auto-detect type (reads the node first, caches the type)
$client->write('ns=2;i=1234', 42);

// Explicit type (validated against the node when auto-detect is on)
$client->write('ns=2;i=1234', 42, BuiltinType::Int32);

The detected type is cached (PSR-16) so subsequent writes to the same node skip the read.

Behavior:

Auto-detect $type passed What happens
ON (default) No Reads node, caches type, writes
ON Yes Uses the type directly, no read
OFF No Throws WriteTypeDetectionException
OFF Yes Uses the type directly, no read

Disable auto-detect:

$client = ClientBuilder::create()
    ->setAutoDetectWriteType(false)
    ->connect('opc.tcp://localhost:4840');

Exceptions:

  • WriteTypeDetectionException — node has no readable value, or auto-detect is off and no type provided

Events:

  • WriteTypeDetecting — dispatched before the type detection starts
  • WriteTypeDetected — dispatched after the type is determined (with $detectedType and $fromCache)

Cache invalidation:

$client->invalidateCache($nodeId); // clears cached write type (and browse cache)
$client->flushCache();             // clears everything

Writing Multiple Values

// Fluent builder — auto-detect type
$results = $client->writeMulti()
    ->node('ns=2;i=1001')->value(3.14)
    ->node('ns=2;i=1002')->value('Hello')
    ->node('ns=2;i=1003')->value(true)
    ->execute();

// Fluent builder — explicit type
$results = $client->writeMulti()
    ->node('ns=2;i=1001')->typed(3.14, BuiltinType::Double)
    ->node('ns=2;i=1002')->typed('Hello', BuiltinType::String)
    ->node('ns=2;i=1003')->typed(true, BuiltinType::Boolean)
    ->execute();

foreach ($results as $i => $statusCode) {
    echo "Item $i: " . StatusCode::getName($statusCode) . "\n";
}

// Or with array (still works)
$results = $client->writeMulti([
    [
        'nodeId' => 'ns=2;i=1001',
        'value' => 3.14,
        'type' => BuiltinType::Double,
    ],
    [
        'nodeId' => 'ns=2;i=1002',
        'value' => 'Hello',
        'type' => BuiltinType::String,
    ],
    [
        'nodeId' => 'ns=2;i=1003',
        'value' => true,
        'type' => BuiltinType::Boolean,
    ],
]);

Tip: The write builder uses ->node() to pick the target, then ->value($val, $type) to set what to write. Call ->execute() to send.

Writing to a Specific Attribute

By default, write() targets the Value attribute (id 13):

$results = $client->writeMulti([
    [
        'nodeId' => NodeId::numeric(2, 1001),
        'value' => 100,
        'type' => BuiltinType::Int32,
        'attributeId' => 13,
    ],
]);

Writing Arrays

use PhpOpcua\Client\Types\Variant;
use PhpOpcua\Client\Types\DataValue;

// Using Variant directly
$variant = new Variant(BuiltinType::Int32, [1, 2, 3, 4, 5]);
$dataValue = new DataValue($variant);

// Or through writeMulti
$results = $client->writeMulti([
    [
        'nodeId' => NodeId::numeric(2, 2001),
        'value' => [10, 20, 30],
        'type' => BuiltinType::Int32,
    ],
]);

Automatic Batching

OPC UA servers can limit how many nodes you read or write per request. The client handles this transparently.

How It Works

After connect(), the client reads the server's MaxNodesPerRead and MaxNodesPerWrite limits. When readMulti() or writeMulti() exceeds that limit, the request is split automatically and results are merged in order.

$client = ClientBuilder::create()
    ->connect('opc.tcp://localhost:4840');

// Server says MaxNodesPerRead = 100
// This is split into 10 requests of 100 each
$results = $client->readMulti($items1000);
// $results contains all 1000 DataValues, in order

You can check the discovered limits:

$client->getServerMaxNodesPerRead();  // e.g. 100, or null
$client->getServerMaxNodesPerWrite(); // e.g. 100, or null

Setting a Manual Batch Size

Override the server limit or set one when the server does not report any:

$client = ClientBuilder::create()
    ->setBatchSize(50)
    ->connect('opc.tcp://localhost:4840');

Priority order: your setBatchSize(N) (N > 0) beats the server-reported limit, which beats no batching.

Disabling Batching

Skip both batching and the server limits discovery on connect:

$client = ClientBuilder::create()
    ->setBatchSize(0)
    ->connect('opc.tcp://localhost:4840');

Tip: Use this if you know the server has no limits and want to save the extra read on connect.

Batching Summary

getBatchSize() Server reports Discovery on connect Effective batch size
null (default) 100 Yes 100
null (default) 0 (no limit) Yes No batching
null (default) Not supported Yes No batching
50 100 Yes 50
50 0 Yes 50
0 (disabled) Any Skipped No batching

Note: Batching only applies to readMulti() and writeMulti(). Single read() and write() calls always go as individual requests.

Supported Data Types

BuiltinType PHP Type Example
Boolean bool true
SByte int -128 to 127
Byte int 0 to 255
Int16 int -32768 to 32767
UInt16 int 0 to 65535
Int32 int -2^31 to 2^31-1
UInt32 int 0 to 2^32-1
Int64 int -2^63 to 2^63-1
UInt64 int 0 to 2^64-1
Float float 3.14
Double float 3.141592653589793
String string 'Hello'
DateTime DateTimeImmutable new DateTimeImmutable()
Guid string '550e8400-e29b-41d4-a716-446655440000'
ByteString string Binary data
NodeId NodeId NodeId::numeric(0, 85)
QualifiedName QualifiedName new QualifiedName(0, 'Name')
LocalizedText LocalizedText new LocalizedText('en', 'Text')

Status Codes

use PhpOpcua\Client\Types\StatusCode;

$statusCode = $dataValue->statusCode;

StatusCode::isGood($statusCode);      // true if 0x0XXXXXXX
StatusCode::isBad($statusCode);       // true if 0x8XXXXXXX
StatusCode::isUncertain($statusCode); // true if 0x4XXXXXXX
StatusCode::getName($statusCode);     // e.g. "BadNodeIdUnknown"

Common status codes:

Constant Value Meaning
StatusCode::Good 0x00000000 Success
StatusCode::BadNodeIdUnknown 0x80340000 Node does not exist
StatusCode::BadTypeMismatch 0x80740000 Value type mismatch
StatusCode::BadNotWritable 0x803B0000 Node is read-only
StatusCode::BadNotReadable 0x803E0000 Node is not readable
StatusCode::BadUserAccessDenied 0x801F0000 Access denied
StatusCode::BadTimeout 0x800A0000 Operation timed out