symfony-opcua · master
Docs · Reference

Exceptions

Every exception the bundle can raise, when, and the right Symfony-side response — Voter denials, controller error pages, Messenger retry hints.

The bundle itself does not define exception classes — every exception raised through OpcuaManager or an injected OpcUaClientInterface originates in the upstream opcua-client package. The canonical hierarchy lives in opcua-client · Exceptions.

This page summarises the slice you most often catch in Symfony controllers, listeners, and Messenger handlers.

Real hierarchy (slice)

All exceptions extend PhpOpcua\Client\Exception\OpcUaException, which extends \RuntimeException.

\RuntimeException
└── PhpOpcua\Client\Exception\OpcUaException
    ├── ConnectionException             — TCP / socket level
    ├── HandshakeException               — Hello / OpenSecureChannel rejection
    ├── SecurityException                  — policy / mode / trust failures
    │   ├── CertificateParseException        — malformed cert
    │   └── UntrustedCertificateException     — cert rejected by trust policy
    ├── ProtocolException                  — server returned a non-conforming frame
    ├── MessageTypeException                  — wrong OPC UA message tag
    ├── ServiceException                    — server returned Bad_*
    │   └── ServiceUnsupportedException      — Bad_ServiceUnsupported
    ├── SignatureVerificationException        — signature check failed
    ├── EncodingException                    — wire codec failure
    ├── ConfigurationException                — bad config at construction time
    ├── OpenSslException                       — OpenSSL FFI failure
    ├── WriteTypeMismatchException            — write value vs node type
    ├── WriteTypeDetectionException           — auto-detect could not resolve type
    ├── CacheCorruptedException                — metadata cache corruption
    ├── InvalidNodeIdException                 — malformed NodeId string
    ├── MissingModuleDependencyException        — required PHP extension missing
    ├── ModuleConflictException                 — conflicting extension version
    └── UnsupportedCurveException                — ECC curve not supported

The classes commonly cited in older drafts — PolicyException, CertificateException, AuthenticationException, InactiveSessionExceptiondo not exist. The closest real equivalents:

Older name Real class
PolicyException SecurityException
CertificateException UntrustedCertificateException (a SecurityException) or CertificateParseException
AuthenticationException ServiceException (server returns Bad_UserAccessDenied / Bad_IdentityTokenRejected)
InactiveSessionException ServiceException (server returns Bad_SessionIdInvalid / Bad_SessionNotActivated) — the client typically reopens on the next call

The exception classes do not expose endpoint, statusName, fingerprint, reason as public properties. The status code is private; use the documented accessors instead (see below).

ConnectionException

TCP layer issues, server unreachable, broken socket.

Causes: server unreachable, firewall, TCP RST mid-flight.

Symfony response:

php controller catch
use PhpOpcua\Client\Exception\ConnectionException;

try {
    $dv = $this->client->read('ns=2;s=Speed');
} catch (ConnectionException $e) {
    $this->logger->warning('PLC unreachable', ['message' => $e->getMessage()]);
    return $this->json(['error' => 'PLC unreachable'], 503);
}

In a Messenger handler:

php messenger catch
try {
    $dv = $this->client->read('ns=2;s=Speed');
} catch (ConnectionException $e) {
    throw new RecoverableMessageHandlingException('Transient', 0, $e);
}

SecurityException

Cert / policy / mode failures during handshake. Subclasses:

  • UntrustedCertificateException — server cert was rejected by the trust policy.
  • CertificateParseException — server cert was malformed.

Inspect the message for the operator alert; trust pinning is done out-of-band (see Trust store).

HandshakeException

The Hello/OpenSecureChannel exchange was rejected. Typical causes: protocol-version mismatch, security policy not offered, endpoint URL typo.

ServiceException

Server returned a Bad_* status. The connection is healthy; the operation was rejected.

The status code is private; access it through:

$code = $e->getStatusCode();          // int
$name = \PhpOpcua\Client\Types\StatusCode::getName($code);  // string

There is no $statusName public property. Always go through StatusCode::getName($code).

Common causes:

