php-opcua/symfony-opcua
Symfony bundle for integrating OPC UA client functionality into Symfony apps. It provides configuration and service wiring to connect to OPC UA servers, browse nodes, and read or write industrial automation data from PHP services.
This package wraps php-opcua/opcua-client in Laravel's container/config/facade idioms, so the mental model is: connections are config, the manager is a singleton, and everything else is a client object you borrow per connection.
composer require <vendor>/laravel-opcua # resolves opcua-client ^4.3.0, session-manager ^4.3.1
php artisan vendor:publish --tag=opcua-config
config/opcua.php first. The connections array is the whole surface area. Each entry needs at minimum an endpoint, optional security settings, and now (v4.3.0) an optional log_channel string. If you only ever touch one file in this package, it's this one.OPCUA_CONNECTION in .env and verify connectivity with a tinker read:
Opcua::connection('plc')->read('ns=2;s=Machine.Speed');
Per-connection log channels. The headline v4.3.0 feature for day-to-day work is log_channel. Give each PLC/line its own Monolog channel and you get grep-able, per-machine logs without any custom plumbing:
'connections' => [
'line-a' => [
'endpoint' => env('OPCUA_LINE_A'),
'log_channel' => 'opcua.line-a',
],
'line-b' => [
'endpoint' => env('OPCUA_LINE_B'),
'log_channel' => 'opcua.line-b',
],
],
Then define opcua.line-a / opcua.line-b in config/logging.php as normal channels. Because resolution is lazy at runtime and the package's ServiceLocator maps channels to monolog.logger.<channel> references, you never reference a Monolog class inside config/opcua.php — which means php artisan config:cache is safe with this setup. Use this instead of a shared application log the moment you have more than one endpoint.
Runtime override when context matters more than config. The resolution chain is runtime override → config logger → config log_channel → default logger. That makes setLogger() the right tool for job-scoped correlation logging:
public function handle(OpcuaManager $opcua)
{
$opcua->setLogger(new TimestampedLogger($this->jobLogger, 'Y-m-d H:i:s.v'));
$opcua->connection('line-a')->write('ns=2;s=Recipe', $recipe);
}
getLogger() lets you assert in tests that the right logger got picked up. Note setLogger() propagation to existing connections is best-effort — resolve connections after setting the logger, or set it on the manager before you fan out.
Console commands get millisecond timestamps for free.
public function handle(OpcuaManager $opcua): int
{
$opcua->useConsoleLogger($this->output); // 'Y-m-d H:i:s.v' by default
// $opcua->useConsoleLogger($this->output, dateFormat: null); // raw ConsoleLogger
...
}
This is the fastest path to a usable opcua:diagnose-style command; the millisecond default matters because OPC UA round-trips and subscription callbacks are frequently sub-second and you'll want the ordering.
Custom resolution via the closure. If you resolve loggers from the container or from a tenant-aware factory, pass the 5th constructor arg (BC-safe, default null) through a service-provider binding rather than calling setLogger() everywhere:
$this->app->bind(OpcuaManager::class, fn ($app) => new OpcuaManager(
config: $app['config']->get('opcua'),
loggerResolver: fn (string $channel) => $app['log']->channel($channel),
));
Node management is back in the default module list. If you previously kept a hand-rolled ClientBuilder around just to add/delete nodes, you can drop it: addNodes(), deleteNodes(), addReferences(), deleteReferences() are reachable through the manager again. Wrap first calls defensively:
try {
$opcua->connection('line-a')->addNodes($nodes);
} catch (ServiceUnsupportedException $e) {
// server has no NodeManagement service — expected on many embedded servers
}
Catch ServiceException, not EncodingException. A top-level ServiceFault now decodes to ServiceException (previously you'd see a misleading EncodingException: Buffer underflow). If you wrote handlers matching on EncodingException for fault responses, retarget them — this is a behavioural change even though the API signature didn't move.
Cache codec override for cross-version deployments. When you roll app nodes out gradually and need caches to survive between 4.2 and 4.3 processes, ClientBuilder::setCacheCodec() is the escape hatch. In practice that's rarely worth it — flushing is simpler (see below).
unserialize() from every cache path; WireCacheCodec (JSON gated by an allowlist) is the new default. Old entries are silently discarded on first access, so you won't get an exception — you'll get a slow first request and possibly a subtle behaviour change if anything depended on cached type metadata. Clear your cache store (or the package's cache namespace) as part of the deploy, and audit anything that persisted serialized client state to Redis/DB.ServiceUnsupportedException extends ServiceException. Existing handlers keep matching — but only if you're catching ServiceException. A bare catch (\Throwable) around addNodes() will now mask "this server simply doesn't support node management", which is a normal condition on many servers, not an error.log_channel resolution is lazy — that cuts both ways. Typos in the channel name don't fail at boot or at config:cache; they surface as an unexpected (or duplicated) Monolog channel at first connection. Add a smoke test or a php artisan opcua:check-style command that calls getLogger() on each configured connection.setLogger() only reaches connections that exist at call time (best-effort). The manager's per-connection lifecycle means a connection resolved before the override may keep the old logger. Set it first, or resolve fresh.useConsoleLogger(..., dateFormat: null) disables the timestamp decorator entirely. Useful when piping into an existing log shipper that stamps lines itself — double timestamps are the most common complaint here./var/folders/... on macOS or deep container mounts) raise a clear DaemonException instead of a post-truncation chmod(): No such file or directory. If you were working around the old behaviour with a manually symlinked short path, you can drop the workaround — but existing deployments with over-long paths will now fail fast where they previously "worked" against the wrong socket.umask(0077) around bind closes the socket-permission race. If a supervisor or sidecar process owned the socket dir with a specific mode, re-verify after upgrade; group-readable sockets are no longer the default.list no longer leaks username, and Windows paths/URLs are redacted in error messages. Good for compliance, but if you had log-based monitoring scraping credentials-adjacent strings or exact paths out of daemon errors, that pipeline needs updating.RequestHeader.timestamp as valid UtcTime, anonymous policyId discovery across all security modes, correct NodeManagement TypeIds, ECC sequence numbers per OPC UA 1.05.4) mean servers stricter than UA-.NETStandard — open62541 in particular — are now reachable. Symptom to look for post-upgrade: connections that failed at CreateSession or OpenSecureChannel with generic decode errors may now just work, which can make an unrelated security misconfiguration visible for the first time.opcua-client ^4.3.0 and opcua-session-manager ^4.3.1 both need to move together; a partial composer update that leaves session-manager at 4.2.x is the likely source of confusing session-reconnect behaviour.TimestampedLogger is a generic PSR-3 decorator, so it composes with any logger for any subsystem — don't limit it to OPC UA. LoggerResolverFactory plus the bundle-side ServiceLocator is the seam to use if you need channel mapping that doesn't follow the monolog.logger.<channel> convention.How can I help you explore Laravel packages today?