I'm writing an open source application uses some Symfony components, and using Symfony Console component for interacting with shell.
But, i need to inject dependencies (used in all commands) something like Logger, Config object, Yaml parsers.. I solved this problem with extending Symfony\Component\Console\Command\Command class. But this makes unit testing harder and not looks correct way.
How can i solve this ?
Since Symfony 4.2 the ContainerAwareCommand is deprecated. Use the DI instead.
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Doctrine\ORM\EntityManagerInterface;
final class YourCommand extends Command
{
/**
* #var EntityManagerInterface
*/
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// YOUR CODE
$this->entityManager->persist($object1);
}
}
It is best not to inject the container itself but to inject services from the container into your object. If you're using Symfony2's container, then you can do something like this:
MyBundle/Resources/config/services (or wherever you decide to put this file):
...
<services>
<service id="mybundle.command.somecommand" class="MyBundle\Command\SomeCommand">
<call method="setSomeService">
<argument type="service" id="some_service_id" />
</call>
</service>
</services>
...
Then your command class should look like this:
<?php
namespace MyBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use The\Class\Of\The\Service\I\Wanted\Injected;
class SomeCommand extends Command
{
protected $someService;
public function setSomeService(Injected $someService)
{
$this->someService = $someService;
}
...
I know you said you're not using the dependency injection container, but in order to implement the above answer from #ramon, you have to use it. At least this way your command can be properly unit tested.
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
Extends your Command class from ContainerAwareCommand and get the service with $this->getContainer()->get('my_service_id');
You can use ContainerCommandLoader in order to provide a PSR-11 container as follow:
require 'vendor/autoload.php';
$application = new Application('my-app', '1.0');
$container = require 'config/container.php';
// Lazy load command with container
$commandLoader = new ContainerCommandLoader($container, [
'app:change-mode' => ChangeMode::class,
'app:generate-logs' => GenerateLogos::class,
]);
$application->setCommandLoader($commandLoader);
$application->run();
ChangeMode class could be defined as follow:
class ChangeMode extends Command
{
protected static $defaultName = 'app:change-mode';
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
parent::__construct(static::$defaultName);
}
...
NB.: ChangeMode should be provided in the Container configuration.
I'm speaking for symfony2.8. You cannot add a constructor to the class that extends the ContainerAwareCommand because the extended class has a $this->getContainer() which got you covered in getting your services instead of injecting them via the constructor.
You can do $this->getContainer()->get('service-name');
Go to services.yaml
Add This to the file(I used 2 existing services as an example):
App\Command\MyCommand:
arguments: [
'#request_stack',
'#doctrine.orm.entity_manager'
]
To see a list of all services type in terminal at the root project folder:
php bin/console debug:autowiring --all
You will get a long list of services you can use, an example of one line would look like this:
Stores CSRF tokens.
Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface (security.csrf.token_storage)
So if CSRF token services is what you are looking for(for example) you will use as a service the part in the parenthesis: (security.csrf.token_storage)
So your services.yaml will look somewhat like this:
parameters:
services:
_defaults:
autowire: true
autoconfigure: true
# Here might be some other services...
App\Command\MyCommand:
arguments: [
'#security.csrf.token_storage'
]
Then in your command class use the service in the constructor:
class MyCommand extends Command
{
private $csrfToken;
public function __construct(CsrfToken $csrfToken)
{
parent::__construct();
$this->csrfToken = $csrfToken;
}
}
In Symfony 3.4, if autowire is configured correctly, services can be injected into the constructor of the command.
public function __construct(
\AppBundle\Handler\Service\AwsS3Handler $s3Handler
) {
parent::__construct();
$this->s3Handler = $s3Handler;
}
php 8.1
Symfony 6.1
public function __construct(private EntityManagerInterface $em, string $name = null)
{
parent::__construct($name);
}
Related
I'm writing a test for my code in API Platform (built on Symfony 5) and PHPUnit.
I've created a base testcase named TestContext that extends ApiTestCase. All my tests classes will extend TestContext to reuse shared testing boilerplate code.
I'm also trying to use dependency injection (or service location) to resolve the Repository class, so I can access them in all the child classes.
What would be the cleanest, easiest and best-practice way to have access to my Repositories in my test classes?
In order to inject the Repository for the API tests I have a src/Test/BaseApiTest.php
namespace App\Test;
use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
use Doctrine\ORM\EntityManagerInterface;
use Hautelook\AliceBundle\PhpUnit\ReloadDatabaseTrait;
class BaseApiTestCase extends ApiTestCase
{
use ReloadDatabaseTrait;
protected ?EntityManagerInterface $em;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->em = $kernel->getContainer()
->get('doctrine')
->getManager();
}
}
Then for the test in my root tests directory I access it as he following:
//tests/UserRessourceTest.php
<?php
namespace App\Tests;
use App\Entity\User;
use App\Test\BaseApiTestCase;
class UserResourceTest extends BaseApiTestCase
{
public function testDeleteAccount(): void
{
...
/** #var User $deletedUser */
$deletedUser = $this->em->getRepository(User::class)->findOneById($user->getId());
...
}
}
This works well for most of my cases. I hope it will work for yours ;)
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();
// ...
}
}
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.
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');
We have service as
services:
service_name:
class: One\SomeBundle\Controller\OurController
What need we add, to make $this->getDoctrine() works in our "OurController" ?
I tried in "OurController"
$this->countainer->get('doctrine')
// and
$this->getDoctrine()
but nothing happens, just errors.
I want that this controller use native methods such as $this->getDoctrine(). As I know we can give arguments to controller(arguments in services.yml), and then apply it, but can we set it as default? Without
function __construct($em)
{
$this->em = $em;
}
and other additional stuff. All i need is to make $this->getDocrine() works.
i think the doctrine is avaiable in all controllers like this
$em = $this->getDoctrine()->getEntityManager();
if you want the doctrine available in your service then use something like this
services:
your.service:
class: YourVendor\YourBundle\Service\YourService
arguments: [ #doctrine.orm.entity_manager ]
When you define a service, you have to pass parameters as arguments as follow (since a service doesn't have access to the main container by default):
<services>
<service id="myservice" class="path/to/my/class">
<argument type="service" id="doctrine" />
...
</service>
</services>
This is configured in xml but I'll let you convert it in yml if you like.
Then in your service class you just need to set your constructor as so:
class MyServiceClass
{
protected $doctrine;
public function __construct(\Doctrine $doctrine, ...)
{
$this->doctrine = $doctrine;
....
}
}
Now the doctrine service will be available in your own service Class.
And your can usage JMSDiExtra for set services to properties in Controller:
Controller code:
<?php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use JMS\DiExtraBundle\Annotation as DI;
/**
* Controller class
*/
class MyController extends Controller
{
/**
* #DI\Inject
*/
protected $request;
/**
* #DI\Inject("doctrine.orm.entity_manager")
*/
protected $em;
// .....
/**
* Action
*/
public function myAction()
{
// ....
$this->em->persist($myObject);
// ....
}
}
More documentation with JMSDiExtra - http://jmsyst.com/bundles/JMSDiExtraBundle
This bundle is a default in Symfony2 Framework