Examples

Complete, copy-paste-ready code examples for all major features. All examples use Symfony dependency injection patterns — constructor injection of OpcuaManager or OpcUaClientInterface.

Read a Single Value

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class ServerStatusService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function getServerState(): int
    {
        $client = $this->opcua->connect();

        $dv = $client->read('i=2259');
        $state = $dv->getValue(); // 0 = Running

        $client->disconnect();

        return $state;
    }
}

Read Multiple Values (Array)

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class SensorReadService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function readAll(): array
    {
        $client = $this->opcua->connect();

        $results = $client->readMulti([
            ['nodeId' => 'i=2259'],
            ['nodeId' => 'ns=2;i=1001'],
            ['nodeId' => 'ns=2;s=Temperature'],
        ]);

        $values = [];
        foreach ($results as $i => $dv) {
            $values[$i] = $dv->getValue();
        }

        $client->disconnect();

        return $values;
    }
}

Read Multiple Values (Fluent Builder)

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class FluentReadService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function readWithMetadata(): array
    {
        $client = $this->opcua->connect();

        $results = $client->readMulti()
            ->node('i=2259')->value()
            ->node('ns=2;i=1001')->displayName()
            ->node('ns=2;s=Temperature')->value()
            ->execute();

        $values = [];
        foreach ($results as $dv) {
            $values[] = $dv->getValue();
        }

        $client->disconnect();

        return $values;
    }
}

Write a Value

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use PhpOpcua\Client\Types\BuiltinType;
use PhpOpcua\Client\Types\StatusCode;

class WriteService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function setSetpoint(int $value): bool
    {
        $client = $this->opcua->connect();

        $status = $client->write('ns=2;i=1001', $value, BuiltinType::Int32);

        $client->disconnect();

        return StatusCode::isGood($status);
    }
}

Write Without Explicit Type (Auto-Detection, v4.0+)

When auto_detect_write_type is enabled (the default), you can omit the type parameter. The client reads the node's DataType attribute, caches it, and uses it for the write.

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use PhpOpcua\Client\Types\StatusCode;

class AutoTypeWriteService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function setSetpoint(int $value): bool
    {
        $client = $this->opcua->connect();

        // No BuiltinType needed — the client auto-detects that ns=2;i=1001 is Int32
        $status = $client->write('ns=2;i=1001', $value);

        $client->disconnect();

        return StatusCode::isGood($status);
    }
}

Write Multiple Values (Fluent Builder)

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use PhpOpcua\Client\Types\StatusCode;

class BatchWriteService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function updateAll(): array
    {
        $client = $this->opcua->connect();

        $results = $client->writeMulti()
            ->node('ns=2;i=1001')->int32(42)
            ->node('ns=2;i=1002')->double(3.14)
            ->node('ns=2;s=Label')->string('active')
            ->execute();

        $statuses = [];
        foreach ($results as $i => $status) {
            $statuses[$i] = StatusCode::isGood($status) ? 'OK' : 'FAIL';
        }

        $client->disconnect();

        return $statuses;
    }
}

Read with Refresh Parameter (v4.0+)

When read metadata cache is enabled, use the refresh parameter to bypass the cache:

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class CachedReadService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function readWithCache(): void
    {
        $client = $this->opcua->connect();
        $client->setReadMetadataCache(true);

        // Cached read (default)
        $dv = $client->read('ns=2;i=1001');
        echo "Cached: " . $dv->getValue() . "\n";

        // Force fresh read from server
        $dv = $client->read('ns=2;i=1001', refresh: true);
        echo "Fresh: " . $dv->getValue() . "\n";

        $client->disconnect();
    }
}

Browse the Address Space

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class BrowseService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function browseObjects(): array
    {
        $client = $this->opcua->connect();

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

        $nodes = [];
        foreach ($refs as $ref) {
            $nodes[] = "{$ref->displayName} ({$ref->nodeClass->name}) — {$ref->nodeId}";
        }

        $client->disconnect();

        return $nodes;
    }
}

Recursive Browse

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class RecursiveBrowseService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function browseTree(): void
    {
        $client = $this->opcua->connect();

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

        $this->printTree($tree);

        $client->disconnect();
    }

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

