v4.4.0
[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
viaClient::__call()(not inOpcUaClientInterface).- 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.
AggregateOptionsDTO:stepped,treatUncertainAsBad,useSlopedExtrapolation,percentDataBad/Good.StatusCodeextended withUncertainDataSubNormal,BadAggregateInvalidInputs/NotSupported/ConfigurationRejected, Historian InfoBits (Calculated,Interpolated,Partial,ExtraData,MultiValue) andwithDataValueInfoBits()helper.- 32 unit tests + 6 integration tests against UA-.NETStandard.
Added — HistoryUpdate
- 9 new methods on
OpcUaClientInterface/Client/MockClient, all delegating toHistoryModule: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[])
PerformUpdateTypeenum (Insert/Replace/Update/Remove, OPC UA Part 11 §6.9.2).HistoryUpdateResultDTO (statusCode + per-operation status codes), WireSerializable.HistoryUpdateServiceprotocol service.ServiceTypeId::HISTORY_UPDATE_REQUEST = 700.- 14 unit tests + 7 integration tests (5 Data ops with strict round-trip assertions against the new
open62541-historizingserver inphp-opcua/extra-test-suitev1.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 byhistoryInsertData/ReplaceData/UpdateData.HistoryDataDeleted(client, nodeId, kind, statusCode, operationResults)— emitted byhistoryDeleteRawModified(kind='rawModified') andhistoryDeleteAtTime(kind='atTime').HistoryEventUpdated(client, nodeId, PerformUpdateType, eventCount, operationResults)— emitted byhistoryInsertEvent/ReplaceEvent/UpdateEvent.HistoryEventDeleted(client, nodeId, eventCount, operationResults)— emitted byhistoryDeleteEvent.AggregateComputed(client, AggregateFunction, rawInputCount, intervalCount, ?nodeId)— emitted byaggregate()andhistoryAggregate().
Added — Pluggable transport
ClientTransportInterface— wire-transport contract with 6 methods (connect,send,receive,setReceiveBufferSize,close,isConnected). Lives atPhpOpcua\Client\Transport\ClientTransportInterface.TcpTransportnow implementsClientTransportInterface— pure additive, no behaviour change.Client::__constructgains a new optional 28th parameter?ClientTransportInterface $transport = nullthat defaults tonew TcpTransport()(BC-safe — all existing callers using named args keep compiling).Client::$transportproperty is now typed against the interface.ClientBuilder::setTransport(ClientTransportInterface)+getTransport(): ?ClientTransportInterface, plumbed through to theClientctor. Both also declared onClientBuilderInterface.ManagesHandshakeTrait::performDiscoveryHandshake()parameter now typed against the interface (the discovery probe itself still instantiatesnew TcpTransport()internally).InMemoryTransporttest helper attests/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 fromdocs/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.mdcovering the contract, when to write a custom transport (and when not — PubSub stays in its own package), wiring via the builder, theInMemoryTransportworked 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, bypassingstream_socket_client(). The factory takes ownership of the socket; non-resource input raisesConnectionException.ManagesConnectionTrait::performConnect()skipstransport->connect($host, $port)whentransport->isConnected()is alreadytrue. Standard connector flow is unchanged.ClientTransportInterfaceis untouched — the factory is on the concreteTcpTransport.- 10 new unit tests (
tests/Unit/Transport/TcpTransportFromConnectedSocketTest.php) usingstream_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.mdcoveringfromConnectedSocket().
Added — HTTPS transport seam
ClientTransportInterface::createProbe(): self— returns a fresh, independent transport sibling for the discovery probe.Client::connect()opens a side connection toGetEndpointsbefore the main secure channel; previously the probe was hardcoded tonew TcpTransport(), which broke when the main transport spoke anything else (e.g. HTTPS).ClientTransportInterface::isSecureChannelExternal(): bool— whentrue, the client skips the OPC UAOpenSecureChannelexchange because the transport already wraps the wire in a confidential, authenticated channel (e.g. TLS in HTTPS).ManagesSecureChannelTrait::openSecureChannelExternal()— new branch invoked whenisSecureChannelExternal() === true. InitialisesSessionServicewith syntheticsecureChannelId/tokenId(read-and-discarded by the response decoder), registers all built-in modules against the session viainitServices(), and skips OPN entirely.ManagesSecureChannelTrait::closeSecureChannel()— early-returns when the transport reportsisSecureChannelExternal() === true. There is no UA-levelCloseSecureChannelto 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 reportsisSecureChannelExternal() === true.TcpTransportimplements both new methods (createProbereturnsnew self(),isSecureChannelExternalreturnsfalse). The test fixtureInMemoryTransportmirrors the same defaults.tests/Unit/Transport/ClientTransportInterfaceTest.phpupdated 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 packagephp-opcua/opcua-client-ext-transport-https. The core only exposes the seam.
Added — File Transfer (Part 5)
FileTransferModule— 10th default module (registered automatically byClientBuilder). Wraps the six methods of the OPC UA File Transfer service set (Part 5 §C.2) into typed PHP calls. Lives atPhpOpcua\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.
OpenFileModeenum (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
translateBrowsePathson first use, then reuses the cached result. Cache is cleared byClient::disconnect()(module'sreset()). - Edge-case handling — non-Good
CallResultis wrapped inServiceExceptionwith the StatusCode mnemonic in the message. NewStatusCodeconstants:BadInvalidState,BadFileHandleInvalid,BadFileNotOpened.BadNotWritablewas 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) againstuanetstandard-test-suitev1.3.0 fixtures on port 4840: Open/Read/Close onReadOnlyFile, empty-file read, chunked 256 KB drain onLargeFile, round-trip onWritableFile, GetPosition/SetPosition cooperation,BadNotWritableon 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.mdcovering the six methods, theOpenFileModeenum, examples for read/chunked-read/overwrite, Method NodeId caching strategy, failure modes, dispatched events. Cascading updates indocs/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(newOpenFileModesection).
Added — File Transfer · FileDirectoryType wrappers
- 4 new methods on
OpcUaClientInterface/Client/MockClientcovering OPC UA Part 5 §C.3 (theFileDirectoryTypemanagement surface):createDirectory(NodeId|string $directoryNodeId, string $directoryName): NodeId.createFileInDirectory(NodeId|string $directoryNodeId, string $fileName, bool $requestFileOpen = false): CreateFileResult. Returns a two-tuple(NodeId, fileHandle);fileHandleis0when 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.
CreateFileResultDTO (readonly,WireSerializable) — registered on the wire registry by the module'sregisterWireTypes().- 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 theCreateFileResultDTO + move-vs-copy worked examples) anddocs/reference/client-api.md(four new@methodlines under the existing "File Transfer" divider).
Added — DataValue type accessor
DataValue::$type(public readonly ?BuiltinType) — derived from the innerVariantat construction time. Mirrors what was previously only reachable as$dv->getVariant()->type, removing the dependency on the@deprecatedgetVariant()method and onDataValue::$value(which is private — the deprecation note's "use->valueinstead" wording cannot be acted on directly).DataValue::getType(): ?BuiltinType— symmetric with the existinggetValue(): one returns the unwrapped value, the other the OPC UA data type of that value. Returnsnullwhen theDataValuewas constructed without aVariant(e.g. aDataValue::bad($statusCode)fault).- The new accessor is what GitHub Discussion #9 raised — discovering the
BuiltinTypeof 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 everyBuiltinTypecase). 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), theopcua-clientv4.4.0 skill'sreferences/TYPES.md+references/ARCHITECTURE.md+references/PITFALLS.md.