Skip to content

v4.4.0

Choose a tag to compare

@GianfriAur GianfriAur released this 28 May 08:49
· 5 commits to master since this release
01516b8

[v4.4.0] - 2026-05-28

  • Bump extra-test-suite to v1.2.0
  • Bump uanetstandard-test-suite to v1.5.0

Minor release. New AggregateModule for client-side aggregate computation, HistoryModule gains write access via the OPC UA HistoryUpdate service, the wire transport is now pluggable via ClientTransportInterface, a TcpTransport::fromConnectedSocket() seam enables the companion opcua-client-ext-reverse-connect listener (OPC UA Part 6 §7.1.2.3), two new transport-contract methods (createProbe + isSecureChannelExternal) plus an openSecureChannelExternal() branch open the door for the opcua-client-ext-transport-https opc.https:// transport (OPC UA Part 6 §7.4), and a new FileTransferModule wraps the OPC UA Part 5 File Transfer service set.

Added — AggregateModule

  • AggregateModule — built-in module (registered by default) that computes
    OPC UA aggregate functions client-side from a raw DataValue buffer. Exposed
    via Client::__call() (not in OpcUaClientInterface).
  • Supported functions: Interpolate, Minimum, Maximum, Average, Count.
  • Two methods:
    • aggregate(DataValue[], start, end, intervalMs, AggregateFunction, ?AggregateOptions)
    • historyAggregate(NodeId|string, start, end, intervalMs, AggregateFunction, ?AggregateOptions) — fetch raw history + aggregate.
  • AggregateOptions DTO: stepped, treatUncertainAsBad, useSlopedExtrapolation, percentDataBad/Good.
  • StatusCode extended with UncertainDataSubNormal, BadAggregateInvalidInputs/NotSupported/ConfigurationRejected, Historian InfoBits (Calculated, Interpolated, Partial, ExtraData, MultiValue) and withDataValueInfoBits() helper.
  • 32 unit tests + 6 integration tests against UA-.NETStandard.

Added — HistoryUpdate

  • 9 new methods on OpcUaClientInterface / Client / MockClient, all delegating to HistoryModule:
    • historyInsertData(), historyReplaceData(), historyUpdateData() (DataValue[] → int[] per-entry status)
    • historyDeleteRawModified() (range → int overall status)
    • historyDeleteAtTime() (timestamps → int[])
    • historyInsertEvent(), historyReplaceEvent(), historyUpdateEvent() (selectFields + Variant[][] → int[])
    • historyDeleteEvent() (eventIds → int[])
  • PerformUpdateType enum (Insert/Replace/Update/Remove, OPC UA Part 11 §6.9.2).
  • HistoryUpdateResult DTO (statusCode + per-operation status codes), WireSerializable.
  • HistoryUpdateService protocol service.
  • ServiceTypeId::HISTORY_UPDATE_REQUEST = 700.
  • 14 unit tests + 7 integration tests (5 Data ops with strict round-trip assertions against the new open62541-historizing server in php-opcua/extra-test-suite v1.2.0 — port 24842 — plus 2 protocol-level Event round trips).
  • TestHelper::ENDPOINT_HISTORIZING + connectForHistorizing() factory.

Added — Events (PSR-14)

5 new event classes dispatched after the corresponding operations:

  • HistoryDataUpdated(client, nodeId, PerformUpdateType, valueCount, operationResults) — emitted by historyInsertData/ReplaceData/UpdateData.
  • HistoryDataDeleted(client, nodeId, kind, statusCode, operationResults) — emitted by historyDeleteRawModified (kind='rawModified') and historyDeleteAtTime (kind='atTime').
  • HistoryEventUpdated(client, nodeId, PerformUpdateType, eventCount, operationResults) — emitted by historyInsertEvent/ReplaceEvent/UpdateEvent.
  • HistoryEventDeleted(client, nodeId, eventCount, operationResults) — emitted by historyDeleteEvent.
  • AggregateComputed(client, AggregateFunction, rawInputCount, intervalCount, ?nodeId) — emitted by aggregate() and historyAggregate().