Path Resolution

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class PathResolveService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function resolveAndRead(): void
    {
        $client = $this->opcua->connect();

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

        echo "ServerStatus NodeId: {$nodeId}\n";
        echo "Value: {$dv->getValue()}\n";

        $client->disconnect();
    }
}

Call a Method

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use PhpOpcua\Client\Types\Variant;
use PhpOpcua\Client\Types\BuiltinType;
use PhpOpcua\Client\Types\StatusCode;

class MethodCallService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function callHypotenuse(float $a, float $b): ?float
    {
        $client = $this->opcua->connect();

        $result = $client->call(
            'ns=2;i=100',   // parent object
            'ns=2;i=200',   // method
            [
                new Variant(BuiltinType::Double, $a),
                new Variant(BuiltinType::Double, $b),
            ],
        );

        $client->disconnect();

        if (StatusCode::isGood($result->statusCode)) {
            return $result->outputArguments[0]->value;
        }

        return null;
    }
}

Subscribe to Data Changes

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class SubscriptionService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function monitorNodes(): void
    {
        $client = $this->opcua->connect();

        $sub = $client->createSubscription(publishingInterval: 500.0);

        $client->createMonitoredItems($sub->subscriptionId, [
            ['nodeId' => 'ns=2;i=1001', 'clientHandle' => 1],
            ['nodeId' => 'ns=2;i=1002', 'clientHandle' => 2],
        ]);

        // Poll for notifications
        for ($i = 0; $i < 10; $i++) {
            $pub = $client->publish();

            foreach ($pub->notifications as $notif) {
                echo "[handle={$notif['clientHandle']}] {$notif['dataValue']->getValue()}\n";
            }

            usleep(500_000);
        }

        $client->deleteSubscription($sub->subscriptionId);
        $client->disconnect();
    }
}

Historical Data

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class HistoryService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function getLastHour(): array
    {
        $client = $this->opcua->connect();

        $history = $client->historyReadRaw(
            'ns=2;i=1001',
            new \DateTimeImmutable('-1 hour'),
            new \DateTimeImmutable('now'),
        );

        $data = [];
        foreach ($history as $dv) {
            $data[] = [
                'time' => $dv->sourceTimestamp->format('H:i:s'),
                'value' => $dv->getValue(),
            ];
        }

        $client->disconnect();

        return $data;
    }
}

Secure Connection with Authentication

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class SecureConnectionService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function readFromSecurePlc(): mixed
    {
        // Option 1: Via YAML config (recommended)
        // php_opcua_symfony_opcua:
        //     connections:
        //         secure:
        //             endpoint: 'opc.tcp://10.0.0.10:4840'
        //             security_policy: Basic256Sha256
        //             security_mode: SignAndEncrypt
        //             username: operator
        //             password: secret
        //             client_certificate: /etc/opcua/certs/client.pem
        //             client_key: /etc/opcua/certs/client.key
        $client = $this->opcua->connect('secure');

        // Option 2: Via connectTo with inline config
        $client = $this->opcua->connectTo('opc.tcp://10.0.0.10:4840', [
            'security_policy'    => 'Basic256Sha256',
            'security_mode'      => 'SignAndEncrypt',
            'username'           => 'operator',
            'password'           => 'secret',
            'client_certificate' => '/etc/opcua/certs/client.pem',
            'client_key'         => '/etc/opcua/certs/client.key',
        ]);

        $value = $client->read('ns=2;i=1001');

        $client->disconnect();

        return $value->getValue();
    }
}

ECC Secure Connection

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class EccConnectionService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function readWithEcc(): mixed
    {
        // ECC security — no client certificate needed (auto-generated)
        $client = $this->opcua->connectTo('opc.tcp://10.0.0.10:4840', [
            'security_policy' => 'ECC_nistP256',
            'security_mode'   => 'SignAndEncrypt',
            'username'        => 'operator',
            'password'        => 'secret',
        ]);

        $value = $client->read('ns=2;i=1001');

        $client->disconnect();

        return $value->getValue();
    }
}

