Connections
Each entry in connections is one OPC UA server. Name it sensibly, configure its security, set its per-connection log channel — then call ::connection('name') wherever you need it.
A "connection" in laravel-opcua is a named record under
connections in config/opcua.php that describes how to reach
one OPC UA server. Pattern: one entry per server, named after
the role the server plays.
Anatomy of a connection
'plc-line-a' => [
// Required
'endpoint' => 'opc.tcp://line-a.plant.local:4840',
// Channel security
'security_policy' => 'Basic256Sha256', // None, Basic256Sha256, Aes256Sha256RsaPss, Ecc*, ...
'security_mode' => 'SignAndEncrypt', // None, Sign, SignAndEncrypt
// User identity
'username' => env('OPCUA_LINE_A_USER'),
'password' => env('OPCUA_LINE_A_PASS'),
// Application certificate (when security policy != None)
'client_certificate' => '/etc/opcua/client.pem',
'client_key' => '/etc/opcua/client.key',
'ca_certificate' => null,
// User X.509 (alternative to username/password)
'user_certificate' => null,
'user_key' => null,
// Client behaviour
'timeout' => 5.0,
'auto_retry' => 3,
'batch_size' => null, // auto-detected from server
'browse_max_depth' => 10,
// Trust store (validating the server's certificate)
'trust_store_path' => '/var/lib/opcua/trust',
'trust_policy' => 'fingerprint+expiry',
'auto_accept' => false,
'auto_accept_force' => false,
// Library behaviour flags
'auto_detect_write_type' => true,
'read_metadata_cache' => false,
// Logging — Laravel log channel
'log_channel' => 'opcua',
// Auto-connect (with auto_publish)
'auto_connect' => false,
'subscriptions' => [],
],
Most keys are optional. The minimum is endpoint:
'default' => [
'endpoint' => 'opc.tcp://localhost:4840',
],
Naming conventions
Connection names appear in:
Opcua::connection('plc-line-a')->read(...)— call siteconfig/opcua.phpkeys- Logs (when
log_channelis per-connection) - The session manager's
listoutput
Conventions that scale:
| Style | Example | When |
|---|---|---|
| Role-based | historian, live |
Single-server-per-role architectures |
| Location-based | plc-line-a, plc-line-b |
Plant-floor topologies |
| Tenant-based | tenant-acme, tenant-corp |
Multi-tenant SaaS |
| Hierarchical | factory-1.line-a.station-3 |
Deep equipment hierarchies |
Avoid:
- Names that change with deployment environment
(
prod-plc,staging-plc). Use the same name across envs and let theendpointchange via env vars. - Embedded host names (
192.168.1.100). Names should be stable even if the IP changes.
The default connection
Whichever connection is referenced by the default key in
config/opcua.php:
'default' => env('OPCUA_CONNECTION', 'default'),
'connections' => [
'default' => [ /* ... */ ],
'plc-line-a' => [ /* ... */ ],
],
The facade methods called without ::connection(...) use this
one:
Opcua::read('ns=2;s=...');
// Same as
Opcua::connection('default')->read('ns=2;s=...');
// Same as (when 'default' resolves to 'plc-line-a' via OPCUA_CONNECTION)
Opcua::connection('plc-line-a')->read('ns=2;s=...');
Override at deployment time with OPCUA_CONNECTION=plc-line-a
in .env — useful for multi-tenant deployments where each
deployment hits a different physical PLC.
Connection caching
OpcuaManager caches Client instances per name within the
request lifecycle. Three implications:
- Two
Opcua::read(...)calls in the same controller method share the sameClient. No duplicate connection cost. - PHP-FPM destroys the singleton between requests — the next
request rebuilds the
Client. - Octane / FrankenPHP preserve the singleton across requests
in the same worker. The
Clientlives for the worker's lifetime. See Octane and FrankenPHP.
The session manager changes none of this — the Client is
ManagedClient in managed mode, still cached per-name on the
manager.
Multiple-connection patterns
Pattern 1 — One PLC per production line
'connections' => [
'plc-line-a' => [
'endpoint' => 'opc.tcp://plc-a.factory.local:4840',
// ...
],
'plc-line-b' => [
'endpoint' => 'opc.tcp://plc-b.factory.local:4840',
// ...
],
'plc-line-c' => [
'endpoint' => 'opc.tcp://plc-c.factory.local:4840',
// ...
],
],
Each line has its own connection. Application code:
foreach (['plc-line-a', 'plc-line-b', 'plc-line-c'] as $line) {
$speed = Opcua::connection($line)->read('ns=2;s=PLC/Speed')->getValue();
// ...
}
Pattern 2 — Different roles per server
'connections' => [
'live' => [
'endpoint' => 'opc.tcp://scada.plant.local:4840',
'read_metadata_cache' => false, // values change constantly, no point
'timeout' => 2.0, // fast-fail for UI responsiveness
],
'historian' => [
'endpoint' => 'opc.tcp://historian.plant.local:4840',
'read_metadata_cache' => true, // metadata is stable
'timeout' => 30.0, // history reads can take time
],
],
Different settings per server role. The application picks the right one:
$current = Opcua::connection('live')->read('ns=2;s=Tag')->getValue();
$past = Opcua::connection('historian')
->historyReadRaw(
'ns=2;s=Tag',
new \DateTimeImmutable('-1 hour'),
new \DateTimeImmutable(),
);
Pattern 3 — Multi-tenant
For multi-tenant SaaS where each tenant has its own OPC UA server, connections come from the database:
$tenantConfig = auth()->user()->tenant->opcua_config;
$client = Opcua::connectTo(
$tenantConfig['endpoint'],
$tenantConfig,
as: 'tenant-' . auth()->user()->tenant_id,
);
See Using the client · Ad-hoc connections and Recipes · Multi-plant tenant.
Per-connection log channel
log_channel inside a connection points at a channel in
config/logging.php. The OPC UA client of that connection
writes diagnostic logs there.
'channels' => [
'opcua' => [
'driver' => 'daily',
'path' => storage_path('logs/opcua.log'),
'level' => 'info',
'days' => 14,
],
'opcua-line-a' => [
'driver' => 'daily',
'path' => storage_path('logs/opcua-line-a.log'),
'level' => 'info',
'days' => 14,
],
],
'connections' => [
'plc-line-a' => [
'endpoint' => 'opc.tcp://line-a.plant.local:4840',
'log_channel' => 'opcua-line-a',
],
],
Per-connection channels keep each PLC's logs isolated — invaluable when diagnosing a single line's issues without sifting through unrelated traffic.
Per-call connection switching
You don't have to commit to one connection at the call site:
public function compare(): array
{
$a = Opcua::connection('plc-line-a')->read('ns=2;s=PLC/Speed')->getValue();
$b = Opcua::connection('plc-line-b')->read('ns=2;s=PLC/Speed')->getValue();
return ['a' => $a, 'b' => $b];
}
Both Client instances are cached on the manager. The two reads
each pay their own OPC UA round-trip (the protocol is per-session)
but no extra connection cost.
What if a connection name is unknown?
Opcua::connection('not-defined');
// throws InvalidArgumentException
The manager rejects unknown names at the connection step rather than when the first method is called — fail-fast against typos and stale routing code.
Hot-reloading connections
Adding a new connection to config/opcua.php:
# Edit config/opcua.php
php artisan config:clear # if config:cache is enabled
The next HTTP request picks up the new connection. In Octane, the worker may still have the old config cached:
php artisan octane:reload
What to read next
- Environment variables — every env var the package reads.
- Using the client · Named connections — the call-site mechanics.
- Recipes · Multi-plant tenant — dynamic per-tenant connections.