laravel-opcua
A thin, idiomatic Laravel layer over php-opcua/opcua-client. Three things to remember:
- The Facade
PhpOpcua\LaravelOpcua\Facades\Opcua proxies the full OpcUaClientInterface. Anything opcua-client can do, the Facade can do.
OpcuaManager::shouldUseSessionManager() decides per-call whether to instantiate a direct Client (TCP straight to the server) or a ManagedClient (IPC to the long-lived daemon). The decision is transparent to application code.
- v4.4.0 picked up 21 new client methods (HistoryUpdate, File transfer, Aggregates). They are reachable through
Opcua::* and Opcua::connection('plc-1')->* without any config or service-provider change.
What this package is for
| You want to |
Use |
| Read / write OPC UA nodes from a controller, job, command |
Opcua::read(), Opcua::write() (Facade) |
| Talk to multiple OPC UA servers |
Named connections in config/opcua.php, Opcua::connection('plc-1') |
| Connect to a runtime-discovered endpoint |
Opcua::connectTo($url, $configOverrides, as: 'cache-key') |
| Avoid one new TCP connection per HTTP request |
Run php artisan opcua:session as a supervised daemon |
| React to data changes, alarms, etc. via PSR-14 → Laravel Event system |
Configure auto_publish: true + auto_connect: true + subscriptions: [...] |
| Test code that touches OPC UA without a server |
PhpOpcua\Client\MockClient + Facade swap, see references/TESTING.md |
| Stream notifications to Livewire / Broadcasting / Notifications / Filament |
Register listeners on DataChangeReceived, AlarmActivated, etc. (see references/INTEGRATIONS.md) |
Mental model
Application code
└── Opcua::* (Facade)
└── OpcuaManager::connection($name)
├── shouldUseSessionManager() == true?
│ └── ManagedClient (IPC → daemon → TCP → server)
│ └── TransportFactory picks UnixSocketTransport (Linux/macOS) or TcpLoopbackTransport (Windows)
└── shouldUseSessionManager() == false?
└── ClientBuilder::create()->...->connect() (direct TCP, new connection per call)
The two branches expose the same OpcUaClientInterface. Your code does not know which it has.
Quick start
composer require php-opcua/laravel-opcua
php artisan vendor:publish --tag=opcua-config
# .env
OPCUA_ENDPOINT=opc.tcp://plc.example:4840
OPCUA_USERNAME=operator
OPCUA_PASSWORD=changeme
OPCUA_SECURITY_POLICY=Basic256Sha256
OPCUA_SECURITY_MODE=SignAndEncrypt
use PhpOpcua\LaravelOpcua\Facades\Opcua;
Opcua::read('i=2259')->getValue(); // 0 = Running
Opcua::write('ns=2;s=Setpoint', 42.5); // auto-detects Double
Opcua::browseRecursive('i=85', maxDepth: 3);
The 3 patterns you will use 90% of the time
Pattern A — one-shot read/write (no daemon)
Best for HTTP requests, scheduled jobs, Artisan commands. The Facade opens a TCP connection per call, reads/writes, then closes.
public function showServerState(): array
{
$state = Opcua::read('i=2259')->getValue();
return ['state' => $state, 'running' => $state === 0];
}
Pattern B — daemon-backed, transparent session reuse
When you run php artisan opcua:session under Supervisor/systemd, every Facade call goes through the daemon. Sessions are reused; you no longer pay the connect + create-session + activate-session round-trip per request.
Run the daemon:
php artisan opcua:session --log-channel=stack --cache-store=redis
Application code does not change. Same Opcua::read(...), but now backed by ManagedClient automatically.
Pattern C — auto_publish + Laravel events
Subscribe declaratively in config; receive notifications as Laravel events.
// config/opcua.php
'session_manager' => ['auto_publish' => true],
'connections' => [
'plc-1' => [
'endpoint' => 'opc.tcp://plc.example:4840',
'auto_connect' => true,
'subscriptions' => [[
'publishing_interval' => 500.0,
'monitored_items' => [
['node_id' => 'ns=2;s=Temperature', 'client_handle' => 1],
],
]],
],
],
// app/Providers/EventServiceProvider.php
use PhpOpcua\Client\Event\DataChangeReceived;
Event::listen(DataChangeReceived::class, function (DataChangeReceived $e) {
SensorReading::create([
'client_handle' => $e->clientHandle,
'value' => $e->dataValue->getValue(),
'sampled_at' => $e->dataValue->sourceTimestamp,
]);
});
The daemon's auto-publish loop dispatches PSR-14 events through Laravel's event dispatcher. Listeners can be queued, broadcast, etc. — see references/INTEGRATIONS.md.
Facade method surface (one-line summary)
Connection management: connection(), connect(), connectTo(), disconnect(), disconnectAll(), isSessionManagerRunning(), getDefaultConnection().
Proxied to the active connection (auto-routed via __call):
- Reading:
read, readMulti
- Writing:
write, writeMulti
- Browsing:
browse, browseAll, browseRecursive, browseWithContinuation, browseNext, resolveNodeId, translateBrowsePaths
- Method calls:
call
- Subscriptions:
createSubscription, createMonitoredItems, createEventMonitoredItem, modifyMonitoredItems, setTriggering, deleteMonitoredItems, deleteSubscription, publish, transferSubscriptions, republish
- History read:
historyReadRaw, historyReadProcessed, historyReadAtTime
- History update (v4.4):
historyInsertData, historyReplaceData, historyUpdateData, historyDeleteRawModified, historyDeleteAtTime, historyInsertEvent, historyReplaceEvent, historyUpdateEvent, historyDeleteEvent
- File transfer (v4.4):
openFile, closeFile, readFile, writeFile, getFilePosition, setFilePosition, createDirectory, createFileInDirectory, deleteFileSystemObject, moveOrCopyFileSystemObject
- Aggregates (v4.4):
aggregate, historyAggregate
- Trust store:
trustCertificate, untrustCertificate, getTrustStore, getTrustPolicy
- Discovery:
getEndpoints, discoverDataTypes, getExtensionObjectRepository
- Cache / logging:
getLogger, getCache, invalidateCache, flushCache
- Connection state:
connect, disconnect, reconnect, isConnected, getConnectionState, getTimeout, getAutoRetry, getBatchSize, getDefaultBrowseMaxDepth, getServerMaxNodesPerRead, getServerMaxNodesPerWrite
Full PHPDoc with all signatures: src/Facades/Opcua.php.
When to follow the references
Progressive disclosure — only load what the task needs:
references/CONFIG.md — every config/opcua.php key, env vars, named connections, defaults, version-specific keys
references/SESSION_MANAGER.md — daemon command, Supervisor/systemd setup, IPC endpoints, auto-publish vs manual publish, monitoring
references/EVENTS.md — full list of 56 PSR-14 events, payload shapes, queued listener pattern, common listener recipes
references/INTEGRATIONS.md — Octane/FrankenPHP, Horizon/queues, Livewire, Filament, Broadcasting, Notifications, Telescope/Pulse
references/SECURITY.md — policies, modes, trust store, certificate auto-generation, X.509 user auth, env-driven config
references/TESTING.md — Pest setup, MockClient + Facade swap, integration tests with Docker test-suite
references/PITFALLS.md — common gotchas: facade in config files, Octane state, mixed daemon versions, etc.
assets/recipes.md — copy-pasteable code snippets for the 15 most common end-to-end tasks
Idiomatic patterns
Inject OpcuaManager, not the Facade, in long-lived classes. The Facade resolves the manager every call; injection caches it.
public function __construct(private OpcuaManager $opcua) {}
public function handle(): void { $this->opcua->read(...); }
Use named connections per server. Don't string-build endpoints in code. Define plc-1, plc-2, historian in config, then Opcua::connection('historian')->historyReadRaw(...).
Don't disconnect in HTTP requests when the daemon is enabled. ManagedClient::disconnect() closes the daemon-side session, undoing the connection pooling. Let the session manager handle lifecycle.
For auto-published subscriptions, never call publish() yourself. It returns auto_publish_active error. Subscribe to events instead.
Use useCache: false for fresh reads of high-churn nodes. The read metadata cache is the default — pass refresh: true to bypass.
Queue listeners for heavy event handling. A DataChangeReceived listener that hits a database should be ShouldQueue. Otherwise the daemon publish loop blocks on it.
Opcua::connectTo() is for ad-hoc; cache by name with the as: parameter when reused across the request.
Trust store goes on disk, not in DB. storage/app/opcua-trust-store/ by default; check it into a deploy volume, not git.
Run the daemon under a dedicated UID with socket_mode: 0600. The Facade-side process must be in the same group/UID.
In Octane, configure OpcuaManager as request-scoped via flushed singletons — see references/INTEGRATIONS.md for the OctaneServiceProvider::tick hook.
Exit codes (Artisan opcua:session)
| Code |
Meaning |
| 0 |
Daemon exited cleanly (SIGTERM/SIGINT) |
| 1 |
Configuration error (invalid socket_path, missing required key) |
| 2 |
Bind failure (port in use, socket-path EACCES, parent dir missing) |
| 3 |
Runtime error inside daemon loop (logged via PSR-3 channel) |
Non-zero exits should be caught by Supervisor autorestart=true or systemd Restart=on-failure.
Versioning
The Laravel package versions lock-step with php-opcua/opcua-client and php-opcua/opcua-session-manager. Always upgrade in this order:
- Daemon first. Stop
opcua:session, composer update, restart.
- Application second.
composer update php-opcua/laravel-opcua.
If you upgrade application before daemon and call a v4.4 method (e.g. historyInsertData), ManagedClient::__call() will fail with BadMethodCallException because the daemon has no handler for it.
What this skill does NOT cover
- The raw OPC UA protocol — see the
opcua-client skill.
- The session-manager daemon's IPC protocol — see the
opcua-session-manager skill.
- CLI usage — see the
opcua-cli skill.
- Companion-spec types (DI, IA, AutoID, etc.) — see the
opcua-client-nodeset skill.
Cross-skill workflow example (docs/recipes/persistent-tag-history.md):
opcua-cli generate:nodeset Vendor.NodeSet2.xml ... (nodeset skill)
- App reads typed nodes via
Opcua::read() (this skill)
- Persists into a historian via
Opcua::historyInsertData() (this skill + opcua-client)
- A Filament panel browses results (this skill + Filament integration)
1---2name: laravel-opcua3description: Laravel 11/12/13 integration for OPC UA. Provides a Facade (Opcua::*), service provider, .env-based named connections, an Artisan daemon command (opcua:session), and transparent session persistence via the opcua-session-manager daemon. Use this skill whenever the user is working with OPC UA from a Laravel application — controllers, jobs, Livewire components, Filament panels, broadcasting, Horizon queues, Octane workers, scheduled tasks, or Pest tests.4license: MIT5---67# laravel-opcua89A thin, idiomatic Laravel layer over `php-opcua/opcua-client`. Three things to remember:10111. The Facade `PhpOpcua\LaravelOpcua\Facades\Opcua` proxies the full `OpcUaClientInterface`. Anything `opcua-client` can do, the Facade can do.122. `OpcuaManager::shouldUseSessionManager()` decides per-call whether to instantiate a direct `Client` (TCP straight to the server) or a `ManagedClient` (IPC to the long-lived daemon). The decision is transparent to application code.133. v4.4.0 picked up 21 new client methods (HistoryUpdate, File transfer, Aggregates). They are reachable through `Opcua::*` and `Opcua::connection('plc-1')->*` without any config or service-provider change.1415## What this package is for1617| You want to | Use |18|---|---|19| Read / write OPC UA nodes from a controller, job, command | `Opcua::read()`, `Opcua::write()` (Facade) |20| Talk to multiple OPC UA servers | Named connections in `config/opcua.php`, `Opcua::connection('plc-1')` |21| Connect to a runtime-discovered endpoint | `Opcua::connectTo($url, $configOverrides, as: 'cache-key')` |22| Avoid one new TCP connection per HTTP request | Run `php artisan opcua:session` as a supervised daemon |23| React to data changes, alarms, etc. via PSR-14 → Laravel Event system | Configure `auto_publish: true` + `auto_connect: true` + `subscriptions: [...]` |24| Test code that touches OPC UA without a server | `PhpOpcua\Client\MockClient` + Facade swap, see `references/TESTING.md` |25| Stream notifications to Livewire / Broadcasting / Notifications / Filament | Register listeners on `DataChangeReceived`, `AlarmActivated`, etc. (see `references/INTEGRATIONS.md`) |2627## Mental model2829```30Application code31 └── Opcua::* (Facade)32 └── OpcuaManager::connection($name)33 ├── shouldUseSessionManager() == true?34 │ └── ManagedClient (IPC → daemon → TCP → server)35 │ └── TransportFactory picks UnixSocketTransport (Linux/macOS) or TcpLoopbackTransport (Windows)36 └── shouldUseSessionManager() == false?37 └── ClientBuilder::create()->...->connect() (direct TCP, new connection per call)38```3940The two branches expose the same `OpcUaClientInterface`. Your code does not know which it has.4142## Quick start4344```bash45composer require php-opcua/laravel-opcua46php artisan vendor:publish --tag=opcua-config47```4849```env50# .env51OPCUA_ENDPOINT=opc.tcp://plc.example:484052OPCUA_USERNAME=operator53OPCUA_PASSWORD=changeme54OPCUA_SECURITY_POLICY=Basic256Sha25655OPCUA_SECURITY_MODE=SignAndEncrypt56```5758```php59use PhpOpcua\LaravelOpcua\Facades\Opcua;6061Opcua::read('i=2259')->getValue(); // 0 = Running62Opcua::write('ns=2;s=Setpoint', 42.5); // auto-detects Double63Opcua::browseRecursive('i=85', maxDepth: 3);64```6566## The 3 patterns you will use 90% of the time6768### Pattern A — one-shot read/write (no daemon)6970Best for HTTP requests, scheduled jobs, Artisan commands. The Facade opens a TCP connection per call, reads/writes, then closes.7172```php73public function showServerState(): array74{75 $state = Opcua::read('i=2259')->getValue();76 return ['state' => $state, 'running' => $state === 0];77}78```7980### Pattern B — daemon-backed, transparent session reuse8182When you run `php artisan opcua:session` under Supervisor/systemd, every Facade call goes through the daemon. Sessions are reused; you no longer pay the connect + create-session + activate-session round-trip per request.8384Run the daemon:85```bash86php artisan opcua:session --log-channel=stack --cache-store=redis87```8889Application code does not change. Same `Opcua::read(...)`, but now backed by `ManagedClient` automatically.9091### Pattern C — `auto_publish` + Laravel events9293Subscribe declaratively in config; receive notifications as Laravel events.9495```php96// config/opcua.php97'session_manager' => ['auto_publish' => true],98'connections' => [99 'plc-1' => [100 'endpoint' => 'opc.tcp://plc.example:4840',101 'auto_connect' => true,102 'subscriptions' => [[103 'publishing_interval' => 500.0,104 'monitored_items' => [105 ['node_id' => 'ns=2;s=Temperature', 'client_handle' => 1],106 ],107 ]],108 ],109],110```111112```php113// app/Providers/EventServiceProvider.php114use PhpOpcua\Client\Event\DataChangeReceived;115116Event::listen(DataChangeReceived::class, function (DataChangeReceived $e) {117 SensorReading::create([118 'client_handle' => $e->clientHandle,119 'value' => $e->dataValue->getValue(),120 'sampled_at' => $e->dataValue->sourceTimestamp,121 ]);122});123```124125The daemon's auto-publish loop dispatches PSR-14 events through Laravel's event dispatcher. Listeners can be queued, broadcast, etc. — see `references/INTEGRATIONS.md`.126127## Facade method surface (one-line summary)128129Connection management: `connection()`, `connect()`, `connectTo()`, `disconnect()`, `disconnectAll()`, `isSessionManagerRunning()`, `getDefaultConnection()`.130131Proxied to the active connection (auto-routed via `__call`):132- Reading: `read`, `readMulti`133- Writing: `write`, `writeMulti`134- Browsing: `browse`, `browseAll`, `browseRecursive`, `browseWithContinuation`, `browseNext`, `resolveNodeId`, `translateBrowsePaths`135- Method calls: `call`136- Subscriptions: `createSubscription`, `createMonitoredItems`, `createEventMonitoredItem`, `modifyMonitoredItems`, `setTriggering`, `deleteMonitoredItems`, `deleteSubscription`, `publish`, `transferSubscriptions`, `republish`137- History read: `historyReadRaw`, `historyReadProcessed`, `historyReadAtTime`138- **History update (v4.4)**: `historyInsertData`, `historyReplaceData`, `historyUpdateData`, `historyDeleteRawModified`, `historyDeleteAtTime`, `historyInsertEvent`, `historyReplaceEvent`, `historyUpdateEvent`, `historyDeleteEvent`139- **File transfer (v4.4)**: `openFile`, `closeFile`, `readFile`, `writeFile`, `getFilePosition`, `setFilePosition`, `createDirectory`, `createFileInDirectory`, `deleteFileSystemObject`, `moveOrCopyFileSystemObject`140- **Aggregates (v4.4)**: `aggregate`, `historyAggregate`141- Trust store: `trustCertificate`, `untrustCertificate`, `getTrustStore`, `getTrustPolicy`142- Discovery: `getEndpoints`, `discoverDataTypes`, `getExtensionObjectRepository`143- Cache / logging: `getLogger`, `getCache`, `invalidateCache`, `flushCache`144- Connection state: `connect`, `disconnect`, `reconnect`, `isConnected`, `getConnectionState`, `getTimeout`, `getAutoRetry`, `getBatchSize`, `getDefaultBrowseMaxDepth`, `getServerMaxNodesPerRead`, `getServerMaxNodesPerWrite`145146Full PHPDoc with all signatures: `src/Facades/Opcua.php`.147148## When to follow the references149150Progressive disclosure — only load what the task needs:151152- `references/CONFIG.md` — every `config/opcua.php` key, env vars, named connections, defaults, version-specific keys153- `references/SESSION_MANAGER.md` — daemon command, Supervisor/systemd setup, IPC endpoints, auto-publish vs manual publish, monitoring154- `references/EVENTS.md` — full list of 56 PSR-14 events, payload shapes, queued listener pattern, common listener recipes155- `references/INTEGRATIONS.md` — Octane/FrankenPHP, Horizon/queues, Livewire, Filament, Broadcasting, Notifications, Telescope/Pulse156- `references/SECURITY.md` — policies, modes, trust store, certificate auto-generation, X.509 user auth, env-driven config157- `references/TESTING.md` — Pest setup, MockClient + Facade swap, integration tests with Docker test-suite158- `references/PITFALLS.md` — common gotchas: facade in config files, Octane state, mixed daemon versions, etc.159- `assets/recipes.md` — copy-pasteable code snippets for the 15 most common end-to-end tasks160161## Idiomatic patterns1621631. **Inject `OpcuaManager`, not the Facade, in long-lived classes.** The Facade resolves the manager every call; injection caches it.164 ```php165 public function __construct(private OpcuaManager $opcua) {}166 public function handle(): void { $this->opcua->read(...); }167 ```1681692. **Use named connections per server.** Don't string-build endpoints in code. Define `plc-1`, `plc-2`, `historian` in config, then `Opcua::connection('historian')->historyReadRaw(...)`.1701713. **Don't disconnect in HTTP requests when the daemon is enabled.** `ManagedClient::disconnect()` closes the daemon-side session, undoing the connection pooling. Let the session manager handle lifecycle.1721734. **For auto-published subscriptions, never call `publish()` yourself.** It returns `auto_publish_active` error. Subscribe to events instead.1741755. **Use `useCache: false` for fresh reads of high-churn nodes.** The read metadata cache is the default — pass `refresh: true` to bypass.1761776. **Queue listeners for heavy event handling.** A `DataChangeReceived` listener that hits a database should be `ShouldQueue`. Otherwise the daemon publish loop blocks on it.1781797. **`Opcua::connectTo()` is for ad-hoc; cache by name** with the `as:` parameter when reused across the request.1801818. **Trust store goes on disk, not in DB.** `storage/app/opcua-trust-store/` by default; check it into a deploy volume, not git.1821839. **Run the daemon under a dedicated UID with `socket_mode: 0600`.** The Facade-side process must be in the same group/UID.18418510. **In Octane, configure `OpcuaManager` as request-scoped via flushed singletons** — see `references/INTEGRATIONS.md` for the `OctaneServiceProvider::tick` hook.186187## Exit codes (Artisan `opcua:session`)188189| Code | Meaning |190|---|---|191| 0 | Daemon exited cleanly (SIGTERM/SIGINT) |192| 1 | Configuration error (invalid socket_path, missing required key) |193| 2 | Bind failure (port in use, socket-path EACCES, parent dir missing) |194| 3 | Runtime error inside daemon loop (logged via PSR-3 channel) |195196Non-zero exits should be caught by Supervisor `autorestart=true` or systemd `Restart=on-failure`.197198## Versioning199200The Laravel package versions lock-step with `php-opcua/opcua-client` and `php-opcua/opcua-session-manager`. Always upgrade in this order:2012021. **Daemon first.** Stop `opcua:session`, `composer update`, restart.2032. **Application second.** `composer update php-opcua/laravel-opcua`.204205If you upgrade application before daemon and call a v4.4 method (e.g. `historyInsertData`), `ManagedClient::__call()` will fail with `BadMethodCallException` because the daemon has no handler for it.206207## What this skill does NOT cover208209- The raw OPC UA protocol — see the `opcua-client` skill.210- The session-manager daemon's IPC protocol — see the `opcua-session-manager` skill.211- CLI usage — see the `opcua-cli` skill.212- Companion-spec types (DI, IA, AutoID, etc.) — see the `opcua-client-nodeset` skill.213214Cross-skill workflow example (`docs/recipes/persistent-tag-history.md`):215- `opcua-cli generate:nodeset Vendor.NodeSet2.xml ...` (nodeset skill)216- App reads typed nodes via `Opcua::read()` (this skill)217- Persists into a historian via `Opcua::historyInsertData()` (this skill + opcua-client)218- A Filament panel browses results (this skill + Filament integration)