Skip to content

v4.2.0

Choose a tag to compare

@GianfriAur GianfriAur released this 17 Apr 15:57
· 44 commits to master since this release
ac81841

[v4.2.0] - 2026-04-17

Added

  • 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.
  • 8 built-in modules: ReadWriteModule, BrowseModule, SubscriptionModule, HistoryModule, NodeManagementModule, TranslateBrowsePathModule, ServerInfoModule, TypeDiscoveryModule.
  • 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):
    • getServerProductName() — reads ns=0;i=2262, returns ?string
    • getServerManufacturerName() — reads ns=0;i=2263, returns ?string
    • getServerSoftwareVersion() — reads ns=0;i=2264, returns ?string
    • getServerBuildNumber() — reads ns=0;i=2265, returns ?string
    • getServerBuildDate() — reads ns=0;i=2266, returns ?DateTimeImmutable
    • 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.