Search code examples
phpsymfonyfosrestbundle

Adding services to a Controller through "container.service_subscriber" not working as expected


I am trying to use the container.service_subscriber tag on my Controller to make some services available without injecting them through the constructor. In our project we don't want to use the autowiring and also can't use the autoconfigure option.

The structure of the Controller is as follow:

I have a base BaseController which extends from the AbstractFOSRestController of FOSRestBundle which has some common used methods for all my Controllers. That service will be used as parent for my other Controllers.

The service definition looks like this:

WM\ApiBundle\Controller\BaseController:
    class: WM\ApiBundle\Controller\BaseController
    abstract: true
    arguments:
        - "@service1"
        - "@service2"
        - ...

WM\ApiBundle\Controller\UserController:
    parent: WM\ApiBundle\Controller\BaseController
    public: true
    #autowire: true
    class: WM\ApiBundle\Controller\UserController
    tags:
        - { name: 'container.service_subscriber'}
        - { name: 'container.service_subscriber', key: 'servicexyz', id: 'servicexyz' }

The class looks like this:

/**
 * User controller.
 */
class UserController extends AbstractCRUDController implements ClassResourceInterface
{

    public static function getSubscribedServices()
    {
        return array_merge(parent::getSubscribedServices(), [
        'servicexyz' => ServiceXYZ::class,
        ]);
    }
   .......
}

The problem I have is, if I set autowire: false, it always automatically sets the full container and with this the appropriate deprecation message (as I am not setting it myself):

User Deprecated: Auto-injection of the container for "WM\ApiBundle\Controller\UserController" is deprecated since Symfony 4.2. Configure it as a service instead.

When setting autowire: true Symfony does respect the container.service_subscriber tag and only sets the partial container (ServiceLocator), which also would solve the deprecation message. I would have expected that autowiring should not make any differences in this case because I am explicitly telling the service which other services it should have.
Am I using the tags wrong or do I have a general problem in understanding how to subscribe a service to a Controller?


Solution

  • The basic issue is that the builtin service subscriber functionality will only inject the service locator into the constructor. A conventional controller which extends AbstractController uses autoconfigure to basically override this and uses setContainer instead of the constructor.

    # ApiBundle/Resources/config/services.yaml
    services:
      _defaults:
        autowire: false
        autoconfigure: false
    
      Api\Controller\UserController:
        public: true
        tags: ['container.service_subscriber']
    
    class UserController extends AbstractController
    {
        protected $container;
    
        public function __construct(ContainerInterface $container)
        {
            $this->container = $container;
        }
        public static function getSubscribedServices()
        {
            return array_merge(parent::getSubscribedServices(), [
                // ...
                'logger' => LoggerInterface::class,
            ]);
        }
        public function index()
        {
            $url = $this->generateUrl('user'); // Works as expected
    
            // $signer = $this->get('uri_signer'); // Fails as expected
    
            $logger = $this->get('logger'); // Works as expected
    
            return new Response('API Index Controller ' . get_class($this->container));
        }
    }
    

    Results in:

        API Index Controller Symfony\Component\DependencyInjection\Argument\ServiceLocator
    

    Indicating that a service locator (as opposed to the global container is being injected).

    You can also configure your service to use the setContainer method and eliminate the need for a constructor. Either approach will work.

      Api\Controller\UserController:
        public: true
        tags: ['container.service_subscriber']
        calls: [['setContainer', ['@Psr\Container\ContainerInterface']]]