Is there a way to inject EntityManager into a service - symfony

While using Symfony 3.3, I am declaring a service like this:
class TheService implements ContainerAwareInterface
{
use ContainerAwareTrait;
...
}
Inside each action where I need the EntityManager, I get it from the container:
$em = $this->container->get('doctrine.orm.entity_manager');
This is a bit annoying, so I'm curious whether Symfony has something that acts like EntityManagerAwareInterface.

Traditionally, you would have created a new service definition in your services.yml file set the entity manager as argument to your constructor
app.the_service:
class: AppBundle\Services\TheService
arguments: ['#doctrine.orm.entity_manager']
More recently, with the release of Symfony 3.3, the default symfony-standard-edition changed their default services.yml file to default to using autowire and add all classes in the AppBundle to be services. This removes the need for adding the custom service and using a type hint in your constructor will automatically inject the right service.
Your service class would then look like the following:
use Doctrine\ORM\EntityManagerInterface;
class TheService
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
// ...
}
For more information about automatically defining service dependencies, see https://symfony.com/doc/current/service_container/autowiring.html
The new default services.yml configuration file is available here: https://github.com/symfony/symfony-standard/blob/3.3/app/config/services.yml

Sometimes I inject the EM into a service on the container like this in services.yml:
application.the.service:
class: path\to\te\Service
arguments:
entityManager: '#doctrine.orm.entity_manager'
And then on the service class get it on the __construct method.
Hope it helps.

I ran into the same issue and solved it by editing the migration code.
I replaced
$this->addSql('ALTER TABLE user ADD COLUMN name VARCHAR(255) NOT NULL');
by
$this->addSql('ALTER TABLE user ADD COLUMN name VARCHAR(255) NOT NULL DEFAULT "-"');
I don't know why bin/console make:entity doesn't prompt us to provide a default in those cases. Django does it and it works well.

So I wanted to answer your subquestion:
This is a bit annoying, so I'm curious whether Symfony has something
that acts like EntityManagerAwareInterface.
And I think there is a solution to do so (I use it myself).
The idea is that you slightly change your kernel so tha it checks for all services which implement the EntityManagerAwareInterface and injects it for them.
You can also add write an EntityManagerAwareTrait that implements the $entityManager property and the setEntityManager()setter. The only thing left after that is to implement/use the interface/trait couple the way you would do for the Logger for example.
(you could have done this through a compiler pass as well).
<?php
// src/Kernel.php
namespace App;
use App\Entity\EntityManagerAwareInterface;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use function array_key_exists;
class Kernel extends BaseKernel implements CompilerPassInterface
{
use MicroKernelTrait;
public function process(ContainerBuilder $container): void
{
$definitions = $container->getDefinitions();
foreach ($definitions as $definition) {
if (!$this->isAware($definition, EntityManagerAwareInterface::class)) {
continue;
}
$definition->addMethodCall('setEntityManager', [$container->getDefinition('doctrine.orm.default_entity_manager')]);
}
}
private function isAware(Definition $definition, string $awarenessClass): bool
{
$serviceClass = $definition->getClass();
if ($serviceClass === null) {
return false;
}
$implementedClasses = #class_implements($serviceClass, false);
if (empty($implementedClasses)) {
return false;
}
if (array_key_exists($awarenessClass, $implementedClasses)) {
return true;
}
return false;
}
}
The interface:
<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\EntityManagerInterface;
interface EntityManagerAwareInterface
{
public function setEntityManager(EntityManagerInterface $entityManager): void;
}
The trait:
<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\EntityManagerInterface;
trait EntityManagerAwareTrait
{
/** #var EntityManagerInterface */
protected $entityManager;
public function setEntityManager(EntityManagerInterface $entityManager): void
{
$this->entityManager = $entityManager;
}
}
And now you can use it:
<?php
// src/SomeService.php
declare(strict_types=1);
namespace App;
use Exception;
use App\Entity\EntityManagerAwareInterface;
use App\Entity\Entity\EntityManagerAwareTrait;
use App\Entity\Entity\User;
class SomeService implements EntityManagerAwareInterface
{
use EntityManagerAwareTrait;
public function someMethod()
{
$users = $this->entityManager->getRepository(User::Class)->findAll();
// ...
}
}

Related

Symfony 6 - Attempted to call an undefined method named "getDoctrine" [duplicate]