Added — Pluggable transport

  • ClientTransportInterface — wire-transport contract with 6 methods (connect, send, receive, setReceiveBufferSize, close, isConnected). Lives at PhpOpcua\Client\Transport\ClientTransportInterface.
  • TcpTransport now implements ClientTransportInterface — pure additive, no behaviour change.
  • Client::__construct gains a new optional 28th parameter ?ClientTransportInterface $transport = null that defaults to new TcpTransport() (BC-safe — all existing callers using named args keep compiling).
  • Client::$transport property is now typed against the interface.
  • ClientBuilder::setTransport(ClientTransportInterface) + getTransport(): ?ClientTransportInterface, plumbed through to the Client ctor. Both also declared on ClientBuilderInterface.
  • ManagesHandshakeTrait::performDiscoveryHandshake() parameter now typed against the interface (the discovery probe itself still instantiates new TcpTransport() internally).
  • InMemoryTransport test helper at tests/Unit/Helpers/InMemoryTransport.php — records sent messages, replays queued responses, satisfies the contract end-to-end. Doubles as the canonical "how to write a custom transport" example referenced from docs/extensibility/transport.md.
  • 16 new unit tests (tests/Unit/Transport/ClientTransportInterfaceTest.php + tests/Unit/ClientBuilderTransportTest.php); full suite stays at 1399 passing.
  • New doc page docs/extensibility/transport.md covering the contract, when to write a custom transport (and when not — PubSub stays in its own package), wiring via the builder, the InMemoryTransport worked example, and the invariant rules each implementation must respect.

Added — Reverse Connect transport seam

  • TcpTransport::fromConnectedSocket(mixed $socket, ?float $readTimeout = null): self — public factory that wraps a stream socket already in CONNECTED state, bypassing stream_socket_client(). The factory takes ownership of the socket; non-resource input raises ConnectionException.
  • ManagesConnectionTrait::performConnect() skips transport->connect($host, $port) when transport->isConnected() is already true. Standard connector flow is unchanged.
  • ClientTransportInterface is untouched — the factory is on the concrete TcpTransport.
  • 10 new unit tests (tests/Unit/Transport/TcpTransportFromConnectedSocketTest.php) using stream_socket_server() over loopback TCP so the suite stays portable on Linux, macOS, and Windows. Full suite at 1426 passing.
  • The listener, RHE parser, whitelist validator, and orchestration live in php-opcua/opcua-client-ext-reverse-connect. The core only exposes the seam.
  • New doc section in docs/extensibility/transport.md covering fromConnectedSocket().

Added — HTTPS transport seam

  • ClientTransportInterface::createProbe(): self — returns a fresh, independent transport sibling for the discovery probe. Client::connect() opens a side connection to GetEndpoints before the main secure channel; previously the probe was hardcoded to new TcpTransport(), which broke when the main transport spoke anything else (e.g. HTTPS).
  • ClientTransportInterface::isSecureChannelExternal(): bool — when true, the client skips the OPC UA OpenSecureChannel exchange because the transport already wraps the wire in a confidential, authenticated channel (e.g. TLS in HTTPS).
  • ManagesSecureChannelTrait::openSecureChannelExternal() — new branch invoked when isSecureChannelExternal() === true. Initialises SessionService with synthetic secureChannelId / tokenId (read-and-discarded by the response decoder), registers all built-in modules against the session via initServices(), and skips OPN entirely.
  • ManagesSecureChannelTrait::closeSecureChannel() — early-returns when the transport reports isSecureChannelExternal() === true. There is no UA-level CloseSecureChannel to send: TLS (or the equivalent lower-layer transport) owns the channel and closes it through its own mechanism.
  • ManagesHandshakeTrait::performDiscoveryHandshake() — short-circuits the probe-side OPN exchange when the probe transport reports isSecureChannelExternal() === true.
  • TcpTransport implements both new methods (createProbe returns new self(), isSecureChannelExternal returns false). The test fixture InMemoryTransport mirrors the same defaults.
  • tests/Unit/Transport/ClientTransportInterfaceTest.php updated for the new 8-method contract. Full suite at 1426 passing.
  • The opc.https:// transport itself (Part 6 §7.4 — binary, JSON, XML-SOAP) lives in the companion package php-opcua/opcua-client-ext-transport-https. The core only exposes the seam.