ECC disclaimer: ECC security policies (ECC_nistP256, ECC_nistP384, ECC_brainpoolP256r1, ECC_brainpoolP384r1) are fully implemented and tested against the OPC Foundation's UA-.NETStandard reference stack. No commercial OPC UA vendor supports ECC endpoints yet.

Multiple Connections

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class MultiConnectionService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function readBothLines(): array
    {
        // Connect to two PLCs simultaneously
        $plc1 = $this->opcua->connect('plc-line-1');
        $plc2 = $this->opcua->connect('plc-line-2');

        $temp1 = $plc1->read('ns=2;s=Temperature')->getValue();
        $temp2 = $plc2->read('ns=2;s=Temperature')->getValue();

        $this->opcua->disconnectAll();

        return [
            'line_1' => $temp1,
            'line_2' => $temp2,
        ];
    }
}

DI in a Symfony Controller

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use PhpOpcua\Client\Types\StatusCode;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

class PlcController extends AbstractController
{
    public function __construct(private OpcuaManager $opcua) {}

    #[Route('/plc/status', methods: ['GET'])]
    public function status(): JsonResponse
    {
        $client = $this->opcua->connect();
        $state = $client->read('i=2259')->getValue();
        $client->disconnect();

        return $this->json([
            'server_state' => $state,
            'daemon_active' => $this->opcua->isSessionManagerRunning(),
        ]);
    }

    #[Route('/plc/write', methods: ['POST'])]
    public function write(): JsonResponse
    {
        $client = $this->opcua->connect();
        // Type is optional in v4 — auto-detected when auto_detect_write_type is enabled
        $status = $client->write('ns=2;i=1001', 42);
        $client->disconnect();

        return $this->json([
            'success' => StatusCode::isGood($status),
        ]);
    }
}

You can also inject OpcUaClientInterface directly for the default connection:

use PhpOpcua\Client\OpcUaClientInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

class SensorController extends AbstractController
{
    public function __construct(private OpcUaClientInterface $client) {}

    #[Route('/sensor/temperature', methods: ['GET'])]
    public function temperature(): JsonResponse
    {
        $dv = $this->client->read('ns=2;s=Temperature');

        return $this->json([
            'temperature' => $dv->getValue(),
            'timestamp' => $dv->sourceTimestamp?->format('c'),
        ]);
    }
}

Type Discovery

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class TypeDiscoveryService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function discoverAndRead(): void
    {
        $client = $this->opcua->connect();

        // Discover all custom types
        $count = $client->discoverDataTypes();
        echo "Discovered {$count} custom types\n";

        // Now reading structured types works without manual codecs
        $point = $client->read('ns=2;s=MyPoint')->getValue();
        // ['x' => 1.5, 'y' => 2.5, 'z' => 3.5]

        $client->disconnect();
    }
}

Error Handling

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use PhpOpcua\Client\Exception\ConnectionException;
use PhpOpcua\Client\Exception\ServiceException;
use PhpOpcua\Client\Types\StatusCode;

class SafeReadService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function readSafely(): ?int
    {
        try {
            $client = $this->opcua->connect();

            $dv = $client->read('ns=99;i=99999');
            if (StatusCode::isBad($dv->statusCode)) {
                echo "Bad status: {$dv->statusCode}\n";
                return null;
            }

            $client->disconnect();

            return $dv->getValue();
        } catch (ConnectionException $e) {
            echo "Connection failed: {$e->getMessage()}\n";
            return null;
        } catch (ServiceException $e) {
            echo "Service error: {$e->getMessage()}\n";
            return null;
        }
    }
}

Trust Store Configuration (v4.0+)

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use PhpOpcua\Client\Security\TrustPolicy;

class TrustStoreService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function connectWithTrustStore(): void
    {
        $client = $this->opcua->connection();

        // Configure trust store
        $client->setTrustStorePath('/var/opcua/trust');
        $client->setTrustPolicy(TrustPolicy::FingerprintAndExpiry);
        $client->autoAccept(true); // TOFU mode

        $client->connect('opc.tcp://192.168.1.100:4840');

        $dv = $client->read('i=2259');
        echo "ServerState: " . $dv->getValue() . "\n";

        $client->disconnect();
    }
}

