laravel-opcua · v4.3.x
Docs · Using the client

Facade vs injection

Facade or dependency injection? Both resolve to the same singleton. Choose by ergonomics, not performance — and by what your tests need to do.

laravel-opcua exposes its main service two ways:

  • The Opcua facadeuse PhpOpcua\LaravelOpcua\Facades\Opcua.
  • Constructor injection of the underlying OpcuaManager.

They wrap the same singleton from the container. There's no runtime cost difference. The choice is about who reads the code.

The facade

php facade — controller
use PhpOpcua\LaravelOpcua\Facades\Opcua;

class TagsController
{
    public function show(string $nodeId): JsonResponse
    {
        $dv = Opcua::read($nodeId);

        return response()->json(['value' => $dv->getValue()]);
    }
}

Pros

  • Concise. No constructor boilerplate.
  • Familiar Laravel idiom (everyone has used Cache::get()).
  • Opcua::shouldReceive() / Opcua::partialMock() / Opcua::spy() in tests work out of the box — see Mocking the facade.

Cons

  • Implicit dependency. A reader of TagsController can't see that it talks to OPC UA without scanning the body.
  • Static analysis tools (Psalm/PHPStan) need the @method docblocks on the facade to resolve method types.

Constructor injection

php injection — controller
use PhpOpcua\LaravelOpcua\OpcuaManager;

class TagsController
{
    public function __construct(private readonly OpcuaManager $opcua) {}

    public function show(string $nodeId): JsonResponse
    {
        $dv = $this->opcua->read($nodeId);

        return response()->json(['value' => $dv->getValue()]);
    }
}

Pros

  • Explicit dependency. The class signature documents what it needs.
  • Easy to test — replace the bound instance in the container.
  • Easier for static analysis. OpcuaManager::read() is a real method.

Cons

  • More verbose.
  • A reader who's used to Cache::get() has to look up OpcuaManager the first time.

Same thing under the hood

Opcua::read() dispatches through Laravel's Facade::resolveFacadeInstance('opcua'). The provider binds the singleton on the class string, then aliases 'opcua' to it:

php OpcuaServiceProvider::register
$this->app->singleton(OpcuaManager::class, function ($app) {
    return new OpcuaManager(
        $app['config']['opcua'],
        $app->bound(LoggerInterface::class)        ? $app->make(LoggerInterface::class)        : null,
        $app->bound(CacheInterface::class)         ? $app->make(CacheInterface::class)         : null,
        $app->bound(EventDispatcherInterface::class) ? $app->make(EventDispatcherInterface::class) : null,
        /* loggerResolver: */ /* … */
    );
});

$this->app->alias(OpcuaManager::class, 'opcua');

So app(OpcuaManager::class), app('opcua'), and the Opcua facade all resolve to the same singleton.

When to prefer which

Code shape Recommendation Reason
Controller, 1-2 OPC UA calls per action Facade Concise; no setup cost
Service class that's all OPC UA Injection Signature documents the dependency
Eloquent model accessor Facade Models can't have constructor args bound from the container
Artisan command Either Command::handle() resolves args from the container
Queued job Avoid holding either across serialize boundary — re-resolve inside handle() The job is serialized onto the queue; the service is not serializable
Test target Either, but inject OpcuaManager if you want PHPUnit doubles Facades use Opcua::shouldReceive() / partialMock() / spy(); classes use Mockery::mock(OpcuaManager::class)

Queued jobs — the gotcha

Note

Don't store OpcuaManager as a job property. Laravel serialises job classes onto the queue; the manager holds open connections, which are not serialisable.

php job — wrong
class SamplePlc implements ShouldQueue
{
    public function __construct(private OpcuaManager $opcua) {}  // ❌
    public function handle(): void
    {
        $this->opcua->read('ns=2;s=Speed');
    }
}
php job — right
class SamplePlc implements ShouldQueue
{
    public function __construct(public string $nodeId) {}

    public function handle(OpcuaManager $opcua): void  // ✅ resolved at run time
    {
        $opcua->read($this->nodeId);
    }
}

Resolve in handle(), never store across the queue boundary.

Type-safety on the facade

The facade carries 50+ @method annotations matching the underlying OpcuaManager surface. Psalm / PHPStan resolves Opcua::read('...') to DataValue, Opcua::write(...) to bool, and so on.

If your editor flags Opcua::someMethod as unknown, check the facade's docblock for the most recent method list. Tools like IDE Helper for Laravel can auto-generate fresh stubs:

bash terminal — ide helper
php artisan ide-helper:generate

(That's barryvdh/laravel-ide-helper, not part of the package — but the facade plays nicely with it.)

Mixed style is fine

A typical codebase uses the facade in controllers, the manager class in services, and resolves through handle($manager) in jobs. There's no rule. Optimise for the reader of each call site.