Using builders
Fluent builders for read, write, browse, methods, subscriptions, translateBrowsePaths. The same upstream API, used through the autowired Symfony service.
opcua-client exposes fluent builders for the major operations.
The Symfony bundle adds nothing on top — you call the same
builders through the autowired client.
How builders are obtained
There is no readBuilder() / writeBuilder() / browseBuilder() /
callBuilder() / historyBuilder() method. Instead, several of
the multi-operation methods accept ?array $items:
- Called with an array they execute one round-trip and return the result array.
- Called with
null(or no args) they return the fluent builder.
| Method | Return when null |
Return when called with args |
|---|---|---|
readMulti(?array $items = null) |
ReadMultiBuilder |
DataValue[] |
writeMulti(?array $items = null) |
WriteMultiBuilder |
int[] (status codes) |
translateBrowsePaths(?array $paths) |
BrowsePathsBuilder |
BrowsePathResult[] |
createMonitoredItems(int $sid, ?array $items = null) |
MonitoredItemsBuilder |
MonitoredItemCreateResult[] |
The history API has no builder — it's three flat methods
(historyReadRaw, historyReadProcessed, historyReadAtTime).
See Operations · History.
The single-node convenience methods (read, write, browse,
call) are not builders — they round-trip immediately.
Read — readMulti(null) builder
use PhpOpcua\Client\Types\AttributeId;
$client = $this->opcua->connect();
// One-shot single read
$dv = $client->read('ns=2;s=Speed');
// Batch — many tags, one round-trip
$values = $client->readMulti(null)
->node('ns=2;s=Speed')
->node('ns=2;s=Temperature')
->node('ns=2;s=Pressure')
->execute();
// Non-Value attribute
$displayName = $client->read('ns=2;s=Speed', AttributeId::DisplayName)
->getValue();
execute() returns an array of DataValue in the order of nodes
added. See Operations · Reading.
Write — writeMulti(null) builder
use PhpOpcua\Client\Types\BuiltinType;
$client = $this->opcua->connect();
// Batch
$statuses = $client->writeMulti(null)
->node('ns=2;s=Setpoint')->typed(75.0, BuiltinType::Float)
->node('ns=2;s=Mode')->value('Auto')
->node('ns=2;s=Run')->value(true)
->execute();
The builder is "stage-based": call ->node(...) first, then
->value(...) (auto-detect) or ->typed(..., BuiltinType::...)
(explicit). See Operations · Writing
for the auto-detection rules.
Browse — browse() is one-shot
browse() runs immediately:
use PhpOpcua\Client\Types\BrowseDirection;
use PhpOpcua\Client\Types\NodeClass;
$client = $this->opcua->connect();
$nodes = $client->browse(
'ns=2;s=Folder',
BrowseDirection::Forward,
true, // includeSubtypes
NodeClass::Variable->value, // nodeClassMask
);
$tree = $client->browseRecursive(
'ns=4;s=Tags',
BrowseDirection::Forward,
maxDepth: 5,
);
Translate browse paths — translateBrowsePaths(null) builder
$results = $client->translateBrowsePaths(null)
->from('ns=2;s=Folder')
->path('/Subfolder/Speed')
->from('ns=2;s=Folder')
->path('/Subfolder/Temperature')
->execute();
Returns BrowsePathResult[] — not a list of strings.
Method call — call() is one-shot
use PhpOpcua\Client\Types\CallResult;
$result = $client->call(
'ns=2;s=Recipe', // object
'ns=2;s=Recipe.Load', // method
['NewRecipe', 42], // input arguments
);
// $result is a CallResult: $result->statusCode (int), $result->outputArguments (array)
There is no callBuilder() and no callMethod(). The
return value is a CallResult object — not a [int, array]
tuple. See Operations · Method calls.
Subscription — flat API
The subscription API is not a fluent builder:
$sub = $client->createSubscription(publishingInterval: 500.0);
$client->createMonitoredItems(
$sub->subscriptionId,
[['nodeId' => 'ns=2;s=Speed', 'clientHandle' => 1]],
);
// In direct mode: drive the publish loop
while (true) { $client->publish(); }
In managed mode with auto_publish: true, the daemon drives the
loop and notifications arrive on the EventDispatcher as
DataChangeReceived events — see
Session manager · Auto-publish
and Events · Data events.
createMonitoredItems(null) returns a MonitoredItemsBuilder
for ergonomic item construction.
Chaining with connect()
Every builder is rooted in a connection:
$values = $this->opcua
->connect('historian')
->readMulti(null)
->node('ns=4;s=Tag1')
->node('ns=4;s=Tag2')
->execute();
Reads left-to-right: pick connection → pick builder → set parameters → execute.
When to use the builder vs the one-shot
| Operation | One-shot | Use the builder when… |
|---|---|---|
| Read one Value | $client->read(...) |
Non-Value attribute, batch |
| Write one Value | $client->write(...) |
Explicit type, batch |
| Browse one folder | $client->browse(...) |
(no builder — pass args directly) |
| Method call | $client->call(...) |
(no builder) |
| History | $client->historyReadRaw(...) |
(no builder — three flat methods) |
Builders accumulate state — don't reuse
Each builder is a fresh instance. Don't reuse a configured builder across iterations:
// OK — single batch:
$builder = $client->readMulti(null);
foreach ($tags as $node) {
$builder->node($node);
}
$values = $builder->execute();
// NOT this — second iteration leaks the first's state:
$builder = $client->readMulti(null);
foreach ($groups as $group) {
foreach ($group as $node) {
$builder->node($node);
}
$values = $builder->execute(); // accumulates across groups!
}
Call the *Multi(null) accessor again for each fresh batch.
Static analysis
PHPStan / Psalm resolve builder return types from the fluent
setters. $client->readMulti(null)->node('...')->execute() is
DataValue[]. No annotations needed.
Async / queued builders
There's no async builder. PHP-OPC-UA is synchronous. To run an operation off the request thread, dispatch a Messenger message that uses the builder inside the handler:
#[AsMessageHandler]
final class SampleBatchHandler
{
public function __construct(private OpcUaClientInterface $client) {}
public function __invoke(SampleBatch $message): void
{
$builder = $this->client->readMulti(null);
foreach ($message->nodeIds as $node) {
$builder->node($node);
}
$values = $builder->execute();
// ...
}
}
Reference
The builder classes themselves are documented in the upstream
opcua-client docs.
The Symfony bundle adds nothing — the API is the same.
Where to read next
You've finished Using the client. Continue with Operations · Reading for per-operation detail.