Skip to content

Commit 3e1a5b7

Browse files
committed
[DOC] rel v4.0.3, AutoPublisher feature
1 parent 97a14a8 commit 3e1a5b7

7 files changed

Lines changed: 450 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## [4.0.3] - 2026-04-08
4+
5+
### Added
6+
7+
- **Auto-publish.** When an `EventDispatcherInterface` is provided and `autoPublish` is enabled, the daemon automatically calls `publish()` for every session that has active subscriptions. The client's existing PSR-14 event dispatch fires `DataChangeReceived`, `EventNotificationReceived`, `AlarmActivated`, and all other subscription events automatically — no manual publish loop required. Acknowledgements are tracked and sent internally. A self-rescheduling one-shot timer adapts to each session's minimum publishing interval.
8+
- **`AutoPublisher`** — new internal class managing per-session publish cycles with self-rescheduling timers, automatic acknowledgement tracking, connection recovery, and backoff on consecutive errors (stops after 5).
9+
- **Auto-connect.** `SessionManagerDaemon::autoConnect(array $connections)` accepts pre-configured connection definitions. On the first event loop tick after startup, the daemon connects to each endpoint, creates subscriptions, and registers monitored items and event monitored items as specified. Combined with auto-publish, this enables fully declarative monitoring — zero application code needed.
10+
- **`CommandHandler::autoConnectSession()`** — opens a session to a given endpoint and creates subscriptions with monitored items in a single call. Subscription tracking (and auto-publish start) is wired automatically.
11+
- **Event dispatcher injection.** `CommandHandler` accepts an optional `EventDispatcherInterface` and injects it into every `ClientBuilder` created via `handleOpen()`. This enables PSR-14 event delivery for all OPC UA client events in daemon-managed sessions.
12+
- **Manual `publish()` blocking.** When auto-publish is active for a session, manual `publish()` calls via IPC return an `auto_publish_active` error to prevent conflicting publish cycles.
13+
- `Session::getMinPublishingInterval()` — returns the minimum publishing interval (in seconds) across all tracked subscriptions, used by `AutoPublisher` for timer scheduling.
14+
- `Session::addSubscription()` now accepts an optional `float $publishingInterval` parameter (default 500.0 ms) to track the revised publishing interval from `SubscriptionResult`.
15+
- `CommandHandler::attemptSessionRecovery()` visibility changed from `private` to `public` to allow the daemon to pass it as a recovery callback to `AutoPublisher`.
16+
17+
### Changed
18+
19+
- `SessionManagerDaemon` constructor accepts two new optional parameters: `?EventDispatcherInterface $clientEventDispatcher` and `bool $autoPublish`.
20+
- `CommandHandler` constructor accepts a new optional parameter: `?EventDispatcherInterface $clientEventDispatcher`.
21+
- `trackSubscriptionChanges()` now stores `revisedPublishingInterval` from `SubscriptionResult` and triggers `AutoPublisher::startSession()`/`stopSession()` when a session's first subscription is created or its last subscription is deleted.
22+
- `cleanupExpiredSessions()` and `shutdown()` now stop auto-publish timers before disconnecting sessions.
23+
324
## [4.0.2] - 2026-04-07
425

526
### Changed

README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ PHP's request/response model destroys all state — including network connection
2929
- **Drop-in replacement**`ManagedClient` implements the same `OpcUaClientInterface` as the direct `Client`. Swap one line, keep all your code
3030
- **All OPC UA operations** — browse, read, write, method calls, subscriptions, history, path resolution, type discovery
3131
- **Security hardening** — method whitelist, IPC authentication, credential stripping, error sanitization, connection limits
32+
- **Auto-publish** — daemon automatically publishes for sessions with active subscriptions and dispatches PSR-14 events (`DataChangeReceived`, `AlarmActivated`, etc.) — no manual publish loop needed
33+
- **Auto-connect** — daemon can auto-connect and register subscriptions at startup from pre-configured connection definitions
3234
- **Automatic cleanup** — expired sessions are disconnected after configurable inactivity timeout
3335
- **Graceful shutdown** — SIGTERM/SIGINT cleanly disconnect all active sessions
3436