As my IDE points out, the AbstractController::getDoctrine() method is now deprecated.
I haven't found any reference for this deprecation neither in the official documentation nor in the Github changelog.
What is the new alternative or workaround for this shortcut?
As mentioned here:
Instead of using those shortcuts, inject the related services in the constructor or the controller methods.
You need to use dependency injection.
For a given controller, simply inject ManagerRegistry on the controller's constructor.
use Doctrine\Persistence\ManagerRegistry;
class SomeController {
public function __construct(private ManagerRegistry $doctrine) {}
public function someAction(Request $request) {
// access Doctrine
$this->doctrine;
}
}
You can use EntityManagerInterface $entityManager:
public function delete(Request $request, Test $test, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$test->getId(), $request->request->get('_token'))) {
$entityManager->remove($test);
$entityManager->flush();
}
return $this->redirectToRoute('test_index', [], Response::HTTP_SEE_OTHER);
}
As per the answer of #yivi and as mentionned in the documentation, you can also follow the example below by injecting Doctrine\Persistence\ManagerRegistry directly in the method you want:
// src/Controller/ProductController.php
namespace App\Controller;
// ...
use App\Entity\Product;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
/**
* #Route("/product", name="create_product")
*/
public function createProduct(ManagerRegistry $doctrine): Response
{
$entityManager = $doctrine->getManager();
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(1999);
$product->setDescription('Ergonomic and stylish!');
// tell Doctrine you want to (eventually) save the Product (no queries yet)
$entityManager->persist($product);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new product with id '.$product->getId());
}
}
Add code in controller, and not change logic the controller
<?php
//...
use Doctrine\Persistence\ManagerRegistry;
//...
class AlsoController extends AbstractController
{
public static function getSubscribedServices(): array
{
return array_merge(parent::getSubscribedServices(), [
'doctrine' => '?'.ManagerRegistry::class,
]);
}
protected function getDoctrine(): ManagerRegistry
{
if (!$this->container->has('doctrine')) {
throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
}
return $this->container->get('doctrine');
}
...
}
read more https://symfony.com/doc/current/service_container/service_subscribers_locators.html#including-services
In my case, relying on constructor- or method-based autowiring is not flexible enough.
I have a trait used by a number of Controllers that define their own autowiring. The trait provides a method that fetches some numbers from the database. I didn't want to tightly couple the trait's functionality with the controller's autowiring setup.
I created yet another trait that I can include anywhere I need to get access to Doctrine. The bonus part? It's still a legit autowiring approach:
<?php
namespace App\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Symfony\Contracts\Service\Attribute\Required;
trait EntityManagerTrait
{
protected readonly ManagerRegistry $managerRegistry;
#[Required]
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
{
// #phpstan-ignore-next-line PHPStan complains that the readonly property is assigned outside of the constructor.
$this->managerRegistry = $managerRegistry;
}
protected function getDoctrine(?string $name = null, ?string $forClass = null): ObjectManager
{
if ($forClass) {
return $this->managerRegistry->getManagerForClass($forClass);
}
return $this->managerRegistry->getManager($name);
}
}
and then
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\Foobar;
class SomeController extends AbstractController
{
use EntityManagerTrait
public function someAction()
{
$result = $this->getDoctrine()->getRepository(Foobar::class)->doSomething();
// ...
}
}
If you have multiple managers like I do, you can use the getDoctrine() arguments to fetch the right one too.

getUser() method doesn't work in the controllers constructor

