Symfony 3 service configuration providing dynamic configuration - symfony

I would like to know how I could avoid setter injection under the following use case.
I have a collection of tagged services:
interface EmailFormatter
class CvEmailFormatter implements EmailFormatter
class RegistrationEmailFormatter implements EmailFormatter
class LostPasswordEmailFormatter implements EmailFormatter
I use a CompilerPass to inject these into a mailer service by invoking its addEmailFormatter() method.
Each EmailFormatter instance requires some configuration that must be retrieved at run time from the current user:
/**
* #var FormatterConfig[]
* #ORM\OneToMany(targetEntity="AppBundle\Entity\FormatterConfig", mappedBy="user", cascade={"persist"}, orphanRemoval=true)
*/
protected $formatterConfigs;
Right now I am checking to see if the user has created a configuration for a particular EmailFormatter inside the mailer service's addEmailFormatter() method. If they have then setConfig() injects the configuration object. If no config exists yet the EmailFormatter does not get added at all.
public function addEmailFormatter(EmailFormatter $formatter, $alias)
{
$formatterConfig = $this->user->getFormatterConfig($alias);
if (null !== $formatterConfig) {
$this->formatters[$alias] = $formatter;
$formatter->setConfig($formatterConfig);
}
}
Could this be a legitimate use case given I am working with a mutable collection of services?

Related

Dependency injection in Symfony 3.4 : check existence of a service

I am in the process of migrating an application from Symfony 2.8 to Symfony 3.4
The services are now private and therefore instead of making a direct call to the services from the container, we must use dependency injection as a workaround.
So this is the following script and i'd like to check the existence and after that call profiler service using dependency injection :
<?php
namespace DEL\Bundle\ApiBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Class EstimatePDFController
*
* #package DEL\Bundle\ApiBundle\Controller
*/
class EstimateController extends Controller
{
/**
*
* #param Request $request Request object.
*
* #return Response A Response instance
*/
public function sendAction(Request $request)
{
// disable debug env outputs
if ($this->container->has('profiler')) {
$this->container->get('profiler')->disable();
}
return new Response('OK');
}
}
As far as I know, this is not possible using autowiring. But the documentation provides an alternative:
add the profiler to your controller as a property
add a setter like setProfiler(Profiler $profiler) that sets the property
add a conditional setter to your service definition:
calls:
- [setProfiler, ['#?profiler']]
check whether $this->profiler is null or not in your sendAction method
Checking the existence means the Profiler exists before using it right? So you can autowire the Profiler with a default value and if it is not null, it exists. Something like this:
/**
* #param Request $request Request object.
* #param Profiler $profiler The Profiler if the service exists
*
* #return Response A Response instance
*/
public function sendAction(Request $request, Profiler $profiler = null): Response
{
// disable debug env outputs
if ($profiler !== null) {
$profiler->disable();
}
return new Response('OK');
}
This is the default behaviour by the way. It tries to resolve the argument, but if it fails, it skips it. And if you have no default value, then PHP fails.

How to Inject Services in another Service using Factory to create them

I have a little problem with the creation of my services in symfony 4.1
I use Factories to create my services, and to force the Factory to have the expected method I made an Interface
<?php
namespace App\Service\Factory\Interfaces;
use App\Service\Interfaces\BaseModelServiceInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
/**
* Interface ModelServiceFactoryInterfaces
* #package App\Service\Factory\Interfaces
*/
interface ModelServiceFactoryInterfaces
{
/**
* Create the Model related Service
*
* #return BaseModelServiceInterface
*/
public function createService(DocumentManager $dm);
}
I get the DocumentManager from autowired services to create the Repository in the Factory and pass it to the service, like this
/**
* Class ChapterServiceFactory
* #package App\Service\Factory
*/
class ChapterServiceFactory implements ModelServiceFactoryInterfaces
{
/**
* #param DocumentManager $dm
* #return ChapterService|BaseModelServiceInterface
*/
public function createService(DocumentManager $dm)
{
$chapterRepository = $dm->getRepository(Chapter::class);
/**
* #var $chapterRepository ChapterRepository
*/
return new ChapterService($chapterRepository);
}
}
The problem with that is, if I want to have another service in my ChapterService I can't autowire it in the Factory because of the Interface, but I don't want to delete the Interface either.
Is there a way to have "dynamic arguments" with an Interface, or another way than the Interface to force the Factories to have the createService method ?
There's no workaround for this: if you declare an interface as the argument, then you need to provide an explicit implementation for that interface. This means that you cannot declare two default implementations for the same interface. Only thing you can do in this case is to declare explicitly the service and all its arguments.
By the way, as you're writing about Doctrine Repositories, I suggest you to take a look at ServiceEntityRepository: extending this class from one of your repos will make automatically the report a service that you can inject where needed.

