Best practice of Dependency Injection in Symfony2 - php

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

Related

Symfony inject EntityManager in SwitchUserListener

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!

Symfony2 get logged in user in doctrine event listener

I have created an event listener class for postPersist, PostUpdate and postRemove methods of doctrine.
I need logged in user id in my class, i have tried injecting #security.context , #security.token_storage and #session
I got circular reference error even i have tried injecting #service_container and use container->get() i got same circular reference error.
ServiceCircularReferenceException: Circular reference detected for > service "doctrine.orm.default_entity_manager"
my code in serviec.yml code is like
my.listener:
class: \projectCreateEventListener
arguments: ["#service_container"]
tags:
- { name: doctrine.event_listener, event: postPersist }
- { name: doctrine.event_listener, event: postUpdate }
- { name: doctrine.event_listener, event: postRemove }
my event listener class is like
class myListener
{
private $container;
public function
__construct(ContainerInterface $container)
{
$this->container = $container;
}
public function prePersist(LifeCycleEventArgs $args)
{
$entity = $args->getEntity();
//Circular reference error
$user = $this->container->get('security.context')-
>getToken()->getUser();
//getToken() is always null
//Circular reference error
$user = $this->container->get('security.token_storage')-
>getToken()->getUser();
//getToken() is always null
//Circular reference error
$userId = $this->container->get('auth.user')-
>getIdentity()['id'];
}
}
Although i am getting logged in user information in my code before $this->persist() in $this->container->get('auth.user')->getIdentity()['id'];
That's a tricky one. When creating the doctrine service the listener is attached while constructing. If the listener you are trying to build uses another service (or some other service down the line) that requires doctrine in any way you get the circular reference.
But you can build around it.
Inject the eventDispatcher into your Listener
Create a custom event (or multiple) http://symfony.com/doc/current/components/event_dispatcher.html#creating-and-dispatching-an-event
Dispatch the event in the prePersist, postPersists methods.
Build another listener that subscribes to your custom event and handle your logic there.
What this achieves is: Your custom event listener will only be initialized when your custom event is actually fired. At that point the crucial services like doctrine are already up and running and you avoid the circular reference problem.
Had a similar issue in my project and solved it this way. Not really sure if it's the most elegant way but it definitely works. (If someone has a better solution i'd welcome it too).

Symfony event listener not firing

I'm scratching my head over this one, so maybe someone can help me out. I've done this before without any issue, but I'm new to Symfony so it's likely I'm missing something this time around.
I'm trying to load an event listener to fire some code whenever I save an entity.
In my app/config/config.yml I put this.
services:
fu_bar.listener:
class: Fu\BarBundle\EventListener\AuthCheckListener
tags:
- { name: doctrine.event_listener, event: postPersist, connection: default }
- { name: doctrine.event_listener, event: preUpdate, connection: default }
In my Fu\BarBundle\EventListener\AuthCheckListener I'm just doing this so I can see I'm hitting these methods, which I'm not.
<?php
namespace Fu\BarBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
class AuthCheckListener {
public function preUpdate(LifecycleEventArgs $args) {
file_put_contents('/tmp/yyy', 'ffdf');
}
public function postPersist(LifecycleEventArgs $args) {
file_put_contents('/tmp/xxx', 'fff');
//$this->syncAuth($args);
}
}
When I save an entity, I'm expecting to see some file(s) in /tmp, but I'm not. It doesn't appear that the event listener is being registered.
What am I missing here?
doctrine:event_listener should be doctrine.event_listener
use dot notation for services and parameters
use colon notation for controllers,views, and these kind of "objects"

symfony2 service tagged as doctrine.event_subscriber not receiving events when using service with arguments

When I setup the service sme_task.listener.status_change with arguments it is not receiving events.
services:
sme_task.service.task_template:
class: Sme\TaskBundle\Service\TaskTemplateService
arguments: [#doctrine.orm.entity_manager]
sme_task.listener.status_change:
class: Sme\TaskBundle\Listener\StatusChangeListener
arguments: ["#sme_task.service.task_template"]
tags:
- { name: doctrine.event_subscriber, connection: default }
If I remove the arguments the event is reached.
sme_task.listener.status_change:
class: Sme\TaskBundle\Listener\StatusChangeListener
tags:
- { name: doctrine.event_subscriber, connection: default }
Sme\TaskBundle\Listener\StatusChangeListener.php
class StatusChangeListener implements EventSubscriber {
private $taskTemplateService;
public function __construct($taskTemplate=null) {
$this->taskTemplateService=$taskTemplate;
}
[...]
public function onFlush(OnFlushEventArgs $eventArgs) {
throw new \Exception("Event reached");
[... some calls to TaskTemplateService ...]
}
function getSubscribedEvents()
{
return array("onFlush");
}
}
Anyone have an idea why this happens and how I can fix it?
You have circular references. To create EntityManager, all event listeners must be created first. Now as your listener has dependency on a service, which has dependency on entity manager, you get circular reference.
Best way to fix it would be avoid creating these references - you can get entity manager to listener by event arguments, passed to it.
Another way would be to inject service container into listener and get the service only when it's needed.

Symfony2: How to access to service from 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']

Categories