laravel-opcua · master
Docs · Session manager

Auto-publish

Auto-publish — the daemon-side feature that drives the publish loop and dispatches the real opcua-client events to whatever PSR-14 dispatcher was injected. In Laravel, that dispatcher is Illuminate\\Events\\Dispatcher, so the events arrive at your listeners natively.

When session_manager.auto_publish is on, the daemon drives the OPC UA publish loop itself for every active session. As notifications arrive (data changes, events, alarms), the daemon dispatches the real PhpOpcua\Client\Event\* classes on the PSR-14 EventDispatcherInterface that opcua:session injected.

Because Laravel's Illuminate\Events\Dispatcher implements PSR-14, those events are delivered to listeners registered with Event::listen(...) — no bridge class is needed.

What it gives you

text data flow
OPC UA Server              Daemon                                 Laravel
   │                        │                                       │
   │ PublishResponse        │                                       │
   ├───────────────────────►│                                       │
   │                        │ PSR-14: DataChangeReceived            │
   │                        ├──────────────────────────────────────►│ (Illuminate\Events\Dispatcher)
   │                        │                                       ├─► registered Laravel listeners
   │                        │                                       │

In application code, you write listeners on the real event class:

php listener
use PhpOpcua\Client\Event\DataChangeReceived;

class StoreSpeedReading
{
    public function handle(DataChangeReceived $event): void
    {
        PlcReading::create([
            'client_handle' => $event->clientHandle,
            'value'         => $event->dataValue->getValue(),
            'source_at'     => $event->dataValue->sourceTimestamp,
        ]);
    }
}

The daemon is doing the OPC UA work. Your listener just reacts.

Enabling

