Browsing the Address Space

Browsing a Node

browse() returns the references (children) of a node:

use PhpOpcua\Client\Types\NodeId;

// Using string format
$references = $client->browse('i=85'); // Objects folder

// Or with a NodeId object
$references = $client->browse(NodeId::numeric(0, 85));
// Events: dispatches NodeBrowsed after each browse() call, CacheHit/CacheMiss when cache is active

foreach ($references as $ref) {
    echo sprintf(
        "%s (NodeId: ns=%d;i=%s, Class: %s)\n",
        $ref->displayName,
        $ref->nodeId->namespaceIndex,
        $ref->nodeId->identifier,
        $ref->nodeClass->name,
    );
}

Browse Parameters

use PhpOpcua\Client\Types\BrowseDirection;

use PhpOpcua\Client\Types\NodeClass;

$references = $client->browse(
    nodeId: NodeId::numeric(0, 85),
    direction: BrowseDirection::Forward,
    referenceTypeId: NodeId::numeric(0, 33),  // HierarchicalReferences
    includeSubtypes: true,
    nodeClasses: [NodeClass::Object, NodeClass::Variable],  // filter by class
);

Browse direction:

Direction Value Meaning
Forward 0 Children
Inverse 1 Parents
Both 2 Both directions

Common reference types:

NodeId Name
NodeId::numeric(0, 33) HierarchicalReferences
NodeId::numeric(0, 35) Organizes
NodeId::numeric(0, 47) HasComponent
NodeId::numeric(0, 46) HasProperty
NodeId::numeric(0, 40) HasTypeDefinition

Node class filter:

Pass an array of NodeClass enum values to filter results. Empty array (default) means all classes.

use PhpOpcua\Client\Types\NodeClass;

// Only objects and variables
$refs = $client->browse($nodeId, nodeClasses: [NodeClass::Object, NodeClass::Variable]);

// Only methods
$refs = $client->browse($nodeId, nodeClasses: [NodeClass::Method]);

// All classes (default)
$refs = $client->browse($nodeId);

Available NodeClass values: Object, Variable, Method, ObjectType, VariableType, ReferenceType, DataType, View.

ReferenceDescription Properties

Each reference returned by browse() has these properties:

$ref->referenceTypeId;   // NodeId
$ref->isForward;         // bool
$ref->nodeId;            // NodeId
$ref->browseName;        // QualifiedName
$ref->displayName;       // LocalizedText
$ref->nodeClass;         // NodeClass enum
$ref->typeDefinition;    // ?NodeId

Handling Continuation

Some servers paginate large result sets. You have two options.

browseAll() follows all continuation points and returns the complete list:

$refs = $client->browseAll('i=85');

Manual

If you need control over pagination:

$result = $client->browseWithContinuation(NodeId::numeric(0, 85));

$allRefs = $result->references;
$continuationPoint = $result->continuationPoint;

while ($continuationPoint !== null) {
    $next = $client->browseNext($continuationPoint);
    $allRefs = array_merge($allRefs, $next->references);
    $continuationPoint = $next->continuationPoint;
}

Recursive Browse

browseRecursive() walks the address space from a starting node and builds a tree of BrowseNode objects. Continuation points are handled at each level, and cycle detection prevents infinite loops on circular references.

$tree = $client->browseRecursive('i=85', maxDepth: 2);

foreach ($tree as $node) {
    echo $node->displayName . "\n";

    foreach ($node->getChildren() as $child) {
        echo "  " . $child->displayName . "\n";
    }
}

Parameters

$tree = $client->browseRecursive(
    nodeId: NodeId::numeric(0, 85),
    direction: BrowseDirection::Forward,
    maxDepth: 3,
    referenceTypeId: NodeId::numeric(0, 33),
    includeSubtypes: true,
    nodeClasses: [NodeClass::Object, NodeClass::Variable],
);
Parameter Default Description
nodeId (required) Starting node
direction Forward Browse direction
maxDepth null (configured default: 10) Max recursion depth. -1 for unlimited (capped at 256)
referenceTypeId null Filter by reference type
includeSubtypes true Include reference subtypes
nodeClasses [] Filter by NodeClass enum values. Empty = all classes

Depth Limits

maxDepth Behavior
null Uses configured default (10, or your setDefaultBrowseMaxDepth() value)
1 Direct children only
-1 Unlimited (capped at 256)
> 256 Capped at 256

You can change the default globally:

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

$tree = $client->browseRecursive($nodeId);              // uses 20
$tree = $client->browseRecursive($nodeId, maxDepth: 3); // override: 3

Warning: High depth values can cause problems. Each level sends one browse request per node -- thousands of nodes means thousands of round-trips. Large trees eat memory, and massive browsing can overwhelm resource-constrained PLCs. Start small and increase only when needed.

BrowseNode Properties

Each node in the tree wraps a ReferenceDescription and holds its children:

$node->reference;      // ReferenceDescription
$node->nodeId;         // NodeId
$node->displayName;    // LocalizedText
$node->browseName;     // QualifiedName
$node->nodeClass;      // NodeClass enum
$node->getChildren();  // BrowseNode[]
$node->hasChildren();  // bool

Cycle Detection

The method tracks every visited NodeId. If a node appears again, it is included as a leaf with no children, cutting the recursion. This matches the behavior of open62541, node-opcua, and the OPC Foundation .NET SDK.

Printing a Tree

