symfony-opcua · master
Docs · Getting started

Quick start

Four idiomatic Symfony shapes — controller, console command, Messenger handler, scheduled task — each exercising the bundle differently.

Four shapes a typical Symfony app reaches for first. Each one covers a different runtime context.

Assumed .env:

bash .env
OPCUA_ENDPOINT=opc.tcp://127.0.0.1:4840

Assumed config/packages/php_opcua_symfony_opcua.yaml:

text bundle config
php_opcua_symfony_opcua:
    connections:
        default:
            endpoint: '%env(OPCUA_ENDPOINT)%'

1. HTTP controller

The most common entry point. OpcuaManager is autowired into the constructor.

php src/Controller/SpeedController.php
namespace App\Controller;

use PhpOpcua\SymfonyOpcua\OpcuaManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

class SpeedController extends AbstractController
{
    public function __construct(private readonly OpcuaManager $opcua) {}

    #[Route('/api/plc/speed', methods: ['GET'])]
    public function show(): JsonResponse
    {
        $dv = $this->opcua->connect()->read('ns=2;s=Speed');

        return $this->json([
            'value' => $dv->getValue(),
            'good'  => $dv->statusCode === 0,
            'at'    => $dv->sourceTimestamp?->format('c'),
        ]);
    }
}

Hit it:

bash terminal
curl http://localhost:8000/api/plc/speed

2. Console command

For one-shot scripts, scheduled tasks, or admin tooling.

php src/Command/CheckPlcCommand.php
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:check-plc', description: 'Probe the PLC and print the Server state')]
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);

        $client = $this->opcua->connect();
        $state  = $client->read('i=2259')->getValue();      // Server_ServerStatus_State

        $io->success("PLC reports state: {$state} (0 = Running)");

        return Command::SUCCESS;
    }
}

Run:

bash terminal
php bin/console app:check-plc

Pair with useConsoleLogger() for -v / -vv / -vvv to flow into the OPC UA client's logger — see Observability · Logging.

3. Messenger handler

For work that shouldn't block a request. The message holds the input; the handler resolves OpcuaManager from the container.

php src/Message/SamplePlc.php
namespace App\Message;

final readonly class SamplePlc
{
    public function __construct(public string $nodeId) {}
}
php src/MessageHandler/SamplePlcHandler.php
namespace App\MessageHandler;

use App\Message\SamplePlc;
use App\Repository\PlcReadingRepository;
use PhpOpcua\SymfonyOpcua\OpcuaManager;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class SamplePlcHandler
{
    public function __construct(
        private readonly OpcuaManager $opcua,
        private readonly PlcReadingRepository $repo,
    ) {}

    public function __invoke(SamplePlc $message): void
    {
        $dv = $this->opcua->connect()->read($message->nodeId);

        $this->repo->save([
            'node_id' => $message->nodeId,
            'value'   => $dv->getValue(),
            'good'    => $dv->statusCode === 0,
            'at'      => $dv->sourceTimestamp ?? new \DateTimeImmutable(),
        ]);
    }
}

Dispatch from anywhere:

php dispatching
use App\Message\SamplePlc;
use Symfony\Component\Messenger\MessageBusInterface;

$bus->dispatch(new SamplePlc('ns=2;s=Speed'));

See Integrations · Messenger for async transports, retries, and the deeper async pattern.

4. Scheduled task

Run the command on a cron-like schedule using Symfony Scheduler:

php src/Scheduler/PlcSchedule.php
namespace App\Scheduler;

use App\Message\SamplePlc;
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())
            ->add(RecurringMessage::every('1 minute', new SamplePlc('ns=2;s=Speed')))
            ->add(RecurringMessage::every('5 minutes', new SamplePlc('ns=2;s=Temperature')));
    }
}

Run the scheduler worker:

bash terminal
php bin/console messenger:consume scheduler_plc

Every minute a SamplePlc message is dispatched and the handler above picks it up. See Console and scheduler.

Two-line write

For completeness — writing is just as terse:

php write
$this->opcua->connect()->write('ns=2;s=Setpoint', 75.0);

The package auto-detects the OPC UA type from the PHP value. See Operations · Writing for the explicit-type path.

A subscription

A subscription delivers value changes via the EventDispatcher (in managed mode with auto-publish enabled — see Session manager · Auto-publish).

php src/EventListener/StoreSpeedReading.php
namespace App\EventListener;

use PhpOpcua\Client\Event\DataChangeReceived;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

final class StoreSpeedReading
{
    private const SPEED_HANDLE = 1;

    #[AsEventListener]
    public function __invoke(DataChangeReceived $event): void
    {
        if ($event->clientHandle === self::SPEED_HANDLE) {
            // ... persist
        }
    }
}

See Events · Overview.