@@ -145,6 +147,48 @@ foreach ($response->notifications as $notif) {
145147
}
146148
```
147149

150+
### Auto-publish (no manual publish loop)
151+
152+
When the daemon is started with an `EventDispatcherInterface` and `autoPublish: true`, it automatically calls `publish()` for sessions that have subscriptions. The client's PSR-14 events are dispatched to your listeners:
153+
154+
```php
155+
use PhpOpcua\Client\Event\DataChangeReceived;
156+
use PhpOpcua\Client\Event\AlarmActivated;
157+
use Psr\EventDispatcher\EventDispatcherInterface;
158+
159+
// 1. Start daemon with auto-publish
160+
$daemon = new SessionManagerDaemon(
161+
socketPath: '/tmp/opcua.sock',
162+
clientEventDispatcher: $yourPsr14Dispatcher,
163+
autoPublish: true,
164+
);
165+
166+
// 2. Pre-configure connections to auto-connect on startup
167+
$daemon->autoConnect([
168+
'plc-1' => [
169+
'endpoint' => 'opc.tcp://192.168.1.10:4840',
170+
'config' => [],
171+
'subscriptions' => [
172+
[
173+
'publishing_interval' => 500.0,
174+
'max_keep_alive_count' => 5,
175+
'monitored_items' => [
176+
['node_id' => 'ns=2;s=Temperature', 'client_handle' => 1],
177+
['node_id' => 'ns=2;s=Pressure', 'client_handle' => 2],
178+
],
179+
'event_monitored_items' => [
180+
['node_id' => 'i=2253', 'client_handle' => 10],
181+
],
182+
],
183+
],
184+
],
185+
]);
186+
187+
$daemon->run();
188+
// DataChangeReceived, EventNotificationReceived, AlarmActivated events
189+
// are dispatched to your PSR-14 listeners automatically.
190+
```
191+
148192
### Secure connection with authentication
149193

150194
```php
@@ -213,6 +257,8 @@ Request N: [read 5ms] → total ~5ms
213257
| **Security** | 6 policies, 3 auth modes, IPC authentication, method whitelist |
214258
| **Auto-Retry** | Automatic reconnect on connection failures |
215259
| **Auto-Batching** | Transparent batching for `readMulti()`/`writeMulti()` |
260+
| **Auto-Publish** | Daemon automatically calls `publish()` for sessions with subscriptions and dispatches PSR-14 events |
261+
| **Auto-Connect** | Daemon connects and registers subscriptions at startup from pre-configured definitions |
216262
| **Automatic Cleanup** | Expired sessions closed after inactivity timeout |
217263
| **Graceful Shutdown** | SIGTERM/SIGINT disconnect all sessions cleanly |
218264

