Reputation: 18729
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?
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
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
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
Reputation: 69
I recently struggle with the same problem. This is the solution.
Suppose we have the Vendor/FooBundle
bundle.
<?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
]);
}
}
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']]
# 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