symfony-opcua · master
Docs · Getting started

Overview

Symfony bundle for OPC UA — autowireable manager, YAML-driven connections, an artisanal session-manager daemon, and PSR-14 events flowing through the Symfony EventDispatcher.

symfony-opcua is a Symfony bundle that wires php-opcua/opcua-client and php-opcua/opcua-session-manager into the Symfony framework with the idioms Symfony developers already use:

  • YAML semantic configuration with a typed TreeBuilder.
  • Autowireable servicesOpcuaManager and OpcUaClientInterface injectable anywhere.
  • PSR-3 logging via Symfony's Monolog channels.
  • PSR-16 caching via Symfony cache pools.
  • PSR-14 events routed through Symfony's EventDispatcher with #[AsEventListener] support.
  • php bin/console opcua:session — Symfony Console command for the session-manager daemon.

The package targets Symfony 7.3+ and 8.x (the composer.json constraint is ^7.3|^8.0). Everything autowires by default; nothing requires manual service definitions.

What this looks like in a controller

php src/Controller/PlcController.php
namespace App\Controller;

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

class PlcController extends AbstractController
{
    public function __construct(private readonly OpcuaManager $opcua) {}

    #[Route('/api/plc/speed', methods: ['GET'])]
    public function speed(): JsonResponse
    {
        $dv = $this->opcua->connect()->read('ns=2;s=Speed');

        return $this->json([
            'value' => $dv->getValue(),
            'good'  => $dv->statusCode === 0,
            'at'    => $dv->sourceTimestamp?->format('c'),
        ]);
    }
}

That's a complete, working controller. The bundle handled the container wiring, the YAML schema, the logger injection, the cache pool — everything but the business logic.

Two operating modes

symfony-opcua switches transparently between two runtime shapes:

Mode Backed by When it picks itself
Direct Client from opcua-client The session-manager daemon socket isn't reachable
Managed ManagedClient from opcua-session-manager The daemon socket exists and is responding

You don't choose explicitly. The OpcuaManager probes the configured socket (Unix-domain or TCP loopback) and routes traffic accordingly. The same controller code works in both modes.

See How symfony-opcua fits.

The address-space model

OPC UA exposes its data through a hierarchical address space of typed nodes. Every node has a stable NodeId. Operations look up nodes by id:

Operation Wire service API method
Read value Read $client->read('ns=2;s=Speed')
Write value Write $client->write('ns=2;s=Setpoint', 75.0)
Browse Browse $client->browse('ns=0;i=85')
Invoke method Call $client->call($obj, $method, $args)
Subscribe CreateSubscription + CreateMonitoredItems $client->createSubscription(...) + createMonitoredItems(...)
History HistoryRead $client->historyReadRaw(...)

The full surface — including addNodes / deleteNodes — is covered under Operations.

Symfony-side wiring at a glance

Symfony component What symfony-opcua does with it
framework-bundle Bundle auto-discovered via Flex
dependency-injection OpcuaManager + OpcUaClientInterface autowireable
config YAML TreeBuilder validates php_opcua_symfony_opcua.*
console opcua:session runs the daemon
cache Symfony cache pool wrapped as PSR-16 and injected
event-dispatcher PSR-14 events from the client dispatched as Symfony events
Monolog (via monolog-bundle) Logger injected; per-connection log_channel supported
Secrets vault %env(secret:OPCUA_PASSWORD)% works in YAML
Messenger (optional) Long-running listeners → async transports
Mercure (optional) Real-time push to the browser
Profiler (optional) OPC UA calls visible in the WebProfilerBundle

You only opt into what you need — the bundle's runtime cost is a single autowired service plus the connection cache.

How tests look

php tests/Functional/PlcControllerTest.php
namespace App\Tests\Functional;

use PhpOpcua\Client\Testing\MockClient;
use PhpOpcua\Client\Types\DataValue;
use PhpOpcua\SymfonyOpcua\OpcuaManager;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class PlcControllerTest extends WebTestCase
{
    public function testReadSpeed(): void
    {
        $client = static::createClient();

        $mock = MockClient::create()
            ->onRead('ns=2;s=Speed', fn() => DataValue::ofDouble(75.0));

        static::getContainer()->set(OpcuaManager::class, new class($mock) extends OpcuaManager {
            public function __construct(private MockClient $m) { parent::__construct(['default' => 'd', 'connections' => ['d' => []]]); }
            public function connect(?string $name = null): \PhpOpcua\Client\OpcUaClientInterface { return $this->m; }
        });

        $client->request('GET', '/api/plc/speed');

        self::assertResponseIsSuccessful();
        self::assertJsonStringEqualsJsonString(
            json_encode(['value' => 75.0, 'good' => true, 'at' => null]),
            $client->getResponse()->getContent(),
        );
    }
}

Full test patterns under Testing.