@@ -296,7 +342,7 @@ OPCUA_AUTH_TOKEN=$(cat /etc/opcua/daemon.token) php bin/opcua-session-manager \
296342
./vendor/bin/pest tests/Integration/ --group=integration # integration only
297343
```
298344

299-
340+ tests (unit + integration). Integration tests run against [uanetstandard-test-suite](https://github.com/php-opcua/uanetstandard-test-suite) — a Docker-based OPC UA environment built on the OPC Foundation's UA-.NETStandard reference implementation — covering browse, read/write, subscriptions, method calls, path resolution, connection state, security, type serialization, session persistence, session recovery, and all v4.0.0 DTOs.
345+
380+ tests (unit + integration). Integration tests run against [uanetstandard-test-suite](https://github.com/php-opcua/uanetstandard-test-suite) — a Docker-based OPC UA environment built on the OPC Foundation's UA-.NETStandard reference implementation — covering browse, read/write, subscriptions, method calls, path resolution, connection state, security, type serialization, session persistence, session recovery, and all v4.0.0 DTOs.
300346

301347
> **Note on coverage:** `SessionManagerDaemon` is excluded from coverage reports because it runs as a separate long-lived process (ReactPHP event loop). PHP coverage tools (pcov, xdebug) only instrument the test runner process — they cannot track code executing inside a subprocess started via `proc_open()`. The daemon is fully tested by the integration suite, which starts a real daemon, sends IPC commands, and verifies responses. This is a known limitation shared by other daemon-based PHP packages (Laravel Horizon, Symfony Messenger, RoadRunner workers).
302348

doc/04-daemon.md

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,101 @@ The daemon uses ReactPHP's event loop with:
297297
### Shutdown Sequence
298298

299299
1. SIGTERM/SIGINT received
300-
2. All active sessions disconnected
301-
3. Unix socket server closed
302-
4. Socket file and PID file removed
303-
5. Event loop stopped
300+
2. Auto-publish timers stopped (if active)
301+
3. All active sessions disconnected
302+
4. Unix socket server closed
303+
5. Socket file and PID file removed
304+
6. Event loop stopped
305+
306+
## Auto-Publish
307+
308+
When the daemon is started with a PSR-14 `EventDispatcherInterface` and `autoPublish: true`, it automatically manages the publish cycle for sessions with active subscriptions.
309+
310+
### How It Works
311+
312+
1. A session creates its first subscription via `createSubscription()` → the daemon starts an auto-publish timer for that session
313+
2. The timer calls `Client::publish()`, which dispatches PSR-14 events internally: `DataChangeReceived`, `EventNotificationReceived`, `AlarmActivated`, `SubscriptionKeepAlive`, etc.
314+
3. Acknowledgements are tracked and sent automatically on the next publish call
315+
4. When `moreNotifications` is `true`, the next publish is scheduled with near-zero delay to drain queued notifications quickly
316+
5. When all subscriptions are deleted, the auto-publish timer is stopped
317+
318+
### Timer Scheduling
319+
320+
Auto-publish uses self-rescheduling one-shot timers (not periodic timers) to avoid callback accumulation when `publish()` blocks. The next timer delay depends on the result:
321+
322+
| Scenario | Next delay |
323+
|----------|-----------|
324+
| Notifications received, `moreNotifications: false` | `session.minPublishingInterval × 0.75` |
325+
| Notifications received, `moreNotifications: true` | 10ms (drain quickly) |
326+
| Connection error, recovery succeeded | 1s |
327+
| Generic error (transient) | 5s (backoff) |
328+
| 5 consecutive generic errors | auto-publish stopped |
329+
330+
### Blocking Behavior
331+
332+
`Client::publish()` is a synchronous call that blocks the ReactPHP event loop until the OPC UA server responds with a notification or a keep-alive. The maximum block duration is bounded by `maxKeepAliveCount × publishingInterval` (default: 10 × 500ms = 5s). IPC requests queue during the block but are not lost (30s IPC timeout). To minimize blocking, use a lower `maxKeepAliveCount` (e.g., 3–5).
333+
334+
### Manual Publish Blocking
335+
336+
When auto-publish is active for a session, manual `publish()` calls via IPC return an `auto_publish_active` error. This prevents conflicting publish cycles.
337+
338+
### Programmatic Configuration
339+
340+
```php
341+
use PhpOpcua\SessionManager\Daemon\SessionManagerDaemon;
342+
use Psr\EventDispatcher\EventDispatcherInterface;
343+
344+
$daemon = new SessionManagerDaemon(
345+
socketPath: '/tmp/opcua.sock',
346+
clientEventDispatcher: $dispatcher, // PSR-14 dispatcher
347+
autoPublish: true,
348+
);
349+
350+
$daemon->run();
351+
```
352+
353+
## Auto-Connect
354+
355+
The daemon can auto-connect to pre-configured endpoints and register subscriptions at startup. Combined with auto-publish, this enables fully declarative monitoring with zero application code.
356+
357+
### Programmatic Configuration
358+
359+
```php
360+
$daemon = new SessionManagerDaemon(
361+
socketPath: '/tmp/opcua.sock',
362+
clientEventDispatcher: $dispatcher,
363+
autoPublish: true,
364+
);
365+
366+
$daemon->autoConnect([
367+
'plc-1' => [
368+
'endpoint' => 'opc.tcp://192.168.1.10:4840',
369+
'config' => [
370+
'username' => 'operator',
371+
'password' => 'secret',
372+
'opcuaTimeout' => 3.0,
373+
],
374+
'subscriptions' => [
375+
[
376+
'publishing_interval' => 500.0,
377+
'max_keep_alive_count' => 5,
378+
'monitored_items' => [
379+
['node_id' => 'ns=2;s=Temperature', 'client_handle' => 1],
380+
['node_id' => 'ns=2;s=Pressure', 'client_handle' => 2],
381+
],
382+
'event_monitored_items' => [
383+
[
384+
'node_id' => 'i=2253',
385+
'client_handle' => 10,
386+
'select_fields' => ['EventId', 'EventType', 'SourceName', 'Time', 'Message', 'Severity'],
387+
],
388+
],
389+
],
390+
],
391+
],
392+
]);
393+
394+
$daemon->run();
395+
```
396+
397+
Connections are established on the first event loop tick after the daemon starts. Failed connections are logged but do not prevent the daemon from starting.

doc/09-examples.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,84 @@ try {
216216
try { $client->disconnect(); } catch (\Throwable) {}
217217
}
218218
```
219+
220+
## Auto-Publish with PSR-14 Events
221+
222+
Start the daemon with auto-publish enabled. The client's existing PSR-14 events are dispatched automatically for every subscription notification — no manual `publish()` loop needed.
223+
224+
```php
225+
use PhpOpcua\Client\Event\DataChangeReceived;
226+
use PhpOpcua\Client\Event\EventNotificationReceived;
227+
use PhpOpcua\Client\Event\AlarmActivated;
228+
use PhpOpcua\SessionManager\Daemon\SessionManagerDaemon;
229+
use Psr\EventDispatcher\EventDispatcherInterface;
230+
231+
$dispatcher = /* your PSR-14 event dispatcher */;
232+
233+
$daemon = new SessionManagerDaemon(
234+
socketPath: '/tmp/opcua.sock',
235+
clientEventDispatcher: $dispatcher,
236+
autoPublish: true,
237+
);
238+
239+
$daemon->autoConnect([
240+
'plc-1' => [
241+
'endpoint' => 'opc.tcp://192.168.1.10:4840',
242+
'config' => ['username' => 'operator', 'password' => 'secret'],
243+
'subscriptions' => [
244+
[
245+
'publishing_interval' => 500.0,
246+
'max_keep_alive_count' => 5,
247+
'monitored_items' => [
248+
['node_id' => 'ns=2;s=Temperature', 'client_handle' => 1],
249+
['node_id' => 'ns=2;s=Pressure', 'client_handle' => 2],
250+
['node_id' => 'ns=2;s=MachineState', 'client_handle' => 3],
251+
],
252+
'event_monitored_items' => [
253+
[
254+
'node_id' => 'i=2253',
255+
'client_handle' => 10,
256+
'select_fields' => [
257+
'EventId', 'EventType', 'SourceName', 'Time',
258+
'Message', 'Severity', 'ActiveState',
259+
],
260+
],
261+
],
262+
],
263+
],
264+
],
265+
]);
266+
267+
$daemon->run();
268+
```
269+
270+
Register listeners on your PSR-14 dispatcher to handle notifications:
271+
272+
```php
273+
$dispatcher->listen(DataChangeReceived::class, function (DataChangeReceived $e) {
274+
echo "Subscription {$e->subscriptionId}, handle {$e->clientHandle}: {$e->dataValue->getValue()}\n";
275+
});
276+
277+
$dispatcher->listen(AlarmActivated::class, function (AlarmActivated $e) {
278+
echo "ALARM from {$e->sourceName}: {$e->message} (severity: {$e->severity})\n";
279+
});
280+
```
281+
282+
## Auto-Publish with Runtime Subscriptions
283+
284+
Auto-publish also works for subscriptions created at runtime via `ManagedClient`. Any session that creates a subscription gets auto-published automatically:
285+
286+
```php
287+
use PhpOpcua\SessionManager\Client\ManagedClient;
288+
289+
$client = new ManagedClient();
290+
$client->connect('opc.tcp://localhost:4840');
291+
292+
$sub = $client->createSubscription(publishingInterval: 500.0);
293+
$client->createMonitoredItems($sub->subscriptionId, [
294+
['nodeId' => 'ns=2;s=Temperature', 'clientHandle' => 1],
295+
]);
296+
297+
// No need to call publish() — the daemon handles it automatically.
298+
// DataChangeReceived events are dispatched to the PSR-14 dispatcher.
299+
```