Injecting #session into EntityRepository

I wanted to override the default Doctrine\ORM\EntityRepository class in my Symfony2 project so that I can have access to the #session service so that all of my repositories have access to a certain session variable if it is set.
On investigation it appeared to be less simple than I had hoped, as the EntityRepository is instantiated from within the Doctrine\ORM\EntityManager, and this class is instantiated itself using a static "create" method.
I followed the answer in Injecting dependency into entity repository but have hit a roadblock in actually implementing the custom manager class (specifically where the answer's author says "But since you're making a custom entity manager, you can wire it up to the service container and inject whatever dependencies you need").
I have defined my overridden EntityManager class, with an overridden "create" function and have also overridden the "getRepository" function. It is in this function that I believe I need to add the session to the Repository as it is created using a "setSession" method on my overridden EntityRepository class, but I am unsure as to how to actually get the session into the manager in the first place, as the other constructor arguments for the EntityManager class (Connection $conn, Configuration $config, EventManager $eventManager) are supplied in the Symfony\Bundle\DoctrineBundle\DependencyInjection\DoctrineExtension "ormLoad" method.
I have also specified
doctrine.orm.entity_manager.class: Me\MyBundle\Doctrine\ORM\EntityManager
in my config.yml file.
How can I have Symfony use my custom EntityManager class when creating repositories, and inject the session into it as well?
Florian, here, explained how to create repository via service:
my_service:
class: Doctrine\Common\Persistence\ObjectRepository
factory_service: doctrine # this is an instance of Registry
factory_method: getRepository
arguments: [ %mytest.entity% ]
You could add calls to invoke setSession (as deferred DI):
my_service:
...
calls:
- [setSession, ["#session"]]
Is this you're trying to do?
I ended up going with something slightly different:
I overrode the doctrine.orm.entity_manager.class parameter with my custom class which simple extended the default class with an additional $session parameter (complete with getter and setter), along with overridden create and getRepository functions (to return instances of my class instead of the default.
I then overrode the EntityRepository class and implemented a "getSession" method which returned
$this->_em->getSession();
and finally, in a custom event listener which has access to the entity manager, I called
$this->entityManager->setSession($session);
which gave me access to the session from every repository.
In Symfony 4+ you can just make it a ServiceEntityRepository and with autowiring there's no need for any services.yaml changes.
namespace App\Repository;
use App\Entity\YourEntity;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class YourRepository extends ServiceEntityRepository
{
private $session;
public function __construct(ManagerRegistry $registry, SessionInterface $session)
{
$this->session = $session;
parent::__construct($registry, YourEntity::class);
}
public function findSomethingUsingSession()
{
$someValue = $this->session->get('some_index');
// ...
}
}
Then in your controller (for example)
$repository = $this->getDoctrine()->getRepository(YourEntity::class);
$result = $repository->findSomethingUsingSession();
Or use dependency injection (recommended)
public function someAction(YourRepository $repository)
{
$result = $repository->findSomethingUsingSession();
// ...
}

How should I configure an object dynamically when using Symfony's DI?

I'm using a YAML configuration to wire my dependencies, and I need to provide some runtime information to get a useful object back. I was going to run a setter method from my code once the object has been injected, but I Was wondering if there was a better way of doing it (or if there's something I'm missing).
This is the gist of my configuration:
services:
example_object : "myObject"
arguments : ["%object_parameter1%"]
parameters:
object_parameter1 : Some Static Data
object_parameter2 : #Rutime info required
For retrieving the current logged in user in any service, inject the security.context. In this case I use setter injection to simply user mock injection.
namespace Acme\ExampleBundle\Foo;
use Symfony\Component\Security\Core\SecurityContextInterface;
class MyService
{
private $param;
private $user;
public function __construct($param)
{
$this->param = $param;
}
/**
* Retrieve the current logged in user from the security context.
*/
public function setUserFromContext(SecurityContextInterface $context)
{
$this->user = $context->getToken()->getUser();
}
/**
* Set any user object.
*
* Usefull for testing, to inject a simple user mock.
*/
public function setUser($user)
{
$this->user = $user;
}
public function doSomething()
{
// do something with the user object
}
}
Define the service:
services:
my_service:
class: Acme\ExampleBundle\Foo\MyService
arguments: ["%object_parameter1%"]
calls:
- [ setUserFromContext, [#security.context] ]
You should not try to add dynamic values directly into the DI configuration. Symfony services configuration is reflected by compiled DI container and recompilation is very heavy operation.
If you do not want to couple your service with Symfony's security system directly, you can add your custom "user provider" service as a dependency. Then you will need to rewrite this service if the source of information will change. It may be also easily mocked.
You can also use a factory to inject a user object instead of user provider service.

FOSUserBundle: Get EntityManager instance overriding Form Handler

I am starting with Symfony2 and I am trying to override FOS\UserBundle\Form\Handler\RegistrationFormHandler of FOSUserBundle.
My code is:
<?php
namespace Testing\CoreBundle\Form\Handler;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Form\Handler\RegistrationFormHandler as BaseHandler;
use Testing\CoreBundle\Entity\User as UserDetails;
class RegistrationFormHandler extends BaseHandler
{
protected function onSuccess(UserInterface $user, $confirmation)
{
// I need an instance of Entity Manager but I don't know where get it!
$em = $this->container->get('doctrine')->getEntityManager();
// or something like: $em = $this->getDoctrine()->getEntityManager
$userDetails = new UserDetails;
$em->persist($userDetails);
$user->setId($userDetails->getId());
parent::onSuccess($user, $confirmation);
}
}
So, the point is that I need an instance of Doctrine's Entity Manager but I don't know where/how get it in this case!
Any idea?
Thanks in advance!
You should not use EntityManager directly in most of the cases. Use a proper manager/provider service instead.
In case of FOSUserBundle service implementing UserManagerInterface is such a manager. It is accessible through fos_user.user_manager key in the service container (which is an allias to fos_user.user_manager.default). Of course registration form handler uses that service, it is accessible through userManager property.
You should not treat your domain-model (i.a. Doctrine's entities) as if it was exact representation of the database-model. This means, that you should assign objects to other objects (not their ids).
Doctrine is capable of handling nested objects within your entities (UserDetails and User objects have a direct relationship). Eventually you will have to configure cascade options for User entity.
Finally, UserDetails seems to be a mandatory dependency for each User. Therefore you should override UserManagerInterface::createUser() not the form handler - you are not dealing with user's details there anyway.
Create your own UserManagerInterface implementation:
class MyUserManager extends \FOS\UserBundle\Entity\UserManager {
/**
* {#inheritdoc}
*/
public function createUser() {
$user = parent::createUser();
$user->setUserDetails(new UserDetails());
// some optional code required for a proper
// initialization of User/UserDetails object
// that might require access to other objects
// not available inside the entity
return $user;
}
}
Register your own manager as a serive inside DIC:
<service id="my_project.user_manager" class="\MyProject\UserManager" parent="fos_user.user_manager.default" />
Configure FOSUserBundle to use your own implementation:
# /app/config/config.yml
fos_user:
...
service:
user_manager: my_project.user_manager

Resources