Status name Cause
Bad_NodeIdInvalid Node ID doesn't exist
Bad_NodeIdUnknown Node not in the address space
Bad_AttributeIdInvalid Wrong attribute for the node type
Bad_TypeMismatch Write value doesn't match expected type
Bad_NotWritable Node isn't writable
Bad_UserAccessDenied User lacks permission
Bad_OutOfRange Value outside engineering range
Bad_SessionIdInvalid Session expired server-side

Response: logic bug or permission misconfig. Surface the status name to the developer/operator.

php targeted catch
use PhpOpcua\Client\Exception\ServiceException;
use PhpOpcua\Client\Types\StatusCode;

try {
    $this->client->write('ns=2;s=Setpoint', 9999);
} catch (ServiceException $e) {
    if (StatusCode::getName($e->getStatusCode()) === 'Bad_OutOfRange') {
        return $this->json(['error' => 'value out of range'], 422);
    }
    throw $e;
}

ServiceUnsupportedException

Specifically Bad_ServiceUnsupported. Subclass of ServiceException, so existing catch (ServiceException $e) keeps working.

Cause: server doesn't implement this service set (typical: NodeManagement against UA-.NETStandard).

Response: feature isn't available for this server. Disable the feature in the UI or fallback to a different approach.

EncodingException

Wire codec failure. Typically a server-side bug or protocol-version mismatch.

Response: report upstream. These are rare.

ConfigurationException

Bad config at construction (missing required keys, conflicting options, bad cert path).

Response: fix the config; the exception message points at the field. Symfony's container compilation catches these at boot time.

Symfony exception handler

For unhandled OPC UA exceptions, register a kernel exception listener:

php src/EventListener/OpcuaExceptionListener.php
namespace App\EventListener;

use PhpOpcua\Client\Exception\{ConnectionException, SecurityException, ServiceException};
use PhpOpcua\Client\Types\StatusCode;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;

final class OpcuaExceptionListener
{
    public function __construct(
        #[Autowire(service: 'monolog.logger.opcua')]
        private LoggerInterface $logger,
    ) {}

    #[AsEventListener(event: ExceptionEvent::class, priority: 0)]
    public function __invoke(ExceptionEvent $event): void
    {
        $e = $event->getThrowable();

        if ($e instanceof ConnectionException) {
            $this->logger->warning($e->getMessage());
            $event->setResponse(new JsonResponse(['error' => 'OPC UA unavailable'], 503));
            return;
        }

        if ($e instanceof SecurityException) {
            $this->logger->error('OPC UA security error', ['message' => $e->getMessage()]);
            $event->setResponse(new JsonResponse(['error' => 'OPC UA security error'], 502));
            return;
        }

        if ($e instanceof ServiceException) {
            $statusName = StatusCode::getName($e->getStatusCode());
            $this->logger->error('OPC UA service error', [
                'status' => $statusName,
                'message' => $e->getMessage(),
            ]);
            $event->setResponse(new JsonResponse([
                'error'   => 'OPC UA service error',
                'status'  => $statusName,
                'message' => $e->getMessage(),
            ], 502));
        }
    }
}

Centralises error responses across all controllers.

Sentry / Bugsnag

Fingerprint by status code (not message):

php Sentry scope
use PhpOpcua\Client\Exception\ServiceException;
use PhpOpcua\Client\Types\StatusCode;
use Sentry\State\Scope;

\Sentry\configureScope(function (Scope $scope) use ($e) {
    if ($e instanceof ServiceException) {
        $scope->setFingerprint([
            '{{ default }}',
            'opcua-service',
            StatusCode::getName($e->getStatusCode()),
        ]);
    }
});

Groups Bad_NodeIdInvalid separately from Bad_TypeMismatch.

In tests

php throw in mock
use PhpOpcua\Client\Exception\ConnectionException;
use PhpOpcua\Client\Testing\MockClient;

$mock = MockClient::create()
    ->onRead(function (string $nodeId) {
        if ((string) $nodeId === 'ns=2;s=Speed') {
            throw new ConnectionException('PLC down');
        }
        // ...
    });

static::getContainer()->set(OpcUaClientInterface::class, $mock);

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

You've finished Reference. Next: Recipes · Persistent tag history — the canonical end-to-end pipeline.

Documentation