skip to Main Content

In Symfony, if you want to autowire some dependencies in a service, many times you use an interface instead of a concrete class. For example, you might have:

    public function __construct(
        private readonly TranslatorInterface $translator
    ) {
    }

However, the class that you receive will be a concrete class, in this case for example you might receive a DataCollectorTranslator

If we try to use a function which is present in the concrete class, but not in the interface, for example:

$this->translator->setLocale($locale);

Then PHPStan will (rightly) complain that setLocale() is not defined in TranslatorInterface which is the type that we used for the property when we defined it. We cannot define the property with the type of the actual class because that class has not been defined as a service and Symfony will not be able to autowire it.

What is the best way to let PHPStan know the actual type of the property that we receive in this case? I have been doing:

/** @var DataCollectorTranslator $translator */
$translator = $this->translator;
$translator->setLocale($locale->getCode());

But that looks cumbersome, with a variable added just to be able to define the actual type of a property. Are there any better solutions? Thanks!

2

Answers


  1. This is not safe in the first place:

    $this->translator->setLocale($locale);
    

    You should ensure that the method setLocale exists before calling it, e.g. by using assert and instanceof:

    assert($this->translator instanceof DataCollectorTranslator);
    $this->translator->setLocale($locale);
    

    The assert calls can be skipped through php.ini, so it will not increase runtime overhead. This method is also recommended in the phpstan’s docs.

    Login or Signup to reply.
  2. Why not define the class as a service? Why not manually wire it instead of relying on autowiring? At least to me, those would be two alternative approaches.

    That said, there is one simple reason why Symfony doesn’t do that: If you call setLocale() on it, you are modifying the service’s internal state which has an effect everywhere that it’s used. Effectively, those services are shared global objects, so they should be immutable to avoid the dangers of shared mutable state. Instead, you could include the locale ID in the service ID/name. Then, inject the DI container itself and retrieve it dynamically:

        public function __construct(ContainerInterface $container) ...
    
        public function handler()
        {
            $translator = $this->container->get(
                TranslatorInterface::class . '.' . $locale
            );
            ... // use $translator here
        }
    

    If the translator is stateful, consider using the FactoryInterface and make() instead of the ContainerInterface and get(). Another option is to implement the Factory Pattern to get at a translator for a locale by writing a dedicated factory for them.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search