Certificate Trust Management (v4.0+)

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use PhpOpcua\Client\Exception\UntrustedCertificateException;

class CertificateTrustService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function connectWithApproval(): void
    {
        try {
            $client = $this->opcua->connect();
        } catch (UntrustedCertificateException $e) {
            echo "Untrusted certificate: " . $e->getFingerprint() . "\n";

            // Approve the certificate and retry
            $client = $this->opcua->connection();
            $client->trustCertificate($e->getCertificate());
            $client->connect('opc.tcp://192.168.1.100:4840');
        }

        // Later, revoke trust
        // $client->untrustCertificate($derBytes);

        $client->disconnect();
    }
}

Event Dispatcher Setup (v4.0+)

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use Psr\EventDispatcher\EventDispatcherInterface;

class EventDispatcherService
{
    public function __construct(
        private OpcuaManager $opcua,
        private EventDispatcherInterface $dispatcher,
    ) {}

    public function connectWithEvents(): void
    {
        $client = $this->opcua->connect();

        // Attach Symfony's event dispatcher
        $client->setEventDispatcher($this->dispatcher);

        // Now all OPC UA operations fire PSR-14 events
        $dv = $client->read('i=2259'); // fires BeforeRead + AfterRead

        $client->disconnect(); // fires Disconnected
    }
}

Note: When using the bundle's YAML configuration, the Symfony event dispatcher is injected automatically into every connection. You only need to call setEventDispatcher() manually for ad-hoc connections created outside the manager.

Modify Monitored Items (v4.0+)

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class MonitoringTuningService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function adjustSamplingRate(): void
    {
        $client = $this->opcua->connect();

        $sub = $client->createSubscription(publishingInterval: 500.0);

        $client->createMonitoredItems($sub->subscriptionId, [
            ['nodeId' => 'ns=2;i=1001', 'clientHandle' => 1, 'samplingInterval' => 1000.0],
            ['nodeId' => 'ns=2;i=1002', 'clientHandle' => 2, 'samplingInterval' => 1000.0],
        ]);

        // Later, change the sampling interval for item 1
        $client->modifyMonitoredItems($sub->subscriptionId, [
            ['monitoredItemId' => 1, 'samplingInterval' => 200.0],
        ]);

        $client->deleteSubscription($sub->subscriptionId);
        $client->disconnect();
    }
}

Set Triggering (v4.0+)

Link monitored items so that one item triggers reporting of others:

use PhpOpcua\SymfonyOpcua\OpcuaManager;

class TriggeringService
{
    public function __construct(private OpcuaManager $opcua) {}

    public function setupTriggering(): void
    {
        $client = $this->opcua->connect();

        $sub = $client->createSubscription(publishingInterval: 500.0);

        $client->createMonitoredItems($sub->subscriptionId, [
            ['nodeId' => 'ns=2;i=1001', 'clientHandle' => 1],  // triggering item
            ['nodeId' => 'ns=2;i=1002', 'clientHandle' => 2],  // triggered item
            ['nodeId' => 'ns=2;i=1003', 'clientHandle' => 3],  // triggered item
        ]);

        // When item 1 changes, also report items 2 and 3
        $client->setTriggering(
            $sub->subscriptionId,
            1,          // triggering monitored item ID
            [2, 3],     // links to add
            [],         // links to remove
        );

        $client->deleteSubscription($sub->subscriptionId);
        $client->disconnect();
    }
}

Testing with MockClient

use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use PhpOpcua\Client\Types\StatusCode;

// In a Pest / PHPUnit test
it('reads temperature from PLC', function () {
    $mock = MockClient::create()
        ->onRead('ns=2;s=Temperature', fn() => DataValue::ofDouble(23.5));

    $value = $mock->read('ns=2;s=Temperature');

    expect($value->getValue())->toBe(23.5);
    expect($value->statusCode)->toBe(StatusCode::Good);
    expect($mock->callCount('read'))->toBe(1);
});