I want to fetch the user object in a controllers constructur in a Symfony 4.3.2 project. According to the docs on https://symfony.com/doc/4.0/security.html#retrieving-the-user-object, I just need to call $this->getUser(). And yes, this works in action methods.
BUT: trying to get the user in the constructor doesn't work, because the container will NOT be initialized here and the getUser method throws an exception "Call to a member function has() on null": the container is null at this point in time.
This works:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class TestController extends AbstractController
{
public function indexAction()
{
dump($this->getUser());
}
}
This doesn't:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class TestController extends AbstractController
{
public function __contruct()
{
dump($this->getUser());
}
public function indexAction()
{
}
}
And when I inject the container manually, then all is fine too:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class TestController extends AbstractController
{
public function __construct(ContainerInterface $container)
{
$this->container = $container;
dump($this->getUser());
}
public function indexAction()
{
}
}
btw, this is the getUser method in AbstractController:
protected function getUser()
{
if (!$this->container->has('security.token_storage')) {
throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".');
}
......
Is this a bug, that the container is not initialized in the constructor or is it a feature, that you have to initialize this by hand when you need the user in the constructor?
Edit: using the way shown in https://symfony.com/blog/new-in-symfony-3-2-user-value-resolver-for-controllers does work in actions, but it doesn't work in the constructor:
....
private $user;
public function __construct(UserInterface $user)
{
$this->user = $user;
}
produces the following error message: Cannot autowire service "App\Controller\TestController": argument "$user" of method "__construct()" references interface "Symfony\Component\Security\Core\User\UserInterface" but no such service exists. Did you create a class that implements this interface?. And that is where I would like to set the user object.
NEVER USE $security->getUser() or $this->getUser() in constructor!!
auth may not be complete yet. (In Service Instead, store the entire Security object. :
symfony.com/doc/security.html#a-fetching-the-user-object
... and you can use $this->getUser() in any Controller what extended with the AbstractController. (Just not in the constructor)
The container gets set by the ControllerResolver after the Controller has been instanced by calling the setContainer method that you mention. Thus, when the constructor is called the container is not available by design.
You might have a use case, but I don't see why you want to do this since in your controller methods you will have to access the $user property and it'll just save you typing get(). You can inject the whole container as shown in your sample or you can inject just the Security service.
use Symfony\Component\Security\Core\Security;
class TestController extends AbstractController
{
private $user;
public function __construct(Security $security)
{
$this->user = $security->getUser();
}
public function indexAction()
{
$user = $this->user; // Just saved you typing five characters
// At this point the container is available
}
}
I'm not actually setting the security service because it'll become available later through the container.
If you want to do this to enforce access control for the whole class you can use the Security annotations:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
/**
* #IsGranted('ROLE_USER')
*/
class TestController extends AbstractController
{
// Only authenticated user will be able to access this methods
}

Base class controller with global twig service

First of all, I have to say that I have been seeing answers and documentation for several days but none of them answer my question.
The only and simple thing I want to do is to use the twig service as a global service in a BaseController.
This is my code:
<?php
namespace App\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use App\Service\Configuration;
use App\Utils\Util;
abstract class BaseController extends Controller
{
protected $twig;
protected $configuration;
public function __construct(\Twig_Environment $twig,Configuration $configuration)
{
$this->twig = $twig;
$this->configuration = $configuration;
}
}
Then in all my controllers extend the twig and configuration service, without having to inject it again & again.
//...
//......
/**
* #Route("/configuration", name="configuration_")
*/
class ConfigurationController extends BaseController
{
public function __construct()
{
//parent::__construct();
$this->twig->addGlobal('menuActual', "config");
}
As you can see the only thing I want is to have some services global to have everything more organized and also to create some global shortcuts for all my controllers. In this example I am assigning a global variable to make a link active in the menu of my template and in each controller I have to add a new value for menuActual, for example in the UserController the variable would be addGlobal('menuActual', "users").
I think this should be in the good practices of symfony which I don't find :(.
Having to include the \Twig_Environment in each controller to assign a variable to the view seems very repetitive to me. This should come by default in the controller.
Thanks
I've had that problem as well - trying to not have to repeat a bit of code for every controller / action.
I solved it using an event listener:
# services.yaml
app.event_listener.controller_action_listener:
class: App\EventListener\ControllerActionListener
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
#src/EventListener/ControllerActionListener.php
namespace App\EventListener;
use App\Controller\BaseController;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
/**
* Class ControllerActionListener
*
* #package App\EventListener
*/
class ControllerActionListener
{
public function onKernelController(FilterControllerEvent $event)
{
//fetch the controller class if available
$controllerClass = null;
if (!empty($event->getController())) {
$controllerClass = $event->getController()[0];
}
//make sure your global instantiation only fires if the controller extends your base controller
if ($controllerClass instanceof BaseController) {
$controllerClass->getTwig()->addGlobal('menuActual', "config");
}
}
}

Getting a Logger in Symfony WheNot in a Controller

I'm working on an EntityRepository class, and I need to dump some data to my log file. I can't use dump() because this isn't going to build a page; it's just going to return some JSON. Eventually.
Normally, in a Controller, I'd use:
$logger = $this->getLogger();
But I'm not in a Controller.
Thx for your help.
UPDATE: this is for forensic logging. I'm just using it for debugging purposes. It'll be removed afterwards.
I looked into this a bit. My first hunch is "Well, if you could define EntityRepositories as services, then that would make this easy because you could then just inject the logger"
But how do you inject the logger into repositories that doctrine is creating? It turns out you can specify your own repository factory
I'm going to assume all it needs is to implement the Doctrine\ORM\Repository\RepositoryFactory interface, but you'll probably want to subclass Doctrine\ORM\Repository\DefaultRepositoryFactory.
You will also need to create your own, base repository class that can hold a logger. Let's start there
src/AppBundle/Doctrine/EntityRepository.php
<?php
namespace AppBundle\Doctrine;
use Doctrine\ORM\EntityRepository;
use Psr\Log\LoggerInterface;
class LoggerAwareEntityRepository extends EntityRepository
{
protected $logger;
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
}
Now, the factory
src/AppBundle/Doctrine/LoggerAwareRepositoryFactory.php
<?php
namespace AppBundle\Doctrine;
use Doctrine\ORM\Repository\DefaultRepositoryFactory;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use AppBundle\Doctrine\LoggerAwareEntityRepository;
class LoggerAwareRepositoryFactory extends DefaultRepositoryFactory
{
protected $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
protected function createRepository(EntityManagerInterface $entityManager, $entityName)
{
$repository = parent::createRepository($entityManager, $entityName);
if ($repository instanceof LoggerAwareEntityRepository) {
$repository->setLogger($this->logger);
}
return $repository;
}
}
Now for the confguration glue to make it all work
app/config/services.yml
services:
logger_aware_repository_factory:
class: AppBundle\Doctrine\LoggerAwareRepositoryFactory
arguments: ['#logger']
app/config/config.yml
doctrine:
orm:
entity_managers:
default:
repository_factory: "#logger_aware_repository_factory"
Lastly, for the actual implementation
src/AppBundle/Entity/SomeCustomRepository.php
<?php
namespace AppBundle\Entity;
use AppBundle\Doctrine\LoggerAwareEntityRepository;
class SomeCustomRepository extends LoggerAwareEntityRepository
{
public function findSomethingCustom()
{
// Log something
$this->logger->log('message');
}
}
Full disclosure: this is untested code - there might be bugs!
Depending on what you want to log the most neat solution would be to create either a doctrine or doctrine entity listener, probably on post load. Inject the logger in the listener.
Once you decide you don't need it, just remove the listener.

Symfony2 getdoctrine outside of Model/Controller

I'm trying to getDoctrine() outside of the controller.
I've created this service:
config/services.yml
services:
update_command:
class: project\projBundle\Command\Update
arguments: ['#doctrine.orm.entity_manager']
and in my app/config/config.yml
imports:
- { resource: "#projectprojBundle/Resources/config/services.yml" }
so and the class that I want to use:
namespace project\projBundle\Command;
use Doctrine\ORM\EntityManager;
class Update {
protected $em;
public function __construct(EntityManager $em) {
$this->em = $em;
}
but every time I want to do this: (I'm doing this right?)
$up = new Update();
i got this error:
Catchable Fatal Error: Argument 1 passed to ...\Update::__construct() must be an instance of Doctrine\ORM\EntityManager, none given, called in .../Update.php line 7
Simple solution
If you're implementing a Symfony command (that can be executed in a cron tab), you can access the service container from the command.
<?php
namespace MyProject\MyBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class UpdateCommand extends ContainerAwareCommand
{
protected $em;
protected function configure()
{
$this->setName('myproject:mybundle:update') ;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->em = $this->getContainer()->get('doctrine.orm.entity_manager');
}
}
That way, you get the entity manager from a command and don't need to declare this class as a service. You can therefore remove the configuration you added in the services.yml file.
An other solution (cleaner)
This solution allows better separation of concerns and can therefore be easily unit tested and reused in other parts of your Symfony application (not only as a command).
Move all the logic part of your "update" command to a dedicated class that you will declare as a service:
<?php
namespace MyProject\MyBundle\Service;
use Doctrine\ORM\EntityManager;
class MyUpdater
{
protected $em;
public function __construct($em)
{
$this->em = $em;
}
public function runUpdate()
{
// All your logic code here
}
}
Declare it as a service in your services.yml file:
services:
myproject.mybundle.myupdater:
class: MyProject\MyBundle\Service\MyUpdater
arguments: ['#doctrine.orm.entity_manager']
Simply call your service from your command :
<?php
namespace MyProject\MyBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class UpdateCommand extends ContainerAwareCommand
{
protected function configure()
{
$this->setName('myproject:mybundle:update') ;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$myUpdater = $this->getContainer()->get('myproject.mybundle.myupdater');
$myUpdater->runUpdate();
}
}
You have to either inject your newly created #update_command service or get it from the container in order to have the #doctrine.orm.entity_manager service injected automatically.
You're just creating the object with no argument, not a service. Update expects to retrieve an entity manager instance but you don't provide it.
$up = new Update();
In a ContainerAware class like a controller get your service like this:
$up = $this->container->get('update_command');
Otherwise turn the class where you want to use the update_command into a service aswell and inject #update_command as you did with the entity manager in the service itself.
remove below codes in app/config/config.yml, your services.yml will be autoload...
imports:
- { resource: "#projectprojBundle/Resources/config/services.yml" }
in a Action new a instance you can do:
$up = $this->get('update_command');

Resources