Two requirements:

  1. session_manager.auto_publish is true in config/opcua.php (or OPCUA_AUTO_PUBLISH=true in .env).
  2. The daemon was started with the Laravel-wired php artisan opcua:session command (so Laravel's PSR-14 dispatcher is the one the daemon publishes onto).

That is it. With auto-publish on, the daemon walks every active session on every tick of its publish loop. Application code does not "opt in" — the daemon drives publishing for any session held by the daemon. Your job is to register listeners.

Event types

The daemon dispatches the real opcua-client events — not package-specific re-namings. The most common ones in this context:

Class When the daemon dispatches it
PhpOpcua\Client\Event\DataChangeReceived Monitored item data change
PhpOpcua\Client\Event\EventNotificationReceived Event from an event-notifier node
PhpOpcua\Client\Event\AlarmActivated Alarm-shaped event with ActiveState=Active
PhpOpcua\Client\Event\LimitAlarmExceeded Limit alarm trip
PhpOpcua\Client\Event\PublishResponseReceived Every publish response (incl. keep-alives)
PhpOpcua\Client\Event\SubscriptionKeepAlive Empty publish response

The package does not ship classes like OpcuaStatusChange or OpcuaSubscriptionExpired — there is no direct equivalent in opcua-client. To track server-status changes, subscribe to the Server.ServerStatus.State node (ns=0;i=2259) and react in a DataChangeReceived listener. To detect a subscription that silently died, pair PublishResponseReceived with a "last seen" timestamp and re-subscribe when the gap exceeds the keep-alive interval.

See Events for the full reference.

What the daemon actually emits

The daemon doesn't know about Laravel — it dispatches the typed PhpOpcua\Client\Event\* objects on the Psr\EventDispatcher\EventDispatcherInterface it was given. OpcuaServiceProvider resolves that interface from Laravel's container and SessionCommand::resolveEventDispatcher() passes the resolved instance into the daemon when auto_publish is on.

Because Laravel's Illuminate\Events\Dispatcher implements PSR-14, events flow directly to Event::listen(...) listeners — no OpcuaEventBridge class exists or is needed.

Any other PSR-14 listener wired up in the daemon's container also receives the notifications — useful for daemon-internal metrics.

Subscription lifecycle in managed mode

php end-to-end subscription
use PhpOpcua\Client\Event\DataChangeReceived;

// 1. Start the subscription (from a controller, command, anywhere)
$sub = Opcua::createSubscription(publishingInterval: 500.0);

Opcua::createMonitoredItems($sub->subscriptionId)
    ->add('ns=2;s=Speed', clientHandle: 1)
    ->execute();

// 2. Done — the request returns. The daemon now holds the subscription
//    and will drive publishing on its own schedule when auto-publish is on.

// 3. Listeners receive data changes asynchronously
class WriteSpeedToCache
{
    public function handle(DataChangeReceived $event): void
    {
        if ($event->clientHandle === 1) {
            Cache::put('live:speed', $event->dataValue->getValue());
        }
    }
}

The subscription persists across requests, worker restarts, and deploys (as long as the daemon stays up).

Listener registration

Register listeners the standard Laravel way:

php app/Providers/EventServiceProvider.php
use PhpOpcua\Client\Event\DataChangeReceived;
use PhpOpcua\Client\Event\EventNotificationReceived;

protected $listen = [
    DataChangeReceived::class => [
        WriteSpeedToCache::class,
        StoreSpeedReading::class,
    ],
    EventNotificationReceived::class => [
        RouteAlarmToOperator::class,
    ],
];

Or in Laravel 11+, with #[AsEventListener]:

php auto-discovered listener
use Illuminate\Events\Attributes\AsEventListener;
use PhpOpcua\Client\Event\DataChangeReceived;

#[AsEventListener]
class WriteSpeedToCache
{
    public function handle(DataChangeReceived $event): void { /* ... */ }
}

Queued listeners

For listeners that do non-trivial work (DB writes, broadcasts, HTTP calls), implement ShouldQueue:

php queued listener
use Illuminate\Contracts\Queue\ShouldQueue;
use PhpOpcua\Client\Event\DataChangeReceived;

class StoreSpeedReading implements ShouldQueue
{
    public string $queue = 'opcua-data';

    public function handle(DataChangeReceived $event): void { /* ... */ }
}

The event delivery becomes async — fast event dispatch into the queue, slow work runs on a worker. Essential for high-throughput subscriptions. See Queued listeners for the serialisation caveat ($event->client is a live object and does not serialise cleanly).

Broadcasting

A common pattern — relay OPC UA data changes to the browser via Laravel broadcasting:

php broadcasting wrapper event
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use PhpOpcua\Client\Event\DataChangeReceived;

class TagUpdated implements ShouldBroadcast
{
    public function __construct(public int $clientHandle, public mixed $value) {}

    public function broadcastOn(): Channel
    {
        return new Channel('plc.live');
    }
}

class BroadcastOpcuaChange
{
    public function handle(DataChangeReceived $event): void
    {
        event(new TagUpdated($event->clientHandle, $event->dataValue->getValue()));
    }
}

A Livewire / Alpine.js / vanilla JS client subscribes to plc.live and updates the UI in real time.

End-to-end example: Recipes · Livewire real-time dashboard.

Performance characteristics

Workload What to watch
10 monitored items, 1 Hz Trivial. No tuning needed
100 items, 1 Hz Watch listener time. If > 100 ms, queue them
1000 items, 1 Hz Queue listeners. Group writes. Consider batching
100 items at 100 Hz Pretty rare — queue ruthlessly; deadband heavily

The daemon itself handles thousands of notifications per second without trouble. The bottleneck is what listeners do with them.

When NOT to enable auto-publish

  • You don't use subscriptions at all (only on-demand reads / writes).
  • You run the daemon as a generic IPC service and don't want it emitting Laravel events.
  • You're testing — turn it off to isolate test failures.

The setting is per daemon process, not per connection. To have selective subscriptions, filter inside listeners on $event->clientHandle (the only identifier DataChangeReceived / EventNotificationReceived carry — the nodeId is not on the event itself).

Recovery after daemon restart

When the daemon restarts, all subscriptions are gone. The package doesn't auto-restore them — the application must re-subscribe.

A common pattern: re-subscribe on application boot, or via a scheduled command that checks the daemon's session list and recreates anything missing. The auto_connect plus subscriptions keys in config/opcua.php make the daemon itself do this for you (when auto_publish is on) — see the example block in the published config file.

php re-subscribe command
use PhpOpcua\LaravelOpcua\OpcuaManager;

class ReSubscribeOpcua extends Command
{
    protected $signature = 'opcua:resubscribe';

    public function handle(OpcuaManager $opcua): int
    {
        $client = $opcua->connection();
        $sub = $client->createSubscription(publishingInterval: 500.0);

        $builder = $client->createMonitoredItems($sub->subscriptionId);
        foreach (PlcTag::tracked()->get() as $tag) {
            $builder->add($tag->node_id, clientHandle: $tag->id);
        }
        $builder->execute();

        $this->info('Re-subscribed ' . PlcTag::tracked()->count() . ' items');
        return 0;
    }
}

Run this command from a systemd ExecStartPost= or similar after the daemon comes up.

Documentation