Symfony DI for Event classes - php

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

Related

Subscriber not called in symfony 5.3

Hi I'm starting to learn the symfony event system and to test it I created a "subscriber" that listens to the AuthenticationEvents::AUTHENTICATION_FAILURE event and dumps it.
I then tried to connect with fake data, but nothing happens.
I then tried to listen to another event KernelEvents::REQUEST and it works, so I don't see where the problem can come from.
MY SUBSCRIBER :
namespace App\EventSubscriber;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class AuthenticationSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
AuthenticationEvents::AUTHENTICATION_SUCCESS => 'securityauthenticationsuccess',
AuthenticationEvents::AUTHENTICATION_FAILURE => 'securityauthenticationfailure',
KernelEvents::REQUEST => 'KernelRequest',
];
}
public function securityauthenticationfailure(LoginFailureEvent $event){
dump($event);
}
public function securityauthenticationsuccess(LoginSuccessEvent $event){
dump($event);
}
public function KernelRequest(RequestEvent $event){
dump($event);
}
}```
MY SERVICE.YAML :
```# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
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.
# 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/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
The Core\AuthenticationEvents class is part of the older authentication system. For the new HTTP based system, the event class name is used for the event name. So:
public static function getSubscribedEvents(): array
{
return [
LoginSuccessEvent::class => 'onLoginSuccess',
LoginFailureEvent::class => 'onLoginFailure',
];
}
Should get you one step further.
It might also be instructive to look at some of the listener classes under vendor\symfony\security-http\Authenticator\EventListener
By the way, I tested this code using an make:auth generated authenticator that extends AbstractLoginFormAuthenticator. All out of the box stuff. Works as expected.

Symfony: set "public" property in "services.yaml" for all classes in selected directory

I would like to have an ability to create services by theirs class-names.
One of the way to do it: is setting "public" property in "services.yaml"
BUT I DON'T WANT to set "public" property for ALL classes in my project
services.yaml
services:
_defaults:
public: false # it helps to optimize performance, doesn't it?
App\Service\Service1
public: true
App\Service\Service2
public: true
App\Service\Service3
public: true
App\Service\* # why I can't use something like "*" here ???
public: true
Service1.php
namespace App\Service;
class Service1
{
// important: every service can have one or more dependencies (Foo, Bar, Baz ... etc)
public function __construct(Foo $foo, Bar $bar)
{
$this->foo = $foo;
//..........
}
}
MyController.php
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
// hier $className is any class name like "App/Service/xxxx"
public function myAction (string $className)
{
return $this->container->get($className);
}
Questions:
is there a way to set "public" property for directory?
is there a better way to create an instance of service by class-name?
Thanks
Perhaps you can utilize YAML import features to generalize the configuration that you need, making it easier to maintain.
For example:
# /config/services.yaml
imports:
- { resource: 'services/public/*.yaml' } # Import public services
- { resource: 'services/private/*.yaml' } # Import private services
Though, as mentioned, forcing container services to be public goes against Symfony conventions, and will not be supported in future versions. Use dependency injection instead of interacting with the service container directly.
Yes, this should be possible. I tested it with Symfony 5.3.
See the last part in this services.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.
bind:
$projectDir: '%kernel.project_dir%'
# 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/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
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
App\Messenger\Handler\Event\:
resource: '../src/Messenger/Handler/Event/'
public: true
Similar to controllers you can include multiple classes via resource and make them public. But please be aware of the other answers/comments. Usually you should use dependency injection to load the services you need.
Alternatively (or even better) you could set a tag and use an iterable via dependency injection. See https://symfony.com/doc/5.3/service_container/tags.html#reference-tagged-services for more information.
Why would you need to set services to public? It goes against Symfony best practices which discourage fetching services from the container directly.
If you wish to define services named by their class name, it is enough to reference them with:
services:
App\Service\Service1: ~
App\Service\Service2: ~
App\Service\Service3: ~
App\Service\Service4:
arguments:
- '#App\Service\Service1'
- '#App\Service\Service2'
- '#App\Service\Service3'
and then inject them to your controllers directly, instead of using the container.

Symfony 4 not autowiring dynamic route controllers

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.

Symfony service declaration in services.yml

I am writing a service in Symfony 3 according to latest docs.
I wrote a class:
<?php
namespace julew\AdminBundle\Service;
class FileService {
public function create() {
die('I am here!');
}
}
My app/config/services.yml looks like:
services:
# default configuration for services in *this* file
_defaults:
# automatically injects dependencies in your services
autowire: true
# automatically registers your services as commands, event subscribers, etc.
autoconfigure: true
# this means you cannot fetch services directly from the container via $container->get()
# if you need to do this, you can override this setting on individual services
public: false
julew\AdminBundle\:
resource: '../../src/julew/AdminBundle/*'
exclude: '../../src/julew/AdminBundle/{Entity,Repository}'
But only what i get is this error:
Class Symfony\Bundle\FrameworkBundle\Test\WebTestCase not found in C:\xampp\htdocs\roch\app/config\services.yml (which is being imported from "C:\xampp\htdocs\roch\app/config\config.yml").
What am i doing wrong?
EDIT 1
The problem was, that automatically generated bundles had tests in it - had to add them to exclude paths.
But now other problem occured. Service is not injected via controler type-hint. Error that i get:
Controller "julew\RochNotesBundle\Controller\ParserController::indexAction()" requires that you provide a value for the "$fileService" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.
The controller method looks like:
public function indexAction(FileService $fileService) {
.
.
.
}
And at the top i added:
use julew\AdminBundle\Service\FileService;
If you want to inject various services into a controller, it can be a little easier to do so in the constructor, and avoid the use of the controller.service_arguments tag (which has to be explicitly added for the controller definition in the services.yml).

Symfony3 controller constructor injection is not working

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();

Categories