Console and scheduler
Symfony Console for OPC UA admin tasks, Scheduler for recurring jobs. The Console verbosity flags forwarded into the client logger.
Symfony Console is the standard entry point for admin tools,
one-off scripts, and the daemon (opcua:session). Symfony
Scheduler runs commands on a cadence.
A command with OPC UA
namespace App\Command;
use PhpOpcua\SymfonyOpcua\OpcuaManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'app:plc:check', description: 'Probe the PLC')]
final class CheckPlcCommand extends Command
{
public function __construct(private readonly OpcuaManager $opcua)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
// Forward -v / -vv / -vvv to the OPC UA client logger
$this->opcua->useConsoleLogger($output);
try {
$dv = $this->opcua->connect()->read('i=2259');
} catch (\Throwable $e) {
$io->error("Connection failed: {$e->getMessage()}");
return Command::FAILURE;
}
$io->success(sprintf('PLC state: %d (0 = Running)', $dv->getValue()));
return Command::SUCCESS;
}
}
useConsoleLogger() wraps the ConsoleLogger in a
TimestampedLogger and applies it to all current and future
connections on the manager.
Verbosity levels
| Flag | Logger level visible | Use case |
|---|---|---|
| (none) | warning, error | Default, silent on healthy ops |
-v |
notice | "Reconnecting" etc. |
-vv |
info | Connection events |
-vvv |
debug | Full protocol detail |
The mapping follows Symfony's ConsoleLogger defaults. Override
the second arg of useConsoleLogger($output, $verbosityMap) for
custom mappings.
Disabling timestamps
$this->opcua->useConsoleLogger($output, dateFormat: null);
Useful for cleaner output when running inside another supervisor that already timestamps lines.
Long-running commands
For commands that run for hours (subscription watchers, daemons):
#[AsCommand(name: 'app:plc:watch')]
final class WatchPlcCommand extends Command
{
public function __construct(private OpcuaManager $opcua) { parent::__construct(); }
protected function execute(InputInterface $input, OutputInterface $output): int
{
$client = $this->opcua->connect();
$sub = $client->createSubscription(publishingInterval: 500.0);
$client->createMonitoredItems(
$sub->subscriptionId,
[['nodeId' => 'ns=2;s=Speed', 'clientHandle' => 1]],
);
// In managed mode with auto_publish, the daemon drives the
// publish loop. In direct mode, drain locally:
while (true) {
$client->publish();
}
return Command::SUCCESS;
}
}
Run under systemd / Supervisor — see Production supervisor.
Symfony Scheduler
Symfony Scheduler turns commands into recurring messages.
composer require symfony/scheduler
Define a schedule
namespace App\Scheduler;
use App\Message\SamplePlc;
use Symfony\Component\Console\Messenger\RunCommandMessage;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
#[AsSchedule('plc')]
final class PlcSchedule implements ScheduleProviderInterface
{
public function getSchedule(): Schedule
{
return (new Schedule())
// Cron-style
->add(RecurringMessage::cron('0 3 * * *', new RunCommandMessage('app:plc:discover')))
// Interval-style
->add(RecurringMessage::every('5 minutes', new SamplePlc('ns=2;s=Speed', ['ns=2;s=Speed'])))
// Symfony Period strings
->add(RecurringMessage::every('PT1H', new RunCommandMessage('app:plc:aggregate-1h')));
}
}
Run the scheduler worker
php bin/console messenger:consume scheduler_plc --time-limit=3600
The scheduler dispatches messages at their cron times; messages go to whichever transport routes them.
Cron-style — combined
For full plant-floor scheduling:
#[AsSchedule('plc')]
final class PlcSchedule implements ScheduleProviderInterface
{
public function getSchedule(): Schedule
{
return (new Schedule())
// Every minute
->add(RecurringMessage::every('1 minute', new SampleFleet()))
// Every 5 minutes
->add(RecurringMessage::every('5 minutes', new SampleHistorian()))
// Daily at 03:00
->add(RecurringMessage::cron('0 3 * * *', new RunCommandMessage('app:plc:discover')))
// Daily at 02:00
->add(RecurringMessage::cron('0 2 * * *', new RunCommandMessage('app:plc:prune-readings')))
// Hourly aggregation
->add(RecurringMessage::cron('5 * * * *', new RunCommandMessage('app:plc:aggregate-1h')));
}
}
The "5 minutes past every hour" pattern keeps aggregation from fighting the on-the-minute sampling.
Lockable commands
For commands that should never run in parallel:
composer require symfony/lock
use Symfony\Component\Console\Command\LockableTrait;
#[AsCommand(name: 'app:plc:prune-readings')]
final class PruneReadingsCommand extends Command
{
use LockableTrait;
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$this->lock()) {
$output->writeln('Already running, skipping.');
return Command::SUCCESS;
}
// … do work …
$this->release();
return Command::SUCCESS;
}
}
Useful for prune / aggregate jobs that could overlap on slow DBs.
Command verbosity in production
For systemd-run scheduler workers, pass -v only when
debugging:
[Service]
ExecStart=/usr/bin/php /var/www/html/bin/console messenger:consume scheduler_plc \
--time-limit=3600 --memory-limit=512M
Production typically runs at default verbosity (warning/error
only); raise to -v for the scheduler if you want "task X
dispatched" lines.
Interactive vs unattended
The bundle itself ships only opcua:session. For trust pinning
in deploy scripts use the opcua-cli companion package or your
own programmatic command (see
Security · Trust store). Symfony's
global --no-interaction flag plus your command's own --force
option is the standard non-interactive pattern:
vendor/bin/opcua trust:add --force --no-interaction \
opc.tcp://plc.factory.local:4840
--no-interaction is a global Symfony flag — applies to any
command.
Output styles
For richer output:
$io = new SymfonyStyle($input, $output);
$io->title('OPC UA Health Check');
$io->section('Connections');
$io->table(['Name', 'Status'], [
['plc-line-a', 'OK'],
['plc-line-b', 'TIMEOUT'],
]);
$io->note('Run `app:plc:discover` to refresh tags.');
$io->success('All checks passed');
SymfonyStyle is the standard for human-facing output.
Where to read next
- Notifier — alert routing from commands.
- Recipes · Production deployment — the systemd / Scheduler setup.