You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Wire-serialization infrastructure for cross-process IPC. New PhpOpcua\Client\Wire namespace that lets value-objects travel across a JSON-based RPC boundary (e.g. the opcua-session-manager daemon ↔ ManagedClient) with an explicit __t type allowlist enforced at decode time.
WireSerializable interface — contract for DTO classes that know how to emit their own payload (jsonSerialize(): array) and reconstruct from it (static fromWireArray(array): static), plus declare a stable short wire id (static wireTypeId(): string).
WireTypeRegistry — the security gate. Encodes arbitrary PHP values recursively, wrapping each WireSerializable / BackedEnum / pure UnitEnum / DateTimeImmutable value with an explicit __t discriminator. Decoding rejects any __t that is not explicitly registered, so typed payloads cannot instantiate unknown classes. Enum support covers both backed (::from($scalar)) and pure (cases() name-scan) variants. Reserved ids (empty, DateTime) and id/class collisions throw EncodingException at registration time.
CoreWireTypes::register() — idempotent helper that installs the cross-cutting core types on a registry: NodeId, QualifiedName, LocalizedText, DataValue, Variant, ExtensionObject, BrowseNode, ReferenceDescription, EndpointDescription, UserTokenPolicy + enums BuiltinType, NodeClass, BrowseDirection, ConnectionState.
All built-in module DTOs implement WireSerializable:SubscriptionResult, TransferResult, MonitoredItemResult, MonitoredItemModifyResult, PublishResult, SetTriggeringResult, CallResult, BrowsePathResult, BrowsePathTarget, BrowseResultSet, AddNodesResult, BuildInfo. Byte strings inside Variant::ByteString and ExtensionObject::body are base64-wrapped so that JSON can carry arbitrary binary payloads without mutation.
ServiceModule::registerWireTypes(WireTypeRegistry): void — optional hook (default no-op) that every built-in service module overrides to register the DTOs it emits. Third-party modules override to make their own DTOs transparently reachable through ManagedClient::__call().
ModuleRegistry::buildWireTypeRegistry() — orchestrator that returns a fresh registry populated with the core types plus every loaded module's declared types. Used on both the daemon (to decide what it accepts / emits) and on ManagedClient (to mirror the daemon's allowlist after the describe handshake).
OpcUaClientInterface::getRegisteredMethods(): string[] and ::getLoadedModules(): class-string[] — two introspection methods that expose the method / module surface of the underlying client. Implemented on Client (reads the internal method-handlers map + module registry), MockClient (interface-reflection default), and ManagedClient (from the cached describe response).
Kernel + ServiceModule architecture. The Client now delegates all OPC UA service operations to self-contained ServiceModule classes, replacing the trait-based approach. Each module encapsulates its protocol services, DTOs, and methods in a single directory.
ClientKernel (src/Kernel/ClientKernel.php) — shared infrastructure API for all modules: executeWithRetry(), ensureConnected(), nextRequestId(), send(), receive(), unwrapResponse(), createDecoder(), resolveNodeId(), getAuthToken(), dispatch(), logContext().
ClientKernelInterface (src/Kernel/ClientKernelInterface.php) — public contract for the kernel infrastructure that modules depend on.
ServiceModule abstract base class — each module implements register(), boot(), reset(), and optionally requires() for dependency declaration.
ModuleRegistry — manages module lifecycle with topological dependency sort, method conflict detection, and ordered boot/reset.
ClientBuilder::addModule() — register a custom third-party module.
ClientBuilder::replaceModule() — swap a built-in module with a custom implementation.
Client::hasMethod(string): bool and Client::hasModule(string): bool — runtime introspection for registered methods and modules.
OpcUaClientInterface::hasMethod() and OpcUaClientInterface::hasModule() — added to the public API contract.
ModuleConflictException — thrown when two modules try to register the same method name (use replaceModule() to intentionally swap).
MissingModuleDependencyException — thrown when a module's requires() dependencies are not satisfied.
MockClient::hasMethod() and MockClient::hasModule() — added to match the updated interface.
Changed
Client is now a thin proxy. All built-in service methods (read, write, browse, etc.) are concrete, fully typed one-liners that delegate to the registered module handler. __call() is used only for custom third-party module methods not in the interface.
Module-specific DTOs co-located with their module. Types used by a single module now live in the module's namespace instead of Types\. Shared types (NodeId, DataValue, Variant, StatusCode, etc.) remain in Types\.
Module-specific protocol services co-located with their module. Each module contains its own protocol service class. Shared base class AbstractProtocolService and ServiceTypeId remain in Protocol\.
MockClient implements the full OpcUaClientInterface and keeps its existing handler/tracking API unchanged.
NodeManagementModule is no longer registered by ClientBuilder by default. The module, its public API (addNodes, deleteNodes, addReferences, deleteReferences), and its unit tests remain shipped and tested, but ClientBuilder::defaultModules() omits it until integration coverage is available. UA-.NET Standard — which powers every server in uanetstandard-test-suite — does not implement the NodeManagement service set and replies with a top-level ServiceFault (0x800B0000 BadServiceUnsupported) that the current decoders do not surface as a ServiceException. Consumers targeting servers that do implement the service set can opt in with ClientBuilder::addModule(new NodeManagementModule()). The six integration tests in tests/Integration/NodeManagementTest.php are marked ->skip(...) with a pointer to ROADMAP.md, which now tracks the re-enablement plan.
Also added
Server BuildInfo convenience methods. Six new methods on OpcUaClientInterface for quick access to standard OPC UA Server BuildInfo nodes (mandatory on every server):
getServerBuildInfo() — reads all five nodes in a single readMulti() call, returns a BuildInfo DTO
New BuildInfo readonly DTO (PhpOpcua\Client\Types\BuildInfo) with five public properties: productName, manufacturerName, softwareVersion, buildNumber, buildDate.
New ManagesServerInfoTrait (src/Client/ManagesServerInfoTrait.php) encapsulating the server info logic.
MockClient supports all six server info methods with pre-populated defaults (MockServer, php-opcua, 1.0.0, 1, 2026-01-01). Override any field via onRead('i=2262', ...) — same pattern as all other mock nodes.
NodeManagement Services. Four new methods on OpcUaClientInterface for dynamic address space modification on servers that support it:
addNodes(array $nodesToAdd) — add one or more nodes, returns AddNodesResult[] (status code + server-assigned NodeId per node). Supports all 8 node classes (Object, Variable, Method, ObjectType, VariableType, ReferenceType, DataType, View) with class-specific attributes encoded automatically as ExtensionObject.
deleteNodes(array $nodesToDelete) — delete nodes, returns int[] status codes.
addReferences(array $referencesToAdd) — add references between nodes, returns int[] status codes.
deleteReferences(array $referencesToDelete) — delete references, returns int[] status codes.
New AddNodesResult readonly DTO (PhpOpcua\Client\Types\AddNodesResult) with statusCode and addedNodeId properties.
New NodeManagementService protocol class (src/Protocol/NodeManagementService.php) handling binary encoding/decoding for all four services.
New ManagesNodeManagementTrait (src/Client/ManagesNodeManagementTrait.php) encapsulating node management operations.
MockClient supports all four node management methods with sensible defaults (Good status codes, echoed NodeIds).
Fixed
Client::resolveNodeId() no longer misclassifies NodeId strings whose identifier contains slashes as browse paths. Servers based on the UA-.NET Standard stack routinely expose string NodeIds like ns=1;s=TestServer/Dynamic/Counter. The previous heuristic (str_contains($nodeId, '/')) treated any slash-bearing string as a browse path and dispatched to TranslateBrowsePathModule, producing ServiceException: 0x806F0000 (BadNotFound) on every read/write/browse of such nodes. The resolver now matches the OPC UA NodeId grammar first (/^(ns=\d+;)?[isgb]=/) and only falls back to the browse-path handler when the string does not look like a NodeId and contains a /. Explicit startingNodeId arguments continue to route through the browse-path handler. Six new unit tests in tests/Unit/ClientResolveNodeIdTest.php cover the dispatch table (ns=N;s=a/b/c, s=a/b/c, ns=0;i=N, /Objects/Server browse path, startingNodeId override, and passthrough of NodeId instances).
Client method handlers survive a disconnect / reconnect cycle. Previously resetConnectionState() cleared methodHandlers on disconnect(), so any call into a thin-proxy method (read, browse, write, …) after disconnect triggered Error: Value of type null is not callable instead of the documented ConnectionException('Not connected: call connect() first'). The handler map is now preserved across the reset; the module closure runs, hits $this->kernel->ensureConnected(), and raises the correct exception. registerMethod() was updated to allow the same owner to re-register its methods on reconnect without triggering ModuleConflictException; cross-module conflicts still throw.
Windows compatibility for FileTrustStore and FileCache. Replaced all hardcoded / path separators with DIRECTORY_SEPARATOR in both classes. FileTrustStore::defaultBasePath() now detects Windows via PHP_OS_FAMILY and uses %APPDATA%\opcua (with %LOCALAPPDATA% and sys_get_temp_dir() fallbacks) instead of the Unix-only ~/.opcua. rtrim() calls now strip both / and \ to handle paths from either OS. All affected test files updated accordingly.
Windows test compatibility. Added ->skipOnWindows() to 8 unit tests that rely on pcntl_fork() (Unix-only extension) or platform-specific socket behavior (fwrite() on a closed socket does not fail immediately on Windows). Affected files: ClientHandshakeErrorTest.php (2 tests), ClientDiscoveryCoverageTest.php (5 tests), TcpTransportCoverageTest.php (1 test).
Changed
CI workflow now tests on macOS and Windows. Unit tests run on ubuntu-latest, macos-latest, and windows-latest across PHP 8.2–8.5 (12 combinations). Integration tests remain Ubuntu-only (require Docker for OPC UA test servers).
Updated codecov/codecov-action from v5 to v6 to resolve Node.js 20 deprecation warnings on GitHub Actions runners.