Debugging
When something doesn't work — connection refused, write rejected, events silent. A Symfony-shaped decision tree from console commands to netcat.
Symptom → cheapest probe → next step.
"I can't read anything"
1. Confirm the bundle is wired
php bin/console debug:autowiring opcua
php bin/console debug:config php_opcua_symfony_opcua | head -40
| Symptom | Cause |
|---|---|
debug:autowiring shows nothing |
Bundle missing from config/bundles.php |
debug:config says "no config" |
Missing config/packages/php_opcua_symfony_opcua.yaml |
| Wrong endpoint | Stale env — bin/console cache:clear |
OpcUaClientInterface not autowired |
YAML config doesn't define a default connection |
2. Try a raw read
// In a controller or with bin/console debug:container:
$dv = $this->opcua->connect()->read('i=2256'); // ServerStatus_State
i=2256 always exists on a working OPC UA server. If this
fails but ns=2;s=Speed works, the issue is the node ID. If
this fails too, the connection itself is broken.
3. Check connection logs
tail -50 var/log/opcua-$(date +%Y-%m-%d).log
# or
sudo journalctl -u opcua-session-manager -n 50
Look for ConnectionFailed events — the reason field has the
next step.
"Writes are rejected"
Read the node's attributes:
use PhpOpcua\Client\Types\AttributeId;
$access = $client
->read('ns=2;s=Setpoint', AttributeId::AccessLevel)
->getValue(); // bit 1 must be set for write
$dataType = $client
->read('ns=2;s=Setpoint', AttributeId::DataType)
->getValue(); // what type does the server expect?
| Status | Likely cause |
|---|---|
Bad_TypeMismatch |
PHP type → BuiltinType mismatch — use explicit type |
Bad_NotWritable |
Node isn't writable |
Bad_UserAccessDenied |
Session lacks permission |
Bad_OutOfRange |
Value too high/low |
See Operations · Writing.
"Events stopped firing"
1. Daemon alive?
echo '{"id":1,"t":"req","method":"ping","args":[]}' \
| nc -U /var/run/opcua/sessions.sock
If nc hangs → daemon not listening. If sessions count is 0 →
no sessions held.
2. Auto-publish on?
php bin/console debug:config php_opcua_symfony_opcua | grep auto_publish
If false, the daemon doesn't emit events even though
subscriptions exist.
3. Listeners registered?
php bin/console debug:event-dispatcher "PhpOpcua\Client\Event\DataChangeReceived"
Expected output:
Listeners for "PhpOpcua\Client\Event\DataChangeReceived" event
========================================================================
------- -------------------------------- --------------------------
Order Callable Priority
------- -------------------------------- --------------------------
#1 App\EventListener\StoreSpeed 0
If listeners aren't listed, check the #[AsEventListener]
attribute is on the method or the class.
4. Messenger worker running?
systemctl status messenger-worker
# or with Supervisor
sudo supervisorctl status messenger-worker
php bin/console messenger:stats async_opcua
If the queue is growing, the worker is down. If failed messages
are accumulating, check messenger:failed:show.
"Works in bin/console, not in HTTP"
Likely causes:
- Different connection. Tinker / commands use the default;
the request uses a different one. Check
$opcua->connect(...). - Container cache. Run
php bin/console cache:clearand try again. - FrankenPHP / Octane stale state. Restart workers.
"Tests fail with connection errors"
Tests should not hit a real OPC UA server. Disable in .env.test:
OPCUA_SESSION_MANAGER_ENABLED=false
…with the bundle config pulling that env. See Testing · PHPUnit and Pest setup.
"ServiceException: Bad_SessionIdInvalid after a while"
Server's MaxSessionTimeout fired. The bundle's client reopens
on the next call. If frequent in managed mode:
- The daemon's
session_manager.timeout(the bundle config key —session_manager.timeout, notsession_timeout) should be less than the server'sMaxSessionTimeout(clean teardown before the server times out). - The daemon's cleanup loop should run — check logs for periodic cleanup entries.
"Cert handshake fails"
| Error | Likely cause |
|---|---|
UntrustedCertificateException |
Server cert not in trust store |
SecurityException — expired |
Server cert past NotAfter |
SecurityException — hostname mismatch |
URL host doesn't match cert's SAN |
HandshakeException / SecurityException |
Server doesn't offer the requested policy |
Inspect server cert (against the bundle's cached fingerprint):
openssl s_client -connect plc.factory.local:4840 -showcerts < /dev/null
"Memory leak over time"
In long-running workers (Messenger, FrankenPHP), this happens. Sources:
- Cache without LRU. Use Redis with
allkeys-lru. - Doctrine entity growth in event listeners. Don't accumulate
in static state; call
$em->clear()periodically inside batched handlers. - Builder reuse. Each
readMulti(null)call returns a fresh builder instance — don't accumulate state across iterations.
Bound workers with --memory-limit=512.
Symfony Profiler
APP_DEBUG=1 plus a controller hit shows the WebProfilerBundle
toolbar. The OPC UA bundle doesn't ship a data collector yet,
but you can add a small one — see
Profiler and data collectors.
For now, the Logs tab shows OPC UA log lines from the channel you configured.
See the wire
For protocol-level debugging:
sudo tcpdump -i any -A 'port 4840' -w opcua.pcap
Open in Wireshark with the OPC UA dissector enabled (bundled since Wireshark ≥ 3.0).
Intermittent failures
When timing is unclear:
- Increase logging — set
OPCUA_LOG_LEVEL=debugtemporarily. - Watch the daemon's session count — sudden drops indicate server-side teardown.
- Add a scheduled
app:opcua:pingcommand that records ping latency over time.
Failure escalation
When the failure is in the OPC UA library, capture:
- Exception class + full message.
- Connection config (redacted of secrets).
- Relevant log lines at
debuglevel. - Server product/version from
Server.ServerStatus.BuildInfo.
File against upstream: github.com/php-opcua/opcua-client/issues or github.com/php-opcua/opcua-session-manager/issues.
Where to read next
- Profiler and data collectors — request-level observability.
- Reference · Exceptions — what each exception means.