DEV Community

Cover image for OPC UA from PHP? Yes, with Zero C Extensions — Introducing php-opcua/opcua-client
Gianfrancesco
Gianfrancesco

Posted on

OPC UA from PHP? Yes, with Zero C Extensions — Introducing php-opcua/opcua-client

If you've ever tried to connect a PHP application to industrial machinery — a PLC, a SCADA system, a historian — you've probably hit a wall. The OPC UA standard is the lingua franca of industrial IoT, but most implementations assume you're writing in C++, Python, or .NET. PHP? Good luck finding anything that isn't a thin HTTP wrapper around a gateway you have to run separately.

That changes with php-opcua/opcua-client.


What Is OPC UA?

OPC UA (Unified Architecture) is an industrial communication standard developed by the OPC Foundation. It lets software clients talk to sensors, PLCs, SCADA systems, historians, and IoT devices over a standardized protocol — with built-in security, data typing, discovery, and more. It's the backbone of Industry 4.0 and IIoT architectures worldwide.

The protocol runs over TCP (binary encoding) and is notoriously complex to implement: it includes asymmetric and symmetric encryption, certificate management, session handling, subscriptions, historical queries, and a rich address space model.


Why a Pure PHP Implementation?

Most PHP projects that need OPC UA resort to one of:

  • An HTTP REST gateway (adds a sidecar process, latency, and a point of failure)
  • A COM bridge on Windows (commercial, not portable)
  • Shelling out to a Python or Node.js script

php-opcua/opcua-client is different. It implements the entire OPC UA binary protocol stack natively in PHP, with only ext-openssl as a runtime dependency. No FFI. No COM. No gateway. Just composer require and you're connecting to PLCs from your Laravel app.


Quick Start

composer require php-opcua/opcua-client
Enter fullscreen mode Exit fullscreen mode
use PhpOpcua\Client\ClientBuilder;

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

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

$client->disconnect();
Enter fullscreen mode Exit fullscreen mode

Three lines. No config files, no XML, no service containers.

NodeId strings like 'i=2259', 'ns=2;i=1001', or 'ns=2;s=MyNode' are accepted everywhere. Invalid strings throw InvalidNodeIdException.


Core Features in Practice

Browse the Address Space

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

foreach ($refs as $ref) {
    echo "{$ref->displayName} ({$ref->nodeId})\n";
    // => Server (ns=0;i=2253)
    // => MyPLC (ns=2;i=1000)
}
Enter fullscreen mode Exit fullscreen mode

Read Multiple Values in One Round-Trip

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

foreach ($results as $dataValue) {
    echo $dataValue->getValue() . "\n";
}
Enter fullscreen mode Exit fullscreen mode

The fluent builder auto-batches requests transparently — one TCP round-trip regardless of how many nodes you chain.

Write to a PLC

use PhpOpcua\Client\Types\BuiltinType;

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

// Explicit type
$client->write('ns=2;i=1001', 42, BuiltinType::Int32);
Enter fullscreen mode Exit fullscreen mode

Subscribe to Real-Time Data Changes

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

$client->createMonitoredItems($sub->subscriptionId, [
    ['nodeId' => NodeId::numeric(2, 1001)],
]);

$response = $client->publish();
foreach ($response->notifications as $notif) {
    echo $notif['dataValue']->getValue() . "\n";
}
Enter fullscreen mode Exit fullscreen mode

Call Methods on the Server

use PhpOpcua\Client\Types\Variant;

$result = $client->call(
    'i=2253',   // Server object
    'i=11492',  // GetMonitoredItems method
    [new Variant(BuiltinType::UInt32, 1)],
);

echo $result->statusCode;                   // 0 (Good)
print_r($result->outputArguments[0]->value); // [1001, 1002, ...]
Enter fullscreen mode Exit fullscreen mode

Query Historical Data

$values = $client->historyReadRaw(
    'ns=2;i=1001',
    startTime: new DateTimeImmutable('-1 hour'),
    endTime: new DateTimeImmutable(),
);

foreach ($values as $dv) {
    echo "[{$dv->sourceTimestamp->format('H:i:s')}] {$dv->getValue()}\n";
}
Enter fullscreen mode Exit fullscreen mode

Enterprise-Grade Security

The library supports 6 security policies, from None up to Aes256Sha256RsaPss, and three authentication modes: anonymous, username/password, and X.509 certificates.

use PhpOpcua\Client\Security\SecurityPolicy;
use PhpOpcua\Client\Security\SecurityMode;

$client = ClientBuilder::create()
    ->setSecurityPolicy(SecurityPolicy::Basic256Sha256)
    ->setSecurityMode(SecurityMode::SignAndEncrypt)
    ->setClientCertificate('/certs/client.pem', '/certs/client.key', '/certs/ca.pem')
    ->setUserCredentials('operator', 'secret')
    ->connect('opc.tcp://192.168.1.100:4840');
Enter fullscreen mode Exit fullscreen mode

Omit setClientCertificate() and a self-signed cert is auto-generated in memory — perfect for development or servers with auto-accept enabled.

The library also ships a persistent trust store with Trust-On-First-Use (TOFU) support:

