symfony-opcua · v4.3.x
Docs · Configuration

Parameters and overrides

Per-environment overrides, container parameters, and the DI patterns for decorating or replacing the OpcuaManager.

Three layered ways to change the bundle's behaviour without forking it:

  1. Per-environment YAML — different config per Symfony env.
  2. Container parameters — share values across multiple bundles.
  3. Service decoration / replacement — change the OpcuaManager behaviour.

Per-environment YAML

Symfony loads config/packages/<bundle>.yaml first, then config/packages/<env>/<bundle>.yaml. The latter wins.

Dev: chatty logging, no daemon

text config/packages/dev/php_opcua_symfony_opcua.yaml
php_opcua_symfony_opcua:
    session_manager:
        enabled: false      # never use the daemon in dev
    connections:
        default:
            log_channel: opcua
            timeout: 30.0    # dev PLCs are slow

Test: everything disabled

text config/packages/test/php_opcua_symfony_opcua.yaml
php_opcua_symfony_opcua:
    session_manager:
        enabled: false
    connections:
        default:
            endpoint: 'opc.tcp://localhost:14840'   # test server
            security_policy: None
            security_mode:   None

See Testing · PHPUnit and Pest setup.

Prod: secrets-loaded, strict

text config/packages/prod/php_opcua_symfony_opcua.yaml
php_opcua_symfony_opcua:
    session_manager:
        enabled:           true
        socket_path:       '%env(OPCUA_SOCKET_PATH)%'
        auth_token:        '%env(secret:OPCUA_AUTH_TOKEN)%'
        auto_publish:      true
        log_channel:       opcua
        cache_pool:        cache.redis
        allowed_cert_dirs: ['/etc/opcua/certs']

Per-env YAML merges with the base — you only override what changes per environment.

Container parameters

If you want a value reusable across multiple bundles, declare a container parameter and reference it:

text config/services.yaml
parameters:
    app.opcua.endpoint: 'opc.tcp://plc.factory.local:4840'
    app.opcua.line_a:   'opc.tcp://plc-a.factory.local:4840'
text bundle config
php_opcua_symfony_opcua:
    connections:
        default:
            endpoint: '%app.opcua.endpoint%'
        plc-line-a:
            endpoint: '%app.opcua.line_a%'

Same value also reachable from other services via container injection.

Service decoration

Decorate OpcuaManager to add cross-cutting behaviour (logging, metrics, retries) without forking the bundle:

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

use PhpOpcua\Client\OpcUaClientInterface;
use PhpOpcua\SymfonyOpcua\OpcuaManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;

#[AsDecorator(decorates: OpcuaManager::class)]
final class LoggingOpcuaManager extends OpcuaManager
{
    public function __construct(
        private readonly OpcuaManager $inner,
        private readonly LoggerInterface $logger,
    ) {
        // Don't call parent constructor — we delegate everything to $inner.
    }

    public function connect(?string $name = null): OpcUaClientInterface
    {
        $this->logger->debug('OPC UA connect', ['name' => $name]);
        $client = $this->inner->connect($name);
        $this->logger->debug('OPC UA connected', ['name' => $name, 'class' => get_class($client)]);
        return $client;
    }

    public function disconnect(?string $name = null): void
    {
        $this->logger->debug('OPC UA disconnect', ['name' => $name]);
        $this->inner->disconnect($name);
    }

    // … delegate other methods as needed, or use __call:
    public function __call(string $method, array $args): mixed
    {
        return $this->inner->$method(...$args);
    }
}

Now everywhere autowires OpcuaManager, your decorated class is injected instead.

Replacing the manager

For a more invasive change, replace the bundle's service definition entirely:

text config/services.yaml
services:
    PhpOpcua\SymfonyOpcua\OpcuaManager:
        class: App\Opcua\CustomOpcuaManager
        arguments:
            $config: '%app.opcua_config%'
            # … your own deps

Be aware this breaks the YAML semantic config unless your custom class accepts the same shape.

Overriding the logger resolver

The bundle's LoggerResolverFactory produces a closure that maps log_channel strings to monolog.logger.<channel> services. You can swap the resolver for a custom one:

text config/services.yaml
services:
    php_opcua.logger_resolver:
        class: Closure
        factory: ['App\Opcua\MyLoggerResolver', 'create']
        arguments:
            - !service { class: App\Opcua\LoggerRegistry }

The factory must return a \Closure(string): ?LoggerInterface.

Overriding the cache adapter

The bundle wraps the cache pool with Psr16Cache. To use a different PSR-16 adapter (e.g., a tagged or pre-filled one):

text config/services.yaml
services:
    php_opcua.psr16_cache:
        class: App\Opcua\CustomPsr16Cache
        arguments: ['@my_cache_pool']

The id php_opcua.psr16_cache is what the bundle's internal wiring references.

Compiler passes (advanced)

For deeper customisation — for example, conditionally registering a custom module via ClientBuilder::addModule() — use a compiler pass:

php src/DependencyInjection/Compiler/OpcuaModulesPass.php
namespace App\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

final class OpcuaModulesPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        if (! $container->hasDefinition('PhpOpcua\\SymfonyOpcua\\OpcuaManager')) {
            return;
        }

        // E.g., set a parameter that a custom manager subclass reads
        $container->setParameter('app.opcua.custom_modules', [
            \App\Opcua\AcmeMachineModule::class,
        ]);
    }
}

Register in src/Kernel.php:

php src/Kernel.php
protected function build(\Symfony\Component\DependencyInjection\ContainerBuilder $container): void
{
    parent::build($container);
    $container->addCompilerPass(new \App\DependencyInjection\Compiler\OpcuaModulesPass());
}

The bundle doesn't expose ClientBuilder::addModule() directly via YAML — for now, custom modules need a custom manager.

Validating overrides

After any of the above:

bash terminal
php bin/console cache:clear
php bin/console debug:container PhpOpcua\\SymfonyOpcua\\OpcuaManager
php bin/console debug:autowiring opcua

The second command shows the actual class, decorators in order, and the constructor args the container resolved.