symfony-opcua · master
Docs · Using the client

Named connections

Switch between configured connections by name with $opcua->connect("name"). Caching rules, dynamic routing, and the multi-tenant pattern.

The bundle resolves connection names against connections.* in config/packages/php_opcua_symfony_opcua.yaml. The pattern mirrors doctrine.connections and messenger.transports.

The shape

text bundle config
php_opcua_symfony_opcua:
    default: plc-line-a
    connections:
        plc-line-a:
            endpoint: 'opc.tcp://plc-a.factory.local:4840'
        plc-line-b:
            endpoint: 'opc.tcp://plc-b.factory.local:4840'
        historian:
            endpoint: 'opc.tcp://historian.factory.local:4841'

Switching at the call site

php switching
use PhpOpcua\SymfonyOpcua\OpcuaManager;

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

    public function dailyReport(): array
    {
        // Default
        $live = $this->opcua->connect()->read('ns=2;s=Output');

        // Named
        $lineB = $this->opcua->connect('plc-line-b')->read('ns=2;s=Output');
        $hist  = $this->opcua->connect('historian')->historyReadRaw(
            'ns=4;s=DailyTotal',
            new \DateTimeImmutable('-1 day'),
            new \DateTimeImmutable(),
        );

        return ['live' => $live, 'lineB' => $lineB, 'history' => $hist];
    }
}

connect() (no arg) is shorthand for connect($default).

connect() vs connection()

The bundle exposes two methods that look similar:

Method What it does
$opcua->connection($name) Returns a client. In managed mode, doesn't open a session yet.
$opcua->connect($name) Same — but in managed mode, opens the session immediately.

For most application code, connect() is what you want. connection() exists for cases where you want to configure the managed client before opening (e.g., adjusting timeouts at runtime — though most setters can be applied after connect() too).

In direct mode, both behave identically — the client is already connected by the time it returns.

Method resolution

$opcua->read(...) (without connect()) also works — the manager's __call proxy forwards to the default connection:

php implicit proxy
$dv = $opcua->read('ns=2;s=Speed');
// equivalent to
$dv = $opcua->connect()->read('ns=2;s=Speed');

The implicit form is convenient in scripts; the explicit form (->connect()->read(...)) is more readable in controllers.

Caching

Each connection resolves to one client instance per OpcuaManager instance. The first connect('plc-line-b') opens it; subsequent calls return the cached client.

  • HTTP request scope: typically the Symfony container is recreated per request, so the cache is per-request.
  • Long-running workers (Messenger consumer, daemon): the cache persists for the worker's lifetime — desirable for performance.

See Connection lifecycle.

Disconnect

php disconnect
$opcua->disconnect('plc-line-b');
$opcua->disconnectAll();

After disconnect, the next connect('plc-line-b') opens a fresh client. The bundle does not register a global shutdown hook; cached clients are released by normal PHP object teardown when the process exits. Explicit disconnectAll() is useful in long-running workers if you need a deterministic point at which to release sessions.

Dynamic routing

For multi-tenant or per-request routing, encapsulate the "which connection" decision in a service:

php src/Opcua/ConnectionResolver.php
namespace App\Opcua;

use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

final class ConnectionResolver
{
    public function __construct(private TokenStorageInterface $tokens) {}

    public function forCurrentUser(): string
    {
        $user = $this->tokens->getToken()?->getUser();
        if ($user instanceof \App\Entity\User) {
            return 'plc-tenant-' . $user->getTenant()->getSlug();
        }
        return 'default';
    }
}

…then in the controller:

php controller
final class TagsController
{
    public function __construct(
        private OpcuaManager $opcua,
        private ConnectionResolver $resolver,
    ) {}

    public function show(): JsonResponse
    {
        $name = $this->resolver->forCurrentUser();
        $dv = $this->opcua->connect($name)->read('ns=2;s=Speed');
        return new JsonResponse(['value' => $dv->getValue()]);
    }
}

The resolver is one autowireable class; controllers stay slim.

Unknown connection names

php error
$opcua->connect('typo');
// → InvalidArgumentException: OPC UA connection [typo] is not configured.

Validation happens at resolution time, not at config-load — so typos surface on the first call. In tests, exercise unhappy paths to catch them in CI.

Listing configured connections

bash terminal
php bin/console debug:config php_opcua_symfony_opcua connections

Or, from a service that already has OpcuaManager injected, write a small helper that introspects the constructor $config:

php listing
// Inside a custom service that extends or wraps OpcuaManager:
$names = array_keys($this->configArray['connections'] ?? []);

The bundle does not export the merged config as a container parameter — there is no %php_opcua_symfony_opcua.config% or php_opcua_symfony_opcua_connections parameter. If you need it in another service, copy the relevant slice into your own parameters: block in services.yaml, or write a small service that takes OpcuaManager and exposes the connection names you need.

Naming conventions

Recap from Configuration · Connections:

  • Lowercase, kebab-case (plc-line-a, historian, plc-tenant-acme).
  • Role over instance (plc-line-a not plc-192-168-1-10).
  • Include tenancy when relevant (plc-tenant-acme).

Health endpoint for every connection

php health controller
namespace App\Controller;

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

final class HealthController extends AbstractController
{
    public function __construct(
        private OpcuaManager $opcua,
        /** @var list<string> */
        private array $connectionNames,    // bound via services.yaml
    ) {}

    #[Route('/health/opcua', methods: ['GET'])]
    public function opcua(): JsonResponse
    {
        $results = [];
        foreach ($this->connectionNames as $name) {
            try {
                $this->opcua->connect($name)->read('i=2256');   // ServerStatus
                $results[$name] = 'ok';
            } catch (\Throwable $e) {
                $shortName = (new \ReflectionClass($e))->getShortName();
                $results[$name] = 'unhealthy: ' . $shortName;
            }
        }
        return $this->json($results);
    }
}

Bind the list explicitly in services.yaml (the bundle doesn't export a %php_opcua_symfony_opcua.config% parameter):

text config/services.yaml
parameters:
    app.opcua_connections: ['default', 'plc-line-a', 'historian']

services:
    App\Controller\HealthController:
        arguments:
            $connectionNames: '%app.opcua_connections%'

class_basename() is a Laravel helper and is not available in Symfony; the example above uses (new \ReflectionClass($e))->getShortName() instead.

Documentation