llms-full.txt

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ PHP Request (short-lived) ←→ Unix Socket IPC ←→ SessionManagerDaemon (Re
2323

2424
Components:
2525
- ManagedClient: drop-in OpcUaClientInterface proxy, translates method calls to IPC
26-
- SessionManagerDaemon: ReactPHP event loop, Unix socket server, cleanup timer, signal handlers
27-
- CommandHandler: IPC dispatch, method whitelist (37 methods), credential sanitization
28-
- TypeSerializer: bidirectional JSON serialization for all OPC UA types and v3.0.0 DTOs
26+
- SessionManagerDaemon: ReactPHP event loop, Unix socket server, cleanup timer, signal handlers, auto-publish, auto-connect
27+
- AutoPublisher: per-session auto-publish cycle with self-rescheduling timers, ack management, recovery
28+
- CommandHandler: IPC dispatch, method whitelist (37 methods), credential sanitization, auto-connect sessions
29+
- TypeSerializer: bidirectional JSON serialization for all OPC UA types and v4.0.0 DTOs
2930
- SessionStore: in-memory session registry with expiration
3031

3132
### Installation
@@ -92,6 +93,50 @@ Auth priority: OPCUA_AUTH_TOKEN env > --auth-token-file > --auth-token
9293

9394
systemd or Supervisor supported. See doc/daemon.md for examples.
9495

96+
### Auto-Publish
97+
98+
When `autoPublish: true` and an `EventDispatcherInterface` is provided, the daemon automatically calls `publish()` for sessions with active subscriptions. The client's internal `dispatchPublishEvents()` fires PSR-14 events (`DataChangeReceived`, `EventNotificationReceived`, `AlarmActivated`, `SubscriptionKeepAlive`, etc.) — no manual publish loop needed.
99+
100+
```php
101+
$daemon = new SessionManagerDaemon(
102+
socketPath: '/tmp/opcua.sock',
103+
clientEventDispatcher: $dispatcher,
104+
autoPublish: true,
105+
);
106+
$daemon->run();
107+
```
108+
109+
Acknowledgements are tracked internally. Self-rescheduling timers adapt to each session's minimum publishing interval. When `moreNotifications` is true, the next publish fires with 10ms delay. On connection errors, recovery is attempted automatically. After 5 consecutive generic errors, auto-publish stops for that session.
110+
111+
Manual `publish()` via IPC is blocked while auto-publish is active (returns `auto_publish_active` error).
112+
113+
### Auto-Connect
114+
115+
Pre-configure connections to auto-connect at daemon startup:
116+
117+
```php
118+
$daemon->autoConnect([
119+
'plc-1' => [
120+
'endpoint' => 'opc.tcp://192.168.1.10:4840',
121+
'config' => ['username' => 'op', 'password' => 'secret', 'opcuaTimeout' => 3.0],
122+
'subscriptions' => [
123+
[
124+
'publishing_interval' => 500.0,
125+
'max_keep_alive_count' => 5,
126+
'monitored_items' => [
127+
['node_id' => 'ns=2;s=Temperature', 'client_handle' => 1],
128+
],
129+
'event_monitored_items' => [
130+
['node_id' => 'i=2253', 'client_handle' => 10],
131+
],
132+
],
133+
],
134+
],
135+
]);
136+
```
137+
138+
Connections are established on the first event loop tick. Failed connections are logged but don't prevent the daemon from starting.
139+
95140
---
96141

97142
## 3. ManagedClient API
@@ -359,7 +404,8 @@ src/
359404
│ └── SocketConnection.php # Unix socket transport
360405
├── Daemon/
361406
│ ├── SessionManagerDaemon.php # ReactPHP daemon
362-
│ ├── CommandHandler.php # Command dispatch + security
407+
│ ├── AutoPublisher.php # Per-session auto-publish cycle manager
408+
│ ├── CommandHandler.php # Command dispatch + security + auto-connect
363409
│ ├── Session.php # Session wrapper
364410
│ └── SessionStore.php # In-memory registry
365411
├── Serialization/
@@ -376,9 +422,10 @@ src/
376422

377423
- PhpOpcua\SessionManager\Client\ManagedClient — implements OpcUaClientInterface, proxies to daemon
378424
- PhpOpcua\SessionManager\Client\SocketConnection — static send() for Unix socket JSON transport
379-
- PhpOpcua\SessionManager\Daemon\SessionManagerDaemon — ReactPHP daemon with cleanup and signal handling
380-
- PhpOpcua\SessionManager\Daemon\CommandHandler — IPC command processor with 37-method whitelist
381-
- PhpOpcua\SessionManager\Daemon\Session — readonly id, client, endpointUrl, config; mutable lastUsed
425+
- PhpOpcua\SessionManager\Daemon\SessionManagerDaemon — ReactPHP daemon with cleanup, signal handling, auto-publish, auto-connect
426+
- PhpOpcua\SessionManager\Daemon\AutoPublisher — per-session auto-publish cycle with self-rescheduling timers and ack management
427+
- PhpOpcua\SessionManager\Daemon\CommandHandler — IPC command processor with 37-method whitelist, auto-connect sessions
428+
- PhpOpcua\SessionManager\Daemon\Session — readonly id, client, endpointUrl, config; mutable lastUsed; subscription interval tracking
382429
- PhpOpcua\SessionManager\Daemon\SessionStore — create, get, remove, touch, getExpired, count, all
383430
- PhpOpcua\SessionManager\Serialization\TypeSerializer — serialize/deserialize for all OPC UA types and DTOs
384431
- PhpOpcua\SessionManager\Exception\DaemonException — socket/IPC errors

0 commit comments

Comments
 (0)