function printTree(array $nodes, int $indent = 0): void
{
    foreach ($nodes as $node) {
        echo str_repeat('  ', $indent) . $node->displayName . "\n";
        printTree($node->getChildren(), $indent + 1);
    }
}

$tree = $client->browseRecursive(NodeId::numeric(0, 85), maxDepth: 3);
printTree($tree);

Path Resolution

Instead of browsing step by step, resolve a human-readable path to a NodeId in one call:

$nodeId = $client->resolveNodeId('/Objects/Server/ServerStatus/State');
$dataValue = $client->read($nodeId);

This calls TranslateBrowsePathsToNodeIds under the hood -- a single round-trip, much faster than manual browsing.

Path Format

  • Segments separated by /
  • Leading / is optional
  • Default start is Root (ns=0;i=84)
  • For non-zero namespaces, use ns:Name format
// Simple path
$nodeId = $client->resolveNodeId('/Objects/Server');

// With namespaced segments
$nodeId = $client->resolveNodeId('/Objects/2:MyPLC/2:Temperature');

// Custom starting node
$nodeId = $client->resolveNodeId('Server', NodeId::numeric(0, 85));

Advanced: translateBrowsePaths

For full control over TranslateBrowsePathsToNodeIds, including resolving multiple paths in a single request:

use PhpOpcua\Client\Types\QualifiedName;
use PhpOpcua\Client\Types\StatusCode;

// Fluent builder
$results = $client->translateBrowsePaths()
    ->from('i=85')->path('Server', 'ServerStatus')
    ->execute();

if (StatusCode::isGood($results[0]->statusCode)) {
    $targetNodeId = $results[0]->targets[0]->targetId;
}

// Or with array (still works)
$results = $client->translateBrowsePaths([
    [
        'startingNodeId' => NodeId::numeric(0, 85),
        'relativePath' => [
            ['targetName' => new QualifiedName(0, 'Server')],
            ['targetName' => new QualifiedName(0, 'ServerStatus')],
        ],
    ],
]);

Tip: The builder's ->from() sets the starting node, and ->path() accepts segment names as separate arguments. Call ->from() again to add another path in the same request.

Each path element supports:

Field Default Description
targetName (required) QualifiedName of the target
referenceTypeId HierarchicalReferences Reference type to follow
isInverse false Follow inverse references
includeSubtypes true Include subtypes

Caching

Browse results are cached by default using an in-memory PSR-16 cache with a 300-second TTL. This avoids redundant server round-trips when the address space is browsed repeatedly — common in industrial PLC environments where the node tree rarely changes.

Default Behavior

Caching is active out of the box. No setup required:

$refs = $client->browse('i=85');       // hits the server
$refs = $client->browse('i=85');       // served from cache

Cache Bypass

Skip the cache for a single call with useCache: false:

$refs = $client->browse('i=85', useCache: false);
$refs = $client->browseAll('i=85', useCache: false);
$nodeId = $client->resolveNodeId('/Objects/Server', useCache: false);

Custom Cache Driver

Any PSR-16 CacheInterface implementation works — including Laravel's cache:

use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\Cache\InMemoryCache;
use PhpOpcua\Client\Cache\FileCache;

// In-memory (default)
$client = ClientBuilder::create()
    ->setCache(new InMemoryCache(ttl: 300))
    ->connect('opc.tcp://localhost:4840');

// File-based (survives PHP process restart)
$client = ClientBuilder::create()
    ->setCache(new FileCache('/tmp/opcua-cache', ttl: 600))
    ->connect('opc.tcp://localhost:4840');

// Laravel
$client = ClientBuilder::create()
    ->setCache(app('cache')->store('redis'))
    ->connect('opc.tcp://localhost:4840');

// Disable caching entirely
$client = ClientBuilder::create()
    ->setCache(null)
    ->connect('opc.tcp://localhost:4840');

Cache Invalidation

// Invalidate results for a specific node
$client->invalidateCache(NodeId::numeric(0, 85));

// Flush all cached results
$client->flushCache();

Cached Methods

Method Cached
browse() Yes
browseAll() Yes
resolveNodeId() Yes
getEndpoints() Yes
discoverDataTypes() Yes (discovered type definitions are cached and replayed)
browseWithContinuation() No
browseNext() No
browseRecursive() No (but each internal browseAll() call is cached)

Cache Key Format

Keys include the endpoint URL hash, operation type, NodeId, and browse parameters. Two clients pointing at different servers never collide:

opcua:{endpoint_hash}:browse:{nodeId}:{direction}:{includeSubtypes}:{nodeClassMask}
opcua:{endpoint_hash}:browseAll:{nodeId}:{direction}:{includeSubtypes}:{nodeClassMask}
opcua:{endpoint_hash}:resolve:{startingNodeId}:{path_hash}
opcua:{endpoint_hash}:endpoints:{url_hash}
opcua:{endpoint_hash}:dataTypes:{namespaceIndex|all}

Well-Known NodeIds

Name NodeId Description
Root NodeId::numeric(0, 84) Root of the address space
Objects NodeId::numeric(0, 85) Objects folder
Types NodeId::numeric(0, 86) Types folder
Views NodeId::numeric(0, 87) Views folder
Server NodeId::numeric(0, 2253) Server object
ServerStatus NodeId::numeric(0, 2256) Server status
ServiceLevel NodeId::numeric(0, 2267) Service level