Andrei Herford
Andrei Herford

Reputation: 18729

How to fix '...SomeController has no container set' on Controllers defined in Symfony 5 bundle?

I have created a custom Symfony 5.3 bundle to share code between different projects. In src/controller/SomeController.php the bundle implements a controller class which extends from Symfony\Bundle\FrameworkBundle\Controller\AbstractController.

When accessing this controller via a route in my Symfony project I get the following error:

"XY\CommensBundle\Controller\SomeController" has no container set, did you forget to define it as a service subscriber?

AbstractController has a setContainer method which is used to inject the service container. On controllers implemented directly in my Symfony project this method is called automatically by autowire / autoconfigure.

However, regarding to the Symfony docs autowire / autoconfigure should not be used for bundle services. Instead, all services should be defined explicitly. So I added this to the bundles services.yaml:

# config/services.yaml
services:    
    xy_commons.controller.some_controller:
        class: XY\CommensBundle\Controller\SomeController
        public: false
        calls:     
            - [ setContainer, [ '@service_container' ]]

After adding the bundle to my Symfony project using Composer the console shows, that the controller is correctly added as a service. Everything seems fine.

php bin/console debug:container 'xy_commons.controller.some_controller'

Information for Service "xy_commons.controller.some_controller"
=============================================================

 ---------------- ------------------------------------------------------- 
  Option           Value                                                  
 ---------------- ------------------------------------------------------- 
  Service ID       xy_commons.controller.some_controller                    
  Class            XY\CommensBundle\Controller\SomeController 
  Tags             -                                                      
  Calls            setContainer                                           
  Public           no                                                     
  Synthetic        no                                                     
  Lazy             no                                                     
  Shared           yes                                                    
  Abstract         no                                                     
  Autowired        no                                                     
  Autoconfigured   no                                                     
 ---------------- ------------------------------------------------------- 

However, the error is still the same. So how to configure controllers / services in Bundles correctly?

EDIT:

SomeController is just a very basic subclass of AbstractController:

class SomeController extends AbstractController {
    public function some(): Response {
        return new Response("<html><body>OK</body></html>");
    }
}

Fully-qualified class name as Service ID

Usually I use FQCNs as Service ID. However, in this case I followed the advise from the Symfony docs (linked above) which explicitly say not to do so:

If the bundle defines services, they must be prefixed with the bundle alias instead of using fully qualified class names like you do in your project services. For example, AcmeBlogBundle services must be prefixed with acme_blog. The reason is that bundles shouldn’t rely on features such as service autowiring or autoconfiguration to not impose an overhead when compiling application services.

In addition, services not meant to be used by the application directly, should be defined as private.

This is way I used a snake name as ID and made the service private. The controller is not really used as service but only created automatically by Symfony when accessing / calling it via a route.

Upvotes: 2

Views: 4416

Answers (3)

Phocacius
Phocacius

Reputation: 1148

Solution for Symfony 6 and 7 (where @service_container does not work anymore): Replace @service_container with the FQCN of the ContainerInterface and add the tag container.service_subscriber:

XML Config:

<service id="..." name="...">
    <tag name="container.service_subscriber" />
    <call method="setContainer">
        <argument type="service" id="Psr\Container\ContainerInterface" />
    </call>
</service>

YAML Config:

Bundle\Controller\YourController:
    class: ...
    tags: ['container.service_subscriber'] 
    calls:
        - ['setContainer', ['@Psr\Container\ContainerInterface']]

Upvotes: 0

Cerad
Cerad

Reputation: 48865

So @ArturDoruch has the correct main points. You should use the fully qualified class name as the service id though I suppose you could use snake case as long as you also used it in your routes file. But there is no particular reason not to use the class name.

Your controller service also needs to be public so the controller resolver can pull it from the DI container. If the service is not public then the resolver just tries to new the controller and will never call set container or inject any constructor args. That is why you get the error about no container set. By the way, a magical side effect of tagging the service with controller.service_arguments is that the service becomes public.

The thing is that using controllers in bundles is just not something you see much anymore mostly because it's a pain if application want to slightly tweak the way controller works.

So if you look in the best practices you see things like bundle controllers should not extend AbstractConroller and bundles should not use autowire. Good advice for most bundles but if you just want to get stuff working then it's easy to get bogged down.

I would suggest starting with the same services.yaml file that comes with the application. You need to tweak the paths slightly:

# Resources/config/services.yaml
services:

    _defaults:
        autowire: true
        autoconfigure: true

    MyBundle\:
        resource: '../../'
        exclude:
            - '../../Resources/'
            - '../../DependencyInjection/'
            - '../../Entity/'

# bin/console debug:container MyController
  Service ID       MyBundle\Controller\MyController  
  Class            MyBundle\Controller\MyController  
  Tags             controller.service_arguments      
                   container.service_subscriber      
  Calls            setContainer                      
  Public           yes                               
  Synthetic        no                                
  Lazy             no                                
  Shared           yes                               
  Abstract         no                                
  Autowired        yes                               
  Autoconfigured   yes       

Now you can focus on just getting your bundle to work. Let autowire do the heavy lifting. Once the bundle has stabilized then maybe you can go back in and start to manually define your services as recommended in the best practices. Or maybe not.

Upvotes: 3

Artur Doruch
Artur Doruch

Reputation: 69

I recently struggle with the same problem. This is the solution.

Suppose we have the Vendor/FooBundle bundle.

  1. Controller with route annotations
<?php

namespace Vendor\FooBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class EntryCategoryController extends AbstractController
{
    /**
     * @Route("/")
     */
    public function list()
    {
        $entryCategories = [];

        return $this->render('@VendorFoo/entry_category/list.html.twig', [
            'entryCategories' => $entryCategories
        ]);
    }
}
  1. Register controller as service. The service id must be the controller fully-qualified class name, not a snake case string like vendor_foo.controller.entry_category. This is the key thing! Eventually you can create an alias for the controller service, but this is redundant.
# Resources/config/services.yaml

services:
    # The service id must be the controller fully-qualified class name.
    # If it a snake case string like `vendor_foo.controller.entry_category`,
    # then an alias must be created.
    # Vendor\FooBundle\Controller\EntryCategoryController:
    #     alias: '@vendor_foo.controller.entry_category'
    #     public: false

    Vendor\FooBundle\Controller\EntryCategoryController:
        # Setting class is redundant, but adds autocompletions for the IDE.
        class: Vendor\FooBundle\Controller\EntryCategoryController
        arguments:
        # Add this tag to inject services into controller actions.
        tags: ['controller.service_arguments'] 
        # Call the setContainer method to get access to the services via
        # $this->get() method.
        calls:
            - ['setContainer', ['@service_container']]

  1. Routing file.
# Resources/config/routing.yaml

vendorfoo_entrycategories:
    resource: '@VendorFooBundle/Controller/EntryCategoryController.php'
    type: annotation
    prefix: /entry-categories

Import routing file in your app

# config/routes.yaml

vendor_foo:
    resource: '@VendorFooBundle/Resources/config/routing.yml'
    prefix: /foo

That's all.

Upvotes: 6

Related Questions