Added — File Transfer (Part 5)

  • FileTransferModule — 10th default module (registered automatically by ClientBuilder). Wraps the six methods of the OPC UA File Transfer service set (Part 5 §C.2) into typed PHP calls. Lives at PhpOpcua\Client\Module\FileTransfer\FileTransferModule.
  • 6 new methods on OpcUaClientInterface / Client / MockClient:
    • openFile(NodeId|string $fileNodeId, OpenFileMode|int $mode): int — returns the server-assigned fileHandle.
    • closeFile(NodeId|string $fileNodeId, int $fileHandle): void.
    • readFile(NodeId|string $fileNodeId, int $fileHandle, int $length): string — short-reads allowed at EOF (Part 5 §C.2.3).
    • writeFile(NodeId|string $fileNodeId, int $fileHandle, string $data): void.
    • getFilePosition(NodeId|string $fileNodeId, int $fileHandle): int.
    • setFilePosition(NodeId|string $fileNodeId, int $fileHandle, int $position): void.
  • OpenFileMode enum (int-backed bit field: Read=1, Write=2, EraseExisting=4, Append=8) + OpenFileMode::toByte(...) helper for OR-combining cases. openFile() accepts either the enum or a pre-combined Byte.
  • Per-file Method NodeId cache — each FileType instance carries its own Open/Close/Read/Write/GetPosition/SetPosition Method children. The module resolves them once via translateBrowsePaths on first use, then reuses the cached result. Cache is cleared by Client::disconnect() (module's reset()).
  • Edge-case handling — non-Good CallResult is wrapped in ServiceException with the StatusCode mnemonic in the message. New StatusCode constants: BadInvalidState, BadFileHandleInvalid, BadFileNotOpened. BadNotWritable was already present.
  • 4 new event classes dispatched lazily after the corresponding operations:
    • FileOpened(client, fileNodeId, fileHandle, mode)
    • FileClosed(client, fileNodeId, fileHandle)
    • FileBytesRead(client, fileNodeId, fileHandle, bytesRead, requestedLength)
    • FileBytesWritten(client, fileNodeId, fileHandle, bytesWritten)
  • 17 new unit tests (tests/Unit/Module/FileTransfer/FileTransferModuleTest.php) covering registration, Open with enum / Byte mode, per-method Variant typing on the wire, error propagation from both translate and call paths, per-(file, method) cache hits, dispatch via Closure pattern. Full suite stays at 1416 passing.
  • 6 integration tests (tests/Integration/FileTransferTest.php) against uanetstandard-test-suite v1.3.0 fixtures on port 4840: Open/Read/Close on ReadOnlyFile, empty-file read, chunked 256 KB drain on LargeFile, round-trip on WritableFile, GetPosition/SetPosition cooperation, BadNotWritable on attempted Write of a read-only file. Each test self-skips if the fixture is missing (server v < 1.3.0).
  • New doc page docs/operations/file-transfer.md covering the six methods, the OpenFileMode enum, examples for read/chunked-read/overwrite, Method NodeId caching strategy, failure modes, dispatched events. Cascading updates in docs/index.md, docs/overview.md (9 → 10 modules), docs/extensibility/modules.md (nine → ten + new row), docs/reference/client-api.md (new "File Transfer" divider), docs/observability/event-reference.md (52 → 56), docs/reference/enums.md (new OpenFileMode section).

Added — File Transfer · FileDirectoryType wrappers

  • 4 new methods on OpcUaClientInterface / Client / MockClient covering OPC UA Part 5 §C.3 (the FileDirectoryType management surface):
    • createDirectory(NodeId|string $directoryNodeId, string $directoryName): NodeId.
    • createFileInDirectory(NodeId|string $directoryNodeId, string $fileName, bool $requestFileOpen = false): CreateFileResult. Returns a two-tuple (NodeId, fileHandle); fileHandle is 0 when the caller did not also ask to open the file.
    • deleteFileSystemObject(NodeId|string $directoryNodeId, NodeId|string $targetNodeId): void.
    • moveOrCopyFileSystemObject(NodeId|string $directoryNodeId, NodeId|string $sourceNodeId, NodeId|string $targetDirectoryNodeId, bool $createCopy, string $newName = ''): NodeId. Both move (createCopy = false, source removed) and copy (createCopy = true, source preserved) are supported.
  • CreateFileResult DTO (readonly, WireSerializable) — registered on the wire registry by the module's registerWireTypes().
  • Method NodeId resolution uses the same per-(directory, method) cache as the FileType six.
  • Cascading updates in docs/operations/file-transfer.md (new "FileDirectoryType" section with the CreateFileResult DTO + move-vs-copy worked examples) and docs/reference/client-api.md (four new @method lines under the existing "File Transfer" divider).

Added — DataValue type accessor

  • DataValue::$type (public readonly ?BuiltinType) — derived from the inner Variant at construction time. Mirrors what was previously only reachable as $dv->getVariant()->type, removing the dependency on the @deprecated getVariant() method and on DataValue::$value (which is private — the deprecation note's "use ->value instead" wording cannot be acted on directly).
  • DataValue::getType(): ?BuiltinType — symmetric with the existing getValue(): one returns the unwrapped value, the other the OPC UA data type of that value. Returns null when the DataValue was constructed without a Variant (e.g. a DataValue::bad($statusCode) fault).
  • The new accessor is what GitHub Discussion #9 raised — discovering the BuiltinType of a read result previously meant $client->read($id)->getVariant()->type. Now $client->read($id)->type (or ->getType()) covers the same need with one less hop.
  • 3 new unit tests in tests/Unit/Types/TypesTest.php (encode-fixture, null-Variant case, parametric across every BuiltinType case). Full suite stays at 1429 passing.
  • Doc cascade: docs/types/data-value-and-variant.md (new symmetric section + null-Variant pitfall updated), llms.txt / llms-full.txt (DataValue surface), the opcua-client v4.4.0 skill's references/TYPES.md + references/ARCHITECTURE.md + references/PITFALLS.md.