Symfony version: 4.1.3
I have an application which loads dynamic routes based on configuration by virtue of a route loader service, but it appears that Symfony 4 is not autowiring the dynamic route controllers.
I am using the standard Symfony 4 application services.yaml file:
/config/services.yaml
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
Application\Web\:
resource: '../src/*'
exclude: '../src/{Search/Model,Entity,Migrations,Tests,Kernel.php}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
Application\Web\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
Route Loader: src/Component/RouteLoader.php
<?php
namespace Application\Web\Component;
use Application\Symfony\Bundle\ConfigBundle\ReaderInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Class RouteLoader
* #package Application\Web\Component
*/
class RouteLoader
{
/**
* #var ReaderInterface
*/
private $configurationReader;
public function __construct(ReaderInterface $configurationReader)
{
$this->configurationReader = $configurationReader;
}
public function load(): RouteCollection
{
$routes = new RouteCollection();
foreach ($this->configurationReader->find('category_navigation') as $label => $configuration) {
$slug = strtolower($label);
$route = new Route(
$configuration['url'],
[
'_controller' => [$configuration['controller'], 'dispatch'],
'categories_slug' => $slug,
'category_label' => $label,
'page_title' => $configuration['page_title'] ?? null,
'page_description' => $configuration['page_description'] ?? null,
],
[],
[],
'',
[],
['GET']
);
$routes->add($slug . '.home', $route);
}
return $routes;
}
}
Controller constructor: src/Controller/Page.php
<?php
namespace Application\Web\Controller;
//.... other class code
public function __construct(
ClientInterface $client,
ReaderInterface $configurationReader,
\Twig_Environment $twigEnvironment,
ContentSearcher $contentSearcher,
Environment $environment,
TokenStorageInterface $tokenStorage,
UrlGeneratorInterface $urlGenerator
)
When I try to load the page, Symfony whines with the following error:
Controller "\Application\Web\Controller\Page" has required constructor arguments and does not exist in the container. Did you forget to define such a service?
However, when I define the route directly in the config/routes.yaml file, the controller is autowired in style with no issues!
My questions are:
Is this a limitation of Symfony's autowiring capability, i.e. not supported for dynamic route controllers?
Is there something that I'm missing when it comes to defining the routes that will make the autowiring work?
Have I potentially identified a bug?
Any ideas?
EDIT: Stack traces
InvalidArgumentException:
Controller "\Application\Web\Controller\Page" has required constructor arguments and does not exist in the container. Did you forget to define such a service?
at vendor/symfony/http-kernel/Controller/ContainerControllerResolver.php:62
at Symfony\Component\HttpKernel\Controller\ContainerControllerResolver->instantiateController('\\Application\\Web\\Controller\\Page')
(vendor/symfony/framework-bundle/Controller/ControllerResolver.php:54)
at Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver->instantiateController('\\Application\\Web\\Controller\\Page')
(vendor/symfony/http-kernel/Controller/ControllerResolver.php:49)
at Symfony\Component\HttpKernel\Controller\ControllerResolver->getController(object(Request))
(vendor/symfony/http-kernel/Controller/TraceableControllerResolver.php:38)
at Symfony\Component\HttpKernel\Controller\TraceableControllerResolver->getController(object(Request))
(vendor/symfony/http-kernel/HttpKernel.php:132)
at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
(vendor/symfony/http-kernel/HttpKernel.php:66)
at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
(vendor/symfony/http-kernel/Kernel.php:188)
at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
(public/index.php:37)
ArgumentCountError:
Too few arguments to function Application\Web\Controller\Base::__construct(), 0 passed in /var/www/html/vendor/symfony/http-kernel/Controller/ControllerResolver.php on line 133 and exactly 7 expected
at src/Controller/Base.php:55
at Application\Web\Controller\Base->__construct()
(vendor/symfony/http-kernel/Controller/ControllerResolver.php:133)
at Symfony\Component\HttpKernel\Controller\ControllerResolver->instantiateController('\\Application\\Web\\Controller\\Page')
(vendor/symfony/http-kernel/Controller/ContainerControllerResolver.php:55)
at Symfony\Component\HttpKernel\Controller\ContainerControllerResolver->instantiateController('\\Application\\Web\\Controller\\Page')
(vendor/symfony/framework-bundle/Controller/ControllerResolver.php:54)
at Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver->instantiateController('\\Application\\Web\\Controller\\Page')
(vendor/symfony/http-kernel/Controller/ControllerResolver.php:49)
at Symfony\Component\HttpKernel\Controller\ControllerResolver->getController(object(Request))
(vendor/symfony/http-kernel/Controller/TraceableControllerResolver.php:38)
at Symfony\Component\HttpKernel\Controller\TraceableControllerResolver->getController(object(Request))
(vendor/symfony/http-kernel/HttpKernel.php:132)
at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
(vendor/symfony/http-kernel/HttpKernel.php:66)
at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
(vendor/symfony/http-kernel/Kernel.php:188)
at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
(public/index.php:37)
Answering your questions:
No, at least not in this case. The only role of the Route Loader is to build a collection of routes so a request can be matched against it. By providing the _controller parameter, you are telling SF which controller is mapped to that route. The Dependency Injection component is the one in charge of autoloading the controllers into the container as a service. But if the controller reaches the ContainerControllerResolver and passes from lines 50-52, it is definitely due to the fact that your controller is not registered as a service (or more precisely, to the fact that whatever is the value of $class in ContainerControllerResolver::instantiateController() does not exist in the container).
I cannot reproduce, since I don't have your services. But my best guess is that is probably not working passing the _controller argument as some sort of callable array. Try pass it as a string like Application/Web/Pages::instance. The ContainerControllerResolver is able to work with that.
I have my doubts, but it's probable.
It would be very helpful to be able to see an stack trace.
UPDATE:
You have double backslashes in the string that conforms your class name. Try reformatting to: Application\Web\Controller\Page.
If you want to be sure of this fix before refactoring, run bin/console debug:container Page and check if your Page controller, as a service, exists. If it does, then the problem is, most certainly, the FQCN with double backslashes.
Related
I am developing a Symfony 3 application. Symfony profiler logs tell me:
Relying on service auto-registration for type "App\Entity\SubDir\Category"
is deprecated since version 3.4 and won't be supported in 4.0.
Create a service named "App\Entity\SubDir\Category" instead.
Yet, this is a simple ORM bean:
/**
* #ORM\Entity
* #ORM\Table(name="category")
*/
class Category
{
...
How should I get rid of this issue? Do I really need to declare ORM entities as services in services.yaml? If yes, how?
Update
In fact, my entity is in a sub directory. I have amended my question.
In my service.yaml, I have tried:
App\:
resource: '../src/*'
exclude: '../src/{Entity,Repository,Tests,Entity/SubDir}'
...but to no avail.
Do you have any Classes under Service-auto registration which use an Entity as constructor argument?
That's where your problem comes from.
You need to ask yourself if the concerning class really is a service or just a plain object of which you always create the instance yourself.
If it is not used as a service through the container you have 2 options:
You can exclude this class also through the glob pattern like for example
AppBundle\:
resource: '...'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
exclude: '../../{Entity,PathToYourNotService}'
or you can set the following parameter in your config
parameters:
container.autowiring.strict_mode: true
with this option the container won't try to create a service class with arguments that are not available as services and you will get a decisive error. This is the default setting for sf4
A good example for a class that triggers exactly this error would be a custom event class that takes an entity as payload in the constructor:
namespace AppBundle\Event;
use AppBundle\Entity\Item;
use Symfony\Component\EventDispatcher\Event;
class ItemUpdateEvent extends Event
{
const NAME = 'item.update';
protected $item;
public function __construct(Item $item)
{
$this->item = $item;
}
public function getItem()
{
return $this->item;
}
}
Now if this file isn't excluded specifically the container will try to auto register it as service. And because the Entity is excluded it can't autowire it. But in 3.4 there's this fallback which triggers this warning.
Once the strict_mode is activated the event just won't be available as service and if you tried using it as one an error would rise.
I'm integrating Symfony into an older application having its own dependency container based on PSR-11. Been searching for a solution to merge that DI container to the one Symfony uses, but found nothing. To just make it work, I came with one "hacky" solution which I don't like.
I've created this class. It creates an instance of an old DI container inside of it:
class OldAppServiceFactory
{
private ContainerInterface $container;
public function __construct()
{
$this->container = OldContainerFactory::create();
}
public function factory(string $className)
{
return $this->container->get($className);
}
}
and added proper entries to services.yaml:
oldapp.service_factory:
class: Next\Service\LeonContainer\LeonServiceFactory
OldApp\Repository\Repository1:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- 'OldApp\Repository\Repository1'
OldApp\Repository\Repository2:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- 'OldApp\Repository\Repository2'
OldApp\configuration\ConfigurationProviderInterface:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- 'OldApp\configuration\ConfigurationProviderInterface'
With above hack, putting those classes in service class constructors works. Unfortunately it looks bad and it'll be pain to extend it with more of those repositories (especially when having 50 of them). Is it possible to achieve something like this in services.yaml?
OldApp\Repository\:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- << PASS FQCN HERE >>
This would leave me with only one entry in services.yaml for a single namespace of the old application.
But, maybe there is other solution for my problem? Been trying with configuring Kernel.php and prepareContainer(...) method, but I also ended with nothing as the old dependencies are in one PHP file returning an array:
return array [
RepositoryMetadataCache::class => static fn () => RepositoryMetadataCache::createFromCacheFile(),
EntityCollection::class => autowire(EntityCollection::class),
'Model\Repository\*' => static function (ContainerInterface $container, RequestedEntry $entry) { ... }
];
You could probably accomplish this easily with a custom compiler pass.
First tag all the old repository classes by loading the directory where they exist:
OldApp\Repository\:
resource: '../src/OldApp/Repository/*'
autowire: false
autoconfigure: false
tags: ['oldapp_repository']
(I think that you may need to also exclude src/OldApp from the default automatic service loading. E.g.:
App\:
resource: '../src/*'
exclude: '../src/{OldApp/Repository,DependencyInjection,Entity,Tests,Kernel.php}'
... but I'm not 100% sure, test this one).
Then create a compiler pass to go through the tags and define a factory for each one:
class OldAppRepositoryCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$taggedServices = $container->findTaggedServiceIds('oldapp_repository');
foreach ($taggedServices as $serviceId => $tags) {
$definition = $container->getDefinition($serviceId);
$definition
->setFactory([new Reference('oldapp.service_factory'), 'factory'])
->addArgument($serviceId);
}
}
}
And in your Application Kernel build() method add the compiler pass:
// src/Kernel.php
namespace App;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
// ...
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new OldAppRepositoryCompilerPass());
}
}
Can't test this right at this minute, but this should get you going in the right direction. For additional details check the docs:
Working with service tags
You can check this example repo where the above is implemented and working. On this repo the OldApp namespace is outside of App and src, so no need to exclude it from automatic service loading.
I have some events in my project that uses DI. When my events are an instance of App\Manager\ValidatorAwareInterface I configure then to inject the #validator service.
When I have this code in the services.yaml it works :
services:
_instanceof:
App\Manager\Validator\ValidatorAwareInterface:
calls:
- method: setValidator
arguments:
- '#validator'
But when I put this same code in a manager.yaml file that I import in the services.yaml, it does not work anymore :
imports:
- { resource: manager.yaml }
Do you have an idea why ?
Thanks.
#stephan.mada's answer will probably fix your problem.
But I would like to point out a little known annotation called '#required' which eliminates the need to explicitly configure your setter at all.
use Symfony\Component\Validator\Validator\ValidatorInterface;
trait ValidatorTrait
{
/** #var ValidatorInterface */
protected $validator;
/** #required */
public function injectValidator(ValidatorInterface $validator)
{
$this->validator = $this->validator ?: $validator;
}
}
The '#required' before the inject method causes the container to automatically inject the dependency. Your setter stuff in services.yaml can go away completely. You don't see a whole lot of usage of '#required' but it does come in handy for commonly injected services.
You might also notice that I used a trait here. With a trait, I no longer need a base class or interface. Any service that uses the trait will automatically get the validator service injected. You can of course just use a conventional class if you like.
class SomeService
{
use ValidatorTrait; // And the validator is now available
use RouterTrait; // As well as the router
And one final note. I added a guard to ensure that the validator can only be injected once. This protects against rogue programmers who might be tempted to inject a different validator at some other point in the cycle.
I think you should copy symfony's default service configuration in your manager.yaml before defining others services since the default configuration is only applied to the services defined in that file. The new Default services.yaml File
# config/manager.yaml
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
I am developing a Symfony 3 application. Symfony profiler logs tell me:
Relying on service auto-registration for type "App\Entity\SubDir\Category"
is deprecated since version 3.4 and won't be supported in 4.0.
Create a service named "App\Entity\SubDir\Category" instead.
Yet, this is a simple ORM bean:
/**
* #ORM\Entity
* #ORM\Table(name="category")
*/
class Category
{
...
How should I get rid of this issue? Do I really need to declare ORM entities as services in services.yaml? If yes, how?
Update
In fact, my entity is in a sub directory. I have amended my question.
In my service.yaml, I have tried:
App\:
resource: '../src/*'
exclude: '../src/{Entity,Repository,Tests,Entity/SubDir}'
...but to no avail.
Do you have any Classes under Service-auto registration which use an Entity as constructor argument?
That's where your problem comes from.
You need to ask yourself if the concerning class really is a service or just a plain object of which you always create the instance yourself.
If it is not used as a service through the container you have 2 options:
You can exclude this class also through the glob pattern like for example
AppBundle\:
resource: '...'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
exclude: '../../{Entity,PathToYourNotService}'
or you can set the following parameter in your config
parameters:
container.autowiring.strict_mode: true
with this option the container won't try to create a service class with arguments that are not available as services and you will get a decisive error. This is the default setting for sf4
A good example for a class that triggers exactly this error would be a custom event class that takes an entity as payload in the constructor:
namespace AppBundle\Event;
use AppBundle\Entity\Item;
use Symfony\Component\EventDispatcher\Event;
class ItemUpdateEvent extends Event
{
const NAME = 'item.update';
protected $item;
public function __construct(Item $item)
{
$this->item = $item;
}
public function getItem()
{
return $this->item;
}
}
Now if this file isn't excluded specifically the container will try to auto register it as service. And because the Entity is excluded it can't autowire it. But in 3.4 there's this fallback which triggers this warning.
Once the strict_mode is activated the event just won't be available as service and if you tried using it as one an error would rise.
I want to pass the EntityManager instance into the constructor of my controller, using this code:
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Doctrine\ORM\EntityManager;
class UserController extends Controller
{
public function __construct( EntityManager $entityManager )
{
// do some stuff with the entityManager
}
}
I do the constructor injection by putting the parameters into the service.yml file:
parameters:
# parameter_name: value
services:
# service_name:
# class: AppBundle\Directory\ClassName
# arguments: ["#another_service_name", "plain_value", "%parameter_name%"]
app.user_controller:
class: AppBundle\Controller\UserController
arguments: ['#doctrine.orm.entity_manager']
the service.yml is included in the config.yml and when I run
php bin/console debug:container app.user_controller
I get:
Information for Service "app.user_controller"
=============================================
------------------ -------------------------------------
Option Value
------------------ -------------------------------------
Service ID app.user_controller
Class AppBundle\Controller\UserController
Tags -
Public yes
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired no
Autowiring Types -
------------------ -------------------------------------
However, calling a route which is mapped to my controller, I get:
FatalThrowableError in UserController.php line 17: Type error:
Argument 1 passed to
AppBundle\Controller\UserController::__construct() must be an instance
of Doctrine\ORM\EntityManager, none given, called in
/home/michel/Documents/Terminfinder/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php
on line 202
I cant figure out, why the EntityManager is not getting injected?
When using the base classController.php the Container is usually auto-wired by the framework in theControllerResolver.
Basically you are trying to mix up how things actually work.
To solve your problem you basically have two solutions:
Do no try to inject the dependency but fetch it directly from the Container from within your action/method.
public function listUsers(Request $request)
{
$em = $this->container->get('doctrine.orm.entity_manager');
}
Create a controller manually but not extend the Controller base class; and set ip up as a service
To go a bit further on this point, some people will advise to do not use the default Controller provided by Symfony.
While I totally understand their point of view, I'm slightly more moderated on the subject.
The idea behind injecting only the required dependencies is to avoid and force people to have thin controller, which is a good thing.
However, with a little of auto-determination, using the existing shortcut is much simpler.
A Controller / Action is nothing more but the glue between your Views and your Domain/Models.
Prevent yourself from doing too much in your Controller using the ContainerAware facility.
A Controller can thrown away without generate business changes in your system.
Since 2017 and Symfony 3.3+, there is native support for controllers as services.
You can keep your controller the way it is, since you're using constructor injection correctly.
Just modify your services.yml:
# app/config/services.yml
services:
_defaults:
autowire: true
AppBundle\:
resouces: ../../src/AppBundle
It will:
load all controllers and repositories as services
autowire contructor dependencies (in your case EntityManager)
Step further: repositories as services
Ther were many question on SO regarding Doctrine + repository + service + controller, so I've put down one general answer to a post. Definitelly check if you prefer constructor injection and services over static and service locators.
Did you use following pattern to call the controller AppBundle:Default:index? if yes that should be the problem. If you want to use controller as a service you have to use the pattern: app.controller_id:indexAction which uses the id of the service to load the controller.
Otherwise it will try to create an instance of the class without using the service container.
For more information see the symfony documentation about this topic https://symfony.com/doc/current/controller/service.html
The entity manager is available in a controller without needing to inject it. All it takes is:
$em = $this->getDoctrine()->getManager();