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
CLI tool extracted to php-opcua/opcua-cli. The entire src/Cli/ directory, bin/opcua-cli, CLI tests, doc/15-cli.md, and https://github.com/php-opcua/opcua-cli/blob/master/doc/03-code-generation.md have been moved to a standalone package. Install it with composer require php-opcua/opcua-cli. All 10 commands (browse, read, write, endpoints, watch, generate:nodeset, dump:nodeset, trust, trust:list, trust:remove), the NodeSet2.xml code generator, and all CLI documentation now live in the new repository.
Removed "bin" entry from composer.json.
Renamed doc/16-trust-store.md → doc/15-trust-store.md (CLI sections replaced with a link to the new package).
Added
CLI dump:nodeset command. Export a live server's address space to a NodeSet2.xml file: opcua-cli dump:nodeset opc.tcp://server:4840 --output=MyPLC.NodeSet2.xml [--namespace=2]. Browses the entire address space recursively, reads node attributes (DataType, ValueRank, IsAbstract, Symmetric), discovers structured DataType definitions and enumerations, and produces a valid NodeSet2.xml that can be fed directly to generate:nodeset. Filters by namespace index (default: all non-zero). Full security support.
NodeSet2.xml Code Generator. New generate:nodeset CLI command reads OPC UA NodeSet2.xml files (companion specifications, PLC information models) and generates five types of PHP classes:
NodeId constants — one class per file with all node IDs as string constants, usable with read(), write(), browse().
PHP enums — BackedEnum classes for every OPC UA enumeration type in the file.
Typed DTOs — readonly classes with typed properties for structured DataTypes. Enum fields are typed with the generated enum class. Array fields (ValueRank >= 0) use array. Optional fields (IsOptional) are nullable.
Binary codecs — ExtensionObjectCodec implementations that decode into DTOs and encode from DTOs. Array fields use readArray/writeArray helpers. Enum fields cast via ::from().
Registrar — implements GeneratedTypeRegistrar with registerCodecs(), getEnumMappings(), and dependencyRegistrars(). Uses NodeId constants (not raw strings) for codec registration.
Parses <UAObject>, <UAVariable>, <UAMethod>, <UAObjectType>, <UAVariableType>, <UAReferenceType>, <UADataType> with struct and enum <Definition>. Resolves <Aliases> and HasEncoding references. Sanitizes field names and class names (handles special characters and numeric prefixes).
Generated Type Loading and Automatic Dependency Resolution.
loadGeneratedTypes(GeneratedTypeRegistrar $registrar) — registers codecs and enum mappings with the builder (called before connect()). After loading, read() on enum nodes returns PHP BackedEnum instances instead of raw int, and structured types return typed DTO objects with property access ($snapshot->Temperature_C instead of $data['Temperature_C']).
Automatic dependency resolution: each Registrar declares its NodeSet dependencies via dependencyRegistrars(). When loaded, dependencies are resolved recursively — e.g. loading MachineToolRegistrar automatically loads Machinery, DI, and IA.
only: true: skip dependency loading when you need only the specification itself: new MachineToolRegistrar(only: true).
Stackable — call loadGeneratedTypes() multiple times for different NodeSet files. Duplicate registrations are handled gracefully.
Zero impact if not used — full backward compatibility, no changes to existing behavior.
Companion package php-opcua/opcua-client-nodeset provides pre-generated types for all 51 OPC Foundation companion specifications (807 PHP files, 338 enums, 191 DTOs, 191 codecs).
ModifyMonitoredItems. Change sampling interval, queue size, and other parameters on existing monitored items without recreating them. $client->modifyMonitoredItems($subId, [...]) returns MonitoredItemModifyResult[] with revised parameters. Dispatches MonitoredItemModified event per item.
SetTriggering. Configure a monitored item as a trigger for other items — linked items are only sampled when the trigger changes. $client->setTriggering($subId, $triggerId, $linksToAdd, $linksToRemove) returns SetTriggeringResult with per-link status codes. Dispatches TriggeringConfigured event.
Read Metadata Cache. Non-Value attributes (DisplayName, BrowseName, DataType, NodeClass, Description, etc.) can now be cached via PSR-16 to avoid redundant server reads. Opt-in via setReadMetadataCache(true). The Value attribute (id 13) is never cached. Use read($nodeId, $attributeId, refresh: true) to bypass the cache and re-read from the server. invalidateCache($nodeId) clears all cached metadata for a node.
Write Type Auto-Detection. The write() method no longer requires an explicit BuiltinType. When omitted, the client reads the node first to determine the correct type, then caches it via PSR-16 for subsequent writes to the same node.
setAutoDetectWriteType(bool) — enable/disable the feature (default: enabled).
When auto-detect is on and an explicit type is provided, it is validated against the detected node type.
WriteTypeDetectionException — thrown when the type cannot be determined (no value on node, or auto-detect disabled without explicit type).
WriteTypeMismatchException — thrown when the explicit type does not match the detected type. Carries $nodeId, $expectedType, $givenType.
Two new events: WriteTypeDetecting (before detection), WriteTypeDetected (after detection, with $detectedType and $fromCache).
WriteMultiBuilder::value(mixed) — new builder method for writing without specifying a type.
invalidateCache() now also clears cached write types.
PSR-14 Event Dispatcher. The client now dispatches 38 granular events at every key lifecycle point. Inject any PSR-14 EventDispatcherInterface via $builder->setEventDispatcher($dispatcher) on the ClientBuilder. Events cover:
NullEventDispatcher — no-op PSR-14 dispatcher used by default. Zero overhead: event objects are lazily instantiated via closures and never allocated when no real dispatcher is set.
ManagesEventDispatcherTrait — trait providing setEventDispatcher(), getEventDispatcher(), and the internal dispatch() helper with lazy closure support.
psr/event-dispatcher ^1.0 added as dependency (interface-only package, zero runtime code).
All event classes carry an $client property referencing the OpcUaClientInterface instance that emitted them.
Alarm-specific events are automatically deduced from event notification fields (ActiveState, AckedState, ConfirmedState, ShelvingState, Severity, EventType). Known LimitAlarm and OffNormalAlarm type NodeIds are recognized.
MockClient updated with setEventDispatcher() / getEventDispatcher() support.
Unit tests for the event system: NullEventDispatcher, custom dispatcher, event properties, alarm event classes.
Documentation: Events chapter with full event reference, Laravel integration, and practical examples.
Code style enforcement. Added friendsofphp/php-cs-fixer with Laravel-style rules (PSR-12 + opinionated). Run composer format before committing. .editorconfig included for IDE support.
CLI write command. Write a value to a node from the terminal: opcua-cli write <endpoint> <nodeId> <value> [--type=Int32]. The --type flag is optional — when omitted, the type is auto-detected from the node. Supports all scalar types (Boolean, Int32, Double, String, etc.) with automatic value casting.
CLI Tool (bin/opcua-cli). Five commands: browse (flat + recursive tree), read (any attribute), endpoints (discover security), watch (subscription or polling). Full security, JSON output, debug logging. Zero additional dependencies. Documentation: CLI Tool.
MockClient::onGetEndpoints() handler for mocking endpoint discovery results.
Server Trust Store. Persistent server certificate validation for industrial-grade deployments.
FileTrustStore — file-based trust store (~/.opcua/trusted/ default, configurable path). Stores trusted and rejected certificates as DER files.
TrustPolicy enum — three validation levels: Fingerprint (presence in trust store), FingerprintAndExpiry (+ certificate expiration check), Full (+ CA chain verification).
autoAccept(true) — TOFU (Trust On First Use): automatically trusts new certificates and saves them to the store.
autoAccept(true, force: true) — also accepts and updates changed certificates (replaces the stored cert).
autoAccept(true) without force — rejects changed certificates even with auto-accept enabled (security protection against MITM).
trustCertificate(string $certDer) — manually trust a certificate programmatically.
untrustCertificate(string $fingerprint) — remove a certificate from the trust store programmatically.
UntrustedCertificateException — thrown when a server certificate is rejected. Carries $fingerprint and $certDer for programmatic handling.
Five new events: ServerCertificateTrusted (cert passed validation), ServerCertificateRejected (cert rejected), ServerCertificateAutoAccepted (cert auto-accepted via TOFU), ServerCertificateManuallyTrusted (cert added via trustCertificate()), ServerCertificateRemoved (cert removed via untrustCertificate()).
PSR-3 logging: DEBUG for trusted certs, INFO for auto-accepted and manual trust/remove, WARNING for rejected certs.
CLI options: --trust-store=<path> (custom store path), --trust-policy=<policy> (set validation level), --no-trust-policy (disable trust for single command).
CLI shows helpful guidance when UntrustedCertificateException is caught — suggests trust command and --no-trust-policy flag.
Refactored
ClientBuilder/Client split. The Client class has been split into ClientBuilder (configuration, entry point) and Client (connected operations). ClientBuilder::create() is the new preferred entry point. Configuration setters (setSecurityPolicy, setEventDispatcher, setTrustStore, loadGeneratedTypes, etc.) live on ClientBuilder; operation methods (read, write, browse, etc.) live on Client. connect() on the builder returns a Client. ClientBuilder implements ClientBuilderInterface, Client implements OpcUaClientInterface. Builder traits live in src/ClientBuilder/, client traits in src/Client/.
discoverServerCertificate() (72 lines) split into discoverServerCertificate(), performDiscoveryHandshake(), extractServerCertificateFromEndpoints(), and extractTokenPolicies().
openSecureChannelWithSecurity() (68 lines) split into openSecureChannelWithSecurity(), loadClientCertificateAndKey(), and buildCertificateChain().
createAndActivateSession() (56 lines) split into createAndActivateSession(), createSession(), activateSession(), and loadUserCertificate().
Diagnostic info skip helper. Extracted duplicated skipDiagnosticInfo() from 8 Protocol service classes into BinaryDecoder::skipDiagnosticInfo(), skipDiagnosticInfoBody(), and skipDiagnosticInfoArray().
Protocol service base class. Introduced AbstractProtocolService with shared encodeRequestAuto(), writeRequestHeader(), readResponseMetadata(), and wrapInMessage(). All 10 Protocol service classes now extend it, eliminating ~400 lines of duplicated encode/decode boilerplate.
Service NodeId constants. Introduced ServiceTypeId class with named constants for all OPC UA service type IDs, well-known nodes, identity tokens, event filter encodings, and server limit nodes. Replaced all hard-coded NodeId::numeric(0, N) magic numbers across Protocol and Client layers.
ExtensionObject DTO.BinaryDecoder::readExtensionObject() now returns a typed ExtensionObject readonly class instead of array|object. Properties: $typeId (NodeId), $encoding (int), $body (?string, raw bytes), $value (mixed, decoded). Helpers: isDecoded(), isRaw(). DataValue::getValue() auto-extracts the decoded value when a codec is registered — no change needed for decoded access. BinaryEncoder::writeExtensionObject() now accepts ExtensionObject only (no array).
Breaking Changes
ClientBuilder/Client split.new Client() is replaced by ClientBuilder::create() (or new ClientBuilder()). Configuration methods (setSecurityPolicy, setSecurityMode, setClientCertificate, setUserCredentials, setEventDispatcher, setTrustStore, setTrustPolicy, autoAccept, loadGeneratedTypes, setTimeout, setAutoRetry, setBatchSize, setCache, setAutoDetectWriteType, setReadMetadataCache, setDefaultBrowseMaxDepth) are on ClientBuilder, not Client. connect() now returns a Client instance: $client = ClientBuilder::create()->connect('...'). Client constructor is no longer public.
BinaryDecoder::readExtensionObject() returns ExtensionObject instead of array. Code accessing $result['typeId'] must change to $result->typeId, $result['body'] to $result->body.
BinaryEncoder::writeExtensionObject() no longer accepts array — pass ExtensionObject instances.
DataValue::getValue() for raw ExtensionObjects (no codec) now returns ExtensionObject DTO instead of array. Decoded ExtensionObjects (with codec) are unchanged — auto-extracted.