use PhpOpcua\Client\TrustStore\FileTrustStore;
use PhpOpcua\Client\TrustStore\TrustPolicy;

$client = ClientBuilder::create()
    ->setTrustStore(new FileTrustStore())      // ~/.opcua/trusted/
    ->setTrustPolicy(TrustPolicy::Fingerprint)
    ->connect('opc.tcp://192.168.1.100:4840');
Enter fullscreen mode Exit fullscreen mode

Laravel Integration

If you're on Laravel, there's a dedicated package:

composer require php-opcua/laravel-opcua
Enter fullscreen mode Exit fullscreen mode

It ships a service provider, a facade, and config scaffolding so you can inject OpcUaClient from the container and configure connections in config/opcua.php. Fully compatible with Laravel's PSR-3 logger and PSR-14 event dispatcher.


47 PSR-14 Events — Zero Overhead When Unused

The library fires granular events for every lifecycle moment: connection, session, data change, alarms, retries, cache hits, and more. Plug in any PSR-14 dispatcher to react to them:

use PhpOpcua\Client\Event\AlarmActivated;

class AlarmHandler {
    public function handleActivated(AlarmActivated $event): void {
        Log::critical("Alarm: {$event->sourceName} (severity: {$event->severity})");
    }
}
Enter fullscreen mode Exit fullscreen mode

Without a dispatcher, the default NullEventDispatcher ensures zero overhead.


Testing Without a Real PLC

The library ships a MockClient that implements the same interface as the real client — no TCP connection required:

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

$client = MockClient::create();

$client->onRead(function (NodeId $nodeId) {
    return DataValue::ofDouble(23.5);
});

$value = $client->read('ns=2;s=Temperature');
echo $value->getValue(); // 23.5

echo $client->callCount('read'); // 1
Enter fullscreen mode Exit fullscreen mode

Register handlers for onRead(), onWrite(), onBrowse(), onCall(), and onResolveNodeId(). Track calls with getCalls(), callCount(), and resetCalls(). Your CI pipeline never needs a real OPC UA server.


The CLI Tool

A companion CLI package lets you explore OPC UA servers from the terminal:

composer require php-opcua/opcua-cli

# Browse the address space
opcua-cli browse opc.tcp://192.168.1.10:4840 /Objects

# Read a value
opcua-cli read opc.tcp://192.168.1.10:4840 "ns=2;i=1001"

# Watch a value in real time
opcua-cli watch opc.tcp://192.168.1.10:4840 "ns=2;i=1001"

# Discover endpoints
opcua-cli endpoints opc.tcp://192.168.1.10:4840

# Manage trusted server certificates
opcua-cli trust opc.tcp://server:4840
Enter fullscreen mode Exit fullscreen mode

It also generates PHP type files from NodeSet2.xml companion specifications — useful when working with vendor-specific data models.


Pre-Built Companion Types

The ecosystem includes php-opcua/opcua-client-nodeset, which ships pre-generated PHP types for 51 OPC Foundation companion specifications — Robotics, Machinery, MachineTool, ISA-95, CNC, MTConnect, and more:

use PhpOpcua\Nodeset\Robotics\RoboticsRegistrar;
use PhpOpcua\Nodeset\Robotics\Enums\OperationalModeEnumeration;

$client = ClientBuilder::create()
    ->loadGeneratedTypes(new RoboticsRegistrar())
    ->connect('opc.tcp://192.168.1.100:4840');

// Enum values auto-cast to PHP BackedEnum — not raw ints
$mode = $client->read(RoboticsNodeIds::OperationalMode)->getValue();
// OperationalModeEnumeration::MANUAL_REDUCED_SPEED
Enter fullscreen mode Exit fullscreen mode

Structured types return typed DTOs with IDE autocomplete — no more digging through arrays.


What About Long-Lived Connections?

PHP's request/response model doesn't naturally support persistent TCP connections. For continuous monitoring or subscription polling, the ecosystem provides php-opcua/opcua-session-manager — a ReactPHP daemon that keeps OPC UA sessions alive across short-lived PHP requests via Unix sockets. It's a separate package by design: bundling a ReactPHP daemon would break the zero-dependency philosophy of the core library.


Feature Summary

Feature Status
Browse / Path resolution
Read / Write (single & batch)
Subscriptions & data change events
Historical data (raw, processed, at-time)
Method calls
6 security policies (None → Aes256Sha256RsaPss)
Anonymous / Username / X.509 auth
PSR-3 logging, PSR-14 events, PSR-16 cache
MockClient for testing
Laravel service provider + facade
CLI tool
51 companion NodeSet types
PHP 8.2 / 8.3 / 8.4 / 8.5
Zero runtime deps (except ext-openssl)
1290+ tests, 99%+ coverage

Conclusion

php-opcua/opcua-client fills a real gap: industrial systems speak OPC UA, and PHP has always been left out of that conversation. This library brings the full protocol stack to PHP with production-grade security, a clean API, a MockClient for testing, and a growing ecosystem of companion packages.

If you're building anything in the IIoT or Industry 4.0 space with PHP — connecting Laravel to factory floor equipment, pulling data from historians, or monitoring production lines — this library is worth a look.

Links:

Top comments (0)