My question is about how to inject the entity manager in the SwitchUserListener that already has 9 parameters.
I have a custom switch user flow where I need to set the ExternalClient passed along with the _switch_user parameter (?_switch_user=user1&external_client_id=1) in the session. I first have to fetch the ExternalClient from the database before I can set it.
In parameters.yml I've added
security.authentication.switchuser_listener.class: App\Bundle\Listener\SwitchUserListener
And for the content of App\Bundle\Listener\SwitchUserListener I used the Symfony SwitchUserListener Symfony\Component\Security\Http\Firewall\SwitchUserListener.php
Everything works and when I fetch the external_client_id parameter from the request variable in the listener it is populated. But I can't seem to get access to the entity manager.
Things I've tried:
Add decorator in services.yml
app.decorating_switch_user:
class: App\Bundle\Listener\SwitchUserListener
decorates: security.authentication.switchuser_listener
arguments: ['#app.decorating_switch_user.inner', '#doctrine.orm.entity_manager']
public: false
Overriding parent dependencies in services.yml
security.authentication.switchuser_listener:
abstract: true
test:
class: "%security.authentication.switchuser_listener.class%"
parent: security.authentication.switchuser_listener
public: false
# appends the '#doctrine.orm.entity_manager' argument to the parent
# argument list
arguments: ['#doctrine.orm.entity_manager']
Listen to SwitchUserEvent instead
app.switch_user_listener:
class: App\Bundle\Listener\SwitchUserListener
tags:
- { name: kernel.event_listener, event: security.switch_user, method: onSwitchUser }
Here I've replace the contents of 'App\Bundle\Listener\SwitchUserListener' with:
class SwitchUserListener
{
public function onSwitchUser(SwitchUserEvent $event)
{
$request = $event->getRequest();
echo "<pre>";
dump($externalClientId = $request->get('external_client_id'));
echo "</pre>";
exit;
}
}
I'm getting the external_client_id as well with this attempt but I have no idea how to inject the entity manager. And even If I did, I'd have no way of getting the original user that initiated the _switch_user request. SwitchUserEvent only has access to the getTargetUser() method.
Conclusion:
If anybody has experience with this topic and is willing to share it that would be great. Ideally I would add the entity manager service to the previous 9 arguments of the __construct function. I'm expanding that class just like Matt is doing here: Symfony2: Making impersonating a user more friendly
You can override the service as follows. You may need to look up/change the concrete order of service arguments as it changed between symfony versions. Some arguments like $providerKey can be left empty as they will be changed/injected automatically by symfony.
In order to save some time coding you won't need to override the constructor if you use setter injection.
A look at Symfony's default SwitchUserListener (switch to the tag/version used in your application) will help when implementing your new handle method.
# app/config/services.yml
services:
# [..]
security.authentication.switchuser_listener:
class: 'Your\Namespace\SwitchUserListener'
public: false
abstract: true
arguments:
- '#security.context'
- ~
- '#security.user_checker'
- ~
- '#security.access.decision_manager'
- '#?logger'
- '_switch_user'
- ~
- '#?event_dispatcher'
- ~
calls:
- [ 'setEntityManager', [ '#doctrine.orm.entity_manager' ]]
tags:
- { name: monolog.logger, channel: security }
Now your SwitchUserListener might look like this:
namespace Your\Namespace;
use Symfony\Component\Security\Http\Firewall\SwitchUserListener as DefaultListener;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class SwitchUserListener extends DefaultListener
{
/** #var EntityManagerInterface */
protected $em;
public function setEntityManager(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Handles the switch to another user.
*
* #throws \LogicException if switching to a user failed
*/
public function handle(GetResponseEvent $event)
{
// Do your custom switching logic here
}
}
Don't forget to clear your cache!
Related
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?
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']]]
Solution to the problem is to extend the service definition of the Controller with a call to setContainer to inject the '#Psr\Container\ContainerInterface' service:
WM\ApiBundle\Controller\BaseController:
class: WM\ApiBundle\Controller\BaseController
abstract: true
arguments:
- "#service1"
- "#service2"
- ...
calls:
- ['setContainer', ['#Psr\Container\ContainerInterface']]
WM\ApiBundle\Controller\UserController:
parent: WM\ApiBundle\Controller\BaseController
public: true
class: WM\ApiBundle\Controller\UserController
tags:
- { name: 'container.service_subscriber'}
- { name: 'container.service_subscriber', key: 'servicexyz', id: 'servicexyz' }
This will give me a ServiceLocator as container containing only the regiestered services instead of the full container without using the autowire option.
Sidenote: Setting the #service_container would inject the full container.
For completeness, there was already an issue on the symfony project where this was discussed.
Before Doctrine 2.4 the default way to catch lifecycle events (like prePersist) was a global event listener that would fire for ALL entities. Running such a listener as a Symfony service made it easy to inject other services (like the request or request_stack objects).
Now the better solution seems to be an entity listener since that comes with much less overhead!
So let's start this thing up in our entities header...:
* #ORM\EntityListeners({ "AppBundle\Entity\Listener\LanguageListener" })
And here's the class:
namespace AppBundle\Entity\Listener;
use Doctrine\ORM\Event\LifecycleEventArgs;
class LanguageListener
{
public function prePersist($obj_entity, LifecycleEventArgs $obj_eventArgs)
{
$request = ???;
// set entity to users preferred language (for example 'de')
$obj_entity->setLanguage($request->getLocale());
}
}
As you can see, I have not the slightest idea how to access Symfonys services (in this case the request object).
But wait! There is a way:
global $kernel;
if ('AppCache' == get_class($kernel))
{
$kernel = $kernel->getKernel();
}
$request = $kernel->getContainer()->get('request');
And it's working too.
But in all my research I found a lot of related questions strictly warning about excactly that!
Only difference: all those questions were referring to Entities, not to Entity listeners...
... leading me to these two questions:
Is the above solution the way to go?
And if not: how should it be done?
[Edit:] Once again (see first sentence) let me make clear that this question is also about how to not use a service. Services come at some cost, see Expensive Service Construction. And particularly in this case I rarely need the functionality - that's why I want to go with Entity Listeners that do not run as service.
Sorry I didn't over emphasize this side aspect. Not sure why this qualified for a rate down though...
[Edit2:] To make things clearer I added one more code example (the first one) that shows how the thing is mapped.
As of doctrine/doctrine-bundle >= 1.5.0 entity listeners can be created as services and if they are tagged with doctrine.orm.entity_listener they will be registered into the desired entity manager automatically. You can inject the required dependencies into the service e.g. the request stack.
Create a listener:
namespace AppBundle\Doctrine\Listener;
use Symfony\Component\HttpFoundation\RequestStack;
class LanguageListener
{
/**
* #var RequestStack
*/
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function prePersist($entity)
{
if (null !== $request = $this->requestStack->getCurrentRequest()) {
// put the logic here
}
}
}
Register it as a service:
app.doctrine.language_listener:
class: AppBundle\Doctrine\Listener\LanguageListener
public: false
arguments: ["#request_stack"]
tags:
- { name: "doctrine.orm.entity_listener" }
Annotate entities:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Entity\User as BaseUser;
/**
* #ORM\Entity()
* #ORM\EntityListeners("AppBundle\Doctrine\Listener\LanguageListener")
* #ORM\Table("user_")
*/
class User extends BaseUser
{
// ...
}
Note that entity listener registered this way are not lazily loaded, so they and their dependencies will be created when the entity manager is created.
UPDATE:
So if your question is about how to use it lazily. The first solution that comes in my mind to declare it as a lazy service, but in this case it actually does not work as expected because in the annotation we use a concrate class which will be created by the listener resolver when needed, but in this case we should write the proxy class name in the annotation, to use the proxy object instead, which is not possible. There is a solution though (which is not documented at the moment), do not annotate the entity with #EntityListeners, but use tag parameters to register the listener. Something like this:
app.doctrine.language_listener:
class: AppBundle\Doctrine\Listener\LanguageListener
arguments: ["#request_stack"]
lazy: true
tags:
- { name: "doctrine.orm.entity_listener", entity: AppBundle\Entity\User, event: preUpdate }
- { name: "doctrine.orm.entity_listener", entity: AppBundle\Entity\User, event: postUpdate }
This way you can use lazy services, but it only works with doctrine/orm >= 2.5.0.
An another solution would be to create an own entity listener resolver which knows about the container (this is actually not a good thing) and uses it to get the listener when it is needed. There is a blog post about a way to do it.
I am wondering if this is even a good practice. But for my project i need to get a parameter from parameters.yml and use it inside EntityRepository.
So for this I created a service but still the call is not executed.
services:
xxx_repository:
class: XXX\DatabaseBundle\Repository\CitiesRepository
calls:
- [setTheParameter, ["%the_parameter%"]]
parameters.yml
...
the_parameter: 14400
...
And inside the CitiesRepository.php I am doing the following:
class CitiesRepository extends EntityRepository
{
/**
* #var
*/
protected $theParameter;
public function setTheParameter($theParameter)
{
$this->theParameter = $theParameter;
}
....
}
But $this->theParameter is always null.
SO i have 2 questions: Is this a healthy habit? And why is the result always null?
You need to use getRepository method of the doctrine service as factory:
xxx_repository:
class: XXX\DatabaseBundle\Repository\CitiesRepository
factory: ["#doctrine", "getRepository"]
arguments: ["DatabaseBundle:City"]
calls:
- ["setTheParameter", ["%the_parameter%"]]
And then you can access to this repository as service in your controller:
$this->get('xxx_repository');
I have class ModelsRepository:
class ModelsRepository extends EntityRepository
{}
And service
container_data:
class: ProjectName\MyBundle\Common\Container
arguments: [#service_container]
I want get access from ModelsRepository to service container_data. I can't transmit service from controller used constructor.
Do you know how to do it?
IMHO, this shouldn't be needed since you may easily break rules like SRP and Law of Demeter
But if you really need it, here's a way to do this:
First, we define a base "ContainerAwareRepository" class which has a call "setContainer"
services.yml
services:
# This is the base class for any repository which need to access container
acme_bundle.repository.container_aware:
class: AcmeBundle\Repository\ContainerAwareRepository
abstract: true
calls:
- [ setContainer, [ #service_container ] ]
The ContainerAwareRepository may looks like this
AcmeBundle\Repository\ContainerAwareRepository.php
abstract class ContainerAwareRepository extends EntityRepository
{
protected $container;
public function setContainer(ContainerInterface $container)
{
$this->container = $container;
}
}
Then, we can define our Model Repository.
We use here, the doctrine's getRepository method in order to construct our repository
services.yml
services:
acme_bundle.models.repository:
class: AcmeBundle\Repository\ModelsRepository
factory_service: doctrine.orm.entity_manager
factory_method: getRepository
arguments:
- "AcmeBundle:Models"
parent:
acme_bundle.repository.container_aware
And then, just define the class
AcmeBundle\Repository\ModelsRepository.php
class ModelsRepository extends ContainerAwareRepository
{
public function findFoo()
{
$this->container->get('fooservice');
}
}
In order to use the repository, you absolutely need to call it from the service first.
$container->get('acme_bundle.models.repository')->findFoo(); // No errors
$em->getRepository('AcmeBundle:Models')->findFoo(); // No errors
But if you directly do
$em->getRepository('AcmeBundle:Models')->findFoo(); // Fatal error, container is undefined
I tried some versions. Problem was solved follows
ModelRepository:
class ModelRepository extends EntityRepository
{
private $container;
function __construct($container, $em) {
$class = new ClassMetadata('ProjectName\MyBundle\Entity\ModelEntity');
$this->container = $container;
parent::__construct($em, $class);
}
}
security.yml:
providers:
default:
id: model_auth
services.yml
model_auth:
class: ProjectName\MyBundle\Repository\ModelRepository
argument
As a result I got repository with ability use container - as required.
But this realization can be used only in critical cases, because she has limitations for Repository.
Thx 4all.
You should never pass container to the repository, just as you should never let entities handle heavy logic. Repositories have only one purpose - retrieving data from the database. Nothing more (read: http://docs.doctrine-project.org/en/2.0.x/reference/working-with-objects.html).
If you need anything more complex than that, you should probably create a separate (container aware if you wish) service for that.
I would suggest using a factory service:
http://symfony.com/doc/current/components/dependency_injection/factories.html
//Repository
class ModelsRepositoryFactory
{
public static function getRepository($entityManager,$entityName,$fooservice)
{
$em = $entityManager;
$meta = $em->getClassMetadata($entityName);
$repository = new ModelsRepository($em, $meta, $fooservice);
return $repository;
}
}
//service
AcmeBundle.ModelsRepository:
class: Doctrine\ORM\EntityRepository
factory: [AcmeBundle\Repositories\ModelsRepositoryFactory,getRepository]
arguments:
- #doctrine.orm.entity_manager
- AcmeBundle\Entity\Models
- #fooservice
Are you sure that is a good idea to access service from repo?
Repositories are designed for custom SQL where, in case of doctrine, doctrine can help you with find(),findOne(),findBy(), [...] "magic" methods.
Take into account to inject your service where you use your repo and, if you need some parameters, pass it directly to repo's method.
I strongly agree that this should only be done when absolutely necessary. Though there is a quite simpler approach possible now (tested with Symfony 2.8).
Implement in your repository "ContainerAwareInterface"
Use the "ContainerAwareTrait"
adjust the services.yml
RepositoryClass:
namespace AcmeBundle\Repository;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use AcmeBundle\Entity\User;
class UserRepository extends EntityRepository implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function findUserBySomething($param)
{
$service = $this->container->get('my.other.service');
}
}
services.yml:
acme_bundle.repository.user:
lazy: true
class: AcmeBundle\Repository\UserRepository
factory: ['#doctrine.orm.entity_manager', getRepository]
arguments:
- "AcmeBundle:Entity/User"
calls:
- method: setContainer
arguments:
- '#service_container'
the easiest way is to inject the service into repository constructor.
class ModelsRepository extends EntityRepository
{
private $your_service;
public function __construct(ProjectName\MyBundle\Common\Container $service) {
$this->your_service = $service;
}
}
Extending Laurynas Mališauskas answer, to pass service to a constructor make your repository a service too and pass it with arguments:
models.repository:
class: ModelsRepository
arguments: ['#service_you_want_to_pass']
Before persisting an entity I need to copy and format some data in another table of my DB. I want this task to be performed as a service.
So I describe the service in config.yml
services:
my_service:
class: Acme\Bundle\AcmeBundle\DependencyInjections\MyService
arguments:
entityManager: "#doctrine.orm.entity_manager"
I was wondering the best way to call this service. The only way I can figure out is from the controller:
$entity = new Entity($this->get('my_service'));
Is that the best way to proceed ?
If my understanding is good, your service my_service is something you want to do before persisting your entity. It's a service which has to be triggered by a prePersist event.
So, I'd just transform this service to a doctrine listener.
services:
my_service:
class: Acme\Bundle\AcmeBundle\DependencyInjections\MyService
arguments:
entityManager: "#doctrine.orm.entity_manager"
tags:
- { name: doctrine.event_listener, event: prePersist }
In MyService class, you have now to define a prePersist method with everything you want to do.
use Doctrine\ORM\Event\LifecycleEventArgs;
class MyService
{
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
(...)
}
}
You can even remove the arguments of your service since LifecycleEventArgs provides a method to get the entity manager.
Finally, you have this listener
services:
my_service:
class: Acme\Bundle\AcmeBundle\DependencyInjections\MyService
tags:
- { name: doctrine.event_listener, event: prePersist }
I hope this answer your question