Connection events
Connection lifecycle events — fired in both direct and managed mode. The real classes live in opcua-client, not under PhpOpcua\\SymfonyOpcua\\Event.
opcua-client dispatches six PSR-14 events around the connection
lifecycle. They fire in both direct and managed mode. All classes
live in PhpOpcua\Client\Event\ (singular).
| Event class | Fires when | Fields (besides $client) |
|---|---|---|
ClientConnecting |
connect() started |
endpointUrl |
ClientConnected |
Session activation succeeded | endpointUrl |
ClientDisconnecting |
disconnect() started |
— |
ClientDisconnected |
Disconnect completed (clean or broken) | — (only $client) |
ClientReconnecting |
reconnect() started (recovery attempt) |
endpointUrl |
ConnectionFailed |
connect() raised |
endpointUrl, exception (Throwable) |
There is no separate
Reconnectedevent inopcua-client. AfterClientReconnectingfires, a successful recovery dispatchesClientConnectedagain — listen for that.
ClientConnected
Fires when session activation succeeds.
| Field | Type | Meaning |
|---|---|---|
$client |
OpcUaClientInterface |
The live client instance |
$endpointUrl |
string | opc.tcp://... |
Listener example:
namespace App\EventListener;
use PhpOpcua\Client\Event\ClientConnected;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
final class LogConnections
{
public function __construct(
#[Autowire(service: 'monolog.logger.opcua')]
private LoggerInterface $logger,
) {}
#[AsEventListener]
public function __invoke(ClientConnected $event): void
{
$this->logger->info('OPC UA connected', [
'endpoint' => $event->endpointUrl,
]);
}
}
#[Autowire] is Symfony's way to inject a specific Monolog
channel into a service.
ClientDisconnected
Fires on explicit disconnect(), disconnectAll(), or broken
transport. The class carries only $client — there is no
reason field. If you need richer detail, listen for
ConnectionFailed (which carries the original exception) or pair
this event with logger/metric output from your own code.
use PhpOpcua\Client\Event\ClientDisconnected;
#[AsEventListener]
public function __invoke(ClientDisconnected $event): void
{
// Inspect $event->client to identify which connection.
}
ConnectionFailed
Fires when connect() raised an exception.
| Field | Type | Meaning |
|---|---|---|
$client |
OpcUaClientInterface |
The live client instance |
$endpointUrl |
string | URL we tried |
$exception |
\Throwable |
The exception that caused the failure |
Common $exception types (from PhpOpcua\Client\Exception\):
| Class | Typical cause |
|---|---|
ConnectionException |
TCP failure, server unreachable |
HandshakeException |
Hello/Open SecureChannel rejected |
SecurityException |
Policy/mode mismatch or trust failure |
ProtocolException |
Server returned a non-conforming response |
ServiceException |
Server returned a Bad_* service fault |
Use the exception type for routing / alerting:
namespace App\EventListener;
use App\Notification\PlcConnectionLostNotification;
use PhpOpcua\Client\Event\ConnectionFailed;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\Recipient;
final class AlertOpsTeam
{
public function __construct(private NotifierInterface $notifier) {}
#[AsEventListener]
public function __invoke(ConnectionFailed $event): void
{
$notif = new PlcConnectionLostNotification(
endpoint: $event->endpointUrl,
reason: $event->exception::class,
message: $event->exception->getMessage(),
);
$this->notifier->send(
$notif,
new Recipient('[email protected]', '+15551234567'),
);
}
}
ClientReconnecting
Fires when an automatic or explicit reconnect() starts. There is
no follow-up "Reconnected" event — a successful recovery dispatches
ClientConnected again.
use PhpOpcua\Client\Event\ClientReconnecting;
#[AsEventListener]
public function __invoke(ClientReconnecting $event): void
{
// log "attempting reconnect to {$event->endpointUrl}"
}
If you want to know when recovery succeeded (and how long the
outage was), keep a (endpointUrl => lostAt) map on a service and
update it from ClientDisconnected / ConnectionFailed; then in
ClientConnected compute the down-time.
Patterns
Audit log
A single subscriber for the whole lifecycle:
namespace App\EventListener;
use App\Entity\ConnectionAudit;
use Doctrine\ORM\EntityManagerInterface;
use PhpOpcua\Client\Event\{
ClientConnected,
ClientDisconnected,
ClientReconnecting,
ConnectionFailed,
};
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class WriteConnectionAudit implements EventSubscriberInterface
{
public function __construct(private EntityManagerInterface $em) {}
public static function getSubscribedEvents(): array
{
return [
ClientConnected::class => 'logConnected',
ClientDisconnected::class => 'logDisconnected',
ClientReconnecting::class => 'logReconnecting',
ConnectionFailed::class => 'logFailed',
];
}
public function logConnected(ClientConnected $e): void
{
$this->store($e->endpointUrl, 'connected', null);
}
public function logDisconnected(ClientDisconnected $e): void
{
$this->store($this->endpointOf($e->client), 'disconnected', null);
}
public function logReconnecting(ClientReconnecting $e): void
{
$this->store($e->endpointUrl, 'reconnecting', null);
}
public function logFailed(ConnectionFailed $e): void
{
$this->store($e->endpointUrl, 'failed', $e->exception->getMessage());
}
private function store(string $endpoint, string $state, ?string $detail): void
{
$entry = (new ConnectionAudit())
->setEndpoint($endpoint)
->setState($state)
->setDetail($detail)
->setLoggedAt(new \DateTimeImmutable());
$this->em->persist($entry);
$this->em->flush();
}
private function endpointOf(object $client): string
{
return method_exists($client, 'getEndpointUrl')
? (string) $client->getEndpointUrl()
: 'unknown';
}
}
Dashboard tile via Symfony cache
namespace App\EventListener;
use PhpOpcua\Client\Event\{
ClientConnected,
ClientDisconnected,
ConnectionFailed,
};
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
final class TrackConnectionState implements EventSubscriberInterface
{
public function __construct(private CacheInterface $cache) {}
public static function getSubscribedEvents(): array
{
return [
ClientConnected::class => 'up',
ClientDisconnected::class => 'down',
ConnectionFailed::class => 'failed',
];
}
public function up(ClientConnected $event): void
{
$this->state($event->endpointUrl, 'up');
}
public function down(ClientDisconnected $event): void
{
$this->state($this->endpointOf($event->client), 'down');
}
public function failed(ConnectionFailed $event): void
{
$this->state($event->endpointUrl, 'failed');
}
private function state(string $endpoint, string $value): void
{
$key = 'opcua.state.' . sha1($endpoint);
$this->cache->delete($key);
$this->cache->get($key, function (ItemInterface $i) use ($value) {
$i->expiresAfter(3600);
return ['state' => $value, 'at' => (new \DateTimeImmutable())->format('c')];
});
}
private function endpointOf(object $client): string
{
return method_exists($client, 'getEndpointUrl')
? (string) $client->getEndpointUrl()
: 'unknown';
}
}
A /plant/status controller reads from cache for a real-time
tile.
Flap detection
namespace App\EventListener;
use PhpOpcua\Client\Event\ClientReconnecting;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
final class DetectFlapping
{
public function __construct(
private CacheInterface $cache,
#[Autowire(service: 'monolog.logger.opcua')]
private LoggerInterface $logger,
) {}
#[AsEventListener]
public function __invoke(ClientReconnecting $event): void
{
$key = 'opcua.flap.' . sha1($event->endpointUrl);
$count = $this->cache->get($key, function (ItemInterface $i) {
$i->expiresAfter(600); // 10 minutes
return 0;
});
$this->cache->delete($key);
$this->cache->get($key, function (ItemInterface $i) use ($count) {
$i->expiresAfter(600);
return $count + 1;
});
if ($count + 1 >= 5) {
$this->logger->warning('OPC UA flapping', [
'endpoint' => $event->endpointUrl,
]);
$this->cache->delete($key);
}
}
}
Performance notes
Connection events fire once per lifecycle event — inherently low-volume. Queue them only if listeners do expensive work (Slack notifications, external HTTP).
A simple log-channel listener can stay synchronous — microseconds.
Where to read next
- Data events — high-frequency subscription events.
- Alarm events — alarm pipeline.
- Notifier integration — the routing surface for connection alerts.